package spec

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"

	"github.com/go-openapi/jsonpointer"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestResolveRef(t *testing.T) {
	var root interface{}
	require.NoError(t, json.Unmarshal([]byte(PetStore20), &root))

	ref, err := NewRef("#/definitions/Category")
	require.NoError(t, err)

	sch, err := ResolveRef(root, &ref)
	require.NoError(t, err)

	b, err := sch.MarshalJSON()
	require.NoError(t, err)

	assert.JSONEq(t, `{"id":"Category","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}}}`, string(b))

	// WithBase variant
	sch, err = ResolveRefWithBase(root, &ref, &ExpandOptions{
		RelativeBase: "/",
	})
	require.NoError(t, err)

	b, err = sch.MarshalJSON()
	require.NoError(t, err)

	assert.JSONEq(t, `{"id":"Category","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}}}`, string(b))
}

func TestResolveResponse(t *testing.T) {
	specDoc, err := jsonDoc(filepath.Join("fixtures", "expansion", "all-the-things.json"))
	require.NoError(t, err)

	spec := new(Swagger)
	require.NoError(t, json.Unmarshal(specDoc, spec))

	// Resolve with root version
	resp := spec.Paths.Paths["/"].Get.Responses.StatusCodeResponses[200]
	resp2, err := ResolveResponse(spec, resp.Ref)
	require.NoError(t, err)

	// resolve resolves the ref, but dos not expand
	jazon := asJSON(t, resp2)

	assert.JSONEq(t, `{
         "$ref": "#/responses/petResponse"
        }`, jazon)
}

func TestResolveResponseWithBase(t *testing.T) {
	specDoc, err := jsonDoc(crossFileRefFixture)
	require.NoError(t, err)

	spec := new(Swagger)
	require.NoError(t, json.Unmarshal(specDoc, spec))

	// Resolve with root version
	resp := spec.Paths.Paths["/"].Get.Responses.StatusCodeResponses[200]
	resp2, err := ResolveResponseWithBase(spec, resp.Ref, &ExpandOptions{RelativeBase: crossFileRefFixture})
	require.NoError(t, err)

	// resolve resolves the ref, but dos not expand
	jazon := asJSON(t, resp2)

	assert.JSONEq(t, `{
         "$ref": "#/responses/petResponse"
        }`, jazon)
}

func TestResolveParam(t *testing.T) {
	specDoc, err := jsonDoc(filepath.Join("fixtures", "expansion", "all-the-things.json"))
	require.NoError(t, err)

	var spec Swagger
	require.NoError(t, json.Unmarshal(specDoc, &spec))

	param := spec.Paths.Paths["/pets/{id}"].Get.Parameters[0]
	par, err := ResolveParameter(spec, param.Ref)
	require.NoError(t, err)

	jazon := asJSON(t, par)

	assert.JSONEq(t, `{
      "name": "id",
      "in": "path",
      "description": "ID of pet to fetch",
      "required": true,
      "type": "integer",
      "format": "int64"
      }`, jazon)
}

func TestResolveParamWithBase(t *testing.T) {
	specDoc, err := jsonDoc(crossFileRefFixture)
	require.NoError(t, err)

	var spec Swagger
	require.NoError(t, json.Unmarshal(specDoc, &spec))

	param := spec.Paths.Paths["/pets"].Get.Parameters[0]
	par, err := ResolveParameterWithBase(spec, param.Ref, &ExpandOptions{RelativeBase: crossFileRefFixture})
	require.NoError(t, err)

	jazon := asJSON(t, par)

	assert.JSONEq(t, `{
"description":"ID of pet to fetch",
"format":"int64",
"in":"path",
"name":"id",
"required":true,
"type":"integer"
}`, jazon)
}

