// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️
// 📝 GitHub Repository: https://github.com/gofiber/fiber
// 📌 API Documentation: https://docs.gofiber.io

package fiber

import (
	"bytes"
	"context"
	"crypto/tls"
	"math"
	"net"
	"os"
	"strconv"
	"testing"
	"time"
	"unsafe"

	"github.com/gofiber/utils/v2"
	"github.com/stretchr/testify/require"
	"github.com/valyala/fasthttp"
)

func Test_Utils_GetOffer(t *testing.T) {
	t.Parallel()
	require.Empty(t, getOffer([]byte("hello"), acceptsOffer))
	require.Equal(t, "1", getOffer([]byte(""), acceptsOffer, "1"))
	require.Empty(t, getOffer([]byte("2"), acceptsOffer, "1"))

	require.Empty(t, getOffer([]byte(""), acceptsOfferType))
	require.Empty(t, getOffer([]byte("text/html"), acceptsOfferType))
	require.Empty(t, getOffer([]byte("text/html"), acceptsOfferType, "application/json"))
	require.Empty(t, getOffer([]byte("text/html;q=0"), acceptsOfferType, "text/html"))
	require.Empty(t, getOffer([]byte("application/json, */*; q=0"), acceptsOfferType, "image/png"))
	require.Equal(t, "application/xml", getOffer([]byte("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"), acceptsOfferType, "application/xml", "application/json"))
	require.Equal(t, "text/html", getOffer([]byte("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"), acceptsOfferType, "text/html"))
	require.Equal(t, "application/pdf", getOffer([]byte("text/plain;q=0,application/pdf;q=0.9,*/*;q=0.000"), acceptsOfferType, "application/pdf", "application/json"))
	require.Equal(t, "application/pdf", getOffer([]byte("text/plain;q=0,application/pdf;q=0.9,*/*;q=0.000"), acceptsOfferType, "application/pdf", "application/json"))
	require.Equal(t, "text/plain;a=1", getOffer([]byte("text/plain;a=1"), acceptsOfferType, "text/plain;a=1"))
	require.Empty(t, getOffer([]byte("text/plain;a=1;b=2"), acceptsOfferType, "text/plain;b=2"))

	// Spaces, quotes, out of order params, and case insensitivity
	require.Equal(t, "text/plain", getOffer([]byte("text/plain  "), acceptsOfferType, "text/plain"))
	require.Equal(t, "text/plain", getOffer([]byte("text/plain;q=0.4  "), acceptsOfferType, "text/plain"))
	require.Equal(t, "text/plain", getOffer([]byte("text/plain;q=0.4  ;"), acceptsOfferType, "text/plain"))
	require.Equal(t, "text/plain", getOffer([]byte("text/plain;q=0.4  ; p=foo"), acceptsOfferType, "text/plain"))
	require.Equal(t, "text/plain;b=2;a=1", getOffer([]byte("text/plain ;a=1;b=2"), acceptsOfferType, "text/plain;b=2;a=1"))
	require.Equal(t, "text/plain;a=1", getOffer([]byte("text/plain;   a=1   "), acceptsOfferType, "text/plain;a=1"))
	require.Equal(t, `text/plain;a="1;b=2\",text/plain"`, getOffer([]byte(`text/plain;a="1;b=2\",text/plain";q=0.9`), acceptsOfferType, `text/plain;a=1;b=2`, `text/plain;a="1;b=2\",text/plain"`))
	require.Equal(t, "text/plain;A=CAPS", getOffer([]byte(`text/plain;a="caPs"`), acceptsOfferType, "text/plain;A=CAPS"))

	// Priority
	require.Equal(t, "text/plain", getOffer([]byte("text/plain"), acceptsOfferType, "text/plain", "text/plain;a=1"))
	require.Equal(t, "text/plain;a=1", getOffer([]byte("text/plain"), acceptsOfferType, "text/plain;a=1", "", "text/plain"))
	require.Equal(t, "text/plain;a=1", getOffer([]byte("text/plain,text/plain;a=1"), acceptsOfferType, "text/plain", "text/plain;a=1"))
	require.Equal(t, "text/plain", getOffer([]byte("text/plain;q=0.899,text/plain;a=1;q=0.898"), acceptsOfferType, "text/plain", "text/plain;a=1"))
	require.Equal(t, "text/plain;a=1;b=2", getOffer([]byte("text/plain,text/plain;a=1,text/plain;a=1;b=2"), acceptsOfferType, "text/plain", "text/plain;a=1", "text/plain;a=1;b=2"))

	// Takes the last value specified
	require.Equal(t, "text/plain;a=1;b=2", getOffer([]byte("text/plain;a=1;b=1;B=2"), acceptsOfferType, "text/plain;a=1;b=1", "text/plain;a=1;b=2"))

	require.Empty(t, getOffer([]byte("utf-8, iso-8859-1;q=0.5"), acceptsOffer))
	require.Empty(t, getOffer([]byte("utf-8, iso-8859-1;q=0.5"), acceptsOffer, "ascii"))
	require.Equal(t, "utf-8", getOffer([]byte("utf-8, iso-8859-1;q=0.5"), acceptsOffer, "utf-8"))
	require.Equal(t, "iso-8859-1", getOffer([]byte("utf-8;q=0, iso-8859-1;q=0.5"), acceptsOffer, "utf-8", "iso-8859-1"))

	// Accept-Charset wildcard coverage
	require.Equal(t, "utf-8", getOffer([]byte("utf-*"), acceptsOffer, "utf-8"))
	require.Equal(t, "UTF-16", getOffer([]byte("utf-*"), acceptsOffer, "UTF-16", "iso-8859-1"))
	require.Empty(t, getOffer([]byte("utf-*"), acceptsOffer, "iso-8859-1"))
	require.Empty(t, getOffer([]byte("utf-*"), acceptsOffer, "utf"))
	require.Empty(t, getOffer([]byte("utf-*"), acceptsOffer, "x-utf-8"))

	// Complex wildcard negotiation
	require.Equal(t, "utf-16le", getOffer([]byte("utf-8;q=0.4, utf-*;q=0.8, iso-8859-1;q=0.6"), acceptsOffer, "iso-8859-1", "utf-16le"))
	require.Equal(t, "iso-8859-1", getOffer([]byte("utf-*;q=0.9, iso-8859-1;q=1"), acceptsOffer, "x-utf-16", "iso-8859-1"))
	require.Empty(t, getOffer([]byte("utf-*;q=0.5, iso-8859-1;q=0.4"), acceptsOffer, "ascii", "us-ascii"))

	require.Equal(t, "deflate", getOffer([]byte("gzip, deflate"), acceptsOffer, "deflate"))
	require.Empty(t, getOffer([]byte("gzip, deflate;q=0"), acceptsOffer, "deflate"))

	// Accept-Language Basic Filtering
	require.True(t, acceptsLanguageOfferBasic("en", "en-US", nil))
	require.False(t, acceptsLanguageOfferBasic("en-US", "en", nil))
	require.True(t, acceptsLanguageOfferBasic("EN", "en-us", nil))
	require.False(t, acceptsLanguageOfferBasic("en", "en_US", nil))
	require.Equal(t, "en-US", getOffer([]byte("fr-CA;q=0.8, en-US"), acceptsLanguageOfferBasic, "en-US", "fr-CA"))
	require.Empty(t, getOffer([]byte("xx"), acceptsLanguageOfferBasic, "en"))
	require.False(t, acceptsLanguageOfferBasic("en-*", "en-US", nil))
	require.True(t, acceptsLanguageOfferBasic("*", "en-US", nil))

	// Accept-Language Extended Filtering
	require.True(t, acceptsLanguageOfferExtended("en", "en-US", nil))
	require.True(t, acceptsLanguageOfferExtended("en", "en-Latn-US", nil))
	require.True(t, acceptsLanguageOfferExtended("en-*", "en-US", nil))
	require.True(t, acceptsLanguageOfferExtended("*-US", "en-US", nil))
	require.True(t, acceptsLanguageOfferExtended("en-US-*", "en-US", nil))
	require.True(t, acceptsLanguageOfferExtended("en-*", "en-US-CA", nil))
	require.False(t, acceptsLanguageOfferExtended("en-US", "en-GB", nil))
	require.False(t, acceptsLanguageOfferExtended("fr", "en-US", nil))
	require.False(t, acceptsLanguageOfferExtended("", "en-US", nil))
	require.False(t, acceptsLanguageOfferExtended("en", "", nil))
	require.True(t, acceptsLanguageOfferExtended("*", "en-US", nil))
	require.True(t, acceptsLanguageOfferExtended("en-*", "en", nil))
	require.Equal(t, "en-US", getOffer([]byte("fr-CA;q=0.8, en-*"), acceptsLanguageOfferExtended, "en-US", "fr-CA"))

	// Sliding and singleton barriers
	require.True(t, acceptsLanguageOfferExtended("de-*-DE", "de-DE", nil))
	require.True(t, acceptsLanguageOfferExtended("de-*-DE", "de-DE-x-goethe", nil))
	require.True(t, acceptsLanguageOfferExtended("de-*-DE", "de-Latn-DE-1996", nil))
	require.False(t, acceptsLanguageOfferExtended("de-*-DE", "de", nil))
	require.False(t, acceptsLanguageOfferExtended("de-*-DE", "de-x-DE", nil))
	require.True(t, acceptsLanguageOfferExtended("*-CH", "de-CH", nil))
	require.True(t, acceptsLanguageOfferExtended("*-CH", "de-Latn-CH", nil))
}

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

	content := []byte("fiber read content test")
	tempFile, err := os.CreateTemp("", "fiber-read-content-*.txt")
	require.NoError(t, err)
	t.Cleanup(func() {
		require.NoError(t, os.Remove(tempFile.Name()))
	})

	_, err = tempFile.Write(content)
	require.NoError(t, err)
	require.NoError(t, tempFile.Close())

	var buffer bytes.Buffer
	n, err := readContent(&buffer, tempFile.Name())
	require.NoError(t, err)
	require.Equal(t, int64(len(content)), n)
	require.Equal(t, content, buffer.Bytes())
}

