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

package middleware

import (
	stdcontext "context"
	"net/http"
	"net/http/httptest"
	"sort"
	"strings"
	"testing"

	"github.com/go-openapi/analysis"
	"github.com/go-openapi/loads"
	"github.com/go-openapi/runtime/internal/testing/petstore"
	"github.com/go-openapi/runtime/middleware/denco"
	"github.com/go-openapi/runtime/middleware/untyped"
	"github.com/go-openapi/testify/v2/assert"
	"github.com/go-openapi/testify/v2/require"
)

func terminator(rw http.ResponseWriter, _ *http.Request) {
	rw.WriteHeader(http.StatusOK)
}

func TestRouterMiddleware(t *testing.T) {
	spec, api := petstore.NewAPI(t)
	context := NewContext(spec, api, nil)
	mw := NewRouter(context, http.HandlerFunc(terminator))

	recorder := httptest.NewRecorder()
	request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/api/pets", nil)
	require.NoError(t, err)

	mw.ServeHTTP(recorder, request)
	assert.EqualT(t, http.StatusOK, recorder.Code)

	recorder = httptest.NewRecorder()
	request, err = http.NewRequestWithContext(stdcontext.Background(), http.MethodDelete, "/api/pets", nil)
	require.NoError(t, err)

	mw.ServeHTTP(recorder, request)
	assert.EqualT(t, jsonMime, recorder.Header().Get("Content-Type"))
	assert.EqualT(t, http.StatusMethodNotAllowed, recorder.Code)

	methods := strings.Split(recorder.Header().Get("Allow"), ",")
	sort.Strings(methods)
	assert.EqualT(t, "GET,POST", strings.Join(methods, ","))

	recorder = httptest.NewRecorder()
	request, err = http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/nopets", nil)
	require.NoError(t, err)

	mw.ServeHTTP(recorder, request)
	assert.EqualT(t, jsonMime, recorder.Header().Get("Content-Type"))
	assert.EqualT(t, http.StatusNotFound, recorder.Code)

	recorder = httptest.NewRecorder()
	request, err = http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/pets", nil)
	require.NoError(t, err)

	mw.ServeHTTP(recorder, request)
	assert.EqualT(t, http.StatusNotFound, recorder.Code)

	spec, api = petstore.NewRootAPI(t)
	context = NewContext(spec, api, nil)
	mw = NewRouter(context, http.HandlerFunc(terminator))

	recorder = httptest.NewRecorder()
	request, err = http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/pets", nil)
	require.NoError(t, err)

	mw.ServeHTTP(recorder, request)
	assert.EqualT(t, http.StatusOK, recorder.Code)

	recorder = httptest.NewRecorder()
	request, err = http.NewRequestWithContext(stdcontext.Background(), http.MethodDelete, "/pets", nil)
	require.NoError(t, err)

	mw.ServeHTTP(recorder, request)
	assert.EqualT(t, http.StatusMethodNotAllowed, recorder.Code)

	methods = strings.Split(recorder.Header().Get("Allow"), ",")
	sort.Strings(methods)
	assert.EqualT(t, "GET,POST", strings.Join(methods, ","))

	recorder = httptest.NewRecorder()
	request, err = http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/nopets", nil)
	require.NoError(t, err)

	mw.ServeHTTP(recorder, request)
	assert.EqualT(t, http.StatusNotFound, recorder.Code)
}

func TestRouterBuilder(t *testing.T) {
	spec, api := petstore.NewAPI(t)
	analyzed := analysis.New(spec.Spec())

	assert.Len(t, analyzed.RequiredConsumes(), 3)
	assert.Len(t, analyzed.RequiredProduces(), 5)
	assert.Len(t, analyzed.OperationIDs(), 4)

	// context := NewContext(spec, api)
	builder := petAPIRouterBuilder(spec, api, analyzed)
	getRecords := builder.records[http.MethodGet]
	postRecords := builder.records[http.MethodPost]
	deleteRecords := builder.records[http.MethodDelete]

	assert.Len(t, getRecords, 2)
	assert.Len(t, postRecords, 1)
	assert.Len(t, deleteRecords, 1)

	assert.Empty(t, builder.records[http.MethodPatch])
	assert.Empty(t, builder.records[http.MethodOptions])
	assert.Empty(t, builder.records[http.MethodHead])
	assert.Empty(t, builder.records[http.MethodPut])

	rec := postRecords[0]
	assert.EqualT(t, "/pets", rec.Key)
	val, ok := rec.Value.(*routeEntry)
	require.TrueT(t, ok)
	assert.Len(t, val.Consumers, 2)
	assert.Len(t, val.Producers, 2)
	assert.Len(t, val.Consumes, 2)
	assert.Len(t, val.Produces, 2)

	assert.MapContainsT(t, val.Consumers, jsonMime)
	assert.MapContainsT(t, val.Producers, "application/x-yaml")
	assert.SliceContainsT(t, val.Consumes, jsonMime)
	assert.SliceContainsT(t, val.Produces, "application/x-yaml")

	assert.Len(t, val.Parameters, 1)

	recG := getRecords[0]
	assert.EqualT(t, "/pets", recG.Key)
	valG, ok := recG.Value.(*routeEntry)
	require.TrueT(t, ok)
	assert.Len(t, valG.Consumers, 2)
	assert.Len(t, valG.Producers, 4)
	assert.Len(t, valG.Consumes, 2)
	assert.Len(t, valG.Produces, 4)

	assert.Len(t, valG.Parameters, 2)
}

