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

package analysis

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"os"
	"path"
	"path/filepath"
	"regexp"
	"strings"
	"testing"

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

var (
	rex    = regexp.MustCompile(`"\$ref":\s*"(.+)"`)
	oairex = regexp.MustCompile(`oiagen`)
)

type refFixture struct {
	Key      string
	Ref      spec.Ref
	Location string
	Expected any
}

func makeRefFixtures() []refFixture {
	return []refFixture{
		{Key: "#/parameters/someParam/schema", Ref: spec.MustCreateRef("#/definitions/record")},
		{Key: "#/paths/~1some~1where~1{id}/parameters/1/schema", Ref: spec.MustCreateRef("#/definitions/record")},
		{Key: "#/paths/~1some~1where~1{id}/get/parameters/2/schema", Ref: spec.MustCreateRef("#/definitions/record")},
		{Key: "#/responses/someResponse/schema", Ref: spec.MustCreateRef("#/definitions/record")},
		{Key: "#/paths/~1some~1where~1{id}/get/responses/default/schema", Ref: spec.MustCreateRef("#/definitions/record")},
		{Key: "#/paths/~1some~1where~1{id}/get/responses/200/schema", Ref: spec.MustCreateRef("#/definitions/tag")},
		{Key: "#/definitions/namedAgain", Ref: spec.MustCreateRef("#/definitions/named")},
		{Key: "#/definitions/datedTag/allOf/1", Ref: spec.MustCreateRef("#/definitions/tag")},
		{Key: "#/definitions/datedRecords/items/1", Ref: spec.MustCreateRef("#/definitions/record")},
		{Key: "#/definitions/datedTaggedRecords/items/1", Ref: spec.MustCreateRef("#/definitions/record")},
		{Key: "#/definitions/datedTaggedRecords/additionalItems", Ref: spec.MustCreateRef("#/definitions/tag")},
		{Key: "#/definitions/otherRecords/items", Ref: spec.MustCreateRef("#/definitions/record")},
		{Key: "#/definitions/tags/additionalProperties", Ref: spec.MustCreateRef("#/definitions/tag")},
		{Key: "#/definitions/namedThing/properties/name", Ref: spec.MustCreateRef("#/definitions/named")},
	}
}

func TestFlatten_ImportExternalReferences(t *testing.T) {
	log.SetOutput(io.Discard)
	defer log.SetOutput(os.Stdout)

	// this fixture is the same as external_definitions.yml, but no more
	// checks if invalid construct is supported (i.e. $ref in parameters items)
	bp := filepath.Join(".", "fixtures", "external_definitions_valid.yml")
	sp := antest.LoadOrFail(t, bp)

	opts := &FlattenOpts{
		Spec:     New(sp),
		BasePath: bp,
	}

	// NOTE(fredbi): now we no more expand, but merely resolve and iterate until there is no more ext ref
	// so calling importExternalReferences is not idempotent
	_, erx := importExternalReferences(opts)
	require.NoError(t, erx)

	require.Len(t, sp.Definitions, 11)
	require.MapContainsT(t, sp.Definitions, "tag")
	require.MapContainsT(t, sp.Definitions, "named")
	require.MapContainsT(t, sp.Definitions, "record")

	for idx, toPin := range makeRefFixtures() {
		i := idx
		v := toPin
		sp := sp // the pointer passed to Get(node) must be pinned

		t.Run(fmt.Sprintf("import check ref [%d]: %q", i, v.Key), func(t *testing.T) {
			t.Parallel()

			ptr, err := jsonpointer.New(v.Key[1:])
			require.NoErrorf(t, err, "error on jsonpointer.New(%q)", v.Key[1:])

			vv, _, err := ptr.Get(sp)
			require.NoErrorf(t, err, "error on ptr.Get(p for key=%s)", v.Key[1:])

			switch tv := vv.(type) {
			case *spec.Schema:
				require.EqualT(t, v.Ref.String(), tv.Ref.String(), "for %s", v.Key)

			case spec.Schema:
				require.EqualT(t, v.Ref.String(), tv.Ref.String(), "for %s", v.Key)

			case *spec.SchemaOrBool:
				require.EqualT(t, v.Ref.String(), tv.Schema.Ref.String(), "for %s", v.Key)

			case *spec.SchemaOrArray:
				require.EqualT(t, v.Ref.String(), tv.Schema.Ref.String(), "for %s", v.Key)

			default:
				require.Fail(t, "unknown type", "got %T", vv)
			}
		})
	}

	// check the complete result for clarity
	expected, err := os.ReadFile(filepath.Join("fixtures", "expected", "external-references-1.json"))
	require.NoError(t, err)

	assert.JSONMarshalAsT(t, string(expected), sp)

	// iterate again: this time all external schema $ref's should be reinlined
	opts.Spec.reload()

	_, err = importExternalReferences(&FlattenOpts{
		Spec:     New(sp),
		BasePath: bp,
	})
	require.NoError(t, err)

	opts.Spec.reload()
	for _, ref := range opts.Spec.references.schemas {
		require.TrueT(t, ref.HasFragmentOnly)
	}

	// now try complete flatten, with unused definitions removed
	sp = antest.LoadOrFail(t, bp)
	an := New(sp)

	require.NoError(t, Flatten(FlattenOpts{Spec: an, BasePath: bp, Verbose: true, Minimal: true, RemoveUnused: true}))

	expected, err = os.ReadFile(filepath.Join("fixtures", "expected", "external-references-2.json"))
	require.NoError(t, err)

	assert.JSONMarshalAsT(t, string(expected), an.spec)
}