type wrappedListener struct {
	net.Listener
}

type tlsConfigMethodListener struct {
	net.Listener
	cfg *tls.Config
}

func (ln *tlsConfigMethodListener) TLSConfig() *tls.Config {
	return ln.cfg
}

type configMethodListener struct {
	net.Listener
	cfg *tls.Config
}

func (ln *configMethodListener) Config() *tls.Config {
	return ln.cfg
}

type nonStructListener int

func (nonStructListener) Accept() (net.Conn, error) { return nil, net.ErrClosed }
func (nonStructListener) Close() error              { return nil }
func (nonStructListener) Addr() net.Addr            { return &net.TCPAddr{} }

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

	t.Run("tls listener", func(t *testing.T) {
		t.Parallel()

		base := newLocalListener(t)
		cfg := &tls.Config{MinVersion: tls.VersionTLS12}
		tlsListener := tls.NewListener(base, cfg)
		t.Cleanup(func() {
			require.NoError(t, tlsListener.Close())
		})

		require.Same(t, cfg, getTLSConfig(tlsListener), "*tls.Listener should expose its TLS config")
	})

	t.Run("wrapped tls listener", func(t *testing.T) {
		t.Parallel()

		base := newLocalListener(t)
		cfg := &tls.Config{MinVersion: tls.VersionTLS13}
		tlsListener := tls.NewListener(base, cfg)
		wrapped := &wrappedListener{Listener: tlsListener}
		t.Cleanup(func() {
			require.NoError(t, wrapped.Close())
		})

		require.Nil(t, getTLSConfig(wrapped), "wrapping without Config()-like methods should return nil")
	})

	t.Run("listener with tls config method", func(t *testing.T) {
		t.Parallel()

		base := newLocalListener(t)
		cfg := &tls.Config{MinVersion: tls.VersionTLS13}
		listener := &tlsConfigMethodListener{Listener: base, cfg: cfg}
		t.Cleanup(func() {
			require.NoError(t, listener.Close())
		})

		require.Same(t, cfg, getTLSConfig(listener), "TLSConfig() should be preferred for TLS discovery")
	})

	t.Run("listener with config method", func(t *testing.T) {
		t.Parallel()

		base := newLocalListener(t)
		cfg := &tls.Config{MinVersion: tls.VersionTLS12}
		listener := &configMethodListener{Listener: base, cfg: cfg}
		t.Cleanup(func() {
			require.NoError(t, listener.Close())
		})

		require.Same(t, cfg, getTLSConfig(listener), "Config() should be preferred for TLS discovery")
	})

	t.Run("non tls listener", func(t *testing.T) {
		t.Parallel()

		base := newLocalListener(t)
		t.Cleanup(func() {
			require.NoError(t, base.Close())
		})

		require.Nil(t, getTLSConfig(base), "plain listeners should not report TLS config")
	})

	t.Run("non-struct listener", func(t *testing.T) {
		t.Parallel()

		ln := nonStructListener(7)
		require.NotPanics(t, func() {
			require.Nil(t, getTLSConfig(ln), "non-struct listeners should not panic and should not report TLS config")
		})
	})
}

func newLocalListener(t *testing.T) net.Listener {
	t.Helper()

	ln, err := net.Listen("tcp", "127.0.0.1:0")
	require.NoError(t, err)

	return ln
}

// go test -v -run=^$ -bench=Benchmark_Utils_GetOffer -benchmem -count=4
func Benchmark_Utils_GetOffer(b *testing.B) {
	testCases := []struct {
		description string
		accept      string
		offers      []string
	}{
		{
			description: "simple",
			accept:      "application/json",
			offers:      []string{"application/json"},
		},
		{
			description: "6 offers",
			accept:      "text/plain",
			offers:      []string{"junk/a", "junk/b", "junk/c", "junk/d", "junk/e", "text/plain"},
		},
		{
			description: "1 parameter",
			accept:      "application/json; version=1",
			offers:      []string{"application/json;version=1"},
		},
		{
			description: "2 parameters",
			accept:      "application/json; version=1; foo=bar",
			offers:      []string{"application/json;version=1;foo=bar"},
		},
		{
			description: "3 parameters",
			accept:      "application/json; version=1; foo=bar; charset=utf-8",
			offers:      []string{"application/json;version=1;foo=bar;charset=utf-8"},
		},
		{
			description: "10 parameters",
			accept:      "text/plain;a=1;b=2;c=3;d=4;e=5;f=6;g=7;h=8;i=9;j=10",
			offers:      []string{"text/plain;a=1;b=2;c=3;d=4;e=5;f=6;g=7;h=8;i=9;j=10"},
		},
		{
			description: "6 offers w/params",
			accept:      "text/plain; format=flowed",
			offers: []string{
				"junk/a;a=b",
				"junk/b;b=c",
				"junk/c;c=d",
				"text/plain; format=justified",
				"text/plain; format=flat",
				"text/plain; format=flowed",
			},
		},
		{
			description: "mime extension",
			accept:      "utf-8, iso-8859-1;q=0.5",
			offers:      []string{"utf-8"},
		},
		{
			description: "mime extension",
			accept:      "utf-8, iso-8859-1;q=0.5",
			offers:      []string{"iso-8859-1"},
		},
		{
			description: "mime extension",
			accept:      "utf-8, iso-8859-1;q=0.5",
			offers:      []string{"iso-8859-1", "utf-8"},
		},
		{
			description: "mime extension",
			accept:      "gzip, deflate",
			offers:      []string{"deflate"},
		},
		{
			description: "web browser",
			accept:      "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
			offers:      []string{"text/html", "application/xml", "application/xml+xhtml"},
		},
	}

	b.ReportAllocs()
	for _, tc := range testCases {
		accept := []byte(tc.accept)
		b.Run(tc.description, func(b *testing.B) {
			for b.Loop() {
				getOffer(accept, acceptsOfferType, tc.offers...)
			}
		})
	}
}

