// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0

package analysis

import (
	"fmt"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"testing"

	"github.com/go-openapi/analysis/internal/antest"
	"github.com/go-openapi/spec"
	"github.com/go-openapi/testify/v2/assert"
	"github.com/go-openapi/testify/v2/require"
)

const (
	someOperation    = "someOperation"
	anotherOperation = "anotherOperation"
)

func TestAnalyzer_All(t *testing.T) {
	t.Parallel()

	formatParam := spec.QueryParam("format").Typed("string", "")

	limitParam := spec.QueryParam("limit").Typed("integer", "int32")
	limitParam.Extensions = spec.Extensions(map[string]any{})
	limitParam.Extensions.Add("go-name", "Limit")

	skipParam := spec.QueryParam("skip").Typed("integer", "int32")
	pi := spec.PathItem{}
	pi.Parameters = []spec.Parameter{*limitParam}

	op := &spec.Operation{}
	op.Consumes = []string{"application/x-yaml"}
	op.Produces = []string{"application/x-yaml"}
	op.Security = []map[string][]string{
		{"oauth2": {}},
		{"basic": nil},
	}

	op.ID = someOperation
	op.Parameters = []spec.Parameter{*skipParam}
	pi.Get = op

	pi2 := spec.PathItem{}
	pi2.Parameters = []spec.Parameter{*limitParam}
	op2 := &spec.Operation{}
	op2.ID = anotherOperation
	op2.Parameters = []spec.Parameter{*skipParam}
	pi2.Get = op2

	spec := makeFixturepec(pi, pi2, formatParam)
	analyzer := New(spec)

	assert.Len(t, analyzer.consumes, 2)
	assert.Len(t, analyzer.produces, 2)
	assert.Len(t, analyzer.operations, 1)
	assert.Equal(t, analyzer.operations["GET"]["/"], spec.Paths.Paths["/"].Get)

	expected := []string{"application/x-yaml"}
	sort.Strings(expected)
	consumes := analyzer.ConsumesFor(spec.Paths.Paths["/"].Get)
	sort.Strings(consumes)
	assert.Equal(t, expected, consumes)

	produces := analyzer.ProducesFor(spec.Paths.Paths["/"].Get)
	sort.Strings(produces)
	assert.Equal(t, expected, produces)

	expected = []string{"application/json"}
	sort.Strings(expected)
	consumes = analyzer.ConsumesFor(spec.Paths.Paths["/items"].Get)
	sort.Strings(consumes)
	assert.Equal(t, expected, consumes)

	produces = analyzer.ProducesFor(spec.Paths.Paths["/items"].Get)
	sort.Strings(produces)
	assert.Equal(t, expected, produces)

	expectedSchemes := [][]SecurityRequirement{
		{
			{Name: "oauth2", Scopes: []string{}},
			{Name: "basic", Scopes: nil},
		},
	}
	schemes := analyzer.SecurityRequirementsFor(spec.Paths.Paths["/"].Get)
	assert.Equal(t, schemeNames(expectedSchemes), schemeNames(schemes))

	securityDefinitions := analyzer.SecurityDefinitionsFor(spec.Paths.Paths["/"].Get)
	assert.Equal(t, *spec.SecurityDefinitions["basic"], securityDefinitions["basic"])
	assert.Equal(t, *spec.SecurityDefinitions["oauth2"], securityDefinitions["oauth2"])

	parameters := analyzer.ParamsFor("GET", "/")
	assert.Len(t, parameters, 2)

	operations := analyzer.OperationIDs()
	assert.Len(t, operations, 2)

	producers := analyzer.RequiredProduces()
	assert.Len(t, producers, 2)
	consumers := analyzer.RequiredConsumes()
	assert.Len(t, consumers, 2)
	authSchemes := analyzer.RequiredSecuritySchemes()
	assert.Len(t, authSchemes, 3)

	ops := analyzer.Operations()
	assert.Len(t, ops, 1)
	assert.Len(t, ops["GET"], 2)

	op, ok := analyzer.OperationFor("get", "/")
	assert.TrueT(t, ok)
	assert.NotNil(t, op)

	op, ok = analyzer.OperationFor("delete", "/")
	assert.FalseT(t, ok)
	assert.Nil(t, op)

	// check for duplicates in sec. requirements for operation
	pi.Get.Security = []map[string][]string{
		{"oauth2": {}},
		{"basic": nil},
		{"basic": nil},
	}

	spec = makeFixturepec(pi, pi2, formatParam)
	analyzer = New(spec)
	securityDefinitions = analyzer.SecurityDefinitionsFor(spec.Paths.Paths["/"].Get)
	assert.Len(t, securityDefinitions, 2)
	assert.Equal(t, *spec.SecurityDefinitions["basic"], securityDefinitions["basic"])
	assert.Equal(t, *spec.SecurityDefinitions["oauth2"], securityDefinitions["oauth2"])

	// check for empty (optional) in sec. requirements for operation
	pi.Get.Security = []map[string][]string{
		{"oauth2": {}},
		{"": nil},
		{"basic": nil},
	}

	spec = makeFixturepec(pi, pi2, formatParam)
	analyzer = New(spec)
	securityDefinitions = analyzer.SecurityDefinitionsFor(spec.Paths.Paths["/"].Get)
	assert.Len(t, securityDefinitions, 2)
	assert.Equal(t, *spec.SecurityDefinitions["basic"], securityDefinitions["basic"])
	assert.Equal(t, *spec.SecurityDefinitions["oauth2"], securityDefinitions["oauth2"])
}

