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

package middleware

import (
	"math"
	"net/url"
	"reflect"
	"strconv"
	"testing"

	"github.com/go-openapi/errors"
	"github.com/go-openapi/spec"
	"github.com/go-openapi/strfmt"
	"github.com/go-openapi/testify/v2/assert"
	"github.com/go-openapi/testify/v2/require"

	"github.com/go-openapi/runtime"
)

type paramFactory func(string) *spec.Parameter

var paramFactories = []paramFactory{
	spec.QueryParam,
	spec.HeaderParam,
	spec.PathParam,
	spec.FormDataParam,
}

func np(param *spec.Parameter) *untypedParamBinder {
	return newUntypedParamBinder(*param, new(spec.Swagger), strfmt.Default)
}

var stringItems = new(spec.Items)

func init() {
	stringItems.Type = typeString
}

func testCollectionFormat(t *testing.T, param *spec.Parameter, valid bool) {
	binder := &untypedParamBinder{
		parameter: param,
	}
	_, _, _, err := binder.readValue(runtime.Values(nil), reflect.ValueOf(nil)) //nolint:dogsled // we just want to test the error
	if valid {
		require.NoError(t, err)
	} else {
		require.Error(t, err)
		require.EqualError(t, err, errors.InvalidCollectionFormat(param.Name, param.In, param.CollectionFormat).Error())
	}
}

func requiredError(param *spec.Parameter, data any) *errors.Validation {
	return errors.Required(param.Name, param.In, data)
}

func validateRequiredTest(t *testing.T, param *spec.Parameter, value reflect.Value) {
	binder := np(param)

	err := binder.bindValue([]string{}, true, value)
	require.Error(t, err)
	assert.NotNil(t, param)
	require.EqualError(t, requiredError(param, value.Interface()), err.Error())

	err = binder.bindValue([]string{""}, true, value)
	require.Error(t, err)
	require.EqualError(t, requiredError(param, value.Interface()), err.Error())

	// should be impossible data, but let's go with it
	err = binder.bindValue([]string{"a"}, false, value)
	require.Error(t, err)
	require.EqualError(t, err, requiredError(param, value.Interface()).Error())

	err = binder.bindValue([]string{""}, false, value)
	require.Error(t, err)
	require.EqualError(t, requiredError(param, value.Interface()), err.Error())
}

func validateRequiredAllowEmptyTest(t *testing.T, param *spec.Parameter, value reflect.Value) {
	param.AllowEmptyValue = true
	binder := np(param)
	err := binder.bindValue([]string{}, true, value)
	require.NoError(t, err)
	require.NotNil(t, param)

	err = binder.bindValue([]string{""}, true, value)
	require.NoError(t, err)

	err = binder.bindValue([]string{"1"}, false, value)
	require.Error(t, err)
	require.EqualError(t, requiredError(param, value.Interface()), err.Error())

	err = binder.bindValue([]string{""}, false, value)
	require.Error(t, err)
	require.EqualError(t, requiredError(param, value.Interface()), err.Error())
}

func TestRequiredValidation(t *testing.T) {
	strParam := spec.QueryParam("name").Typed(typeString, "").AsRequired()
	validateRequiredTest(t, strParam, reflect.ValueOf(""))
	validateRequiredAllowEmptyTest(t, strParam, reflect.ValueOf(""))

	intParam := spec.QueryParam("id").Typed("integer", "int32").AsRequired()
	validateRequiredTest(t, intParam, reflect.ValueOf(int32(0)))
	validateRequiredAllowEmptyTest(t, intParam, reflect.ValueOf(int32(0)))
	longParam := spec.QueryParam("id").Typed("integer", "int64").AsRequired()
	validateRequiredTest(t, longParam, reflect.ValueOf(int64(0)))
	validateRequiredAllowEmptyTest(t, longParam, reflect.ValueOf(int64(0)))

	floatParam := spec.QueryParam("score").Typed("number", "float").AsRequired()
	validateRequiredTest(t, floatParam, reflect.ValueOf(float32(0)))
	validateRequiredAllowEmptyTest(t, floatParam, reflect.ValueOf(float32(0)))
	doubleParam := spec.QueryParam("score").Typed("number", "double").AsRequired()
	validateRequiredTest(t, doubleParam, reflect.ValueOf(float64(0)))
	validateRequiredAllowEmptyTest(t, doubleParam, reflect.ValueOf(float64(0)))

	dateTimeParam := spec.QueryParam("registered").Typed(typeString, "date-time").AsRequired()
	validateRequiredTest(t, dateTimeParam, reflect.ValueOf(strfmt.DateTime{}))
	// validateRequiredAllowEmptyTest(t, dateTimeParam, reflect.ValueOf(strfmt.DateTime{}))

	dateParam := spec.QueryParam("registered").Typed(typeString, "date").AsRequired()
	validateRequiredTest(t, dateParam, reflect.ValueOf(strfmt.Date{}))
	// validateRequiredAllowEmptyTest(t, dateParam, reflect.ValueOf(strfmt.DateTime{}))

	sliceParam := spec.QueryParam("tags").CollectionOf(stringItems, "").AsRequired()
	validateRequiredTest(t, sliceParam, reflect.MakeSlice(reflect.TypeFor[[]string](), 0, 0))
	validateRequiredAllowEmptyTest(t, sliceParam, reflect.MakeSlice(reflect.TypeFor[[]string](), 0, 0))
}