func Test_Utils_ParamsMatch(t *testing.T) {
	testCases := []struct {
		description string
		accept      headerParams
		offer       string
		match       bool
	}{
		{
			description: "empty accept and offer",
			accept:      nil,
			offer:       "",
			match:       true,
		},
		{
			description: "accept is empty, offer has params",
			accept:      make(headerParams),
			offer:       ";foo=bar",
			match:       true,
		},
		{
			description: "offer is empty, accept has params",
			accept:      headerParams{"foo": []byte("bar")},
			offer:       "",
			match:       false,
		},
		{
			description: "accept has extra parameters",
			accept:      headerParams{"foo": []byte("bar"), "a": []byte("1")},
			offer:       ";foo=bar",
			match:       false,
		},
		{
			description: "matches regardless of order",
			accept:      headerParams{"b": []byte("2"), "a": []byte("1")},
			offer:       ";b=2;a=1",
			match:       true,
		},
		{
			description: "case-insensitive",
			accept:      headerParams{"ParaM": []byte("FoO")},
			offer:       ";pAram=foO",
			match:       true,
		},
	}

	for _, tc := range testCases {
		require.Equal(t, tc.match, paramsMatch(tc.accept, tc.offer), tc.description)
	}
}

func Benchmark_Utils_ParamsMatch(b *testing.B) {
	var match bool

	specParams := headerParams{
		"appLe": []byte("orange"),
		"param": []byte("foo"),
	}
	b.ReportAllocs()
	for b.Loop() {
		match = paramsMatch(specParams, `;param=foo; apple=orange`)
	}
	require.True(b, match)
}

func Test_Utils_AcceptsOfferType(t *testing.T) {
	t.Parallel()
	testCases := []struct {
		description string
		spec        string
		specParams  headerParams
		offerType   string
		accepts     bool
	}{
		{
			description: "no params, matching",
			spec:        "application/json",
			offerType:   "application/json",
			accepts:     true,
		},
		{
			description: "no params, mismatch",
			spec:        "application/json",
			offerType:   "application/xml",
			accepts:     false,
		},
		{
			description: "mismatch with subtype prefix",
			spec:        "application/json",
			offerType:   "application/json+xml",
			accepts:     false,
		},
		{
			description: "params match",
			spec:        "application/json",
			specParams:  headerParams{"format": []byte("foo"), "version": []byte("1")},
			offerType:   "application/json;version=1;format=foo;q=0.1",
			accepts:     true,
		},
		{
			description: "spec has extra params",
			spec:        "text/html",
			specParams:  headerParams{"charset": []byte("utf-8")},
			offerType:   "text/html",
			accepts:     false,
		},
		{
			description: "offer has extra params",
			spec:        "text/html",
			offerType:   "text/html;charset=utf-8",
			accepts:     true,
		},
		{
			description: "ignores optional whitespace",
			spec:        "application/json",
			specParams:  headerParams{"format": []byte("foo"), "version": []byte("1")},
			offerType:   "application/json;  version=1 ;    format=foo   ",
			accepts:     true,
		},
		{
			description: "ignores optional whitespace",
			spec:        "application/json",
			specParams:  headerParams{"format": []byte("foo bar"), "version": []byte("1")},
			offerType:   `application/json;version="1";format="foo bar"`,
			accepts:     true,
		},
	}
	for _, tc := range testCases {
		accepts := acceptsOfferType(tc.spec, tc.offerType, tc.specParams)
		require.Equal(t, tc.accepts, accepts, tc.description)
	}
}

func Test_Utils_GetSplicedStrList(t *testing.T) {
	t.Parallel()
	testCases := []struct {
		description  string
		headerValue  string
		expectedList []string
	}{
		{
			description:  "normal case",
			headerValue:  "gzip, deflate,br",
			expectedList: []string{"gzip", "deflate", "br"},
		},
		{
			description:  "no matter the value",
			headerValue:  "   gzip,deflate, br, zip",
			expectedList: []string{"gzip", "deflate", "br", "zip"},
		},
		{
			description:  "comma with trailing spaces around values",
			headerValue:  "gzip , br",
			expectedList: []string{"gzip", "br"},
		},
		{
			description:  "comma with tabbed whitespace",
			headerValue:  "gzip\t,br",
			expectedList: []string{"gzip", "br"},
		},
		{
			description:  "headerValue is empty",
			headerValue:  "",
			expectedList: nil,
		},
		{
			description:  "has a comma without element",
			headerValue:  "gzip,",
			expectedList: []string{"gzip", ""},
		},
		{
			description:  "has a space between words",
			headerValue:  "  foo bar, hello  world",
			expectedList: []string{"foo bar", "hello  world"},
		},
		{
			description:  "single comma",
			headerValue:  ",",
			expectedList: []string{"", ""},
		},
		{
			description:  "multiple comma",
			headerValue:  ",,",
			expectedList: []string{"", "", ""},
		},
		{
			description:  "comma with space",
			headerValue:  ",  ,",
			expectedList: []string{"", "", ""},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.description, func(t *testing.T) {
			tc := tc // create a new 'tc' variable for the goroutine
			t.Parallel()
			dst := make([]string, 10)
			result := getSplicedStrList(tc.headerValue, dst)
			require.Equal(t, tc.expectedList, result)
		})
	}
}

func Benchmark_Utils_GetSplicedStrList(b *testing.B) {
	destination := make([]string, 5)
	result := destination
	const input = `deflate, gzip,br,brotli,zstd`
	b.ReportAllocs()
	for b.Loop() {
		result = getSplicedStrList(input, destination)
	}
	require.Equal(b, []string{"deflate", "gzip", "br", "brotli", "zstd"}, result)
}

func Test_Utils_SortAcceptedTypes(t *testing.T) {
	t.Parallel()
	acceptedTypes := []acceptedType{
		{spec: "text/html", quality: 1, specificity: 3, order: 0},
		{spec: "text/*", quality: 0.5, specificity: 2, order: 1},
		{spec: "*/*", quality: 0.1, specificity: 1, order: 2},
		{spec: "application/xml", quality: 1, specificity: 3, order: 4},
		{spec: "application/pdf", quality: 1, specificity: 3, order: 5},
		{spec: "image/png", quality: 1, specificity: 3, order: 6},
		{spec: "image/jpeg", quality: 1, specificity: 3, order: 7},
		{spec: "image/*", quality: 1, specificity: 2, order: 8},
		{spec: "image/gif", quality: 1, specificity: 3, order: 9},
		{spec: "text/plain", quality: 1, specificity: 3, order: 10},
		{spec: "application/json", quality: 0.999, specificity: 3, params: headerParams{"a": []byte("1")}, order: 11},
		{spec: "application/json", quality: 0.999, specificity: 3, order: 3},
	}
	sortAcceptedTypes(acceptedTypes)
	require.Equal(t, []acceptedType{
		{spec: "text/html", quality: 1, specificity: 3, order: 0},
		{spec: "application/xml", quality: 1, specificity: 3, order: 4},
		{spec: "application/pdf", quality: 1, specificity: 3, order: 5},
		{spec: "image/png", quality: 1, specificity: 3, order: 6},
		{spec: "image/jpeg", quality: 1, specificity: 3, order: 7},
		{spec: "image/gif", quality: 1, specificity: 3, order: 9},
		{spec: "text/plain", quality: 1, specificity: 3, order: 10},
		{spec: "image/*", quality: 1, specificity: 2, order: 8},
		{spec: "application/json", quality: 0.999, specificity: 3, params: headerParams{"a": []byte("1")}, order: 11},
		{spec: "application/json", quality: 0.999, specificity: 3, order: 3},
		{spec: "text/*", quality: 0.5, specificity: 2, order: 1},
		{spec: "*/*", quality: 0.1, specificity: 1, order: 2},
	}, acceptedTypes)
}