func TestAnalyzer_DefinitionAnalysis(t *testing.T) {
	t.Parallel()

	doc := antest.LoadOrFail(t, filepath.Join("fixtures", "definitions.yml"))

	analyzer := New(doc)
	definitions := analyzer.allSchemas
	require.NotNil(t, definitions)

	for _, toPin := range []string{
		"#/parameters/someParam/schema",
		"#/paths/~1some~1where~1{id}/parameters/1/schema",
		"#/paths/~1some~1where~1{id}/get/parameters/1/schema",

		// responses
		"#/responses/someResponse/schema",
		"#/paths/~1some~1where~1{id}/get/responses/default/schema",
		"#/paths/~1some~1where~1{id}/get/responses/200/schema",

		// definitions
		"#/definitions/tag",
		"#/definitions/tag/properties/id",
		"#/definitions/tag/properties/value",
		"#/definitions/tag/definitions/category",
		"#/definitions/tag/definitions/category/properties/id",
		"#/definitions/tag/definitions/category/properties/value",
		"#/definitions/withAdditionalProps",
		"#/definitions/withAdditionalProps/additionalProperties",
		"#/definitions/withAdditionalItems",
		"#/definitions/withAdditionalItems/items/0",
		"#/definitions/withAdditionalItems/items/1",
		"#/definitions/withAdditionalItems/additionalItems",
		"#/definitions/withNot",
		"#/definitions/withNot/not",
		"#/definitions/withAnyOf",
		"#/definitions/withAnyOf/anyOf/0",
		"#/definitions/withAnyOf/anyOf/1",
		"#/definitions/withAllOf",
		"#/definitions/withAllOf/allOf/0",
		"#/definitions/withAllOf/allOf/1",
		"#/definitions/withOneOf/oneOf/0",
		"#/definitions/withOneOf/oneOf/1",
	} {
		key := toPin
		t.Run(fmt.Sprintf("ref %q exists", key), func(t *testing.T) {
			t.Parallel()

			assertSchemaRefExists(t, definitions, key)
		})
	}

	allOfs := analyzer.allOfs
	assert.Len(t, allOfs, 1)
	assert.MapContainsT(t, allOfs, "#/definitions/withAllOf")
}

func TestAnalyzer_ReferenceAnalysis(t *testing.T) {
	t.Parallel()

	doc := antest.LoadOrFail(t, filepath.Join("fixtures", "references.yml"))
	an := New(doc)

	definitions := an.references

	require.NotNil(t, definitions)
	require.NotNil(t, definitions.parameters)
	require.NotNil(t, definitions.responses)
	require.NotNil(t, definitions.pathItems)
	require.NotNil(t, definitions.schemas)
	require.NotNil(t, definitions.parameterItems)
	require.NotNil(t, definitions.headerItems)
	require.NotNil(t, definitions.allRefs)

	for _, fixture := range []struct {
		Input        map[string]spec.Ref
		ExpectedKeys []string
	}{
		{
			Input: definitions.parameters,
			ExpectedKeys: []string{
				"#/paths/~1some~1where~1{id}/parameters/0",
				"#/paths/~1some~1where~1{id}/get/parameters/0",
			},
		},
		{
			Input: definitions.pathItems,
			ExpectedKeys: []string{
				"#/paths/~1other~1place",
			},
		},
		{
			Input: definitions.responses,
			ExpectedKeys: []string{
				"#/paths/~1some~1where~1{id}/get/responses/404",
			},
		},
		{
			Input: definitions.schemas,
			ExpectedKeys: []string{
				"#/responses/notFound/schema",
				"#/paths/~1some~1where~1{id}/get/responses/200/schema",
				"#/definitions/tag/properties/audit",
			},
		},
		{
			// Supported non-swagger 2.0 constructs ($ref in simple schema items)
			Input: definitions.allRefs,
			ExpectedKeys: []string{
				"#/paths/~1some~1where~1{id}/get/parameters/1/items",
				"#/paths/~1some~1where~1{id}/get/parameters/2/items",
				"#/paths/~1some~1where~1{id}/get/responses/default/headers/x-array-header/items",
			},
		},
		{
			Input: definitions.parameterItems,
			ExpectedKeys: []string{
				"#/paths/~1some~1where~1{id}/get/parameters/1/items",
				"#/paths/~1some~1where~1{id}/get/parameters/2/items",
			},
		},
		{
			Input: definitions.headerItems,
			ExpectedKeys: []string{
				"#/paths/~1some~1where~1{id}/get/responses/default/headers/x-array-header/items",
			},
		},
	} {
		input := fixture.Input
		for _, toPin := range fixture.ExpectedKeys {
			key := toPin
			t.Run(fmt.Sprintf("ref %q exists", key), func(t *testing.T) {
				t.Parallel()
				assertRefExists(t, input, key)
			})
		}
	}

	assert.Lenf(t, an.AllItemsReferences(), 3, "Expected 3 items references in this spec")
}