func TestInvalidCollectionFormat(t *testing.T) {
	validCf1 := spec.QueryParam("validFmt").CollectionOf(stringItems, "multi")
	validCf2 := spec.FormDataParam("validFmt2").CollectionOf(stringItems, "multi")
	invalidCf1 := spec.HeaderParam("invalidHdr").CollectionOf(stringItems, "multi")
	invalidCf2 := spec.PathParam("invalidPath").CollectionOf(stringItems, "multi")

	testCollectionFormat(t, validCf1, true)
	testCollectionFormat(t, validCf2, true)
	testCollectionFormat(t, invalidCf1, false)
	testCollectionFormat(t, invalidCf2, false)
}

func invalidTypeError(param *spec.Parameter, data any) *errors.Validation {
	tpe := param.Type
	if param.Format != "" {
		tpe = param.Format
	}
	return errors.InvalidType(param.Name, param.In, tpe, data)
}

func TestTypeValidation(t *testing.T) {
	for _, newParam := range paramFactories {
		intParam := newParam("badInt").Typed("integer", "int32")
		value := reflect.ValueOf(int32(0))
		binder := np(intParam)
		err := binder.bindValue([]string{"yada"}, true, value)
		// fails for invalid string
		require.Error(t, err)
		require.EqualError(t, err, invalidTypeError(intParam, "yada").Error())

		// fails for overflow
		val := int64(math.MaxInt32)
		str := strconv.FormatInt(val, 10) + "0"
		v := int32(0)
		value = reflect.ValueOf(&v).Elem()

		binder = np(intParam)
		err = binder.bindValue([]string{str}, true, value)
		require.Error(t, err)
		require.EqualError(t, err, invalidTypeError(intParam, str).Error())

		// fails for invalid string
		longParam := newParam("badLong").Typed("integer", "int64")
		value = reflect.ValueOf(int64(0))

		binder = np(longParam)
		err = binder.bindValue([]string{"yada"}, true, value)
		require.Error(t, err)
		require.EqualError(t, err, invalidTypeError(longParam, "yada").Error())

		// fails for overflow
		str2 := strconv.FormatInt(math.MaxInt64, 10) + "0"
		v2 := int64(0)
		vv2 := reflect.ValueOf(&v2).Elem()
		binder = np(longParam)
		err = binder.bindValue([]string{str2}, true, vv2)
		require.Error(t, err)
		require.EqualError(t, err, invalidTypeError(longParam, str2).Error())

		// fails for invalid string
		floatParam := newParam("badFloat").Typed("number", "float")
		value = reflect.ValueOf(float64(0))
		binder = np(floatParam)
		err = binder.bindValue([]string{"yada"}, true, value)
		require.Error(t, err)
		require.EqualError(t, err, invalidTypeError(floatParam, "yada").Error())

		// fails for overflow
		str3 := strconv.FormatFloat(math.MaxFloat64, 'f', 5, 64)
		v3 := reflect.TypeFor[float32]()
		value = reflect.New(v3).Elem()
		binder = np(floatParam)
		err = binder.bindValue([]string{str3}, true, value)
		require.Error(t, err)
		require.EqualError(t, err, invalidTypeError(floatParam, str3).Error())

		// fails for invalid string
		doubleParam := newParam("badDouble").Typed("number", "double")
		value = reflect.ValueOf(float64(0))
		binder = np(doubleParam)
		err = binder.bindValue([]string{"yada"}, true, value)
		require.Error(t, err)
		require.EqualError(t, err, invalidTypeError(doubleParam, "yada").Error())

		// fails for overflow
		str4 := "9" + strconv.FormatFloat(math.MaxFloat64, 'f', 5, 64)
		v4 := reflect.TypeFor[float64]()
		value = reflect.New(v4).Elem()
		binder = np(doubleParam)
		err = binder.bindValue([]string{str4}, true, value)
		require.Error(t, err)
		require.EqualError(t, err, invalidTypeError(doubleParam, str4).Error())

		// fails for invalid string
		dateParam := newParam("badDate").Typed(typeString, "date")
		value = reflect.ValueOf(strfmt.Date{})
		binder = np(dateParam)
		err = binder.bindValue([]string{"yada"}, true, value)
		require.Error(t, err)
		require.EqualError(t, err, invalidTypeError(dateParam, "yada").Error())

		// fails for invalid string
		dateTimeParam := newParam("badDateTime").Typed(typeString, "date-time")
		value = reflect.ValueOf(strfmt.DateTime{})
		binder = np(dateTimeParam)
		err = binder.bindValue([]string{"yada"}, true, value)
		require.Error(t, err)
		require.EqualError(t, err, invalidTypeError(dateTimeParam, "yada").Error())

		// fails for invalid string
		byteParam := newParam("badByte").Typed(typeString, "byte")
		values := url.Values(map[string][]string{})
		values.Add("badByte", "yaüda")
		v5 := []byte{}
		value = reflect.ValueOf(&v5).Elem()
		binder = np(byteParam)
		err = binder.bindValue([]string{"yaüda"}, true, value)
		require.Error(t, err)
		require.EqualError(t, err, invalidTypeError(byteParam, "yaüda").Error())
	}
}