// go test -v -run=^$ -bench=Benchmark_Utils_SortAcceptedTypes_Sorted -benchmem -count=4
func Benchmark_Utils_SortAcceptedTypes_Sorted(b *testing.B) {
	acceptedTypes := make([]acceptedType, 3)
	b.ReportAllocs()
	for b.Loop() {
		acceptedTypes[0] = acceptedType{spec: "text/html", quality: 1, specificity: 1, order: 0}
		acceptedTypes[1] = acceptedType{spec: "text/*", quality: 0.5, specificity: 1, order: 1}
		acceptedTypes[2] = acceptedType{spec: "*/*", quality: 0.1, specificity: 1, order: 2}
		sortAcceptedTypes(acceptedTypes)
	}
	require.Equal(b, "text/html", acceptedTypes[0].spec)
	require.Equal(b, "text/*", acceptedTypes[1].spec)
	require.Equal(b, "*/*", acceptedTypes[2].spec)
}

// go test -v -run=^$ -bench=Benchmark_Utils_SortAcceptedTypes_Unsorted -benchmem -count=4
func Benchmark_Utils_SortAcceptedTypes_Unsorted(b *testing.B) {
	acceptedTypes := make([]acceptedType, 11)
	b.ReportAllocs()
	for b.Loop() {
		acceptedTypes[0] = acceptedType{spec: "text/html", quality: 1, specificity: 3, order: 0}
		acceptedTypes[1] = acceptedType{spec: "text/*", quality: 0.5, specificity: 2, order: 1}
		acceptedTypes[2] = acceptedType{spec: "*/*", quality: 0.1, specificity: 1, order: 2}
		acceptedTypes[3] = acceptedType{spec: "application/json", quality: 0.999, specificity: 3, order: 3}
		acceptedTypes[4] = acceptedType{spec: "application/xml", quality: 1, specificity: 3, order: 4}
		acceptedTypes[5] = acceptedType{spec: "application/pdf", quality: 1, specificity: 3, order: 5}
		acceptedTypes[6] = acceptedType{spec: "image/png", quality: 1, specificity: 3, order: 6}
		acceptedTypes[7] = acceptedType{spec: "image/jpeg", quality: 1, specificity: 3, order: 7}
		acceptedTypes[8] = acceptedType{spec: "image/*", quality: 1, specificity: 2, order: 8}
		acceptedTypes[9] = acceptedType{spec: "image/gif", quality: 1, specificity: 3, order: 9}
		acceptedTypes[10] = acceptedType{spec: "text/plain", quality: 1, specificity: 3, order: 10}
		sortAcceptedTypes(acceptedTypes)
	}
	require.Equal(b, []acceptedType{
		{spec: "text/html", quality: 1, specificity: 3, order: 0},
		{spec: "application/xml", quality: 1, specificity: 3, order: 4},
		{spec: "application/pdf", quality: 1, specificity: 3, order: 5},
		{spec: "image/png", quality: 1, specificity: 3, order: 6},
		{spec: "image/jpeg", quality: 1, specificity: 3, order: 7},
		{spec: "image/gif", quality: 1, specificity: 3, order: 9},
		{spec: "text/plain", quality: 1, specificity: 3, order: 10},
		{spec: "image/*", quality: 1, specificity: 2, order: 8},
		{spec: "application/json", quality: 0.999, specificity: 3, order: 3},
		{spec: "text/*", quality: 0.5, specificity: 2, order: 1},
		{spec: "*/*", quality: 0.1, specificity: 1, order: 2},
	}, acceptedTypes)
}

func Test_Utils_UniqueRouteStack(t *testing.T) {
	t.Parallel()
	route1 := &Route{}
	route2 := &Route{}
	route3 := &Route{}
	require.Equal(
		t,
		[]*Route{
			route1,
			route2,
			route3,
		},
		uniqueRouteStack([]*Route{
			route1,
			route1,
			route1,
			route2,
			route2,
			route2,
			route3,
			route3,
			route3,
			route1,
			route2,
			route3,
		}),
	)
}

func Test_Utils_getGroupPath(t *testing.T) {
	t.Parallel()
	res := getGroupPath("/v1", "/")
	require.Equal(t, "/v1/", res)

	res = getGroupPath("/v1/", "/")
	require.Equal(t, "/v1/", res)

	res = getGroupPath("/", "/")
	require.Equal(t, "/", res)

	res = getGroupPath("/v1/api/", "/")
	require.Equal(t, "/v1/api/", res)

	res = getGroupPath("/v1/api", "group")
	require.Equal(t, "/v1/api/group", res)

	res = getGroupPath("/v1/api", "")
	require.Equal(t, "/v1/api", res)
}

// go test -v -run=^$ -bench=Benchmark_Utils_ -benchmem -count=3
func Benchmark_Utils_getGroupPath(b *testing.B) {
	var res string
	b.ReportAllocs()
	for b.Loop() {
		_ = getGroupPath("/v1/long/path/john/doe", "/why/this/name/is/so/awesome")
		_ = getGroupPath("/v1", "/")
		_ = getGroupPath("/v1", "/api")
		res = getGroupPath("/v1", "/api/register/:project")
	}
	require.Equal(b, "/v1/api/register/:project", res)
}

func Benchmark_Utils_Unescape(b *testing.B) {
	unescaped := ""
	dst := make([]byte, 0)
	b.ReportAllocs()
	for b.Loop() {
		source := "/cr%C3%A9er"
		pathBytes := utils.UnsafeBytes(source)
		pathBytes = fasthttp.AppendUnquotedArg(dst[:0], pathBytes)
		unescaped = utils.UnsafeString(pathBytes)
	}

	require.Equal(b, "/créer", unescaped)
}

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

	testCases := []struct {
		addr, host, port string
	}{
		{addr: "[::1]:3000", host: "[::1]", port: "3000"},
		{addr: "127.0.0.1:3000", host: "127.0.0.1", port: "3000"},
		{addr: "[::1]", host: "[::1]", port: ""},
		{addr: "2001:db8::1", host: "2001:db8::1", port: ""},
		{addr: "/path/to/unix/socket", host: "/path/to/unix/socket", port: ""},
		{addr: "127.0.0.1", host: "127.0.0.1", port: ""},
		{addr: "localhost:8080", host: "localhost", port: "8080"},
		{addr: "example.com", host: "example.com", port: ""},
		{addr: "[fe80::1%lo0]:1234", host: "[fe80::1%lo0]", port: "1234"},
		{addr: "[fe80::1%lo0]", host: "[fe80::1%lo0]", port: ""},
		{addr: ":9090", host: "", port: "9090"},
		{addr: " 127.0.0.1:8080 ", host: "127.0.0.1", port: "8080"},
		{addr: "", host: "", port: ""},
	}

	for _, c := range testCases {
		host, port := parseAddr(c.addr)
		require.Equal(t, c.host, host, "addr host: %q", c.addr)
		require.Equal(t, c.port, port, "addr port: %q", c.addr)
	}
}