type expectedPattern struct {
	Key     string
	Pattern string
}

func TestAnalyzer_PatternAnalysis(t *testing.T) {
	t.Parallel()

	doc := antest.LoadOrFail(t, filepath.Join("fixtures", "patterns.yml"))
	an := New(doc)
	pt := an.patterns

	require.NotNil(t, pt)
	require.NotNil(t, pt.parameters)
	require.NotNil(t, pt.headers)
	require.NotNil(t, pt.schemas)
	require.NotNil(t, pt.items)

	for _, toPin := range []struct {
		Input        map[string]string
		ExpectedKeys []expectedPattern
	}{
		{
			Input: pt.parameters,
			ExpectedKeys: []expectedPattern{
				{Key: "#/parameters/idParam", Pattern: "a[A-Za-Z0-9]+"},
				{Key: "#/paths/~1some~1where~1{id}/parameters/1", Pattern: "b[A-Za-z0-9]+"},
				{Key: "#/paths/~1some~1where~1{id}/get/parameters/0", Pattern: "[abc][0-9]+"},
			},
		},
		{
			Input: pt.headers,
			ExpectedKeys: []expectedPattern{
				{Key: "#/responses/notFound/headers/ContentLength", Pattern: "[0-9]+"},
				{Key: "#/paths/~1some~1where~1{id}/get/responses/200/headers/X-Request-Id", Pattern: "d[A-Za-z0-9]+"},
			},
		},
		{
			Input: pt.schemas,
			ExpectedKeys: []expectedPattern{
				{Key: "#/paths/~1other~1place/post/parameters/0/schema/properties/value", Pattern: "e[A-Za-z0-9]+"},
				{Key: "#/paths/~1other~1place/post/responses/200/schema/properties/data", Pattern: "[0-9]+[abd]"},
				{Key: "#/definitions/named", Pattern: "f[A-Za-z0-9]+"},
				{Key: "#/definitions/tag/properties/value", Pattern: "g[A-Za-z0-9]+"},
			},
		},
		{
			Input: pt.items,
			ExpectedKeys: []expectedPattern{
				{Key: "#/paths/~1some~1where~1{id}/get/parameters/1/items", Pattern: "c[A-Za-z0-9]+"},
				{Key: "#/paths/~1other~1place/post/responses/default/headers/Via/items", Pattern: "[A-Za-z]+"},
			},
		},
	} {
		fixture := toPin
		for _, toPinExpected := range fixture.ExpectedKeys {
			expected := toPinExpected
			t.Run(fmt.Sprintf("pattern at %q exists", expected.Key), func(t *testing.T) {
				t.Parallel()
				assertPattern(t, fixture.Input, expected.Key, expected.Pattern)
			})
		}
	}

	// patternProperties (beyond Swagger 2.0)
	_, ok := an.spec.Definitions["withPatternProperties"]
	assert.TrueT(t, ok)

	_, ok = an.allSchemas["#/definitions/withPatternProperties/patternProperties/^prop[0-9]+$"]
	assert.TrueT(t, ok)
}

func TestAnalyzer_ParamsAsMap(t *testing.T) {
	t.Parallel()

	s := prepareTestParamsValid()
	require.NotNil(t, s)

	m := make(map[string]spec.Parameter)
	pi, ok := s.spec.Paths.Paths["/items"]
	require.TrueT(t, ok)

	s.paramsAsMap(pi.Parameters, m, nil)
	assert.Len(t, m, 1)

	p, ok := m["query#Limit"]
	require.TrueT(t, ok)

	assert.EqualT(t, "limit", p.Name)

	// An invalid spec, but passes this step (errors are figured out at a higher level)
	s = prepareTestParamsInvalid(t, "fixture-1289-param.yaml")
	require.NotNil(t, s)

	m = make(map[string]spec.Parameter)
	pi, ok = s.spec.Paths.Paths["/fixture"]
	require.TrueT(t, ok)

	pi.Parameters = pi.Get.Parameters
	s.paramsAsMap(pi.Parameters, m, nil)
	assert.Len(t, m, 1)

	p, ok = m["body#DespicableMe"]
	require.TrueT(t, ok)

	assert.EqualT(t, "despicableMe", p.Name)
}