func TestResolveRemoteRef_RootSame(t *testing.T) {
	fileserver := http.FileServer(http.Dir(specs))
	server := httptest.NewServer(fileserver)
	defer server.Close()

	rootDoc := new(Swagger)
	b, err := os.ReadFile(filepath.Join(specs, "refed.json"))
	require.NoError(t, err)
	require.NoError(t, json.Unmarshal(b, rootDoc))

	// the filename doesn't matter because ref will eventually point to refed.json
	specBase := normalizeBase(filepath.Join(specs, "anyotherfile.json"))

	var result0 Swagger
	ref0, _ := NewRef(server.URL + "/refed.json#")
	resolver0 := defaultSchemaLoader(rootDoc, nil, nil, nil)
	require.NoError(t, resolver0.Resolve(&ref0, &result0, ""))
	assertSpecs(t, result0, *rootDoc)

	var result1 Swagger
	ref1, _ := NewRef("./refed.json")
	resolver1 := defaultSchemaLoader(rootDoc, &ExpandOptions{
		RelativeBase: specBase,
	}, nil, nil)
	require.NoError(t, resolver1.Resolve(&ref1, &result1, specBase))
	assertSpecs(t, result1, *rootDoc)
}

func TestResolveRemoteRef_FromFragment(t *testing.T) {
	fileserver := http.FileServer(http.Dir(specs))
	server := httptest.NewServer(fileserver)
	defer server.Close()

	rootDoc := new(Swagger)
	b, err := os.ReadFile(filepath.Join(specs, "refed.json"))
	require.NoError(t, err)
	require.NoError(t, json.Unmarshal(b, rootDoc))

	var tgt Schema
	ref, err := NewRef(server.URL + "/refed.json#/definitions/pet")
	require.NoError(t, err)

	context := newResolverContext(&ExpandOptions{PathLoader: jsonDoc})
	resolver := &schemaLoader{root: rootDoc, cache: defaultResolutionCache(), context: context}
	require.NoError(t, resolver.Resolve(&ref, &tgt, ""))
	assert.Equal(t, []string{"id", "name"}, tgt.Required)
}

func TestResolveRemoteRef_FromInvalidFragment(t *testing.T) {
	fileserver := http.FileServer(http.Dir(specs))
	server := httptest.NewServer(fileserver)
	defer server.Close()

	rootDoc := new(Swagger)
	b, err := os.ReadFile(filepath.Join(specs, "refed.json"))
	require.NoError(t, err)
	require.NoError(t, json.Unmarshal(b, rootDoc))

	var tgt Schema
	ref, err := NewRef(server.URL + "/refed.json#/definitions/NotThere")
	require.NoError(t, err)

	resolver := defaultSchemaLoader(rootDoc, nil, nil, nil)
	require.Error(t, resolver.Resolve(&ref, &tgt, ""))
}

/* This next test will have to wait until we do full $ID analysis for every subschema on every file that is referenced */
/* For now, TestResolveRemoteRef_WithNestedResolutionContext replaces this next test */
// func TestResolveRemoteRef_WithNestedResolutionContextWithFragment_WithParentID(t *testing.T) {
// 	server := resolutionContextServer()
// 	defer server.Close()
//
// 	rootDoc := new(Swagger)
// 	b, err := os.ReadFile("fixtures/specs/refed.json")
// 	require.NoError(t, err) && assert.NoError(t, json.Unmarshal(b, rootDoc))
//
//	var tgt Schema
// 	ref, err := NewRef(server.URL + "/resolution2.json#/items/items")
// 	require.NoError(t, err)
//
// 	resolver := defaultSchemaLoader(rootDoc, nil, nil,nil)
// 	require.NoError(t, resolver.Resolve(&ref, &tgt, ""))
// 	assert.Equal(t, StringOrArray([]string{"file"}), tgt.Type)
// }

func TestResolveRemoteRef_ToParameter(t *testing.T) {
	fileserver := http.FileServer(http.Dir(specs))
	server := httptest.NewServer(fileserver)
	defer server.Close()

	rootDoc := new(Swagger)
	b, err := os.ReadFile(filepath.Join(specs, "refed.json"))
	require.NoError(t, err)
	require.NoError(t, json.Unmarshal(b, rootDoc))

	var tgt Parameter
	ref, err := NewRef(server.URL + "/refed.json#/parameters/idParam")
	require.NoError(t, err)

	resolver := defaultSchemaLoader(rootDoc, nil, nil, nil)
	require.NoError(t, resolver.Resolve(&ref, &tgt, ""))

	assert.Equal(t, "id", tgt.Name)
	assert.Equal(t, "path", tgt.In)
	assert.Equal(t, "ID of pet to fetch", tgt.Description)
	assert.True(t, tgt.Required)
	assert.Equal(t, "integer", tgt.Type)
	assert.Equal(t, "int64", tgt.Format)
}