func TestRouterCanonicalBasePath(t *testing.T) {
	spec, api := petstore.NewAPI(t)
	spec.Spec().BasePath = "/api///"
	context := NewContext(spec, api, nil)
	mw := NewRouter(context, http.HandlerFunc(terminator))

	recorder := httptest.NewRecorder()
	request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/api/pets", nil)
	require.NoError(t, err)

	mw.ServeHTTP(recorder, request)
	assert.EqualT(t, http.StatusOK, recorder.Code)
}

func TestRouter_EscapedPath(t *testing.T) {
	spec, api := petstore.NewAPI(t)
	spec.Spec().BasePath = "/api/"
	context := NewContext(spec, api, nil)
	mw := NewRouter(context, http.HandlerFunc(terminator))

	recorder := httptest.NewRecorder()
	request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/api/pets/123", nil)
	require.NoError(t, err)

	mw.ServeHTTP(recorder, request)
	assert.EqualT(t, http.StatusOK, recorder.Code)

	recorder = httptest.NewRecorder()
	request, err = http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/api/pets/abc%2Fdef", nil)
	require.NoError(t, err)

	mw.ServeHTTP(recorder, request)
	assert.EqualT(t, http.StatusOK, recorder.Code)
	ri, _, _ := context.RouteInfo(request)
	require.NotNil(t, ri)
	require.NotNil(t, ri.Params)
	assert.EqualT(t, "abc/def", ri.Params.Get(paramKeyID))
}

func TestRouterStruct(t *testing.T) {
	spec, api := petstore.NewAPI(t)
	router := DefaultRouter(spec, newRoutableUntypedAPI(spec, api, new(Context)))

	methods := router.OtherMethods("post", "/api/pets/{id}")
	assert.Len(t, methods, 2)

	entry, ok := router.Lookup("delete", "/api/pets/{id}")
	assert.TrueT(t, ok)
	require.NotNil(t, entry)
	assert.Len(t, entry.Params, 1)
	assert.EqualT(t, paramKeyID, entry.Params[0].Name)

	_, ok = router.Lookup("delete", "/pets")
	assert.FalseT(t, ok)

	_, ok = router.Lookup("post", "/no-pets")
	assert.FalseT(t, ok)
}

func petAPIRouterBuilder(spec *loads.Document, api *untyped.API, analyzed *analysis.Spec) *defaultRouteBuilder {
	builder := newDefaultRouteBuilder(spec, newRoutableUntypedAPI(spec, api, new(Context)))
	builder.AddRoute(http.MethodGet, "/pets", analyzed.AllPaths()["/pets"].Get)
	builder.AddRoute(http.MethodPost, "/pets", analyzed.AllPaths()["/pets"].Post)
	builder.AddRoute(http.MethodDelete, "/pets/{id}", analyzed.AllPaths()["/pets/{id}"].Delete)
	builder.AddRoute(http.MethodGet, "/pets/{id}", analyzed.AllPaths()["/pets/{id}"].Get)

	return builder
}

func TestPathConverter(t *testing.T) {
	cases := []struct {
		swagger string
		denco   string
	}{
		{"/", "/"},
		{pathSomething, pathSomething},
		{"/{id}", "/:id"},
		{"/{id}/something/{anotherId}", "/:id/something/:anotherId"},
		{"/{petid}", "/:petid"},
		{"/{pet_id}", "/:pet_id"},
		{"/{petId}", "/:petId"},
		{"/{pet-id}", "/:pet-id"},
		// compost parameters tests
		{"/p_{pet_id}", "/p_:pet_id"},
		{"/p_{petId}.{petSubId}", "/p_:petId"},
	}

	for _, tc := range cases {
		actual := pathConverter.ReplaceAllString(tc.swagger, ":$1")
		assert.EqualT(t, tc.denco, actual, "expected swagger path %s to match %s but got %s", tc.swagger, tc.denco, actual)
	}
}

