package codes_test

import (
	"strings"
	"testing"

	"github.com/wangjia/pangolin/server/internal/codes"
)

// TestCrockfordAlphabetCoverage verifies the 32-symbol encoding alphabet.
func TestCrockfordAlphabetCoverage(t *testing.T) {
	forbidden := []rune{'I', 'L', 'O', 'U'}
	alpha := "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
	if len(alpha) != 32 {
		t.Fatalf("alphabet length = %d, want 32", len(alpha))
	}
	for _, ch := range forbidden {
		if strings.ContainsRune(alpha, ch) {
			t.Errorf("forbidden char %c found in alphabet", ch)
		}
	}
}

// TestGenerateCodeFormat verifies that GenerateCode returns a 16-char code
// made entirely of the Crockford alphabet (first 15 chars) + valid check char.
func TestGenerateCodeFormat(t *testing.T) {
	for i := 0; i < 1000; i++ {
		code, err := codes.GenerateCode()
		if err != nil {
			t.Fatalf("GenerateCode error: %v", err)
		}
		if len(code) != 16 {
			t.Errorf("code %q: length = %d, want 16", code, len(code))
		}
		// Canonicalize must accept a freshly generated code.
		canonical, err := codes.Canonicalize(code)
		if err != nil {
			t.Errorf("Canonicalize(%q): %v", code, err)
		}
		if canonical != code {
			t.Errorf("canonical form mismatch: got %q, want %q", canonical, code)
		}
	}
}

// TestGenerateCodeUniqueness checks that 10 000 generated codes have no
// hash collisions (birthday probability ≈ 10^−8 for 75-bit codes).
func TestGenerateCodeUniqueness(t *testing.T) {
	const n = 10_000
	seen := make(map[string]struct{}, n)
	for i := 0; i < n; i++ {
		code, err := codes.GenerateCode()
		if err != nil {
			t.Fatalf("GenerateCode: %v", err)
		}
		h := codes.Hash(code)
		if _, dup := seen[h]; dup {
			t.Fatalf("hash collision at iteration %d: code=%s hash=%s", i, code, h)
		}
		seen[h] = struct{}{}
	}
}

// TestCanonicalizeNormalization verifies the I/L→1 and O→0 substitutions.
func TestCanonicalizeNormalization(t *testing.T) {
	// Generate a valid code to use as a base.
	base, err := codes.GenerateCode()
	if err != nil {
		t.Fatalf("GenerateCode: %v", err)
	}

	// Test lower-case input equals upper-case canonical.
	lower := strings.ToLower(base)
	canonical, err := codes.Canonicalize(lower)
	if err != nil {
		t.Errorf("Canonicalize(lower) error: %v", err)
	}
	if canonical != base {
		t.Errorf("Canonicalize(lower) = %q, want %q", canonical, base)
	}

	// Test substitution: replace a '1' in the code with 'I' and 'L', verify they
	// normalise to the same canonical form.
	idx := strings.IndexByte(base, '1')
	if idx >= 0 && idx < 15 {
		withI := base[:idx] + "I" + base[idx+1:]
		withL := base[:idx] + "L" + base[idx+1:]
		withLower := base[:idx] + "l" + base[idx+1:]
		for _, variant := range []string{withI, withL, withLower} {
			c, err := codes.Canonicalize(variant)
			if err != nil {
				t.Errorf("Canonicalize(%q) error: %v", variant, err)
				continue
			}
			if c != base {
				t.Errorf("Canonicalize(%q) = %q, want %q", variant, c, base)
			}
		}
	}

	// Replace a '0' with 'O' and verify normalisation.
	idx = strings.IndexByte(base, '0')
	if idx >= 0 && idx < 15 {
		withO := base[:idx] + "O" + base[idx+1:]
		c, err := codes.Canonicalize(withO)
		if err != nil {
			t.Errorf("Canonicalize(%q) error: %v", withO, err)
		} else if c != base {
			t.Errorf("Canonicalize(%q) = %q, want %q", withO, c, base)
		}
	}
}

// TestCheckCharDetectsSingleErrors verifies that mutating any single data
// character in a valid code causes Canonicalize to return an error.
func TestCheckCharDetectsSingleErrors(t *testing.T) {
	const alpha = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
	code, err := codes.GenerateCode()
	if err != nil {
		t.Fatalf("GenerateCode: %v", err)
	}

	for pos := 0; pos < 15; pos++ {
		original := rune(code[pos])
		for _, replacement := range alpha {
			if replacement == original {
				continue
			}
			mutated := code[:pos] + string(replacement) + code[pos+1:]
			_, err := codes.Canonicalize(mutated)
			if err == nil {
				t.Errorf("mutating pos %d (%c→%c) not detected: code=%q mutated=%q",
					pos, original, replacement, code, mutated)
			}
		}
	}
}

// TestHashConsistency verifies that Hash is deterministic and that different
// codes produce different hashes.
func TestHashConsistency(t *testing.T) {
	code1, _ := codes.GenerateCode()
	code2, _ := codes.GenerateCode()
	for code1 == code2 {
		code2, _ = codes.GenerateCode()
	}

	h1a := codes.Hash(code1)
	h1b := codes.Hash(code1)
	h2 := codes.Hash(code2)

	if h1a != h1b {
		t.Error("Hash is not deterministic")
	}
	if h1a == h2 {
		t.Error("Different codes produced the same hash")
	}
	if len(h1a) != 64 {
		t.Errorf("Hash length = %d, want 64 (hex SHA-256)", len(h1a))
	}
}

// TestCanonicalizeRejectsInvalidLength tests length validation.
func TestCanonicalizeRejectsInvalidLength(t *testing.T) {
	cases := []string{"", "ABCDE", "ABCDEFGH12345678X"}
	for _, c := range cases {
		if _, err := codes.Canonicalize(c); err == nil {
			t.Errorf("Canonicalize(%q) should fail but did not", c)
		}
	}
}

// TestCanonicalizeRejectsInvalidChars tests that characters outside the
// Crockford alphabet are rejected for data positions.
func TestCanonicalizeRejectsInvalidChars(t *testing.T) {
	base, _ := codes.GenerateCode()
	// Replace position 0 with an invalid character.
	invalid := "!" + base[1:]
	if _, err := codes.Canonicalize(invalid); err == nil {
		t.Errorf("Canonicalize(%q) should fail for invalid char", invalid)
	}
}

// TestHyphenStripping verifies that hyphens inserted for readability are stripped.
func TestHyphenStripping(t *testing.T) {
	code, err := codes.GenerateCode()
	if err != nil {
		t.Fatalf("GenerateCode: %v", err)
	}
	// Insert hyphens: XXXX-XXXX-XXXX-XXXX
	hyphenated := code[:4] + "-" + code[4:8] + "-" + code[8:12] + "-" + code[12:]
	canonical, err := codes.Canonicalize(hyphenated)
	if err != nil {
		t.Errorf("Canonicalize(hyphenated) error: %v", err)
	}
	if canonical != code {
		t.Errorf("Canonicalize(hyphenated) = %q, want %q", canonical, code)
	}
}
