package openapi3

import (
	"bytes"
	"fmt"
	"net"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
)

const addr = "localhost:7965"

func TestLoadYAML(t *testing.T) {
	spec := []byte(`
openapi: 3.0.0
info:
  title: An API
  version: v1

components:
  schemas:
    NewItem:
      required: [name]
      properties:
        name: {type: string}
        tag: {type: string}
    ErrorModel:
      type: object
      required: [code, message]
      properties:
        code: {type: integer}
        message: {type: string}

paths:
  /items:
    put:
      description: ''
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewItem'
      responses:
        default: &defaultResponse # a YAML ref
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorModel'
`)

	loader := NewLoader()

	doc, err := loader.LoadFromData(spec)
	require.NoError(t, err)
	require.Equal(t, "An API", doc.Info.Title)
	require.Equal(t, 2, len(doc.Components.Schemas))
	require.Equal(t, 1, doc.Paths.Len())
	require.Equal(t, "unexpected error", *doc.Paths.Value("/items").Put.Responses.Default().Value.Description)

	err = doc.Validate(loader.Context)
	require.NoError(t, err)
}

func TestIssue731(t *testing.T) {
	spec := []byte(`
openapi: 3.0.0
info:
  title: An API
  version: v1
paths:
  /items:
    put:
      description: ''
      requestBody:
        required: true
      # Note mis-indented content block
      content:
        application/json:
          schema:
            type: object
      responses:
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                type: object
`[1:])

	loader := NewLoader()

	doc, err := loader.LoadFromData(spec)
	require.NoError(t, err)

	err = doc.Validate(loader.Context)
	require.ErrorContains(t, err, `content of the request body is required`)
}

func ExampleLoader() {
	spec := []byte(`
openapi: 3.0.1
paths: {}
info:
  version: 1.1.1
  title: title
  description: An API
`[1:])

	loader := NewLoader()
	doc, err := loader.LoadFromData(spec)
	if err != nil {
		panic(err)
	}

	if err := doc.Validate(loader.Context); err != nil {
		panic(err)
	}

	fmt.Print(doc.Info.Description)
	// Output: An API
}

func TestResolveSchemaRef(t *testing.T) {
	source := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{},"components":{"schemas":{"B":{"type":"string"},"A":{"allOf":[{"$ref":"#/components/schemas/B"}]}}}}`)
	loader := NewLoader()
	doc, err := loader.LoadFromData(source)
	require.NoError(t, err)
	err = doc.Validate(loader.Context)
	require.NoError(t, err)

	refAVisited := doc.Components.Schemas["A"].Value.AllOf[0]
	require.Equal(t, "#/components/schemas/B", refAVisited.Ref)
	require.NotNil(t, refAVisited.Value)
}

func TestResolveResponseExampleRef(t *testing.T) {
	source := []byte(`
openapi: 3.0.1
info:
  title: My API
  version: 1.0.0
components:
  examples:
    test:
      value:
        error: false
paths:
  /:
    get:
      responses:
        200:
          description: A test response
          content:
            application/json:
              examples:
                test:
                  $ref: '#/components/examples/test'`)
	loader := NewLoader()
	doc, err := loader.LoadFromData(source)
	require.NoError(t, err)

	err = doc.Validate(loader.Context)
	require.NoError(t, err)

	example := doc.Paths.Value("/").Get.Responses.Status(200).Value.Content.Get("application/json").Examples["test"]
	require.NotNil(t, example.Value)
	require.Equal(t, example.Value.Value.(map[string]any)["error"].(bool), false)
}

func TestLoadErrorOnRefMisuse(t *testing.T) {
	spec := []byte(`
openapi: '3.0.0'
servers: [{url: /}]
info:
  title: Some API
  version: '1'
components:
  schemas:
    Thing: {type: string}
paths:
  /items:
    put:
      description: ''
      requestBody:
        # Uses a schema ref instead of a requestBody ref.
        $ref: '#/components/schemas/Thing'
      responses:
        '201':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Thing'
`)

	loader := NewLoader()
	_, err := loader.LoadFromData(spec)
	require.Error(t, err)
}

func TestLoadPathParamRef(t *testing.T) {
	spec := []byte(`
openapi: '3.0.0'
info:
  title: ''
  version: '1'
components:
  parameters:
    testParam:
      name: test
      in: query
      schema:
        type: string
paths:
  '/':
    parameters:
      - $ref: '#/components/parameters/testParam'
    get:
      responses:
        '200':
          description: Test call.
`)

	loader := NewLoader()
	doc, err := loader.LoadFromData(spec)
	require.NoError(t, err)

	require.NotNil(t, doc.Paths.Value("/").Parameters[0].Value)
}

func TestLoadRequestExampleRef(t *testing.T) {
	spec := []byte(`