func TestResolveRemoteRef_ToPathItem(t *testing.T) {
	fileserver := http.FileServer(http.Dir(specs))
	server := httptest.NewServer(fileserver)
	defer server.Close()

	rootDoc := new(Swagger)
	b, err := os.ReadFile(filepath.Join(specs, "refed.json"))
	require.NoError(t, err)
	require.NoError(t, json.Unmarshal(b, rootDoc))

	var tgt PathItem
	ref, err := NewRef(server.URL + "/refed.json#/paths/" + jsonpointer.Escape("/pets/{id}"))
	require.NoError(t, err)

	resolver := defaultSchemaLoader(rootDoc, nil, nil, nil)
	require.NoError(t, resolver.Resolve(&ref, &tgt, ""))
	assert.Equal(t, rootDoc.Paths.Paths["/pets/{id}"].Get, tgt.Get)
}

func TestResolveRemoteRef_ToResponse(t *testing.T) {
	fileserver := http.FileServer(http.Dir(specs))
	server := httptest.NewServer(fileserver)
	defer server.Close()

	rootDoc := new(Swagger)
	b, err := os.ReadFile(filepath.Join(specs, "refed.json"))
	require.NoError(t, err)
	require.NoError(t, json.Unmarshal(b, rootDoc))

	var tgt Response
	ref, err := NewRef(server.URL + "/refed.json#/responses/petResponse")
	require.NoError(t, err)

	resolver := defaultSchemaLoader(rootDoc, nil, nil, nil)
	require.NoError(t, resolver.Resolve(&ref, &tgt, ""))
	assert.Equal(t, rootDoc.Responses["petResponse"], tgt)
}

func TestResolveLocalRef_SameRoot(t *testing.T) {
	rootDoc := new(Swagger)
	require.NoError(t, json.Unmarshal(PetStoreJSONMessage, rootDoc))

	result := new(Swagger)
	ref, _ := NewRef("#")
	resolver := defaultSchemaLoader(rootDoc, nil, nil, nil)
	require.NoError(t, resolver.Resolve(&ref, result, ""))
	assert.Equal(t, rootDoc, result)
}

func TestResolveLocalRef_FromFragment(t *testing.T) {
	rootDoc := new(Swagger)
	require.NoError(t, json.Unmarshal(PetStoreJSONMessage, rootDoc))

	var tgt Schema
	ref, err := NewRef("#/definitions/Category")
	require.NoError(t, err)

	resolver := defaultSchemaLoader(rootDoc, nil, nil, nil)
	require.NoError(t, resolver.Resolve(&ref, &tgt, ""))
	assert.Equal(t, "Category", tgt.ID)
}

func TestResolveLocalRef_FromInvalidFragment(t *testing.T) {
	rootDoc := new(Swagger)
	require.NoError(t, json.Unmarshal(PetStoreJSONMessage, rootDoc))

	var tgt Schema
	ref, err := NewRef("#/definitions/NotThere")
	require.NoError(t, err)

	resolver := defaultSchemaLoader(rootDoc, nil, nil, nil)
	require.Error(t, resolver.Resolve(&ref, &tgt, ""))
}