func TestAnalyzer_ParamsAsMapWithCallback(t *testing.T) {
	t.Parallel()

	s := prepareTestParamsInvalid(t, "fixture-342.yaml")
	require.NotNil(t, s)

	// No bail out callback
	m := make(map[string]spec.Parameter)
	e := []string{}
	pi, ok := s.spec.Paths.Paths["/fixture"]
	require.TrueT(t, ok)

	pi.Parameters = pi.Get.Parameters
	s.paramsAsMap(pi.Parameters, m, func(_ spec.Parameter, err error) bool {
		e = append(e, err.Error())

		return true // Continue
	})

	assert.StringContainsT(t, strings.Join(e, ","), `resolved reference is not a parameter: "#/definitions/sample_info/properties/sid"`)
	assert.StringContainsT(t, strings.Join(e, ","), `invalid reference: "#/definitions/sample_info/properties/sids"`)

	// bail out callback
	m = make(map[string]spec.Parameter)
	e = []string{}
	pi, ok = s.spec.Paths.Paths["/fixture"]
	require.TrueT(t, ok)

	pi.Parameters = pi.Get.Parameters
	s.paramsAsMap(pi.Parameters, m, func(_ spec.Parameter, err error) bool {
		e = append(e, err.Error())

		return false // Bail
	})

	// We got one then bail
	assert.Len(t, e, 1)

	// Bail after ref failure: exercising another path
	s = prepareTestParamsInvalid(t, "fixture-342-2.yaml")
	require.NotNil(t, s)

	// bail callback
	m = make(map[string]spec.Parameter)
	e = []string{}
	pi, ok = s.spec.Paths.Paths["/fixture"]
	require.TrueT(t, ok)

	pi.Parameters = pi.Get.Parameters
	s.paramsAsMap(pi.Parameters, m, func(_ spec.Parameter, err error) bool {
		e = append(e, err.Error())

		return false // Bail
	})
	// We got one then bail
	assert.Len(t, e, 1)

	// Bail after ref failure: exercising another path
	s = prepareTestParamsInvalid(t, "fixture-342-3.yaml")
	require.NotNil(t, s)

	// bail callback
	m = make(map[string]spec.Parameter)
	e = []string{}
	pi, ok = s.spec.Paths.Paths["/fixture"]
	require.TrueT(t, ok)

	pi.Parameters = pi.Get.Parameters
	s.paramsAsMap(pi.Parameters, m, func(_ spec.Parameter, err error) bool {
		e = append(e, err.Error())

		return false // Bail
	})
	// We got one then bail
	assert.Len(t, e, 1)
}

func TestAnalyzer_ParamsAsMapPanic(t *testing.T) {
	t.Parallel()

	for _, toPin := range []string{
		"fixture-342.yaml",
		"fixture-342-2.yaml",
		"fixture-342-3.yaml",
	} {
		fixture := toPin
		t.Run("panic_"+fixture, func(t *testing.T) {
			t.Parallel()

			s := prepareTestParamsInvalid(t, fixture)
			require.NotNil(t, s)

			panickerParamsAsMap := func() {
				m := make(map[string]spec.Parameter)
				if pi, ok := s.spec.Paths.Paths["/fixture"]; ok {
					pi.Parameters = pi.Get.Parameters
					s.paramsAsMap(pi.Parameters, m, nil)
				}
			}
			assert.Panics(t, panickerParamsAsMap)
		})
	}
}

func TestAnalyzer_SafeParamsFor(t *testing.T) {
	t.Parallel()

	s := prepareTestParamsInvalid(t, "fixture-342.yaml")
	require.NotNil(t, s)

	e := []string{}
	pi, ok := s.spec.Paths.Paths["/fixture"]
	require.TrueT(t, ok)

	pi.Parameters = pi.Get.Parameters

	errFunc := func(_ spec.Parameter, err error) bool {
		e = append(e, err.Error())

		return true // Continue
	}

	for range s.SafeParamsFor("Get", "/fixture", errFunc) {
		require.Fail(t, "There should be no safe parameter in this testcase")
	}

	assert.StringContainsT(t, strings.Join(e, ","), `resolved reference is not a parameter: "#/definitions/sample_info/properties/sid"`)
	assert.StringContainsT(t, strings.Join(e, ","), `invalid reference: "#/definitions/sample_info/properties/sids"`)
}