func makeFlattenFixtures() []refFixture {
	return []refFixture{
		{
			Key:      "#/responses/notFound/schema",
			Location: "#/responses/notFound/schema",
			Ref:      spec.MustCreateRef("#/definitions/error"),
			Expected: nil,
		},
		{
			Key:      "#/paths/~1some~1where~1{id}/parameters/0",
			Location: "#/paths/~1some~1where~1{id}/parameters/0/name",
			Ref:      spec.Ref{},
			Expected: "id",
		},
		{
			Key:      "#/paths/~1other~1place",
			Location: "#/paths/~1other~1place/get/operationId",
			Ref:      spec.Ref{},
			Expected: "modelOp",
		},
		{
			Key:      "#/paths/~1some~1where~1{id}/get/parameters/0",
			Location: "#/paths/~1some~1where~1{id}/get/parameters/0/name",
			Ref:      spec.Ref{},
			Expected: "limit",
		},
		{
			Key:      "#/paths/~1some~1where~1{id}/get/parameters/1",
			Location: "#/paths/~1some~1where~1{id}/get/parameters/1/name",
			Ref:      spec.Ref{},
			Expected: "some",
		},
		{
			Key:      "#/paths/~1some~1where~1{id}/get/parameters/2",
			Location: "#/paths/~1some~1where~1{id}/get/parameters/2/name",
			Ref:      spec.Ref{},
			Expected: "other",
		},
		{
			Key:      "#/paths/~1some~1where~1{id}/get/parameters/3",
			Location: "#/paths/~1some~1where~1{id}/get/parameters/3/schema",
			Ref:      spec.MustCreateRef("#/definitions/getSomeWhereIdParamsBody"),
			Expected: "",
		},
		{
			Key:      "#/paths/~1some~1where~1{id}/get/responses/200",
			Location: "#/paths/~1some~1where~1{id}/get/responses/200/schema",
			Ref:      spec.MustCreateRef("#/definitions/getSomeWhereIdOKBody"),
			Expected: "",
		},
		{
			Key:      "#/definitions/namedAgain",
			Location: "",
			Ref:      spec.MustCreateRef("#/definitions/named"),
			Expected: "",
		},
		{
			Key:      "#/definitions/namedThing/properties/name",
			Location: "",
			Ref:      spec.MustCreateRef("#/definitions/named"),
			Expected: "",
		},
		{
			Key:      "#/definitions/namedThing/properties/namedAgain",
			Location: "",
			Ref:      spec.MustCreateRef("#/definitions/namedAgain"),
			Expected: "",
		},
		{
			Key:      "#/definitions/datedRecords/items/1",
			Location: "",
			Ref:      spec.MustCreateRef("#/definitions/record"),
			Expected: "",
		},
		{
			Key:      "#/definitions/otherRecords/items",
			Location: "",
			Ref:      spec.MustCreateRef("#/definitions/record"),
			Expected: "",
		},
		{
			Key:      "#/definitions/tags/additionalProperties",
			Location: "",
			Ref:      spec.MustCreateRef("#/definitions/tag"),
			Expected: "",
		},
		{
			Key:      "#/definitions/datedTag/allOf/1",
			Location: "",
			Ref:      spec.MustCreateRef("#/definitions/tag"),
			Expected: "",
		},
		{
			Key:      "#/definitions/nestedThingRecord/items/1",
			Location: "",
			Ref:      spec.MustCreateRef("#/definitions/nestedThingRecordItems1"),
			Expected: "",
		},
		{
			Key:      "#/definitions/nestedThingRecord/items/2",
			Location: "",
			Ref:      spec.MustCreateRef("#/definitions/nestedThingRecordItems2"),
			Expected: "",
		},
		{
			Key:      "#/definitions/nestedThing/properties/record",
			Location: "",
			Ref:      spec.MustCreateRef("#/definitions/nestedThingRecord"),
			Expected: "",
		},
		{
			Key:      "#/definitions/named",
			Location: "#/definitions/named/type",
			Ref:      spec.Ref{},
			Expected: spec.StringOrArray{"string"},
		},
		{
			Key:      "#/definitions/error",
			Location: "#/definitions/error/properties/id/type",
			Ref:      spec.Ref{},
			Expected: spec.StringOrArray{"integer"},
		},
		{
			Key:      "#/definitions/record",
			Location: "#/definitions/record/properties/createdAt/format",
			Ref:      spec.Ref{},
			Expected: "date-time",
		},
		{
			Key:      "#/definitions/getSomeWhereIdOKBody",
			Location: "#/definitions/getSomeWhereIdOKBody/properties/record",
			Ref:      spec.MustCreateRef("#/definitions/nestedThing"),
			Expected: nil,
		},
		{
			Key:      "#/definitions/getSomeWhereIdParamsBody",
			Location: "#/definitions/getSomeWhereIdParamsBody/properties/record",
			Ref:      spec.MustCreateRef("#/definitions/getSomeWhereIdParamsBodyRecord"),
			Expected: nil,
		},
		{
			Key:      "#/definitions/getSomeWhereIdParamsBodyRecord",
			Location: "#/definitions/getSomeWhereIdParamsBodyRecord/items/1",
			Ref:      spec.MustCreateRef("#/definitions/getSomeWhereIdParamsBodyRecordItems1"),
			Expected: nil,
		},
		{
			Key:      "#/definitions/getSomeWhereIdParamsBodyRecord",
			Location: "#/definitions/getSomeWhereIdParamsBodyRecord/items/2",
			Ref:      spec.MustCreateRef("#/definitions/getSomeWhereIdParamsBodyRecordItems2"),
			Expected: nil,
		},
		{
			Key:      "#/definitions/getSomeWhereIdParamsBodyRecordItems2",
			Location: "#/definitions/getSomeWhereIdParamsBodyRecordItems2/allOf/0/format",
			Ref:      spec.Ref{},
			Expected: "date",
		},
		{
			Key:      "#/definitions/getSomeWhereIdParamsBodyRecordItems2Name",
			Location: "#/definitions/getSomeWhereIdParamsBodyRecordItems2Name/properties/createdAt/format",
			Ref:      spec.Ref{},
			Expected: "date-time",
		},
		{
			Key:      "#/definitions/getSomeWhereIdParamsBodyRecordItems2",
			Location: "#/definitions/getSomeWhereIdParamsBodyRecordItems2/properties/name",
			Ref:      spec.MustCreateRef("#/definitions/getSomeWhereIdParamsBodyRecordItems2Name"),
			Expected: "date",
		},
	}
}

func TestFlatten_CheckRef(t *testing.T) {
	bp := filepath.Join("fixtures", "flatten.yml")
	sp := antest.LoadOrFail(t, bp)

	require.NoError(t, Flatten(FlattenOpts{Spec: New(sp), BasePath: bp}))

	for idx, toPin := range makeFlattenFixtures() {
		i := idx
		v := toPin
		sp := sp

		t.Run(fmt.Sprintf("check ref after flatten %q", v.Key), func(t *testing.T) {
			pk := v.Key[1:]
			if v.Location != "" {
				pk = v.Location[1:]
			}

			ptr, err := jsonpointer.New(pk)
			require.NoError(t, err, "at %d for %s", i, v.Key)

			d, _, err := ptr.Get(sp)
			require.NoError(t, err)

			if v.Ref.String() == "" {
				assert.Equal(t, v.Expected, d)

				return
			}

			switch s := d.(type) {
			case *spec.Schema:
				assert.EqualT(t, v.Ref.String(), s.Ref.String(), "at %d for %s", i, v.Key)

			case spec.Schema:
				assert.EqualT(t, v.Ref.String(), s.Ref.String(), "at %d for %s", i, v.Key)

			case *spec.SchemaOrArray:
				var sRef spec.Ref
				if s != nil && s.Schema != nil {
					sRef = s.Schema.Ref
				}
				assert.EqualT(t, v.Ref.String(), sRef.String(), "at %d for %s", i, v.Key)

			case *spec.SchemaOrBool:
				var sRef spec.Ref
				if s != nil && s.Schema != nil {
					sRef = s.Schema.Ref
				}
				assert.EqualT(t, v.Ref.String(), sRef.String(), "at %d for %s", i, v.Key)

			default:
				assert.Fail(t, "unknown type", "got %T at %d for %s", d, i, v.Key)
			}
		})
	}
}