openapi: '3.0.0'
info:
  title: ''
  version: '1'
components:
  examples:
    test:
      value:
        hello: world
paths:
  '/':
    post:
      requestBody:
        content:
          application/json:
            examples:
              test:
                $ref: "#/components/examples/test"
      responses:
        '200':
          description: Test call.
`)

	loader := NewLoader()
	doc, err := loader.LoadFromData(spec)
	require.NoError(t, err)

	require.NotNil(t, doc.Paths.Value("/").Post.RequestBody.Value.Content.Get("application/json").Examples["test"])
}

func createTestServer(t *testing.T, handler http.Handler) *httptest.Server {
	ts := httptest.NewUnstartedServer(handler)
	l, err := net.Listen("tcp", addr)
	require.NoError(t, err)
	ts.Listener.Close()
	ts.Listener = l
	return ts
}

func TestLoadFromRemoteURL(t *testing.T) {
	fs := http.FileServer(http.Dir("testdata"))
	ts := createTestServer(t, fs)
	ts.Start()
	defer ts.Close()

	loader := NewLoader()
	loader.IsExternalRefsAllowed = true
	url, err := url.Parse("http://" + addr + "/test.openapi.json")
	require.NoError(t, err)

	doc, err := loader.LoadFromURI(url)
	require.NoError(t, err)

	require.Equal(t, &Types{"string"}, doc.Components.Schemas["TestSchema"].Value.Type)
}

func TestLoadWithReferenceInReference(t *testing.T) {
	loader := NewLoader()
	loader.IsExternalRefsAllowed = true
	doc, err := loader.LoadFromFile("testdata/refInRef/openapi.json")
	require.NoError(t, err)
	require.NotNil(t, doc)
	err = doc.Validate(loader.Context)
	require.NoError(t, err)
	require.Equal(t, &Types{"string"}, doc.Paths.Value("/api/test/ref/in/ref").Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["definition_reference"].Value.Type)
}

func TestLoadWithRecursiveReferenceInLocalReferenceInParentSubdir(t *testing.T) {
	loader := NewLoader()
	loader.IsExternalRefsAllowed = true
	doc, err := loader.LoadFromFile("testdata/refInLocalRefInParentsSubdir/spec/openapi.json")
	require.NoError(t, err)
	require.NotNil(t, doc)
	err = doc.Validate(loader.Context)
	require.NoError(t, err)
	require.Equal(t, &Types{"object"}, doc.Paths.Value("/api/test/ref/in/ref").Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["definition_reference"].Value.Type)
}

func TestLoadWithRecursiveReferenceInReferenceInLocalReference(t *testing.T) {
	loader := NewLoader()
	loader.IsExternalRefsAllowed = true
	doc, err := loader.LoadFromFile("testdata/refInLocalRef/openapi.json")
	require.NoError(t, err)
	require.NotNil(t, doc)
	err = doc.Validate(loader.Context)
	require.NoError(t, err)
	require.Equal(t, &Types{"integer"}, doc.Paths.Value("/api/test/ref/in/ref").Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["data"].Value.Properties["definition_reference"].Value.Properties["ref_prop_part"].Value.Properties["idPart"].Value.Type)
	require.Equal(t, "int64", doc.Paths.Value("/api/test/ref/in/ref").Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["data"].Value.Properties["definition_reference"].Value.Properties["ref_prop_part"].Value.Properties["idPart"].Value.Format)
}

func TestLoadWithReferenceInReferenceInProperty(t *testing.T) {
	loader := NewLoader()
	loader.IsExternalRefsAllowed = true
	doc, err := loader.LoadFromFile("testdata/refInRefInProperty/openapi.yaml")
	require.NoError(t, err)
	require.NotNil(t, doc)
	err = doc.Validate(loader.Context)
	require.NoError(t, err)
	require.Equal(t, "Problem details", doc.Paths.Value("/api/test/ref/in/ref/in/property").Post.Responses.Value("401").Value.Content["application/json"].Schema.Value.Properties["error"].Value.Title)
}

func TestLoadFileWithExternalSchemaRef(t *testing.T) {
	loader := NewLoader()
	loader.IsExternalRefsAllowed = true
	doc, err := loader.LoadFromFile("testdata/testref.openapi.json")
	require.NoError(t, err)
	require.NotNil(t, doc.Components.Schemas["AnotherTestSchema"].Value.Type)
}

func TestLoadFileWithExternalSchemaRefSingleComponent(t *testing.T) {
	loader := NewLoader()
	loader.IsExternalRefsAllowed = true
	doc, err := loader.LoadFromFile("testdata/testrefsinglecomponent.openapi.json")
	require.NoError(t, err)

	require.NotNil(t, doc.Components.Responses["SomeResponse"])
	desc := "this is a single response definition"
	require.Equal(t, &desc, doc.Components.Responses["SomeResponse"].Value.Description)
}

func TestLoadRequestResponseHeaderRef(t *testing.T) {
	spec := []byte(`
{
    "openapi": "3.0.0",
    "info": {
        "title": "",
        "version": "1"
    },
    "paths": {
      "/test": {
        "post": {
          "responses": {
            "default": {
              "description": "test",
              "headers": {
                "X-TEST-HEADER": {
                  "$ref": "#/components/headers/TestHeader"
                }
              }
            }
          }
        }
      }
    },
    "components": {
      "headers": {
        "TestHeader": {
          "description": "testheader"
        }
      }
    }
}`)

	loader := NewLoader()
	doc, err := loader.LoadFromData(spec)
	require.NoError(t, err)

	require.NotNil(t, doc.Paths.Value("/test").Post.Responses.Default().Value.Headers["X-TEST-HEADER"].Value.Description)
	require.Equal(t, "testheader", doc.Paths.Value("/test").Post.Responses.Default().Value.Headers["X-TEST-HEADER"].Value.Description)
}

func TestLoadFromDataWithExternalRequestResponseHeaderRemoteRef(t *testing.T) {
	spec := []byte(`
{
    "openapi": "3.0.0",
    "info": {
        "title": "",
        "version": "1"
    },
    "paths": {
        "/test": {
            "post": {
                "responses": {
                    "default": {
                        "description": "test",
                        "headers": {
                            "X-TEST-HEADER": {
                                "$ref": "http://` + addr + `/components.openapi.json#/components/headers/CustomTestHeader"
                            }
                        }
                    }
                }
            }
        }
    }
}`)

	fs := http.FileServer(http.Dir("testdata"))
	ts := createTestServer(t, fs)
	ts.Start()
	defer ts.Close()

	loader := NewLoader()
	loader.IsExternalRefsAllowed = true
	doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"})
	require.NoError(t, err)

	require.NotNil(t, doc.Paths.Value("/test").Post.Responses.Default().Value.Headers["X-TEST-HEADER"].Value.Description)
	require.Equal(t, "description", doc.Paths.Value("/test").Post.Responses.Default().Value.Headers["X-TEST-HEADER"].Value.Description)
}

func TestLoadYamlFile(t *testing.T) {
	loader := NewLoader()
	loader.IsExternalRefsAllowed = true
	doc, err := loader.LoadFromFile("testdata/test.openapi.yml")
	require.NoError(t, err)

	require.Equal(t, "OAI Specification in YAML", doc.Info.Title)
}

func TestLoadYamlFileWithExternalSchemaRef(t *testing.T) {
	loader := NewLoader()
	loader.IsExternalRefsAllowed = true
	doc, err := loader.LoadFromFile("testdata/testref.openapi.yml")
	require.NoError(t, err)

	require.NotNil(t, doc.Components.Schemas["AnotherTestSchema"].Value.Type)
}

func TestLoadYamlFileWithExternalPathRef(t *testing.T) {
	loader := NewLoader()
	loader.IsExternalRefsAllowed = true
	doc, err := loader.LoadFromFile("testdata/pathref.openapi.yml")
	require.NoError(t, err)

	require.NotNil(t, doc.Paths.Value("/test").Get.Responses.Value("200").Value.Content["application/json"].Schema.Value.Type)
	require.Equal(t, &Types{"string"}, doc.Paths.Value("/test").Get.Responses.Value("200").Value.Content["application/json"].Schema.Value.Type)
}

func TestResolveResponseLinkRef(t *testing.T) {
	source := []byte(`