func TestAnalyzer_ParamsFor(t *testing.T) {
	t.Parallel()

	// Valid example
	s := prepareTestParamsValid()
	require.NotNil(t, s)

	params := s.ParamsFor("Get", "/items")
	assert.NotEmpty(t, params)

	panickerParamsFor := func() {
		s := prepareTestParamsInvalid(t, "fixture-342.yaml")
		pi, ok := s.spec.Paths.Paths["/fixture"]
		if ok {
			pi.Parameters = pi.Get.Parameters
			s.ParamsFor("Get", "/fixture")
		}
	}

	// Invalid example
	assert.Panics(t, panickerParamsFor)
}

func TestAnalyzer_SafeParametersFor(t *testing.T) {
	t.Parallel()

	s := prepareTestParamsInvalid(t, "fixture-342.yaml")
	require.NotNil(t, s)

	e := []string{}
	pi, ok := s.spec.Paths.Paths["/fixture"]
	require.TrueT(t, ok)

	errFunc := func(_ spec.Parameter, err error) bool {
		e = append(e, err.Error())

		return true // Continue
	}

	pi.Parameters = pi.Get.Parameters
	for range s.SafeParametersFor("fixtureOp", errFunc) {
		require.Fail(t, "There should be no safe parameter in this testcase")
	}

	assert.StringContainsT(t, strings.Join(e, ","), `resolved reference is not a parameter: "#/definitions/sample_info/properties/sid"`)
	assert.StringContainsT(t, strings.Join(e, ","), `invalid reference: "#/definitions/sample_info/properties/sids"`)
}

func TestAnalyzer_ParametersFor(t *testing.T) {
	t.Parallel()

	// Valid example
	s := prepareTestParamsValid()
	params := s.ParamsFor("Get", "/items")
	assert.NotEmpty(t, params)

	panickerParametersFor := func() {
		s := prepareTestParamsInvalid(t, "fixture-342.yaml")
		if s == nil {
			return
		}

		pi, ok := s.spec.Paths.Paths["/fixture"]
		if ok {
			pi.Parameters = pi.Get.Parameters
			// func (s *Spec) ParametersFor(operationID string) []spec.Parameter {
			s.ParametersFor("fixtureOp")
		}
	}

	// Invalid example
	assert.Panics(t, panickerParametersFor)
}

func TestAnalyzer_SecurityDefinitionsFor(t *testing.T) {
	t.Parallel()

	spec := prepareTestParamsAuth()
	pi1 := spec.spec.Paths.Paths["/"].Get
	pi2 := spec.spec.Paths.Paths["/items"].Get

	defs1 := spec.SecurityDefinitionsFor(pi1)
	require.MapContainsT(t, defs1, "oauth2")
	require.MapContainsT(t, defs1, "basic")
	require.MapNotContainsT(t, defs1, "apiKey")

	defs2 := spec.SecurityDefinitionsFor(pi2)
	require.MapContainsT(t, defs2, "oauth2")
	require.MapContainsT(t, defs2, "basic")
	require.MapContainsT(t, defs2, "apiKey")
}

func TestAnalyzer_SecurityRequirements(t *testing.T) {
	t.Parallel()

	spec := prepareTestParamsAuth()
	pi1 := spec.spec.Paths.Paths["/"].Get
	pi2 := spec.spec.Paths.Paths["/items"].Get
	scopes := []string{"the-scope"}

	reqs1 := spec.SecurityRequirementsFor(pi1)
	require.Len(t, reqs1, 2)
	require.Len(t, reqs1[0], 1)
	require.EqualT(t, "oauth2", reqs1[0][0].Name)
	require.Equal(t, reqs1[0][0].Scopes, scopes)
	require.Len(t, reqs1[1], 1)
	require.EqualT(t, "basic", reqs1[1][0].Name)
	require.Empty(t, reqs1[1][0].Scopes)

	reqs2 := spec.SecurityRequirementsFor(pi2)
	require.Len(t, reqs2, 3)
	require.Len(t, reqs2[0], 1)
	require.EqualT(t, "oauth2", reqs2[0][0].Name)
	require.Equal(t, scopes, reqs2[0][0].Scopes)
	require.Len(t, reqs2[1], 1)
	require.Empty(t, reqs2[1][0].Name)
	require.Empty(t, reqs2[1][0].Scopes)
	require.Len(t, reqs2[2], 2)
	require.Contains(t, reqs2[2], SecurityRequirement{Name: "basic", Scopes: []string{}})
	require.Empty(t, reqs2[2][0].Scopes)
	require.Contains(t, reqs2[2], SecurityRequirement{Name: "apiKey", Scopes: []string{}})
	require.Empty(t, reqs2[2][1].Scopes)
}