func TestFlatten_FullWithOAIGen(t *testing.T) {
	bp := filepath.Join("fixtures", "oaigen", "fixture-oaigen.yaml")
	sp := antest.LoadOrFail(t, bp)

	require.NoError(t, Flatten(FlattenOpts{
		Spec: New(sp), BasePath: bp, Verbose: true,
		Minimal: false, RemoveUnused: false,
	}))

	res := getInPath(t, sp, "/some/where", "/get/responses/204/schema")
	assert.JSONEqTf(t, `{"$ref": "#/definitions/uniqueName1"}`, res, "Expected a simple schema for response")

	res = getInPath(t, sp, "/some/where", "/post/responses/204/schema")
	assert.JSONEqTf(t, `{"$ref": "#/definitions/d"}`, res, "Expected a simple schema for response")

	res = getInPath(t, sp, "/some/where", "/get/responses/206/schema")
	assert.JSONEqTf(t, `{"$ref": "#/definitions/a"}`, res, "Expected a simple schema for response")

	res = getInPath(t, sp, "/some/where", "/get/responses/304/schema")
	assert.JSONEqTf(t, `{"$ref": "#/definitions/transitive11"}`, res, "Expected a simple schema for response")

	res = getInPath(t, sp, "/some/where", "/get/responses/205/schema")
	assert.JSONEqTf(t, `{"$ref": "#/definitions/b"}`, res, "Expected a simple schema for response")

	res = getInPath(t, sp, "/some/where", "/post/responses/200/schema")
	assert.JSONEqTf(t, `{"type": "integer"}`, res, "Expected a simple schema for response")

	res = getInPath(t, sp, "/some/where", "/post/responses/default/schema")
	// pointer expanded
	assert.JSONEqTf(t, `{"type": "integer"}`, res, "Expected a simple schema for response")

	res = getDefinition(t, sp, "a")
	assert.JSONEqTf(t,
		`{"type": "object", "properties": { "a": { "$ref": "#/definitions/aAOAIGen" }}}`,
		res, "Expected a simple schema for response")

	res = getDefinition(t, sp, "aA")
	assert.JSONEqTf(t, `{"type": "string", "format": "date"}`, res, "Expected a simple schema for response")

	res = getDefinition(t, sp, "aAOAIGen")
	assert.JSONEqTf(t, `
	{
	  "type": "object",
	  "properties": {
		"b": {
		  "type": "integer"
		}
	  },
	  "x-go-gen-location": "models"
    }
	`, res, "Expected a simple schema for response")

	res = getDefinition(t, sp, "bB")
	assert.JSONEqTf(t, `{"type": "string", "format": "date-time"}`, res, "Expected a simple schema for response")

	_, ok := sp.Definitions["bItems"]
	assert.FalseTf(t, ok, "Did not expect a definition for %s", "bItems")

	res = getDefinition(t, sp, "d")
	assert.JSONEqTf(t, `
	{
	  "type": "object",
	  "properties": {
	    "c": {
		  "type": "integer"
		}
      }
    }
   `, res, "Expected a simple schema for response")

	res = getDefinition(t, sp, "b")
	assert.JSONEqTf(t, `
	{
	  "type": "array",
	  "items": {
	    "$ref": "#/definitions/d"
	  }
	}
	`, res, "Expected a ref in response")

	res = getDefinition(t, sp, "myBody")
	assert.JSONEqTf(t, `
	{
	  "type": "object",
	  "properties": {
	    "aA": {
		  "$ref": "#/definitions/aA"
		},
		"prop1": {
		  "type": "integer"
	    }
      }
	}
	`, res, "Expected a simple schema for response")

	res = getDefinition(t, sp, "uniqueName2")
	assert.JSONEqTf(t, `{"$ref": "#/definitions/notUniqueName2"}`, res, "Expected a simple schema for response")

	res = getDefinition(t, sp, "notUniqueName2")
	assert.JSONEqTf(t, `
	{
	  "type": "object",
	  "properties": {
		"prop6": {
		  "type": "integer"
		}
	  }
	}
	`, res, "Expected a simple schema for response")

	res = getDefinition(t, sp, "uniqueName1")
	assert.JSONEqTf(t, `{
		   "type": "object",
		   "properties": {
		    "prop5": {
		     "type": "integer"
		    }}}`, res, "Expected a simple schema for response")

	// allOf container: []spec.Schema
	res = getDefinition(t, sp, "getWithSliceContainerDefaultBody")
	assert.JSONEqTf(t, `{
		"allOf": [
		    {
		     "$ref": "#/definitions/uniqueName3"
		    },
		    {
		     "$ref": "#/definitions/getWithSliceContainerDefaultBodyAllOf1"
		    }
		   ],
		   "x-go-gen-location": "operations"
		    }`, res, "Expected a simple schema for response")

	res = getDefinition(t, sp, "getWithSliceContainerDefaultBodyAllOf1")
	assert.JSONEqTf(t, `{
		"type": "object",
		   "properties": {
		    "prop8": {
		     "type": "string"
		    }
		   },
		   "x-go-gen-location": "models"
		    }`, res, "Expected a simple schema for response")

	res = getDefinition(t, sp, "getWithTupleContainerDefaultBody")
	assert.JSONEqTf(t, `{
		   "type": "array",
		   "items": [
		    {
		     "$ref": "#/definitions/uniqueName3"
		    },
		    {
		     "$ref": "#/definitions/getWithSliceContainerDefaultBodyAllOf1"
		    }
		   ],
		   "x-go-gen-location": "operations"
		    }`, res, "Expected a simple schema for response")

	// with container SchemaOrArray
	res = getDefinition(t, sp, "getWithTupleConflictDefaultBody")
	assert.JSONEqTf(t, `{
		   "type": "array",
		   "items": [
		    {
		     "$ref": "#/definitions/uniqueName4"
		    },
		    {
		     "$ref": "#/definitions/getWithTupleConflictDefaultBodyItems1"
		    }
		   ],
		   "x-go-gen-location": "operations"
	}`, res, "Expected a simple schema for response")

	res = getDefinition(t, sp, "getWithTupleConflictDefaultBodyItems1")
	assert.JSONEqTf(t, `{
		   "type": "object",
		   "properties": {
		    "prop10": {
		     "type": "string"
		    }
		   },
		   "x-go-gen-location": "models"
	}`, res, "Expected a simple schema for response")
}