func Test_Utils_TestConn_Deadline(t *testing.T) {
	t.Parallel()
	conn := &testConn{}
	require.NoError(t, conn.SetDeadline(time.Time{}))
	require.NoError(t, conn.SetReadDeadline(time.Time{}))
	require.NoError(t, conn.SetWriteDeadline(time.Time{}))
}

func Test_Utils_TestConn_ReadWrite(t *testing.T) {
	t.Parallel()
	conn := &testConn{}

	// Verify read of request
	_, err := conn.r.Write([]byte("Request"))
	require.NoError(t, err)

	req := make([]byte, 7)
	_, err = conn.Read(req)
	require.NoError(t, err)
	require.Equal(t, []byte("Request"), req)

	// Verify write of response
	_, err = conn.Write([]byte("Response"))
	require.NoError(t, err)

	res := make([]byte, 8)
	_, err = conn.w.Read(res)
	require.NoError(t, err)
	require.Equal(t, []byte("Response"), res)
}

func Test_Utils_TestConn_Closed_Write(t *testing.T) {
	t.Parallel()
	conn := &testConn{}

	// Verify write of response
	_, err := conn.Write([]byte("Response 1\n"))
	require.NoError(t, err)

	// Close early, write should fail
	conn.Close() //nolint:errcheck // It is fine to ignore the error here
	_, err = conn.Write([]byte("Response 2\n"))
	require.ErrorIs(t, err, errTestConnClosed)

	res := make([]byte, 11)
	_, err = conn.w.Read(res)
	require.NoError(t, err)
	require.Equal(t, []byte("Response 1\n"), res)
}

func Test_Utils_IsNoCache(t *testing.T) {
	t.Parallel()
	testCases := []struct {
		string
		bool
	}{
		{string: "public", bool: false},
		{string: "no-cache", bool: true},
		{string: "public, no-cache, max-age=30", bool: true},
		{string: "public,no-cache", bool: true},
		{string: "public,no-cacheX", bool: false},
		{string: "no-cache, public", bool: true},
		{string: "Xno-cache, public", bool: false},
		{string: "max-age=30, no-cache,public", bool: true},
		{string: "NO-CACHE", bool: true},
		{string: "public, NO-CACHE", bool: true},
		// RFC 9111 §5.2.2.4: no-cache with field-name argument
		{string: "no-cache=\"Set-Cookie\"", bool: true},
		{string: "public, no-cache=\"Set-Cookie, Set-Cookie2\"", bool: true},
		{string: "no-cache=Set-Cookie", bool: true},
		// Edge cases with spaces
		{string: "no-cache ,public", bool: true},
		{string: "public, no-cache =field", bool: true},
	}

	for _, c := range testCases {
		ok := isNoCache(c.string)
		require.Equal(t, c.bool, ok, "want %t, got isNoCache(%s)=%t", c.bool, c.string, ok)
	}
}

// go test -v -run=^$ -bench=Benchmark_Utils_IsNoCache -benchmem -count=4
func Benchmark_Utils_IsNoCache(b *testing.B) {
	var ok bool
	b.ReportAllocs()
	for b.Loop() {
		_ = isNoCache("public")
		_ = isNoCache("no-cache")
		_ = isNoCache("public, no-cache, max-age=30")
		_ = isNoCache("public,no-cache")
		_ = isNoCache("no-cache, public")
		ok = isNoCache("max-age=30, no-cache,public")
	}
	require.True(b, ok)
}

// go test -run Test_HeaderContainsValue
func Test_HeaderContainsValue(t *testing.T) {
	t.Parallel()
	testCases := []struct {
		header   string
		value    string
		expected bool
	}{
		// Exact match
		{header: "gzip", value: "gzip", expected: true},
		{header: "gzip", value: "deflate", expected: false},
		// Prefix match (value at start with comma)
		{header: "gzip, deflate", value: "gzip", expected: true},
		{header: "gzip,deflate", value: "gzip", expected: true},
		// Suffix match (value at end)
		{header: "deflate, gzip", value: "gzip", expected: true},
		{header: "deflate,gzip", value: "gzip", expected: true}, // No space - OWS is optional per RFC 9110
		{header: "br, gzip", value: "gzip", expected: true},
		// Middle match (value in middle)
		{header: "deflate, gzip, br", value: "gzip", expected: true},
		{header: "deflate,gzip,br", value: "gzip", expected: true}, // No spaces - OWS is optional per RFC 9110
		// No match - similar but not equal
		{header: "gzip2", value: "gzip", expected: false},
		{header: "2gzip", value: "gzip", expected: false},
		{header: "gzip2, deflate", value: "gzip", expected: false},
		// Whitespace handling (OWS per RFC 9110)
		{header: "  gzip  ,  deflate  ", value: "gzip", expected: true},
		{header: "deflate,  gzip  ", value: "gzip", expected: true},
		// Empty cases
		{header: "", value: "gzip", expected: false},
		{header: "gzip", value: "", expected: false},
		{header: "", value: "", expected: false}, // Both empty - should return false
	}

	for _, tc := range testCases {
		result := headerContainsValue(tc.header, tc.value)
		require.Equal(t, tc.expected, result,
			"headerContainsValue(%q, %q) = %v, want %v",
			tc.header, tc.value, result, tc.expected)
	}
}

// go test -v -run=^$ -bench=Benchmark_HeaderContainsValue -benchmem -count=4
func Benchmark_HeaderContainsValue(b *testing.B) {
	var ok bool
	b.ReportAllocs()
	for b.Loop() {
		_ = headerContainsValue("gzip", "gzip")
		_ = headerContainsValue("gzip, deflate, br", "deflate")
		_ = headerContainsValue("deflate, gzip", "gzip")
		ok = headerContainsValue("deflate, gzip, br", "gzip")
	}
	require.True(b, ok)
}

type testGenericParseTypeIntCase struct {
	value int64
	bits  int
}

// go test -run Test_GenericParseTypeInts
func Test_GenericParseTypeInts(t *testing.T) {
	t.Parallel()
	ints := []testGenericParseTypeIntCase{
		{
			value: 0,
			bits:  8,
		},
		{
			value: 1,
			bits:  8,
		},
		{
			value: 2,
			bits:  8,
		},
		{
			value: 3,
			bits:  8,
		},
		{
			value: 4,
			bits:  8,
		},
		{
			value: -1,
			bits:  8,
		},
		{
			value: math.MaxInt8,
			bits:  8,
		},
		{
			value: math.MinInt8,
			bits:  8,
		},
		{
			value: math.MaxInt16,
			bits:  16,
		},
		{
			value: math.MinInt16,
			bits:  16,
		},
		{
			value: math.MaxInt32,
			bits:  32,
		},
		{
			value: math.MinInt32,
			bits:  32,
		},
		{
			value: math.MaxInt64,
			bits:  64,
		},
		{
			value: math.MinInt64,
			bits:  64,
		},
	}

	testGenericTypeInt[int8](t, "test_genericParseTypeInt8s", ints)
	testGenericTypeInt[int16](t, "test_genericParseTypeInt16s", ints)
	testGenericTypeInt[int32](t, "test_genericParseTypeInt32s", ints)
	testGenericTypeInt[int64](t, "test_genericParseTypeInt64s", ints)
	testGenericTypeInt[int](t, "test_genericParseTypeInts", ints)
}

