package strings

import (
	"fmt"
	stdstrings "strings"
	"testing"

	"github.com/gofiber/utils/v2/internal/unsafeconv"
	"github.com/stretchr/testify/require"
)

type benchCase struct {
	name  string
	input string
	lower string
	upper string
}

var testCases = []benchCase{
	{name: "empty", input: "", upper: "", lower: ""},
	{name: "numbers", input: "1234567890", upper: "1234567890", lower: "1234567890"},
	{name: "symbols", input: "!@#$%^&*()", upper: "!@#$%^&*()", lower: "!@#$%^&*()"},
	{name: "single-lower", input: "a", upper: "A", lower: "a"},
	{name: "single-upper", input: "A", upper: "A", lower: "a"},
	{name: "lowercase", input: "abcdefghijklmnopqrstuvwxyz", upper: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", lower: "abcdefghijklmnopqrstuvwxyz"},
	{name: "uppercase", input: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", upper: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", lower: "abcdefghijklmnopqrstuvwxyz"},
	{name: "mixed-case", input: "aBcDeFgHiJkLmNoPqRsTuVwXyZ", upper: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", lower: "abcdefghijklmnopqrstuvwxyz"},
	{name: "non-ascii", input: "µßäöü", upper: "µßäöü", lower: "µßäöü"},
	{name: "mixed-ascii-non-ascii", input: "Goµ", upper: "GOµ", lower: "goµ"},
}

var benchmarkCoreCases = []benchCase{
	{name: "empty", input: "", upper: "", lower: ""},
	{name: "http-get", input: "get", upper: "GET", lower: "get"},
	{name: "http-get-upper", input: "GET", upper: "GET", lower: "get"},
	{
		name:  "header-content-type-mixed",
		input: "Content-Type: text/html; charset=utf-8",
		upper: "CONTENT-TYPE: TEXT/HTML; CHARSET=UTF-8",
		lower: "content-type: text/html; charset=utf-8",
	},
	{name: "large-lower", input: stdstrings.Repeat("a", 64), upper: stdstrings.Repeat("A", 64), lower: stdstrings.Repeat("a", 64)},
	{name: "large-upper", input: stdstrings.Repeat("A", 64), upper: stdstrings.Repeat("A", 64), lower: stdstrings.Repeat("a", 64)},
	{name: "large-mixed", input: stdstrings.Repeat("aB", 32), upper: stdstrings.Repeat("AB", 32), lower: stdstrings.Repeat("ab", 32)},
}

func Test_ToUpper(t *testing.T) {
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			require.Equal(t, tc.upper, ToUpper(tc.input))
		})
	}
}

func Test_ToLower(t *testing.T) {
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			require.Equal(t, tc.lower, ToLower(tc.input))
		})
	}
}

func Test_ASCII_EdgeCases(t *testing.T) {
	t.Parallel()
	for idx := range 128 {
		c := byte(idx)
		t.Run(fmt.Sprintf("ASCII-char-%03d", idx), func(t *testing.T) {
			t.Parallel()
			s := string(c)
			require.Equal(t, stdstrings.ToUpper(s), ToUpper(s))
			require.Equal(t, stdstrings.ToLower(s), ToLower(s))
		})
	}
}

func Test_ToLower_NoMutation(t *testing.T) {
	in := "Content-Type"
	out := ToLower(in)
	require.Equal(t, "content-type", out)
	require.Equal(t, "Content-Type", in)
}

func Test_ToUpper_NoMutation(t *testing.T) {
	in := "content-type"
	out := ToUpper(in)
	require.Equal(t, "CONTENT-TYPE", out)
	require.Equal(t, "content-type", in)
}

func TestUnsafeToLower_Mutates(t *testing.T) {
	buf := []byte("Content-Type")
	s := unsafeconv.UnsafeString(buf)
	out := UnsafeToLower(s)
	require.Equal(t, "content-type", out)
	require.Equal(t, "content-type", string(buf))
}

func TestUnsafeToUpper_Mutates(t *testing.T) {
	buf := []byte("content-type")
	s := unsafeconv.UnsafeString(buf)
	out := UnsafeToUpper(s)
	require.Equal(t, "CONTENT-TYPE", out)
	require.Equal(t, "CONTENT-TYPE", string(buf))
}

func Benchmark_ToUpper(b *testing.B) {
	for _, tc := range benchmarkCoreCases {
		b.Run(tc.name, func(b *testing.B) {
			b.ReportAllocs()
			b.SetBytes(int64(len(tc.input)))
			var fiberRes, stdRes string
			b.Run("fiber", func(b *testing.B) {
				for i := 0; i < b.N; i++ {
					fiberRes = ToUpper(tc.input)
				}
				require.Equal(b, tc.upper, fiberRes)
			})
			b.Run("fiber/unsafe", func(b *testing.B) {
				template := []byte(tc.input)
				work := make([]byte, len(template))
				for i := 0; i < b.N; i++ {
					copy(work, template)
					fiberRes = UnsafeToUpper(unsafeconv.UnsafeString(work))
				}
				require.Equal(b, tc.upper, fiberRes)
			})
			b.Run("default", func(b *testing.B) {
				for i := 0; i < b.N; i++ {
					stdRes = stdstrings.ToUpper(tc.input)
				}
				require.Equal(b, tc.upper, stdRes)
			})
		})
	}
}