func TestFlatten_MinimalWithOAIGen(t *testing.T) {
	var sp *spec.Swagger
	defer func() {
		if t.Failed() && sp != nil {
			t.Log(antest.AsJSON(t, sp))
		}
	}()

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

	var logCapture bytes.Buffer
	log.SetOutput(&logCapture)
	defer log.SetOutput(os.Stdout)

	require.NoError(t, Flatten(FlattenOpts{Spec: New(sp), BasePath: bp, Verbose: true, Minimal: true, RemoveUnused: false}))

	msg := logCapture.String()
	if !assert.StringNotContainsTf(t, msg,
		"warning: duplicate flattened definition name resolved as aAOAIGen", "Expected log message") {
		t.Logf("Captured log: %s", msg)
	}
	if !assert.StringNotContainsTf(t, msg,
		"warning: duplicate flattened definition name resolved as uniqueName2OAIGen", "Expected log message") {
		t.Logf("Captured log: %s", msg)
	}
	res := getInPath(t, sp, "/some/where", "/get/responses/204/schema")
	assert.JSONEqTf(t, `{"$ref": "#/definitions/uniqueName1"}`, res, "Expected a simple schema for response")

	res = getInPath(t, sp, "/some/where", "/post/responses/204/schema")
	assert.JSONEqTf(t, `{"$ref": "#/definitions/d"}`, res, "Expected a simple schema for response")

	res = getInPath(t, sp, "/some/where", "/get/responses/206/schema")
	assert.JSONEqTf(t, `{"$ref": "#/definitions/a"}`, res, "Expected a simple schema for response")

	res = getInPath(t, sp, "/some/where", "/get/responses/304/schema")
	assert.JSONEqTf(t, `{"$ref": "#/definitions/transitive11"}`, res, "Expected a simple schema for response")

	res = getInPath(t, sp, "/some/where", "/get/responses/205/schema")
	assert.JSONEqTf(t, `{"$ref": "#/definitions/b"}`, res, "Expected a simple schema for response")

	res = getInPath(t, sp, "/some/where", "/post/responses/200/schema")
	assert.JSONEqTf(t, `{"type": "integer"}`, res, "Expected a simple schema for response")

	res = getInPath(t, sp, "/some/where", "/post/responses/default/schema")
	// This JSON pointer is expanded
	assert.JSONEqTf(t, `{"type": "integer"}`, res, "Expected a simple schema for response")

	res = getDefinition(t, sp, "aA")
	assert.JSONEqTf(t, `{"type": "string", "format": "date"}`, res, "Expected a simple schema for response")

	res = getDefinition(t, sp, "a")
	assert.JSONEqTf(t, `{
		   "type": "object",
		   "properties": {
		    "a": {
		     "type": "object",
		     "properties": {
		      "b": {
		       "type": "integer"
		      }
		     }
		    }
		   }
		  }`, res, "Expected a simple schema for response")

	res = getDefinition(t, sp, "bB")
	assert.JSONEqTf(t, `{"type": "string", "format": "date-time"}`, res, "Expected a simple schema for response")

	_, ok := sp.Definitions["bItems"]
	assert.FalseTf(t, ok, "Did not expect a definition for %s", "bItems")

	res = getDefinition(t, sp, "d")
	assert.JSONEqTf(t, `{
		   "type": "object",
		   "properties": {
		    "c": {
		     "type": "integer"
		    }
		   }
	}`, res, "Expected a simple schema for response")

	res = getDefinition(t, sp, "b")
	assert.JSONEqTf(t, `{
		   "type": "array",
		   "items": {
			   "$ref": "#/definitions/d"
		   }
	}`, res, "Expected a ref in response")

	res = getDefinition(t, sp, "myBody")
	assert.JSONEqTf(t, `{
		   "type": "object",
		   "properties": {
		    "aA": {
		     "$ref": "#/definitions/aA"
		    },
		    "prop1": {
		     "type": "integer"
		    }
		   }
	}`, res, "Expected a simple schema for response")

	res = getDefinition(t, sp, "uniqueName2")
	assert.JSONEqTf(t, `{"$ref": "#/definitions/notUniqueName2"}`, res, "Expected a simple schema for response")

	// with allOf container: []spec.Schema
	res = getInPath(t, sp, "/with/slice/container", "/get/responses/default/schema")
	assert.JSONEqTf(t, `{
 			"allOf": [
		        {
		         "$ref": "#/definitions/uniqueName3"
		        },
				{
			     "$ref": "#/definitions/getWithSliceContainerDefaultBodyAllOf1"
				}
		       ]
	}`, res, "Expected a simple schema for response")

	// with tuple container
	res = getInPath(t, sp, "/with/tuple/container", "/get/responses/default/schema")
	assert.JSONEqTf(t, `{
		       "type": "array",
		       "items": [
		        {
		         "$ref": "#/definitions/uniqueName3"
		        },
		        {
		         "$ref": "#/definitions/getWithSliceContainerDefaultBodyAllOf1"
		        }
		       ]
	}`, res, "Expected a simple schema for response")

	// with SchemaOrArray container
	res = getInPath(t, sp, "/with/tuple/conflict", "/get/responses/default/schema")
	assert.JSONEqTf(t, `{
		       "type": "array",
		       "items": [
		        {
		         "$ref": "#/definitions/uniqueName4"
		        },
		        {
		         "type": "object",
		         "properties": {
		          "prop10": {
		           "type": "string"
		          }
		         }
		        }
		       ]
	}`, res, "Expected a simple schema for response")
}

func assertNoOAIGen(t *testing.T, bp string, sp *spec.Swagger) (success bool) {
	var logCapture bytes.Buffer
	log.SetOutput(&logCapture)
	defer log.SetOutput(os.Stdout)

	defer func() {
		success = !t.Failed()
	}()

	require.NoError(t, Flatten(FlattenOpts{Spec: New(sp), BasePath: bp, Verbose: true, Minimal: false, RemoveUnused: false}))

	msg := logCapture.String()
	assert.StringNotContainsT(t, msg, "warning")

	for k := range sp.Definitions {
		require.StringNotContainsT(t, k, "OAIGen")
	}

	return
}

func TestFlatten_OAIGen(t *testing.T) {
	for _, fixture := range []string{
		filepath.Join("fixtures", "oaigen", "test3-swagger.yaml"),
		filepath.Join("fixtures", "oaigen", "test3-bis-swagger.yaml"),
		filepath.Join("fixtures", "oaigen", "test3-ter-swagger.yaml"),
	} {
		t.Run("flatten_oiagen_1260_"+fixture, func(t *testing.T) {
			t.Parallel()

			bp := filepath.Join("fixtures", "oaigen", "test3-swagger.yaml")
			sp := antest.LoadOrFail(t, bp)

			require.TrueTf(t, assertNoOAIGen(t, bp, sp), "did not expect an OAIGen definition here")
		})
	}
}