func TestTypeDetectionInvalidItems(t *testing.T) {
	withoutItems := spec.QueryParam("without").CollectionOf(nil, "")
	binder := &untypedParamBinder{
		Name:      "without",
		parameter: withoutItems,
	}
	assert.Nil(t, binder.Type())

	items := new(spec.Items)
	items.Type = typeArray
	withInvalidItems := spec.QueryParam("invalidItems").CollectionOf(items, "")
	binder = &untypedParamBinder{
		Name:      "invalidItems",
		parameter: withInvalidItems,
	}
	assert.Nil(t, binder.Type())

	noType := spec.QueryParam("invalidType")
	noType.Type = "invalid"
	binder = &untypedParamBinder{
		Name:      "invalidType",
		parameter: noType,
	}
	assert.Nil(t, binder.Type())
}

// type emailStrFmt struct {
// 	name      string
// 	tpe       reflect.Type
// 	validator FormatValidator
// }
//
// func (e *emailStrFmt) Name() string {
// 	return e.name
// }
//
// func (e *emailStrFmt) Type() reflect.Type {
// 	return e.tpe
// }
//
// func (e *emailStrFmt) Matches(str string) bool {
// 	return e.validator(str)
// }
//
// func TestTypeDetectionValid(t *testing.T) {
// 	// emlFmt := &emailStrFmt{
// 	// 	name: "email",
// 	// 	tpe:  reflect.TypeOf(email{}),
// 	// }
// 	// formats := []StringFormat{emlFmt}
//
// 	expected := map[string]reflect.Type{
// 		"name":         reflect.TypeOf(""),
// 		"id":           reflect.TypeOf(int64(0)),
// 		"age":          reflect.TypeOf(int32(0)),
// 		"score":        reflect.TypeOf(float32(0)),
// 		"factor":       reflect.TypeOf(float64(0)),
// 		"friend":       reflect.TypeOf(map[string]interface{}{}),
// 		"X-Request-Id": reflect.TypeOf(int64(0)),
// 		"tags":         reflect.TypeOf([]string{}),
// 		"confirmed":    reflect.TypeOf(true),
// 		"planned":      reflect.TypeOf(swagger.Date{}),
// 		"delivered":    reflect.TypeOf(swagger.DateTime{}),
// 		"email":        reflect.TypeOf(email{}),
// 		"picture":      reflect.TypeOf([]byte{}),
// 		"file":         reflect.TypeOf(&swagger.File{}).Elem(),
// 	}
//
// 	params := parametersForAllTypes("")
// 	emailParam := spec.QueryParam("email").Typed(typeString, "email")
// 	params["email"] = *emailParam
//
// 	fileParam := spec.FileParam("file")
// 	params["file"] = *fileParam
//
// 	for _, v := range params {
// 		binder := &paramBinder{
// 			formats:   formats,
// 			name:      v.Name,
// 			parameter: &v,
// 		}
// 		assert.Equal(t, expected[v.Name], binder.Type(), "name: %s", v.Name)
// 	}
// }