func testGenericTypeInt[V GenericTypeInteger](t *testing.T, name string, cases []testGenericParseTypeIntCase) {
	t.Helper()
	t.Run(name, func(t *testing.T) {
		t.Parallel()
		for _, test := range cases {
			v, err := genericParseType[V](strconv.FormatInt(test.value, 10))
			if test.bits <= int(unsafe.Sizeof(V(0)))*8 {
				require.NoError(t, err)
				require.Equal(t, V(test.value), v)
			} else {
				require.ErrorIs(t, err, strconv.ErrRange)
			}
		}
		testGenericParseError[V](t)
	})
}

type testGenericParseTypeUintCase struct {
	value uint64
	bits  int
}

// go test -run Test_GenericParseTypeUints
func Test_GenericParseTypeUints(t *testing.T) {
	t.Parallel()
	uints := []testGenericParseTypeUintCase{
		{
			value: 0,
			bits:  8,
		},
		{
			value: 1,
			bits:  8,
		},
		{
			value: 2,
			bits:  8,
		},
		{
			value: 3,
			bits:  8,
		},
		{
			value: 4,
			bits:  8,
		},
		{
			value: math.MaxUint8,
			bits:  8,
		},
		{
			value: math.MaxUint16,
			bits:  16,
		},
		{
			value: math.MaxUint32,
			bits:  32,
		},
		{
			value: math.MaxUint64,
			bits:  64,
		},
	}

	testGenericTypeUint[uint8](t, "test_genericParseTypeUint8s", uints)
	testGenericTypeUint[uint16](t, "test_genericParseTypeUint16s", uints)
	testGenericTypeUint[uint32](t, "test_genericParseTypeUint32s", uints)
	testGenericTypeUint[uint64](t, "test_genericParseTypeUint64s", uints)
	testGenericTypeUint[uint](t, "test_genericParseTypeUints", uints)
}

func testGenericTypeUint[V GenericTypeInteger](t *testing.T, name string, cases []testGenericParseTypeUintCase) {
	t.Helper()
	t.Run(name, func(t *testing.T) {
		t.Parallel()
		for _, test := range cases {
			v, err := genericParseType[V](strconv.FormatUint(test.value, 10))
			if test.bits <= int(unsafe.Sizeof(V(0)))*8 {
				require.NoError(t, err)
				require.Equal(t, V(test.value), v)
			} else {
				require.ErrorIs(t, err, strconv.ErrRange)
			}
		}
		testGenericParseError[V](t)
	})
}

// go test -run Test_GenericParseTypeFloats
func Test_GenericParseTypeFloats(t *testing.T) {
	t.Parallel()

	floats := []struct {
		str   string
		value float64
	}{
		{
			value: 3.1415,
			str:   "3.1415",
		},
		{
			value: 1.234,
			str:   "1.234",
		},
		{
			value: 2,
			str:   "2",
		},
		{
			value: 3,
			str:   "3",
		},
	}

	t.Run("test_genericParseTypeFloat32s", func(t *testing.T) {
		t.Parallel()
		for _, test := range floats {
			v, err := genericParseType[float32](test.str)
			require.NoError(t, err)
			require.InEpsilon(t, float32(test.value), v, epsilon)
		}
		testGenericParseError[float32](t)
	})

	t.Run("test_genericParseTypeFloat64s", func(t *testing.T) {
		t.Parallel()
		for _, test := range floats {
			v, err := genericParseType[float64](test.str)
			require.NoError(t, err)
			require.InEpsilon(t, test.value, v, epsilon)
		}
		testGenericParseError[float64](t)
	})
}

// go test -run Test_GenericParseTypeBytes
func Test_GenericParseTypeBytes(t *testing.T) {
	t.Parallel()

	cases := []struct {
		str   string
		err   error
		value []byte
	}{
		{
			value: []byte("alex"),
			str:   "alex",
		},
		{
			value: []byte("32.23"),
			str:   "32.23",
		},
		{
			value: []byte("john"),
			str:   "john",
		},
		{
			value: []byte(nil),
			str:   "",
			err:   errParsedEmptyBytes,
		},
	}

	t.Run("test_genericParseTypeBytes", func(t *testing.T) {
		t.Parallel()
		for _, test := range cases {
			v, err := genericParseType[[]byte](test.str)
			if test.err == nil {
				require.NoError(t, err)
			} else {
				require.ErrorIs(t, err, test.err)
			}
			require.Equal(t, test.value, v)
		}
	})
}

// go test -run Test_GenericParseTypeString
func Test_GenericParseTypeString(t *testing.T) {
	t.Parallel()

	tests := []string{"john", "doe", "hello", "fiber"}

	for _, test := range tests {
		t.Run("test_genericParseTypeString", func(t *testing.T) {
			t.Parallel()
			v, err := genericParseType[string](test)
			require.NoError(t, err)
			require.Equal(t, test, v)
		})
	}
}

// go test -run Test_GenericParseTypeBoolean
func Test_GenericParseTypeBoolean(t *testing.T) {
	t.Parallel()

	bools := []struct {
		str   string
		value bool
	}{
		{
			str:   "True",
			value: true,
		},
		{
			str:   "False",
			value: false,
		},
		{
			str:   "true",
			value: true,
		},
		{
			str:   "false",
			value: false,
		},
	}

	t.Run("test_genericParseTypeBoolean", func(t *testing.T) {
		t.Parallel()
		for _, test := range bools {
			v, err := genericParseType[bool](test.str)
			require.NoError(t, err)
			if test.value {
				require.True(t, v)
			} else {
				require.False(t, v)
			}
		}
		testGenericParseError[bool](t)
	})
}

func testGenericParseError[V GenericType](t *testing.T) {
	t.Helper()
	var expected V
	v, err := genericParseType[V]("invalid-string")
	require.Error(t, err)
	require.Equal(t, expected, v)
}

// go test -v -run=^$ -bench=Benchmark_GenericParseTypeInts -benchmem -count=4
func Benchmark_GenericParseTypeInts(b *testing.B) {
	b.Skip("Skipped: too fast to compare reliably (results in sub-ns range are unstable)")
	ints := []testGenericParseTypeIntCase{
		{
			value: 0,
			bits:  8,
		},
		{
			value: 1,
			bits:  8,
		},
		{
			value: 2,
			bits:  8,
		},
		{
			value: 3,
			bits:  8,
		},
		{
			value: 4,
			bits:  8,
		},
		{
			value: -1,
			bits:  8,
		},
		{
			value: math.MaxInt8,
			bits:  8,
		},
		{
			value: math.MinInt8,
			bits:  8,
		},
		{
			value: math.MaxInt16,
			bits:  16,
		},
		{
			value: math.MinInt16,
			bits:  16,
		},
		{
			value: math.MaxInt32,
			bits:  32,
		},
		{
			value: math.MinInt32,
			bits:  32,
		},
		{
			value: math.MaxInt64,
			bits:  64,
		},
		{
			value: math.MinInt64,
			bits:  64,
		},
	}
	for _, test := range ints {
		benchGenericParseTypeInt[int8](b, "bench_genericParseTypeInt8s", test)
		benchGenericParseTypeInt[int16](b, "bench_genericParseTypeInt16s", test)
		benchGenericParseTypeInt[int32](b, "bench_genericParseTypeInt32s", test)
		benchGenericParseTypeInt[int64](b, "bench_genericParseTypeInt64s", test)
		benchGenericParseTypeInt[int](b, "bench_genericParseTypeInts", test)
	}
}

func benchGenericParseTypeInt[V GenericTypeInteger](b *testing.B, name string, test testGenericParseTypeIntCase) {
	b.Helper()
	b.Run(name, func(t *testing.B) {
		var v V
		var err error
		b.ReportAllocs()
		b.ResetTimer()
		b.RunParallel(func(pb *testing.PB) {
			for pb.Next() {
				v, err = genericParseType[V](strconv.FormatInt(test.value, 10))
			}
		})
		if test.bits <= int(unsafe.Sizeof(V(0)))*8 {
			require.NoError(t, err)
			require.Equal(t, V(test.value), v)
		} else {
			require.ErrorIs(t, err, strconv.ErrRange)
		}
	})
}