func TestMoreNameInlinedSchemas(t *testing.T) {
	bp := filepath.Join("fixtures", "more_nested_inline_schemas.yml")
	sp := antest.LoadOrFail(t, bp)

	require.NoError(t, Flatten(FlattenOpts{Spec: New(sp), BasePath: bp, Verbose: true, Minimal: false, RemoveUnused: false}))

	res := getInPath(t, sp, "/some/where/{id}", "/post/responses/200/schema")
	assert.JSONEqTf(t, `
	{
	  "type": "object",
	  "additionalProperties": {
		"type": "object",
		"additionalProperties": {
		  "type": "object", "additionalProperties": {
			"$ref": "#/definitions/postSomeWhereIdOKBodyAdditionalPropertiesAdditionalPropertiesAdditionalProperties"
		  }
	    }
      }
	}`,
		res, "Expected a simple schema for response")

	res = getInPath(t, sp, "/some/where/{id}", "/post/responses/204/schema")
	assert.JSONEqTf(t, `
	{
	  "type": "object",
	  "additionalProperties": {
		 "type": "array",
		 "items": {
		   "type": "object",
		   "additionalProperties": {
		     "type": "array",
		     "items": {
		       "type": "object",
		       "additionalProperties": {
		         "type": "array",
		         "items": {
				   "$ref": "#/definitions/postSomeWhereIdNoContentBodyAdditionalPropertiesItemsAdditionalPropertiesItemsAdditionalPropertiesItems"
		         }
		       }
		     }
		   }
		 }
	   }
	 }
`, res, "Expected a simple schema for response")
}

func TestRemoveUnused(t *testing.T) {
	log.SetOutput(io.Discard)
	defer log.SetOutput(os.Stdout)

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

	require.NoError(t, Flatten(FlattenOpts{Spec: New(sp), BasePath: bp, Verbose: false, Minimal: true, RemoveUnused: true}))

	assert.Nil(t, sp.Parameters)
	assert.Nil(t, sp.Responses)

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

	require.NoError(t, Flatten(FlattenOpts{Spec: an, BasePath: bp, Verbose: false, Minimal: true, RemoveUnused: true}))

	assert.Nil(t, sp.Parameters)
	assert.Nil(t, sp.Responses)

	op, ok := an.OperationFor("GET", "/some/where")
	assert.TrueT(t, ok)
	assert.Lenf(t, op.Parameters, 4, "Expected 4 parameters expanded for this operation")
	assert.Lenf(t, an.ParamsFor("GET", "/some/where"), 7, "Expected 7 parameters (with default) expanded for this operation")

	op, ok = an.OperationFor("PATCH", "/some/remote")
	assert.TrueT(t, ok)
	assert.Lenf(t, op.Parameters, 1, "Expected 1 parameter expanded for this operation")
	assert.Lenf(t, an.ParamsFor("PATCH", "/some/remote"), 2, "Expected 2 parameters (with default) expanded for this operation")

	_, ok = sp.Definitions["unused"]
	assert.FalseT(t, ok, "Did not expect to find #/definitions/unused")

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

	require.NoError(t, Flatten(FlattenOpts{Spec: New(sp), BasePath: bp, Verbose: true, Minimal: false, RemoveUnused: true}))

	assert.Nil(t, sp.Parameters)
	assert.Nil(t, sp.Responses)
	_, ok = sp.Definitions["unused"]
	assert.FalseTf(t, ok, "Did not expect to find #/definitions/unused")
}

func TestOperationIDs(t *testing.T) {
	bp := filepath.Join("fixtures", "operations", "fixture-operations.yaml")
	sp := antest.LoadOrFail(t, bp)

	an := New(sp)
	require.NoError(t, Flatten(FlattenOpts{Spec: an, BasePath: bp, Verbose: false, Minimal: false, RemoveUnused: false}))

	t.Run("should GatherOperations", func(t *testing.T) {
		res := operations.GatherOperations(New(sp), []string{"getSomeWhere", "getSomeWhereElse"})

		assert.MapContainsTf(t, res, "getSomeWhere", "expected to find operation")
		assert.MapContainsTf(t, res, "getSomeWhereElse", "expected to find operation")
		assert.MapNotContainsTf(t, res, "postSomeWhere", "did not expect to find operation")
	})

	op, ok := an.OperationFor("GET", "/some/where/else")
	assert.TrueT(t, ok)
	assert.NotNil(t, op)
	assert.Len(t, an.ParametersFor("getSomeWhereElse"), 2)

	op, ok = an.OperationFor("POST", "/some/where/else")
	assert.TrueT(t, ok)
	assert.NotNil(t, op)
	assert.Len(t, an.ParametersFor("postSomeWhereElse"), 1)

	op, ok = an.OperationFor("PUT", "/some/where/else")
	assert.TrueT(t, ok)
	assert.NotNil(t, op)
	assert.Len(t, an.ParametersFor("putSomeWhereElse"), 1)

	op, ok = an.OperationFor("PATCH", "/some/where/else")
	assert.TrueT(t, ok)
	assert.NotNil(t, op)
	assert.Len(t, an.ParametersFor("patchSomeWhereElse"), 1)

	op, ok = an.OperationFor("DELETE", "/some/where/else")
	assert.TrueT(t, ok)
	assert.NotNil(t, op)
	assert.Len(t, an.ParametersFor("deleteSomeWhereElse"), 1)

	op, ok = an.OperationFor("HEAD", "/some/where/else")
	assert.TrueT(t, ok)
	assert.NotNil(t, op)
	assert.Len(t, an.ParametersFor("headSomeWhereElse"), 1)

	op, ok = an.OperationFor("OPTIONS", "/some/where/else")
	assert.TrueT(t, ok)
	assert.NotNil(t, op)
	assert.Len(t, an.ParametersFor("optionsSomeWhereElse"), 1)

	assert.Empty(t, an.ParametersFor("outOfThisWorld"))
}

func TestFlatten_Pointers(t *testing.T) {
	log.SetOutput(io.Discard)
	defer log.SetOutput(os.Stdout)

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

	an := New(sp)
	require.NoError(t, Flatten(FlattenOpts{Spec: an, BasePath: bp, Verbose: true, Minimal: true, RemoveUnused: false}))

	// re-analyse and check all $ref's point to #/definitions
	bn := New(sp)
	for _, r := range bn.AllRefs() {
		assert.EqualT(t, definitionsPath, path.Dir(r.String()))
	}
}

// unit test guards in flatten not easily testable with actual specs.
func TestFlatten_ErrorHandling(t *testing.T) {
	log.SetOutput(io.Discard)
	defer log.SetOutput(os.Stdout)

	const wantedFailure = "Expected a failure"
	bp := filepath.Join("fixtures", "errors", "fixture-unexpandable.yaml")

	// invalid spec expansion
	sp := antest.LoadOrFail(t, bp)
	require.Errorf(t, Flatten(FlattenOpts{Spec: New(sp), BasePath: bp, Expand: true}), wantedFailure)

	// reload original spec
	sp = antest.LoadOrFail(t, bp)
	require.Errorf(t, Flatten(FlattenOpts{Spec: New(sp), BasePath: bp, Expand: false}), wantedFailure)

	bp = filepath.Join("fixtures", "errors", "fixture-unexpandable-2.yaml")
	sp = antest.LoadOrFail(t, bp)
	require.Errorf(t, Flatten(FlattenOpts{Spec: New(sp), BasePath: bp, Expand: false}), wantedFailure)

	// reload original spec
	sp = antest.LoadOrFail(t, bp)
	require.Errorf(t, Flatten(FlattenOpts{Spec: New(sp), BasePath: bp, Minimal: true, Expand: false}), wantedFailure)
}