openapi: 3.0.1
info:
  title: My API
  version: 1.0.0
components:
  links:
    Father:
        description: link to to the father
        operationId: getUserById
        parameters:
          "id": "$response.body#/fatherId"
paths:
  /users/{id}:
    get:
      operationId: getUserById,
      parameters:
        - name: id,
          in: path
          required: true
          schema:
            type: string
      responses:
        200:
          description: A test response
          content:
            application/json:
          links:
            father:
              $ref: '#/components/links/Father'
`)
	loader := NewLoader()
	doc, err := loader.LoadFromData(source)
	require.NoError(t, err)

	err = doc.Validate(loader.Context)
	require.NoError(t, err)

	response := doc.Paths.Value("/users/{id}").Get.Responses.Status(200).Value
	link := response.Links[`father`].Value
	require.NotNil(t, link)
	require.Equal(t, "getUserById", link.OperationID)
	require.Equal(t, "link to to the father", link.Description)
}

func TestLinksFromOAISpec(t *testing.T) {
	loader := NewLoader()
	doc, err := loader.LoadFromFile("testdata/link-example.yaml")
	require.NoError(t, err)
	err = doc.Validate(loader.Context)
	require.NoError(t, err)
	response := doc.Paths.Value("/2.0/repositories/{username}/{slug}").Get.Responses.Status(200).Value
	link := response.Links[`repositoryPullRequests`].Value
	require.Equal(t, map[string]any{
		"username": "$response.body#/owner/username",
		"slug":     "$response.body#/slug",
	}, link.Parameters)
}

func TestResolveNonComponentsRef(t *testing.T) {
	spec := []byte(`