func TestAnalyzer_SecurityRequirementsDefinitions(t *testing.T) {
	t.Parallel()

	spec := prepareTestParamsAuth()
	pi1 := spec.spec.Paths.Paths["/"].Get
	pi2 := spec.spec.Paths.Paths["/items"].Get

	reqs1 := spec.SecurityRequirementsFor(pi1)
	defs11 := spec.SecurityDefinitionsForRequirements(reqs1[0])
	require.MapContainsT(t, defs11, "oauth2")
	defs12 := spec.SecurityDefinitionsForRequirements(reqs1[1])
	require.MapContainsT(t, defs12, "basic")
	require.MapNotContainsT(t, defs12, "apiKey")

	reqs2 := spec.SecurityRequirementsFor(pi2)
	defs21 := spec.SecurityDefinitionsForRequirements(reqs2[0])
	require.Len(t, defs21, 1)
	require.MapContainsT(t, defs21, "oauth2")
	require.MapNotContainsT(t, defs21, "basic")
	require.MapNotContainsT(t, defs21, "apiKey")
	defs22 := spec.SecurityDefinitionsForRequirements(reqs2[1])
	require.NotNil(t, defs22)
	require.Empty(t, defs22)
	defs23 := spec.SecurityDefinitionsForRequirements(reqs2[2])
	require.Len(t, defs23, 2)
	require.MapNotContainsT(t, defs23, "oauth2")
	require.MapContainsT(t, defs23, "basic")
	require.MapContainsT(t, defs23, "apiKey")
}

func TestAnalyzer_MoreParamAnalysis(t *testing.T) {
	t.Parallel()

	bp := filepath.Join("fixtures", "parameters", "fixture-parameters.yaml")
	sp := antest.LoadOrFail(t, bp)

	an := New(sp)

	res := an.AllPatterns()
	assert.Lenf(t, res, 6, "Expected 6 patterns in this spec")

	res = an.SchemaPatterns()
	assert.Lenf(t, res, 1, "Expected 1 schema pattern in this spec")

	res = an.HeaderPatterns()
	assert.Lenf(t, res, 2, "Expected 2 header pattern in this spec")

	res = an.ItemsPatterns()
	assert.Lenf(t, res, 2, "Expected 2 items pattern in this spec")

	res = an.ParameterPatterns()
	assert.Lenf(t, res, 1, "Expected 1 simple param pattern in this spec")

	refs := an.AllRefs()
	assert.Lenf(t, refs, 10, "Expected 10 reference usage in this spec")

	references := an.AllReferences()
	assert.Lenf(t, references, 14, "Expected 14 reference usage in this spec")

	references = an.AllItemsReferences()
	assert.Emptyf(t, references, "Expected 0 items reference in this spec")

	references = an.AllPathItemReferences()
	assert.Lenf(t, references, 1, "Expected 1 pathItem reference in this spec")

	references = an.AllResponseReferences()
	assert.Lenf(t, references, 3, "Expected 3 response references in this spec")

	references = an.AllParameterReferences()
	assert.Lenf(t, references, 6, "Expected 6 parameter references in this spec")

	schemaRefs := an.AllDefinitions()
	assert.Lenf(t, schemaRefs, 14, "Expected 14 schema definitions in this spec")
	schemaRefs = an.SchemasWithAllOf()
	assert.Lenf(t, schemaRefs, 1, "Expected 1 schema with AllOf definition in this spec")

	method, path, op, found := an.OperationForName("postSomeWhere")
	assert.EqualT(t, "POST", method)
	assert.EqualT(t, "/some/where", path)
	require.NotNil(t, op)
	require.TrueT(t, found)

	sec := an.SecurityRequirementsFor(op)
	assert.Nil(t, sec)
	secScheme := an.SecurityDefinitionsFor(op)
	assert.Nil(t, secScheme)

	bag := an.ParametersFor("postSomeWhere")
	assert.Lenf(t, bag, 6, "Expected 6 parameters for this operation")

	method, path, op, found = an.OperationForName("notFound")
	assert.Empty(t, method)
	assert.Empty(t, path)
	assert.Nil(t, op)
	assert.FalseT(t, found)

	// does not take ops under pathItem $ref
	ops := an.OperationMethodPaths()
	assert.Lenf(t, ops, 3, "Expected 3 ops")
	ops = an.OperationIDs()
	assert.Lenf(t, ops, 3, "Expected 3 ops")
	assert.SliceContainsT(t, ops, "postSomeWhere")
	assert.SliceContainsT(t, ops, "GET /some/where/else")
	assert.SliceContainsT(t, ops, "GET /some/where")
}