func TestFlatten_PointersLoop(t *testing.T) {
	log.SetOutput(io.Discard)
	defer log.SetOutput(os.Stdout)

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

	an := New(sp)
	require.Error(t, Flatten(FlattenOpts{Spec: an, BasePath: bp, Verbose: true, Minimal: true, RemoveUnused: false}))
}

func TestFlatten_Bitbucket(t *testing.T) {
	log.SetOutput(io.Discard)
	defer log.SetOutput(os.Stdout)

	bp := filepath.Join("fixtures", "bugs", "bitbucket.json")
	sp := antest.LoadOrFail(t, bp)

	an := New(sp)
	require.NoError(t, Flatten(FlattenOpts{Spec: an, BasePath: bp, Verbose: true, Minimal: true, RemoveUnused: false}))

	// reload original spec
	sp = antest.LoadOrFail(t, bp)
	an = New(sp)
	require.NoError(t, Flatten(FlattenOpts{Spec: an, BasePath: bp, Verbose: true, Minimal: false, RemoveUnused: false}))

	// reload original spec
	sp = antest.LoadOrFail(t, bp)
	an = New(sp)
	require.NoError(t, Flatten(FlattenOpts{Spec: an, BasePath: bp, Verbose: true, Expand: true, RemoveUnused: false}))

	// reload original spec
	sp = antest.LoadOrFail(t, bp)
	an = New(sp)
	require.NoError(t, Flatten(FlattenOpts{Spec: an, BasePath: bp, Verbose: true, Expand: true, RemoveUnused: true}))

	assert.Len(t, sp.Definitions, 2) // only 2 remaining refs after expansion: circular $ref
	_, ok := sp.Definitions["base_commit"]
	assert.TrueT(t, ok)
	_, ok = sp.Definitions["repository"]
	assert.TrueT(t, ok)
}

func TestFlatten_Issue_1602(t *testing.T) {
	log.SetOutput(io.Discard)
	defer log.SetOutput(os.Stdout)

	// $ref as schema to #/responses or #/parameters

	// minimal repro test case
	bp := filepath.Join("fixtures", "bugs", "1602", "fixture-1602-1.yaml")
	sp := antest.LoadOrFail(t, bp)
	an := New(sp)

	require.NoError(t, Flatten(FlattenOpts{
		Spec: an, BasePath: bp, Verbose: true,
		Minimal:      true,
		Expand:       false,
		RemoveUnused: false,
	}))

	// reload spec
	sp = antest.LoadOrFail(t, bp)
	an = New(sp)
	require.NoError(t, Flatten(FlattenOpts{
		Spec: an, BasePath: bp, Verbose: false,
		Minimal:      false,
		Expand:       false,
		RemoveUnused: false,
	}))

	// reload spec
	// with  prior expansion, a pseudo schema is produced
	sp = antest.LoadOrFail(t, bp)
	an = New(sp)
	require.NoError(t, Flatten(FlattenOpts{
		Spec: an, BasePath: bp, Verbose: false,
		Minimal:      false,
		Expand:       true,
		RemoveUnused: false,
	}))
}

func TestFlatten_Issue_1602_All(t *testing.T) {
	for _, toPin := range []string{
		filepath.Join("fixtures", "bugs", "1602", "fixture-1602-full.yaml"),
		filepath.Join("fixtures", "bugs", "1602", "fixture-1602-1.yaml"),
		filepath.Join("fixtures", "bugs", "1602", "fixture-1602-2.yaml"),
		filepath.Join("fixtures", "bugs", "1602", "fixture-1602-3.yaml"),
		filepath.Join("fixtures", "bugs", "1602", "fixture-1602-4.yaml"),
		filepath.Join("fixtures", "bugs", "1602", "fixture-1602-5.yaml"),
		filepath.Join("fixtures", "bugs", "1602", "fixture-1602-6.yaml"),
	} {
		fixture := toPin
		t.Run("issue_1602_all_"+fixture, func(t *testing.T) {
			t.Parallel()
			sp := antest.LoadOrFail(t, fixture)

			an := New(sp)
			require.NoError(t, Flatten(FlattenOpts{
				Spec: an, BasePath: fixture, Verbose: false, Minimal: true, Expand: false,
				RemoveUnused: false,
			}))
		})
	}
}

func TestFlatten_Issue_1614(t *testing.T) {
	log.SetOutput(io.Discard)
	defer log.SetOutput(os.Stdout)

	// $ref as schema to #/responses or #/parameters
	// test warnings

	bp := filepath.Join("fixtures", "bugs", "1614", "gitea.yaml")
	sp := antest.LoadOrFail(t, bp)
	an := New(sp)
	require.NoError(t, Flatten(FlattenOpts{
		Spec: an, BasePath: bp, Verbose: true, Minimal: true, Expand: false,
		RemoveUnused: false,
	}))

	// check that responses subject to warning have been expanded
	jazon := antest.AsJSON(t, sp)
	assert.StringNotContainsT(t, jazon, `#/responses/forbidden`)
	assert.StringNotContainsT(t, jazon, `#/responses/empty`)
}

func TestFlatten_Issue_1621(t *testing.T) {
	// repeated remote refs

	// minimal repro test case
	bp := filepath.Join("fixtures", "bugs", "1621", "fixture-1621.yaml")
	sp := antest.LoadOrFail(t, bp)
	an := New(sp)
	require.NoError(t, Flatten(FlattenOpts{
		Spec: an, BasePath: bp, Verbose: true,
		Minimal:      true,
		Expand:       false,
		RemoveUnused: false,
	}))

	sch1 := sp.Paths.Paths["/v4/users/"].Get.Responses.StatusCodeResponses[200].Schema
	assert.JSONMarshalAsT(t, `{"type": "array", "items": {"$ref": "#/definitions/v4UserListItem" }}`, sch1)

	sch2 := sp.Paths.Paths["/v4/user/"].Get.Responses.StatusCodeResponses[200].Schema
	assert.JSONMarshalAsT(t, `{"$ref": "#/definitions/v4UserListItem"}`, sch2)

	sch3 := sp.Paths.Paths["/v4/users/{email}/"].Get.Responses.StatusCodeResponses[200].Schema
	assert.JSONMarshalAsT(t, `{"$ref": "#/definitions/v4UserListItem"}`, sch3)
}

func TestFlatten_Issue_1796(t *testing.T) {
	// remote cyclic ref
	bp := filepath.Join("fixtures", "bugs", "1796", "queryIssue.json")
	sp := antest.LoadOrFail(t, bp)
	an := New(sp)

	require.NoError(t, Flatten(FlattenOpts{
		Spec: an, BasePath: bp, Verbose: true,
		Minimal: true, Expand: false,
		RemoveUnused: false,
	}))

	// assert all $ref match  "$ref": "#/definitions/something"
	for _, ref := range an.AllReferences() {
		assert.TrueT(t, strings.HasPrefix(ref, "#/definitions"))
	}
}

