//go:build !(js && wasm)

package validation //nolint:testpackage

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

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

	type TestCase struct {
		Input string
		Want  string
	}

	testCases := []TestCase{
		{
			Input: "required",
			Want:  "required",
		},
		{
			// Only whitespace is trimmed; casing is preserved
			Input: "  alphanumeric  ",
			Want:  "alphanumeric",
		},
		{
			// Casing is preserved as-is
			Input: "REQUIRED",
			Want:  "REQUIRED",
		},
		{
			Input: `regex("^[A-Z]{2}-\d{4}$")`,
			Want:  `regex("^[A-Z]{2}-\d{4}$")`,
		},
		{
			Input: "length(1,3)",
			Want:  "length(1,3)",
		},
	}

	for _, tc := range testCases {
		got := normalizeRuleString(tc.Input)

		assert.Equal(t, tc.Want, got)
	}
}

// normalizeAndConvert is a test helper that normalizes a rule string and converts it
// to a CustomValidationRule in one step.
func normalizeAndConvert(ruleString string) (CustomValidationRule, error) {
	return convertSingleValidationRule(normalizeRuleString(ruleString))
}

// TestConvertSingleValidationRule_Regex tests normalizeRuleString + convertSingleValidationRule
// for regex rules. The production path goes through UnmarshalValidationsField, but these tests
// verify the string-parsing pipeline independently.
func TestConvertSingleValidationRule_Regex(t *testing.T) {
	t.Parallel()

	// Backtick-quoted rule inputs use regular Go string literals (not raw literals)
	// because the rule itself contains backtick characters. The \\d sequences become
	// \d at runtime.
	validCases := []struct {
		name          string
		ruleInput     string
		expectedArgs  []any
		validValues   []string
		invalidValues []string
	}{
		{
			name:          "lowercase alphanumeric double-quoted pattern",
			ruleInput:     `regex("^[a-z0-9]+$")`,
			expectedArgs:  []any{"^[a-z0-9]+$"},
			validValues:   []string{"hello123"},
			invalidValues: []string{"Hello!"},
		},
		{
			name:          "case-sensitive backtick-quoted pattern",
			ruleInput:     "regex(`^[A-Z]{2}-\\d{4}$`)",
			expectedArgs:  []any{`^[A-Z]{2}-\d{4}$`},
			validValues:   []string{"AB-1234"},
			invalidValues: []string{"ab-1234"},
		},
		{
			name:          "double-quoted pattern with backslash shorthand",
			ruleInput:     `regex("^[A-Z]{2}-\d{4}$")`,
			expectedArgs:  []any{`^[A-Z]{2}-\d{4}$`},
			validValues:   []string{"AB-1234"},
			invalidValues: []string{"AB-XXXX"},
		},
		{
			name:          "pattern with spaces",
			ruleInput:     `regex("^[a-z ]+$")`,
			expectedArgs:  []any{"^[a-z ]+$"},
			validValues:   []string{"hello world"},
			invalidValues: []string{"Hello123"},
		},
		{
			name:          "double-quoted pattern with escaped quotes",
			ruleInput:     "regex(`They said: \"Hello world!\"`)",
			expectedArgs:  []any{`They said: "Hello world!"`},
			validValues:   []string{`They said: "Hello world!"`},
			invalidValues: []string{`They said something else`},
		},
		{
			name:          "double-quoted pattern with escaped quotes",
			ruleInput:     `regex("They said: \"Hello world!\"")`,
			expectedArgs:  []any{`They said: "Hello world!"`},
			validValues:   []string{`They said: "Hello world!"`},
			invalidValues: []string{`They said something else`},
		},
		{
			name:          "backtick-quoted pattern",
			ruleInput:     "regex(`^[a-z0-9-]+$`)",
			expectedArgs:  []any{"^[a-z0-9-]+$"},
			validValues:   []string{"hello-world-123"},
			invalidValues: []string{"Hello World!"},
		},
		{
			name:          "backtick-quoted pattern with literal quotes",
			ruleInput:     "regex(`They said: \"Hello world!\"`)",
			expectedArgs:  []any{`They said: "Hello world!"`},
			validValues:   []string{`They said: "Hello world!"`},
			invalidValues: []string{`no match here`},
		},
	}

	for _, tc := range validCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			rule, err := normalizeAndConvert(tc.ruleInput)
			require.NoError(t, err)
			assert.Equal(t, tc.expectedArgs, rule.Args)

			for _, val := range tc.validValues {
				require.NoError(t, rule.Validator.Validate(val), "expected %q to match", val)
			}

			for _, val := range tc.invalidValues {
				assert.Error(t, rule.Validator.Validate(val), "expected %q to not match", val)
			}
		})
	}

	errorCases := []struct {
		name        string
		ruleInput   string
		errContains string
	}{
		{
			name:        "invalid regex returns error",
			ruleInput:   `regex("invalid[")`,
			errContains: "invalid regex pattern",
		},
		{
			name:        "missing quotes returns error",
			ruleInput:   `regex(^[A-Z]{2}-\d{4}$)`,
			errContains: "pattern must be a quoted string",
		},
		{
			name:        "unescaped inner quotes returns error",
			ruleInput:   `regex("They said: "Hello world!"")`,
			errContains: "unescaped",
		},
	}

	for _, tc := range errorCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			_, err := normalizeAndConvert(tc.ruleInput)
			require.Error(t, err)
			assert.Contains(t, err.Error(), tc.errContains)
		})
	}
}

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

	tests := []struct {
		name        string
		input       string
		want        string
		errContains string
		wantErr     bool
	}{
		{name: "double-quoted simple", input: `"^[a-z]+$"`, want: `^[a-z]+$`},
		{name: "backtick-quoted simple", input: "`^[a-z]+$`", want: `^[a-z]+$`},
		{name: `\d passes through`, input: `"^\d{3}$"`, want: `^\d{3}$`},
		{name: `\w passes through`, input: `"^\w+$"`, want: `^\w+$`},
		{name: `\s passes through`, input: `"\s+"`, want: `\s+`},
		{name: `\" unescapes to quote`, input: `"say \"hi\""`, want: `say "hi"`},
		{name: `\\ unescapes to backslash`, input: `"a\\\\b"`, want: `a\\b`},
		{name: "backtick preserves everything", input: "`\\d \\w \\\\ \\\"`", want: `\d \w \\ \"`},
		{name: "empty double-quoted", input: `""`, want: ""},
		{name: "empty backtick", input: "``", want: ""},
		{name: "empty string", input: ``, wantErr: true, errContains: "pattern must be a quoted string"},
		{name: "mismatched quotes", input: "`foo\"", wantErr: true, errContains: "pattern must be a quoted string"},
		{name: "unquoted string", input: `hello`, wantErr: true, errContains: "pattern must be a quoted string"},
		{name: "unescaped inner quote", input: `"foo"bar"`, wantErr: true, errContains: "unescaped"},
		{name: "unescaped inner quotes in sentence", input: `"They said: "Hello world!""`, wantErr: true, errContains: "unescaped"},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			got, err := unquoteRegexPattern(tc.input)
			if tc.wantErr {
				require.Error(t, err)

				if tc.errContains != "" {
					assert.Contains(t, err.Error(), tc.errContains)
				}

				return
			}

			require.NoError(t, err)
			assert.Equal(t, tc.want, got)
		})
	}
}

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

	validCases := []struct {
		name            string
		ruleInput       string
		expectedMessage string
		expectedArgs    []any
		validValues     []string
		invalidValues   []string
	}{
		{
			name:            "with spaces around args",
			ruleInput:       "length(5, 22)",
			expectedMessage: "Must be between 5 and 22 characters long",
			expectedArgs:    []any{5, 22},
			validValues:     []string{"hello"},
			invalidValues:   []string{"hi"},
		},
		{
			name:            "without spaces around args",
			ruleInput:       "length(1,3)",
			expectedMessage: "Must be between 1 and 3 characters long",
			expectedArgs:    []any{1, 3},
			validValues:     []string{"ab"},
			invalidValues:   []string{"abcd"},
		},
	}

	for _, tc := range validCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			rule, err := normalizeAndConvert(tc.ruleInput)
			require.NoError(t, err)
			assert.Equal(t, tc.expectedMessage, rule.Message)
			assert.Equal(t, tc.expectedArgs, rule.Args)

			for _, val := range tc.validValues {
				require.NoError(t, rule.Validator.Validate(val), "expected %q to pass", val)
			}

			for _, val := range tc.invalidValues {
				assert.Error(t, rule.Validator.Validate(val), "expected %q to fail", val)
			}
		})
	}

	errorCases := []struct {
		name        string
		ruleInput   string
		errContains string
	}{
		{
			name:        "wrong case returns error",
			ruleInput:   "LENGTH(10, 30)",
			errContains: "unrecognized validation rule",
		},
		{
			name:        "missing comma returns error",
			ruleInput:   "length(5)",
			errContains: "expected length(min, max)",
		},
		{
			name:        "non-numeric min returns error",
			ruleInput:   "length(abc, 10)",
			errContains: "invalid min",
		},
		{
			name:        "non-numeric max returns error",
			ruleInput:   "length(5, xyz)",
			errContains: "invalid max",
		},
	}

	for _, tc := range errorCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			_, err := normalizeAndConvert(tc.ruleInput)
			require.Error(t, err)
			assert.Contains(t, err.Error(), tc.errContains)
		})
	}
}

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

	simpleRules := []string{"required", "url", "email", "alpha", "digit", "alphanumeric", "countrycode2", "semver"}

	for _, ruleStr := range simpleRules {
		t.Run(ruleStr, func(t *testing.T) {
			t.Parallel()

			rule, err := normalizeAndConvert(ruleStr)
			require.NoError(t, err)
			assert.Nil(t, rule.Args, "simple rule %q should have nil Args", ruleStr)
		})
	}
}

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

	t.Run("regex pattern with spaces via YAML list", func(t *testing.T) {
		t.Parallel()

		// Simulate what YAML parsing produces for:
		//   validations:
		//     - required
		//     - 'regex("^[a-z ]+$")'
		fields := map[string]any{
			"validations": []any{
				"required",
				`regex("^[a-z ]+$")`,
			},
		}

		rules, err := UnmarshalValidationsField(fields)
		require.NoError(t, err)
		require.Len(t, rules, 2)

		assert.Equal(t, "Must not be empty", rules[0].Message)
		assert.Nil(t, rules[0].Args)
		assert.Equal(t, "Must match pattern: ^[a-z ]+$", rules[1].Message)
		assert.Equal(t, []any{"^[a-z ]+$"}, rules[1].Args)

		// Should accept string with spaces
		err = rules[1].Validator.Validate("hello world")
		require.NoError(t, err)

		// Should reject digits
		err = rules[1].Validator.Validate("hello123")
		require.Error(t, err)
	})
}

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

	t.Run("nil validations returns nil", func(t *testing.T) {
		t.Parallel()

		fields := map[string]any{}

		rules, err := UnmarshalValidationsField(fields)
		require.NoError(t, err)
		assert.Empty(t, rules)
	})

	t.Run("scalar string returns error", func(t *testing.T) {
		t.Parallel()

		fields := map[string]any{
			"validations": "required",
		}

		_, err := UnmarshalValidationsField(fields)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "must be a YAML list, not a string")
	})

	t.Run("regex in list preserves case", func(t *testing.T) {
		t.Parallel()

		// Use backtick quoting since the pattern contains \d
		fields := map[string]any{
			"validations": []any{
				// Alternatively, we could use double-quoted strings:
				// `regex("^[A-Z]{2}-\d{4}$")``,
				"regex(`^[A-Z]{2}-\\d{4}$`)",
			},
		}

		rules, err := UnmarshalValidationsField(fields)
		require.NoError(t, err)
		require.Len(t, rules, 1)
		assert.Equal(t, []any{`^[A-Z]{2}-\d{4}$`}, rules[0].Args)

		err = rules[0].Validator.Validate("AB-1234")
		require.NoError(t, err)

		err = rules[0].Validator.Validate("ab-1234")
		require.Error(t, err)
	})

	t.Run("non-string item in list returns error", func(t *testing.T) {
		t.Parallel()

		fields := map[string]any{
			"validations": []any{
				"required",
				42,
			},
		}

		_, err := UnmarshalValidationsField(fields)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "must be a string, got int")
	})

	t.Run("unsupported type returns error", func(t *testing.T) {
		t.Parallel()

		fields := map[string]any{
			"validations": 42,
		}

		_, err := UnmarshalValidationsField(fields)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "must be a list of strings")
	})
}