func TestAnalyzer_EdgeCases(t *testing.T) {
	t.Parallel()

	// check return values are consistent in some nil/empty edge cases
	sp := Spec{}
	res1 := sp.AllPaths()
	assert.Nil(t, res1)

	res2 := sp.OperationIDs()
	assert.Nil(t, res2)

	res3 := sp.OperationMethodPaths()
	assert.Nil(t, res3)

	res4 := sp.structMapKeys(nil)
	assert.Nil(t, res4)

	res5 := sp.structMapKeys(make(map[string]struct{}, 10))
	assert.Nil(t, res5)

	// check AllRefs() skips empty $refs
	sp.references.allRefs = make(map[string]spec.Ref, 3)
	for i := range 3 {
		sp.references.allRefs["ref"+strconv.Itoa(i)] = spec.Ref{}
	}
	assert.Len(t, sp.references.allRefs, 3)
	res6 := sp.AllRefs()
	assert.Empty(t, res6)

	// check AllRefs() skips duplicate $refs
	sp.references.allRefs["refToOne"] = spec.MustCreateRef("#/ref1")
	sp.references.allRefs["refToOneAgain"] = spec.MustCreateRef("#/ref1")
	res7 := sp.AllRefs()
	assert.NotNil(t, res7)
	assert.Len(t, res7, 1)
}

func TestAnalyzer_EnumAnalysis(t *testing.T) {
	t.Parallel()

	doc := antest.LoadOrFail(t, filepath.Join("fixtures", "enums.yml"))

	an := New(doc)
	en := an.enums

	// parameters
	assertEnum(t, en.parameters, "#/parameters/idParam", []any{"aA", "b9", "c3"})
	assertEnum(t, en.parameters, "#/paths/~1some~1where~1{id}/parameters/1", []any{"bA", "ba", "b9"})
	assertEnum(t, en.parameters, "#/paths/~1some~1where~1{id}/get/parameters/0", []any{"a0", "b1", "c2"})

	// responses
	assertEnum(t, en.headers, "#/responses/notFound/headers/ContentLength", []any{"1234", "123"})
	assertEnum(t, en.headers,
		"#/paths/~1some~1where~1{id}/get/responses/200/headers/X-Request-Id", []any{"dA", "d9"})

	// definitions
	assertEnum(t, en.schemas,
		"#/paths/~1other~1place/post/parameters/0/schema/properties/value", []any{"eA", "e9"})
	assertEnum(t, en.schemas, "#/paths/~1other~1place/post/responses/200/schema/properties/data",
		[]any{"123a", "123b", "123d"})
	assertEnum(t, en.schemas, "#/definitions/named", []any{"fA", "f9"})
	assertEnum(t, en.schemas, "#/definitions/tag/properties/value", []any{"gA", "ga", "g9"})
	assertEnum(t, en.schemas, "#/definitions/record",
		[]any{`{"createdAt": "2018-08-31"}`, `{"createdAt": "2018-09-30"}`})

	// array enum
	assertEnum(t, en.parameters, "#/paths/~1some~1where~1{id}/get/parameters/1",
		[]any{[]any{"cA", "cz", "c9"}, []any{"cA", "cz"}, []any{"cz", "c9"}})

	// items
	assertEnum(t, en.items, "#/paths/~1some~1where~1{id}/get/parameters/1/items", []any{"cA", "cz", "c9"})
	assertEnum(t, en.items, "#/paths/~1other~1place/post/responses/default/headers/Via/items",
		[]any{"AA", "Ab"})

	res := an.AllEnums()
	assert.Lenf(t, res, 14, "Expected 14 enums in this spec, but got %d", len(res))

	res = an.ParameterEnums()
	assert.Lenf(t, res, 4, "Expected 4 enums in this spec, but got %d", len(res))

	res = an.SchemaEnums()
	assert.Lenf(t, res, 6, "Expected 6 schema enums in this spec, but got %d", len(res))

	res = an.HeaderEnums()
	assert.Lenf(t, res, 2, "Expected 2 header enums in this spec, but got %d", len(res))

	res = an.ItemsEnums()
	assert.Lenf(t, res, 2, "Expected 2 items enums in this spec, but got %d", len(res))
}

/* helpers for the Analyzer test suite */

func schemeNames(schemes [][]SecurityRequirement) []string {
	var names []string
	for _, scheme := range schemes {
		for _, v := range scheme {
			names = append(names, v.Name)
		}
	}
	sort.Strings(names)

	return names
}

func makeFixturepec(pi, pi2 spec.PathItem, formatParam *spec.Parameter) *spec.Swagger {
	return &spec.Swagger{
		SwaggerProps: spec.SwaggerProps{
			Consumes: []string{"application/json"},
			Produces: []string{"application/json"},
			Security: []map[string][]string{
				{"apikey": nil},
			},
			SecurityDefinitions: map[string]*spec.SecurityScheme{
				"basic":  spec.BasicAuth(),
				"apiKey": spec.APIKeyAuth("api_key", "query"),
				"oauth2": spec.OAuth2AccessToken("http://authorize.com", "http://token.com"),
			},
			Parameters: map[string]spec.Parameter{"format": *formatParam},
			Paths: &spec.Paths{
				Paths: map[string]spec.PathItem{
					"/":      pi,
					"/items": pi2,
				},
			},
		},
	}
}