func TestFlatten_Issue_1767(t *testing.T) {
	// remote cyclic ref again
	bp := filepath.Join("fixtures", "bugs", "1767", "fixture-1767.yaml")
	sp := antest.LoadOrFail(t, bp)
	an := New(sp)
	require.NoError(t, Flatten(FlattenOpts{
		Spec: an, BasePath: bp, Verbose: true,
		Minimal: true, Expand: false,
		RemoveUnused: false,
	}))

	// assert all $ref match  "$ref": "#/definitions/something"
	for _, ref := range an.AllReferences() {
		assert.TrueT(t, strings.HasPrefix(ref, "#/definitions"))
	}
}

func TestFlatten_Issue_1774(t *testing.T) {
	// remote cyclic ref again
	bp := filepath.Join("fixtures", "bugs", "1774", "def_api.yaml")
	sp := antest.LoadOrFail(t, bp)
	an := New(sp)

	require.NoError(t, Flatten(FlattenOpts{
		Spec: an, BasePath: bp, Verbose: true,
		Minimal:      false,
		Expand:       false,
		RemoveUnused: false,
	}))

	// assert all $ref match  "$ref": "#/definitions/something"
	for _, ref := range an.AllReferences() {
		assert.TrueT(t, strings.HasPrefix(ref, "#/definitions"))
	}
}

func TestFlatten_1429(t *testing.T) {
	// nested / remote $ref in response / param schemas
	// issue go-swagger/go-swagger#1429
	bp := filepath.Join("fixtures", "bugs", "1429", "swagger.yaml")
	sp := antest.LoadOrFail(t, bp)

	an := New(sp)
	require.NoError(t, Flatten(FlattenOpts{
		Spec: an, BasePath: bp, Verbose: true,
		Minimal:      true,
		RemoveUnused: false,
	}))
}

func TestFlatten_1851(t *testing.T) {
	// nested / remote $ref in response / param schemas
	// issue go-swagger/go-swagger#1851
	bp := filepath.Join("fixtures", "bugs", "1851", "fixture-1851.yaml")
	sp := antest.LoadOrFail(t, bp)

	an := New(sp)
	require.NoError(t, Flatten(FlattenOpts{
		Spec: an, BasePath: bp, Verbose: true,
		Minimal:      true,
		RemoveUnused: false,
	}))

	serverDefinition, ok := an.spec.Definitions["server"]
	assert.TrueT(t, ok)

	serverStatusDefinition, ok := an.spec.Definitions["serverStatus"]
	assert.TrueT(t, ok)

	serverStatusProperty, ok := serverDefinition.Properties["Status"]
	assert.TrueT(t, ok)

	assert.JSONMarshalAsT(t, `{"$ref": "#/definitions/serverStatus"}`, serverStatusProperty)
	assert.JSONMarshalAsT(t, `{"type": "string", "enum": [ "OK", "Not OK" ]}`, serverStatusDefinition)

	// additional test case: this one used to work
	bp = filepath.Join("fixtures", "bugs", "1851", "fixture-1851-2.yaml")
	sp = antest.LoadOrFail(t, bp)

	an = New(sp)
	require.NoError(t, Flatten(FlattenOpts{Spec: an, BasePath: bp, Verbose: true, Minimal: true, RemoveUnused: false}))

	serverDefinition, ok = an.spec.Definitions["Server"]
	assert.TrueT(t, ok)

	serverStatusDefinition, ok = an.spec.Definitions["ServerStatus"]
	assert.TrueT(t, ok)

	serverStatusProperty, ok = serverDefinition.Properties["Status"]
	assert.TrueT(t, ok)

	assert.JSONMarshalAsT(t, `{"$ref": "#/definitions/ServerStatus"}`, serverStatusProperty)
	assert.JSONMarshalAsT(t, `{"type": "string", "enum": [ "OK", "Not OK" ]}`, serverStatusDefinition)
}

func TestFlatten_RemoteAbsolute(t *testing.T) {
	for _, toPin := range []string{
		// this one has simple remote ref pattern
		filepath.Join("fixtures", "bugs", "remote-absolute", "swagger-mini.json"),
		// this has no remote ref
		filepath.Join("fixtures", "bugs", "remote-absolute", "swagger.json"),
		// this one has local ref, no naming conflict (same as previous but with external ref imported)
		filepath.Join("fixtures", "bugs", "remote-absolute", "swagger-with-local-ref.json"),
		// this one has remote ref, no naming conflict (same as previous but with external ref imported)
		filepath.Join("fixtures", "bugs", "remote-absolute", "swagger-with-remote-only-ref.json"),
	} {
		fixture := toPin
		t.Run("remote_absolute_"+fixture, func(t *testing.T) {
			t.Parallel()

			an := testFlattenWithDefaults(t, fixture)
			checkRefs(t, an.spec, true)
		})
	}

	// This one has both remote and local ref with naming conflict.
	// This creates some "oiagen" definitions to address naming conflict,
	// which are removed by the oaigen pruning process (reinlined / merged with parents).
	an := testFlattenWithDefaults(t, filepath.Join("fixtures", "bugs", "remote-absolute", "swagger-with-ref.json"))
	checkRefs(t, an.spec, false)
}

func TestFlatten_2092(t *testing.T) {
	log.SetOutput(io.Discard)
	defer log.SetOutput(os.Stdout)

	bp := filepath.Join("fixtures", "bugs", "2092", "swagger.yaml")
	rexOAIGen := regexp.MustCompile(`(?i)("\$ref":\s*")(.?oaigen.?)"`)

	// #2092 exhibits a stability issue: repeat 100 times the process to make sure it is stable
	sp := antest.LoadOrFail(t, bp)
	an := New(sp)
	require.NoError(t, Flatten(FlattenOpts{Spec: an, BasePath: bp, Verbose: true, Minimal: true, RemoveUnused: false}))
	firstJSONMinimal := antest.AsJSON(t, an.spec)

	// verify we don't have dangling oaigen refs
	require.FalseTf(t, rexOAIGen.MatchString(firstJSONMinimal), "unmatched regexp for: %s", firstJSONMinimal)

	sp = antest.LoadOrFail(t, bp)
	an = New(sp)
	require.NoError(t, Flatten(FlattenOpts{Spec: an, BasePath: bp, Verbose: true, Minimal: false, RemoveUnused: false}))
	firstJSONFull := antest.AsJSON(t, an.spec)

	// verify we don't have dangling oaigen refs
	require.FalseTf(t, rexOAIGen.MatchString(firstJSONFull), "unmatched regexp for: %s", firstJSONFull)

	for i := range 10 {
		t.Run(fmt.Sprintf("issue_2092_%d", i), func(t *testing.T) {
			t.Parallel()

			// verify that we produce a stable result
			sp := antest.LoadOrFail(t, bp)
			an := New(sp)

			require.NoError(t, Flatten(FlattenOpts{Spec: an, BasePath: bp, Verbose: true, Minimal: true, RemoveUnused: false}))

			jazon := antest.AsJSON(t, an.spec)
			assert.JSONEqT(t, firstJSONMinimal, jazon)

			require.NoError(t, Flatten(FlattenOpts{Spec: an, BasePath: bp, Verbose: true, Minimal: true, RemoveUnused: true}))

			sp = antest.LoadOrFail(t, bp)
			an = New(sp)

			require.NoError(t, Flatten(FlattenOpts{Spec: an, BasePath: bp, Verbose: true, Minimal: false, RemoveUnused: false}))

			jazon = antest.AsJSON(t, an.spec)
			assert.JSONEqT(t, firstJSONFull, jazon)
		})
	}
}