func Benchmark_ToLower(b *testing.B) {
	for _, tc := range benchmarkCoreCases {
		b.Run(tc.name, func(b *testing.B) {
			b.ReportAllocs()
			b.SetBytes(int64(len(tc.input)))
			var fiberRes, stdRes string
			b.Run("fiber", func(b *testing.B) {
				for i := 0; i < b.N; i++ {
					fiberRes = ToLower(tc.input)
				}
				require.Equal(b, tc.lower, fiberRes)
			})
			b.Run("fiber/unsafe", func(b *testing.B) {
				template := []byte(tc.input)
				work := make([]byte, len(template))
				for i := 0; i < b.N; i++ {
					copy(work, template)
					fiberRes = UnsafeToLower(unsafeconv.UnsafeString(work))
				}
				require.Equal(b, tc.lower, fiberRes)
			})
			b.Run("default", func(b *testing.B) {
				for i := 0; i < b.N; i++ {
					stdRes = stdstrings.ToLower(tc.input)
				}
				require.Equal(b, tc.lower, stdRes)
			})
		})
	}
}

// Test_Case_FirstChangePositions plants the single case-changing byte at
// every position of every length up to 100, covering the caseconv scan's
// 32-byte block loop, the word loop behind it, the overlapping tail, and the
// identity path, against a byte-wise reference.
func Test_Case_FirstChangePositions(t *testing.T) {
	t.Parallel()
	refLower := func(s string) string {
		b := []byte(s)
		for i := range b {
			if b[i] >= 'A' && b[i] <= 'Z' {
				b[i] += 'a' - 'A'
			}
		}
		return string(b)
	}
	refUpper := func(s string) string {
		b := []byte(s)
		for i := range b {
			if b[i] >= 'a' && b[i] <= 'z' {
				b[i] -= 'a' - 'A'
			}
		}
		return string(b)
	}
	for n := 0; n <= 100; n++ {
		base := make([]byte, n)
		for i := range base {
			base[i] = "40-\xe9/~:9"[i%8] // no letters at all, incl. >= 0x80
		}
		// Identity inputs: both directions must return the input itself
		// (the scans' no-match fast path), not an equal copy.
		s := string(base)
		lower, upper := ToLower(s), ToUpper(s)
		require.Equal(t, s, lower, "identity lower len %d", n)
		require.Equal(t, s, upper, "identity upper len %d", n)
		if n > 0 {
			sb, lb, ub := unsafeconv.UnsafeBytes(s), unsafeconv.UnsafeBytes(lower), unsafeconv.UnsafeBytes(upper)
			require.Same(t, &sb[0], &lb[0], "identity lower must return the input, len %d", n)
			require.Same(t, &sb[0], &ub[0], "identity upper must return the input, len %d", n)
		}
		for at := 0; at < n; at++ {
			b := append([]byte(nil), base...)
			b[at] = 'Q'
			s := string(b)
			require.Equal(t, refLower(s), ToLower(s), "lower len %d at %d", n, at)
			require.Equal(t, refUpper(s), ToUpper(s), "upper len %d at %d", n, at)
			b[at] = 'q'
			s = string(b)
			require.Equal(t, refLower(s), ToLower(s), "lower len %d at %d", n, at)
			require.Equal(t, refUpper(s), ToUpper(s), "upper len %d at %d", n, at)
		}
	}
}

// Test_Case_PathCoverage exercises every dispatch path of the string case
// converters in-package: sub-word inputs, word-path inputs, identity
// returns, and the in-place variants across both length regimes.
func Test_Case_PathCoverage(t *testing.T) {
	t.Parallel()

	cases := []struct{ in, lower, upper string }{
		{"", "", ""},
		{"a", "a", "A"},
		{"AbC", "abc", "ABC"},
		{"\xe9!z", "\xe9!z", "\xe9!Z"},
		{"abcdefgh", "abcdefgh", "ABCDEFGH"},
		{"nochangehere-1234", "nochangehere-1234", "NOCHANGEHERE-1234"},
		{"aaaaaaaaaXbbbb", "aaaaaaaaaxbbbb", "AAAAAAAAAXBBBB"},
		{"aaaaaaaaaaaaY", "aaaaaaaaaaaay", "AAAAAAAAAAAAY"},
	}
	for _, tc := range cases {
		require.Equal(t, tc.lower, ToLower(tc.in), "ToLower %q", tc.in)
		require.Equal(t, tc.upper, ToUpper(tc.in), "ToUpper %q", tc.in)

		lowerBuf := []byte(tc.in)
		require.Equal(t, tc.lower, UnsafeToLower(unsafeconv.UnsafeString(lowerBuf)), "UnsafeToLower %q", tc.in)
		upperBuf := []byte(tc.in)
		require.Equal(t, tc.upper, UnsafeToUpper(unsafeconv.UnsafeString(upperBuf)), "UnsafeToUpper %q", tc.in)
	}

	// Unchanged inputs come back as-is (the fast identity paths).
	require.Equal(t, "short", ToLower("short"))
	require.Equal(t, "SHORT", ToUpper("SHORT"))
	require.Equal(t, "already lower, one word plus", ToLower("already lower, one word plus"))
	require.Equal(t, "ALREADY UPPER, ONE WORD PLUS", ToUpper("ALREADY UPPER, ONE WORD PLUS"))
}