openapi: 3.0.0
info:
  title: An API
  version: v1

components:
  schemas:
    NewItem:
      required: [name]
      properties:
        name: {type: string}
        tag: {type: string}
    ErrorModel:
      type: object
      required: [code, message]
      properties:
        code: {type: integer}
        message: {type: string}

paths:
  /items:
    put:
      description: ''
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewItem'
      responses:
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorModel'
    post:
      description: ''
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/paths/~1items/put/requestBody/content/application~1json/schema'
      responses:
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorModel'
`)

	loader := NewLoader()
	doc, err := loader.LoadFromData(spec)
	require.NoError(t, err)
	err = doc.Validate(loader.Context)
	require.NoError(t, err)
}

func TestServersVariables(t *testing.T) {
	const spec = `
openapi: 3.0.1
info:
  title: My API
  version: 1.0.0
paths: {}
servers:
- @@@
`
	for value, expected := range map[string]string{
		`{url: /}`:                            "",
		`{url: "http://{x}.{y}.example.com"}`: "invalid servers: server has undeclared variables",
		`{url: "http://{x}.y}.example.com"}`:  "invalid servers: server URL has mismatched { and }",
		`{url: "http://{x.example.com"}`:      "invalid servers: server URL has mismatched { and }",
		`{url: "http://{x}.example.com", variables: {x: {default: "www"}}}`:                "",
		`{url: "http://{x}.example.com", variables: {x: {default: "www", enum: ["www"]}}}`: "",
		`{url: "http://{x}.example.com", variables: {x: {enum: ["www"]}}}`:                 `invalid servers: field default is required in {"enum":["www"]}`,
		`{url: "http://www.example.com", variables: {x: {enum: ["www"]}}}`:                 "invalid servers: server has undeclared variables",
		`{url: "http://{y}.example.com", variables: {x: {enum: ["www"]}}}`:                 "invalid servers: server has undeclared variables",
	} {
		t.Run(value, func(t *testing.T) {
			loader := NewLoader()
			doc, err := loader.LoadFromData([]byte(strings.Replace(spec, "@@@", value, 1)))
			require.NoError(t, err)
			err = doc.Validate(loader.Context)
			if expected == "" {
				require.NoError(t, err)
			} else {
				require.EqualError(t, err, expected)
			}
		})
	}
}

func TestReadFromIoReader(t *testing.T) {
	buffer := bytes.NewReader([]byte(`openapi: 3.0.0
info:
  title: An API
  version: v1
components:
  schemas:
    NewItem:
      required: [name]
      properties:
        name: {type: string}
        tag: {type: string}
    ErrorModel:
      type: object
      required: [code, message]
      properties:
        code: {type: integer}
        message: {type: string}
paths:
  /items:
    put:
      description: ''
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewItem'
      responses:
        default: &defaultResponse # a YAML ref
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorModel'`))

	loader := NewLoader()
	doc, err := loader.LoadFromIoReader(buffer)
	require.NoError(t, err)

	err = doc.Validate(loader.Context)
	require.NoError(t, err)
}

func TestReadFromIoReader_Nil(t *testing.T) {
	loader := NewLoader()
	_, err := loader.LoadFromIoReader(nil)
	require.EqualError(t, err, "invalid reader: <nil>")
}

func TestDefaultJoin(t *testing.T) {
	tests := []struct {
		name     string
		base     string
		rel      string
		expected string
	}{
		{
			name:     "relative path",
			base:     "/home/user/openapi.yaml",
			rel:      "schemas/pet.yaml",
			expected: "/home/user/schemas/pet.yaml",
		},
		{
			name:     "dot-slash relative path",
			base:     "/home/user/openapi.yaml",
			rel:      "./schemas/pet.yaml",
			expected: "/home/user/schemas/pet.yaml",
		},
		{
			name:     "parent directory",
			base:     "/home/user/api/v1/openapi.yaml",
			rel:      "../common/types.yaml",
			expected: "/home/user/api/common/types.yaml",
		},
		{
			name:     "nil base returns relative",
			base:     "",
			rel:      "schemas/pet.yaml",
			expected: "schemas/pet.yaml",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			rel := &url.URL{Path: tt.rel}
			if tt.base == "" {
				result := defaultJoin(nil, rel)
				require.Equal(t, tt.expected, result.Path)
				return
			}
			base := &url.URL{Path: tt.base}
			result := defaultJoin(base, rel)
			require.Equal(t, tt.expected, result.Path)
		})
	}
}