func TestEscapeLiteralColons(t *testing.T) {
	cases := []struct {
		input    string
		expected string
	}{
		{"/", "/"},
		{pathSomething, pathSomething},
		{"/allow/{serverName}/tokenlist:add", "/allow/{serverName}/tokenlist%3Aadd"},
		{"/path:with:colons", "/path%3Awith%3Acolons"},
		{"/{id}:{name}", "/{id}%3A{name}"},
		{"/action:do/{id}", "/action%3Ado/{id}"},
		{"/normal/path", "/normal/path"},
	}

	for _, tc := range cases {
		actual := escapeLiteralColons(tc.input)
		assert.EqualT(t, tc.expected, actual, "expected escapeLiteralColons(%s) to be %s but got %s", tc.input, tc.expected, actual)
	}
}

func TestPathConverterWithLiteralColons(t *testing.T) {
	// Verify the full pipeline: escapeLiteralColons then pathConverter
	cases := []struct {
		swagger string
		denco   string
	}{
		// The main use case from issue #352
		{"/allow/{serverName}/tokenlist:add", "/allow/:serverName/tokenlist%3Aadd"},
		// Literal colons in static segments
		{"/action:do/{id}", "/action%3Ado/:id"},
		{"/path:with:colons", "/path%3Awith%3Acolons"},
		// Multiple colons in different segments
		{"/api:v1/items/{id}", "/api%3Av1/items/:id"},
	}

	for _, tc := range cases {
		actual := pathConverter.ReplaceAllString(escapeLiteralColons(tc.swagger), ":$1")
		assert.EqualT(t, tc.denco, actual, "expected swagger path %s to produce denco path %s but got %s", tc.swagger, tc.denco, actual)
	}
}

func TestDencoRouterWithLiteralColons(t *testing.T) {
	// Test that the denco router correctly handles paths with literal colons
	// when the colons are URL-encoded as %3A.
	t.Run("static path with encoded colons", func(t *testing.T) {
		router := denco.New()
		err := router.Build([]denco.Record{
			denco.NewRecord("/path%3Awith%3Acolons", "static-colon-route"),
		})
		require.NoError(t, err)

		data, _, ok := router.Lookup("/path%3Awith%3Acolons")
		assert.TrueT(t, ok)
		assert.EqualT(t, "static-colon-route", data)

		// Should not match the unescaped version
		_, _, ok = router.Lookup("/path:with:colons")
		assert.FalseT(t, ok)
	})

	t.Run("parametric path with encoded colons in suffix", func(t *testing.T) {
		router := denco.New()
		err := router.Build([]denco.Record{
			denco.NewRecord("/allow/:serverName/tokenlist%3Aadd", "param-colon-route"),
		})
		require.NoError(t, err)

		data, params, ok := router.Lookup("/allow/myserver/tokenlist%3Aadd")
		assert.TrueT(t, ok)
		assert.EqualT(t, "param-colon-route", data)
		require.Len(t, params, 1)
		assert.EqualT(t, "myserver", params[0].Value)
	})

	t.Run("parametric path with encoded colons between params", func(t *testing.T) {
		router := denco.New()
		err := router.Build([]denco.Record{
			denco.NewRecord("/:id/items%3Acheck/:name", "between-params-route"),
		})
		require.NoError(t, err)

		data, params, ok := router.Lookup("/foo/items%3Acheck/bar")
		assert.TrueT(t, ok)
		assert.EqualT(t, "between-params-route", data)
		require.Len(t, params, 2)
		assert.EqualT(t, "foo", params[0].Value)
		assert.EqualT(t, "bar", params[1].Value)
	})
}

func TestExtractCompositParameters(t *testing.T) {
	// name is the composite parameter's name, value is the value of this compost parameter, pattern is the pattern to be matched
	cases := []struct {
		name    string
		value   string
		pattern string
		names   []string
		values  []string
	}{
		{name: valFragment, value: "gie", pattern: "e", names: []string{valFragment}, values: []string{"gi"}},
		{name: valFragment, value: "t.simpson", pattern: ".{subfragment}", names: []string{valFragment, "subfragment"}, values: []string{"t", "simpson"}},
	}
	for _, tc := range cases {
		names, values := decodeCompositParams(tc.name, tc.value, tc.pattern, nil, nil)
		assert.Equal(t, tc.names, names)
		assert.Equal(t, tc.values, values)
	}
}