// go test -v -run=^$ -bench=Benchmark_GenericParseTypeUints -benchmem -count=4
func Benchmark_GenericParseTypeUints(b *testing.B) {
	b.Skip("Skipped: too fast to compare reliably (results in sub-ns range are unstable)")
	uints := []struct {
		value uint64
		bits  int
	}{
		{
			value: 0,
			bits:  8,
		},
		{
			value: 1,
			bits:  8,
		},
		{
			value: 2,
			bits:  8,
		},
		{
			value: 3,
			bits:  8,
		},
		{
			value: 4,
			bits:  8,
		},
		{
			value: math.MaxUint8,
			bits:  8,
		},
		{
			value: math.MaxUint16,
			bits:  16,
		},
		{
			value: math.MaxUint16,
			bits:  16,
		},
		{
			value: math.MaxUint32,
			bits:  32,
		},
		{
			value: math.MaxUint64,
			bits:  64,
		},
	}

	for _, test := range uints {
		benchGenericParseTypeUInt[uint8](b, "benchmark_genericParseTypeUint8s", test)
		benchGenericParseTypeUInt[uint16](b, "benchmark_genericParseTypeUint16s", test)
		benchGenericParseTypeUInt[uint32](b, "benchmark_genericParseTypeUint32s", test)
		benchGenericParseTypeUInt[uint64](b, "benchmark_genericParseTypeUint64s", test)
		benchGenericParseTypeUInt[uint](b, "benchmark_genericParseTypeUints", test)
	}
}

func benchGenericParseTypeUInt[V GenericTypeInteger](b *testing.B, name string, test testGenericParseTypeUintCase) {
	b.Helper()
	b.Run(name, func(t *testing.B) {
		var v V
		var err error
		b.ReportAllocs()
		b.ResetTimer()
		b.RunParallel(func(pb *testing.PB) {
			for pb.Next() {
				v, err = genericParseType[V](strconv.FormatUint(test.value, 10))
			}
		})
		if test.bits <= int(unsafe.Sizeof(V(0)))*8 {
			require.NoError(t, err)
			require.Equal(t, V(test.value), v)
		} else {
			require.ErrorIs(t, err, strconv.ErrRange)
		}
	})
}

// go test -v -run=^$ -bench=Benchmark_GenericParseTypeFloats -benchmem -count=4
func Benchmark_GenericParseTypeFloats(b *testing.B) {
	b.Skip("Skipped: too fast to compare reliably (results in sub-ns range are unstable)")
	floats := []struct {
		str   string
		value float64
	}{
		{
			value: 3.1415,
			str:   "3.1415",
		},
		{
			value: 1.234,
			str:   "1.234",
		},
		{
			value: 2,
			str:   "2",
		},
		{
			value: 3,
			str:   "3",
		},
	}

	for _, test := range floats {
		b.Run("benchmark_genericParseTypeFloat32s", func(t *testing.B) {
			var v float32
			var err error
			b.ReportAllocs()
			b.ResetTimer()
			b.RunParallel(func(pb *testing.PB) {
				for pb.Next() {
					v, err = genericParseType[float32](test.str)
				}
			})
			require.NoError(t, err)
			require.InEpsilon(t, float32(test.value), v, epsilon)
		})
	}

	for _, test := range floats {
		b.Run("benchmark_genericParseTypeFloat64s", func(t *testing.B) {
			var v float64
			var err error
			b.ReportAllocs()
			b.ResetTimer()
			b.RunParallel(func(pb *testing.PB) {
				for pb.Next() {
					v, err = genericParseType[float64](test.str)
				}
			})
			require.NoError(t, err)
			require.InEpsilon(t, test.value, v, epsilon)
		})
	}
}

// go test -v -run=^$ -bench=Benchmark_GenericParseTypeBytes -benchmem -count=4
func Benchmark_GenericParseTypeBytes(b *testing.B) {
	b.Skip("Skipped: too fast to compare reliably (results in sub-ns range are unstable)")
	cases := []struct {
		str   string
		err   error
		value []byte
	}{
		{
			value: []byte("alex"),
			str:   "alex",
		},
		{
			value: []byte("32.23"),
			str:   "32.23",
		},
		{
			value: []byte("john"),
			str:   "john",
		},
		{
			value: []byte(nil),
			str:   "",
			err:   errParsedEmptyBytes,
		},
	}

	for _, test := range cases {
		b.Run("benchmark_genericParseTypeBytes", func(b *testing.B) {
			var v []byte
			var err error
			b.ReportAllocs()
			b.ResetTimer()
			b.RunParallel(func(pb *testing.PB) {
				for pb.Next() {
					v, err = genericParseType[[]byte](test.str)
				}
			})
			if test.err == nil {
				require.NoError(b, err)
			} else {
				require.ErrorIs(b, err, test.err)
			}
			require.Equal(b, test.value, v)
		})
	}
}

// go test -v -run=^$ -bench=Benchmark_GenericParseTypeString -benchmem -count=4
func Benchmark_GenericParseTypeString(b *testing.B) {
	b.Skip("Skipped: too fast to compare reliably (results in sub-ns range are unstable)")
	tests := []string{"john", "doe", "hello", "fiber"}

	for _, test := range tests {
		b.Run("benchmark_genericParseTypeString", func(b *testing.B) {
			var v string
			var err error
			b.ReportAllocs()
			b.ResetTimer()
			b.RunParallel(func(pb *testing.PB) {
				for pb.Next() {
					v, err = genericParseType[string](test)
				}
			})
			require.NoError(b, err)
			require.Equal(b, test, v)
		})
	}
}

// go test -v -run=^$ -bench=Benchmark_GenericParseTypeBoolean -benchmem -count=4
func Benchmark_GenericParseTypeBoolean(b *testing.B) {
	b.Skip("Skipped: too fast to compare reliably (results in sub-ns range are unstable)")
	bools := []struct {
		str   string
		value bool
	}{
		{
			str:   "True",
			value: true,
		},
		{
			str:   "False",
			value: false,
		},
		{
			str:   "true",
			value: true,
		},
		{
			str:   "false",
			value: false,
		},
	}

	for _, test := range bools {
		b.Run("benchmark_genericParseTypeBoolean", func(b *testing.B) {
			var v bool
			var err error
			b.ReportAllocs()
			b.ResetTimer()
			b.RunParallel(func(pb *testing.PB) {
				for pb.Next() {
					v, err = genericParseType[bool](test.str)
				}
			})
			require.NoError(b, err)
			if test.value {
				require.True(b, v)
			} else {
				require.False(b, v)
			}
		})
	}
}

func Test_UnescapeHeaderValue(t *testing.T) {
	t.Parallel()
	cases := []struct {
		in  string
		out []byte
		ok  bool
	}{
		{in: "abc", out: []byte("abc"), ok: true},
		{in: "a\\\"b", out: []byte("a\"b"), ok: true},
		{in: "c\\\\d", out: []byte("c\\d"), ok: true},
		{in: "bad\\", ok: false},
	}
	for _, tc := range cases {
		out, err := unescapeHeaderValue([]byte(tc.in))
		if tc.ok {
			require.NoError(t, err, tc.in)
			require.Equal(t, tc.out, out, tc.in)
		} else {
			require.Error(t, err, tc.in)
		}
	}
}