func TestFlatten_2113(t *testing.T) {
	// flatten $ref under path
	log.SetOutput(io.Discard)
	defer log.SetOutput(os.Stdout)

	bp := filepath.Join("fixtures", "bugs", "2113", "base.yaml")
	sp := antest.LoadOrFail(t, bp)
	an := New(sp)

	require.NoError(t, Flatten(FlattenOpts{
		Spec: an, BasePath: bp, Verbose: true,
		Expand:       true,
		RemoveUnused: false,
	}))

	sp = antest.LoadOrFail(t, bp)
	an = New(sp)

	require.NoError(t, Flatten(FlattenOpts{
		Spec: an, BasePath: bp, Verbose: true,
		Minimal:      true,
		RemoveUnused: false,
	}))

	expected, err := os.ReadFile(filepath.Join("fixtures", "expected", "issue-2113.json"))
	require.NoError(t, err)

	assert.JSONMarshalAsT(t, string(expected), sp)
}

func TestFlatten_2334(t *testing.T) {
	// flatten $ref without altering case
	log.SetOutput(io.Discard)
	defer log.SetOutput(os.Stdout)

	bp := filepath.Join("fixtures", "bugs", "2334", "swagger.yaml")
	sp := antest.LoadOrFail(t, bp)
	an := New(sp)

	require.NoError(t, Flatten(FlattenOpts{
		Spec: an, BasePath: bp, Verbose: false,
		Expand:       false,
		Minimal:      true,
		RemoveUnused: true,
		KeepNames:    true,
	}))

	jazon := antest.AsJSON(t, sp)

	assert.StringContainsT(t, jazon, `"$ref": "#/definitions/Bar"`)
	assert.StringContainsT(t, jazon, `"Bar":`)
	assert.StringContainsT(t, jazon, `"Baz":`)
}

func TestFlatten_1898(t *testing.T) {
	log.SetOutput(io.Discard)
	defer log.SetOutput(os.Stdout)

	bp := filepath.Join("fixtures", "bugs", "1898", "swagger.json")
	sp := antest.LoadOrFail(t, bp)
	an := New(sp)

	require.NoError(t, Flatten(FlattenOpts{
		Spec: an, BasePath: bp, Verbose: true,
		Expand:       false,
		RemoveUnused: false,
	}))
	op, ok := an.OperationFor("GET", "/example/v2/GetEvents")
	require.TrueT(t, ok)

	resp, _, ok := op.SuccessResponse()
	require.TrueT(t, ok)

	require.EqualT(t, "#/definitions/xStreamDefinitionsV2EventMsg", resp.Schema.Ref.String())
	def, ok := sp.Definitions["xStreamDefinitionsV2EventMsg"]
	require.TrueT(t, ok)
	require.Len(t, def.Properties, 2)
	require.MapContainsT(t, def.Properties, "error")
	require.MapContainsT(t, def.Properties, "result")
}

func TestFlatten_RemoveUnused_2657(t *testing.T) {
	log.SetOutput(io.Discard)
	defer log.SetOutput(os.Stdout)

	bp := filepath.Join("fixtures", "bugs", "2657", "schema.json")
	sp := antest.LoadOrFail(t, bp)
	an := New(sp)

	require.NoError(t, Flatten(FlattenOpts{
		Spec: an, BasePath: bp, Verbose: true,
		Minimal:      true,
		Expand:       false,
		RemoveUnused: true,
	}))
	require.Empty(t, sp.Definitions)
}

func TestFlatten_Relative_2743(t *testing.T) {
	log.SetOutput(io.Discard)
	defer log.SetOutput(os.Stdout)

	t.Run("used to work, but should NOT", func(t *testing.T) {
		bp := filepath.Join("fixtures", "bugs", "2743", "working", "spec.yaml")
		sp := antest.LoadOrFail(t, bp)
		an := New(sp)

		require.Error(t, Flatten(FlattenOpts{
			Spec: an, BasePath: bp, Verbose: true,
			Minimal: true,
			Expand:  false,
		}))
	})

	t.Run("used not to, but should work", func(t *testing.T) {
		bp := filepath.Join("fixtures", "bugs", "2743", "not-working", "spec.yaml")
		sp := antest.LoadOrFail(t, bp)
		an := New(sp)

		require.NoError(t, Flatten(FlattenOpts{
			Spec: an, BasePath: bp, Verbose: true,
			Minimal: true,
			Expand:  false,
		}))
	})
}

func getDefinition(t testing.TB, sp *spec.Swagger, key string) string {
	d, ok := sp.Definitions[key]
	require.TrueTf(t, ok, "Expected definition for %s", key)
	res, err := json.Marshal(d)
	if err != nil {
		panic(err)
	}

	return string(res)
}

func getInPath(t testing.TB, sp *spec.Swagger, path, key string) string {
	ptr, erp := jsonpointer.New(key)
	require.NoError(t, erp, "at %s no key", key)

	d, _, erg := ptr.Get(sp.Paths.Paths[path])
	require.NoError(t, erg, "at %s no value for %s", path, key)

	res, err := json.Marshal(d)
	if err != nil {
		panic(err)
	}

	return string(res)
}

func checkRefs(t testing.TB, spec *spec.Swagger, expectNoConflict bool) {
	// all $ref resolve locally
	jazon := antest.AsJSON(t, spec)
	m := rex.FindAllStringSubmatch(jazon, -1)
	require.NotNil(t, m)

	for _, matched := range m {
		subMatch := matched[1]
		assert.TrueT(t, strings.HasPrefix(subMatch, "#/definitions/"),
			"expected $ref to be inlined, got: %s", matched[0])
	}

	if expectNoConflict {
		// no naming conflict
		m := oairex.FindAllStringSubmatch(jazon, -1)
		assert.Empty(t, m)
	}
}

func testFlattenWithDefaults(t *testing.T, bp string) *Spec {
	sp := antest.LoadOrFail(t, bp)
	an := New(sp)
	require.NoError(t, Flatten(FlattenOpts{
		Spec:         an,
		BasePath:     bp,
		Verbose:      true,
		Minimal:      true,
		RemoveUnused: false,
	}))

	return an
}