func assertEnum(t testing.TB, data map[string][]any, key string, enum []any) {
	require.MapContainsT(t, data, key)
	assert.Equal(t, enum, data[key])
}

func assertRefExists(t testing.TB, data map[string]spec.Ref, key string) bool {
	_, ok := data[key]

	return assert.TrueTf(t, ok, "expected %q to exist in the ref bag", key)
}

func assertSchemaRefExists(t testing.TB, data map[string]SchemaRef, key string) bool {
	_, ok := data[key]

	return assert.TrueTf(t, ok, "expected %q to exist in schema ref bag", key)
}

func assertPattern(t testing.TB, data map[string]string, key, pattern string) bool {
	if assert.MapContainsT(t, data, key) {
		return assert.EqualT(t, pattern, data[key])
	}

	return false
}

func prepareTestParamsAuth() *Spec {
	formatParam := spec.QueryParam("format").Typed("string", "")

	limitParam := spec.QueryParam("limit").Typed("integer", "int32")
	limitParam.Extensions = spec.Extensions(map[string]any{})
	limitParam.Extensions.Add("go-name", "Limit")

	skipParam := spec.QueryParam("skip").Typed("integer", "int32")
	pi := spec.PathItem{}
	pi.Parameters = []spec.Parameter{*limitParam}

	op := &spec.Operation{}
	op.Consumes = []string{"application/x-yaml"}
	op.Produces = []string{"application/x-yaml"}
	op.Security = []map[string][]string{
		{"oauth2": {"the-scope"}},
		{"basic": nil},
	}
	op.ID = someOperation
	op.Parameters = []spec.Parameter{*skipParam}
	pi.Get = op

	pi2 := spec.PathItem{}
	pi2.Parameters = []spec.Parameter{*limitParam}
	op2 := &spec.Operation{}
	op2.ID = anotherOperation
	op2.Security = []map[string][]string{
		{"oauth2": {"the-scope"}},
		{},
		{
			"basic":  {},
			"apiKey": {},
		},
	}
	op2.Parameters = []spec.Parameter{*skipParam}
	pi2.Get = op2

	oauth := spec.OAuth2AccessToken("http://authorize.com", "http://token.com")
	oauth.AddScope("the-scope", "the scope gives access to ...")
	spec := &spec.Swagger{
		SwaggerProps: spec.SwaggerProps{
			Consumes: []string{"application/json"},
			Produces: []string{"application/json"},
			Security: []map[string][]string{
				{"apikey": nil},
			},
			SecurityDefinitions: map[string]*spec.SecurityScheme{
				"basic":  spec.BasicAuth(),
				"apiKey": spec.APIKeyAuth("api_key", "query"),
				"oauth2": oauth,
			},
			Parameters: map[string]spec.Parameter{"format": *formatParam},
			Paths: &spec.Paths{
				Paths: map[string]spec.PathItem{
					"/":      pi,
					"/items": pi2,
				},
			},
		},
	}
	analyzer := New(spec)

	return analyzer
}

func prepareTestParamsValid() *Spec {
	formatParam := spec.QueryParam("format").Typed("string", "")

	limitParam := spec.QueryParam("limit").Typed("integer", "int32")
	limitParam.Extensions = spec.Extensions(map[string]any{})
	limitParam.Extensions.Add("go-name", "Limit")

	skipParam := spec.QueryParam("skip").Typed("integer", "int32")
	pi := spec.PathItem{}
	pi.Parameters = []spec.Parameter{*limitParam}

	op := &spec.Operation{}
	op.Consumes = []string{"application/x-yaml"}
	op.Produces = []string{"application/x-yaml"}
	op.Security = []map[string][]string{
		{"oauth2": {}},
		{"basic": nil},
	}
	op.ID = someOperation
	op.Parameters = []spec.Parameter{*skipParam}
	pi.Get = op

	pi2 := spec.PathItem{}
	pi2.Parameters = []spec.Parameter{*limitParam}
	op2 := &spec.Operation{}
	op2.ID = anotherOperation
	op2.Parameters = []spec.Parameter{*skipParam}
	pi2.Get = op2

	spec := makeFixturepec(pi, pi2, formatParam)
	analyzer := New(spec)

	return analyzer
}

func prepareTestParamsInvalid(t testing.TB, fixture string) *Spec {
	bp := filepath.Join("fixtures", fixture)
	spec := antest.LoadOrFail(t, bp)

	analyzer := New(spec)

	return analyzer
}