func TestResolveLocalRef_Parameter(t *testing.T) {
	rootDoc := new(Swagger)
	b, err := os.ReadFile(filepath.Join(specs, "refed.json"))
	require.NoError(t, err)

	basePath := filepath.Join(specs, "refed.json")
	require.NoError(t, json.Unmarshal(b, rootDoc))

	var tgt Parameter
	ref, err := NewRef("#/parameters/idParam")
	require.NoError(t, err)

	resolver := defaultSchemaLoader(rootDoc, nil, nil, nil)
	require.NoError(t, resolver.Resolve(&ref, &tgt, basePath))

	assert.Equal(t, "id", tgt.Name)
	assert.Equal(t, "path", tgt.In)
	assert.Equal(t, "ID of pet to fetch", tgt.Description)
	assert.True(t, tgt.Required)
	assert.Equal(t, "integer", tgt.Type)
	assert.Equal(t, "int64", tgt.Format)
}

func TestResolveLocalRef_PathItem(t *testing.T) {
	rootDoc := new(Swagger)
	b, err := os.ReadFile(filepath.Join(specs, "refed.json"))
	require.NoError(t, err)

	basePath := filepath.Join(specs, "refed.json")
	require.NoError(t, json.Unmarshal(b, rootDoc))

	var tgt PathItem
	ref, err := NewRef("#/paths/" + jsonpointer.Escape("/pets/{id}"))
	require.NoError(t, err)

	resolver := defaultSchemaLoader(rootDoc, nil, nil, nil)
	require.NoError(t, resolver.Resolve(&ref, &tgt, basePath))
	assert.Equal(t, rootDoc.Paths.Paths["/pets/{id}"].Get, tgt.Get)
}

func TestResolveLocalRef_Response(t *testing.T) {
	rootDoc := new(Swagger)
	b, err := os.ReadFile(filepath.Join(specs, "refed.json"))
	require.NoError(t, err)

	basePath := filepath.Join(specs, "refed.json")
	require.NoError(t, json.Unmarshal(b, rootDoc))

	var tgt Response
	ref, err := NewRef("#/responses/petResponse")
	require.NoError(t, err)

	resolver := defaultSchemaLoader(rootDoc, nil, nil, nil)
	require.NoError(t, resolver.Resolve(&ref, &tgt, basePath))
	assert.Equal(t, rootDoc.Responses["petResponse"], tgt)
}

func TestResolvePathItem(t *testing.T) {
	spec := new(Swagger)
	specDoc, err := jsonDoc(pathItemsFixture)
	require.NoError(t, err)

	require.NoError(t, json.Unmarshal(specDoc, spec))

	// Resolve use case
	pth := spec.Paths.Paths["/todos"]
	pathItem, err := ResolvePathItem(spec, pth.Ref, &ExpandOptions{RelativeBase: pathItemsFixture})
	require.NoError(t, err)

	jazon := asJSON(t, pathItem)

	assert.JSONEq(t, `{
         "get": {
          "responses": {
           "200": {
            "description": "List Todos",
            "schema": {
             "type": "array",
             "items": {
              "type": "string"
             }
            }
           },
           "404": {
            "description": "error"
           }
          }
         }
			 }`, jazon)
}

func TestResolveExtraItem(t *testing.T) {
	// go-openapi extra goodie: $ref in simple schema Items and Headers
	spec := new(Swagger)
	specDoc, err := jsonDoc(extraRefFixture)
	require.NoError(t, err)

	require.NoError(t, json.Unmarshal(specDoc, spec))

	// Resolve param Items use case: here we explicitly resolve the unsuppord case
	parm := spec.Paths.Paths["/employees"].Get.Parameters[0]
	parmItem, err := ResolveItems(spec, parm.Items.Ref, &ExpandOptions{RelativeBase: extraRefFixture})
	require.NoError(t, err)

	jazon := asJSON(t, parmItem)

	assert.JSONEq(t, `{
         "type": "integer",
         "format": "int32"
			 }`, jazon)

	// Resolve header Items use case: here we explicitly resolve the unsuppord case
	hdr := spec.Paths.Paths["/employees"].Get.Responses.StatusCodeResponses[200].Headers["X-header"]
	hdrItem, err := ResolveItems(spec, hdr.Items.Ref, &ExpandOptions{RelativeBase: extraRefFixture})
	require.NoError(t, err)

	jazon = asJSON(t, hdrItem)

	assert.JSONEq(t, `{
         "type": "string",
         "format": "uuid"
			 }`, jazon)
}