func Test_JoinHeaderValues(t *testing.T) {
	t.Parallel()
	require.Nil(t, joinHeaderValues(nil))
	require.Equal(t, []byte("a"), joinHeaderValues([][]byte{[]byte("a")}))
	require.Equal(t, []byte("a,b"), joinHeaderValues([][]byte{[]byte("a"), []byte("b")}))
}

func Test_ParamsMatch_InvalidEscape(t *testing.T) {
	t.Parallel()
	match := paramsMatch(headerParams{"foo": []byte("bar")}, `;foo="bar\\`)
	require.False(t, match)
}

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

	require.True(t, matchEtag(`"a"`, `"a"`))
	require.True(t, matchEtag(`W/"a"`, `"a"`))
	require.True(t, matchEtag(`"a"`, `W/"a"`))
	require.False(t, matchEtag(`"a"`, `"b"`))
	require.False(t, matchEtag(`a`, `"a"`))
	require.False(t, matchEtag(`"a"`, `b`))
}

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

	require.True(t, matchEtagStrong(`"a"`, `"a"`))
	require.False(t, matchEtagStrong(`W/"a"`, `"a"`))
	require.False(t, matchEtagStrong(`"a"`, `W/"a"`))
	require.False(t, matchEtagStrong(`"a"`, `"b"`))
	require.False(t, matchEtagStrong(`a`, `"a"`))
	require.False(t, matchEtagStrong(`"a"`, `b`))
}

func Test_IsEtagStale(t *testing.T) {
	t.Parallel()
	app := New()

	// Invalid/unquoted tags are considered a mismatch, so it's stale
	require.True(t, app.isEtagStale(`"a"`, []byte("b")))
	require.True(t, app.isEtagStale(`"a"`, []byte("a")))

	// Matching tags, not stale
	require.False(t, app.isEtagStale(`"a"`, []byte(`"a"`)))
	require.False(t, app.isEtagStale(`W/"a"`, []byte(`"a"`)))

	// List of tags, not stale
	require.False(t, app.isEtagStale(`"c"`, []byte(`"a", "b", "c"`)))
	require.False(t, app.isEtagStale(`W/"c"`, []byte(`"a", "b", "c"`)))
	require.False(t, app.isEtagStale(`"c"`, []byte(`"a", "b", W/"c"`)))
	require.False(t, app.isEtagStale(`"c"`, []byte(`"c", "b", "a"`)))
	require.False(t, app.isEtagStale(`"c"`, []byte(`  "a",   "c"   , "b"  `)))

	// List of tags, stale
	require.True(t, app.isEtagStale(`"d"`, []byte(`"a", "b", "c"`)))
	require.True(t, app.isEtagStale(`W/"d"`, []byte(`"a", "b", "c"`)))

	// Wildcard
	require.False(t, app.isEtagStale(`"a"`, []byte("*")))
	require.False(t, app.isEtagStale(`"a"`, []byte(" *   ")))
	require.False(t, app.isEtagStale(`W/"a"`, []byte("*")))

	// Empty case
	require.True(t, app.isEtagStale(`"a"`, []byte("")))
	require.True(t, app.isEtagStale(`"a"`, []byte("   ")))

	// Weak vs. weak
	require.False(t, app.isEtagStale(`W/"a"`, []byte(`W/"a"`)))
}

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

	cases := []struct {
		name string
		in   string
		out  string
	}{
		{"empty", "", ""},
		{"simple", "simple", "simple"},
		{"backslash", "A\\B", "A\\\\B"},
		{"quote", `He said "Yo"`, `He said \"Yo\"`},
		{"newline", "Hello\n", "Hello\\n"},
		{"carriage", "Hello\r", "Hello\\r"},
		{"controls", string([]byte{0, 31, 127}), "%00%1F%7F"},
		{"mixed", "test \"A\n\r" + string([]byte{1}) + "\\", `test \"A\n\r%01\\`},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			app := New()
			require.Equal(t, tc.out, app.quoteRawString(tc.in))
		})
	}
}

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

	app := New(Config{PassLocalsToContext: true})
	raw := &fasthttp.RequestCtx{}
	c := app.AcquireCtx(raw)
	defer app.ReleaseCtx(c)

	StoreInContext(c, "key", "value")

	localValue, ok := c.Locals("key").(string)
	require.True(t, ok)
	require.Equal(t, "value", localValue)

	contextValue, ok := c.Context().Value("key").(string)
	require.True(t, ok)
	require.Equal(t, "value", contextValue)
}

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

	t.Run("fiber.Ctx", func(t *testing.T) {
		t.Parallel()

		app := New()
		raw := &fasthttp.RequestCtx{}
		c := app.AcquireCtx(raw)
		defer app.ReleaseCtx(c)

		c.Locals("key", "value")

		value, ok := ValueFromContext[string](c, "key")
		require.True(t, ok)
		require.Equal(t, "value", value)
	})

	t.Run("released fiber.Ctx", func(t *testing.T) {
		t.Parallel()

		app := New()
		raw := &fasthttp.RequestCtx{}
		c := app.AcquireCtx(raw)

		c.Locals("key", "value")

		valueBeforeRelease, okBeforeRelease := ValueFromContext[string](c, "key")
		require.True(t, okBeforeRelease)
		require.Equal(t, "value", valueBeforeRelease)

		app.ReleaseCtx(c)

		require.NotPanics(t, func() {
			valueAfterRelease, okAfterRelease := ValueFromContext[string](c, "key")
			require.False(t, okAfterRelease)
			require.Empty(t, valueAfterRelease)
		})
	})

	t.Run("fiber.CustomCtx", func(t *testing.T) {
		t.Parallel()

		app := NewWithCustomCtx(func(app *App) CustomCtx {
			return &customCtx{DefaultCtx: *NewDefaultCtx(app)}
		})
		raw := &fasthttp.RequestCtx{}
		c := app.AcquireCtx(raw)
		defer app.ReleaseCtx(c)

		c.Locals("key", "value")

		value, ok := ValueFromContext[string](c, "key")
		require.True(t, ok)
		require.Equal(t, "value", value)
	})

	t.Run("released fiber.CustomCtx", func(t *testing.T) {
		t.Parallel()

		app := NewWithCustomCtx(func(app *App) CustomCtx {
			return &customCtx{DefaultCtx: *NewDefaultCtx(app)}
		})
		raw := &fasthttp.RequestCtx{}
		c := app.AcquireCtx(raw)

		c.Locals("key", "value")

		valueBeforeRelease, okBeforeRelease := ValueFromContext[string](c, "key")
		require.True(t, okBeforeRelease)
		require.Equal(t, "value", valueBeforeRelease)

		app.ReleaseCtx(c)

		require.NotPanics(t, func() {
			valueAfterRelease, okAfterRelease := ValueFromContext[string](c, "key")
			require.False(t, okAfterRelease)
			require.Empty(t, valueAfterRelease)
		})
	})

	t.Run("fasthttp request ctx", func(t *testing.T) {
		t.Parallel()

		raw := &fasthttp.RequestCtx{}
		raw.SetUserValue("key", "value")

		value, ok := ValueFromContext[string](raw, "key")
		require.True(t, ok)
		require.Equal(t, "value", value)
	})

	t.Run("context.Context", func(t *testing.T) {
		t.Parallel()

		type testContextKey struct{}

		ctx := context.WithValue(context.Background(), testContextKey{}, "value")

		value, ok := ValueFromContext[string](ctx, testContextKey{})
		require.True(t, ok)
		require.Equal(t, "value", value)
	})

	t.Run("unsupported ctx", func(t *testing.T) {
		t.Parallel()

		value, ok := ValueFromContext[string](42, "key")
		require.False(t, ok)
		require.Empty(t, value)
	})
}
