package envcompat

import (
	"os"
	"strings"
	"testing"
)

// TestRegistryWellFormed guards against typos and accidental duplication as
// the rename table grows. Each alias must have a non-empty New name, at least
// one legacy name, and neither New nor legacy names may collide across rows.
func TestRegistryWellFormed(t *testing.T) {
	seenNew := make(map[string]int)
	seenLegacy := make(map[string]int)
	for i, alias := range Registry {
		if strings.TrimSpace(alias.New) == "" {
			t.Fatalf("Registry[%d].New is empty", i)
		}
		if len(alias.Legacy) == 0 {
			t.Fatalf("Registry[%d] (%s) has no legacy names", i, alias.New)
		}
		for _, legacy := range alias.Legacy {
			if strings.TrimSpace(legacy) == "" {
				t.Fatalf("Registry[%d] (%s) contains an empty legacy name", i, alias.New)
			}
			if legacy == alias.New {
				t.Fatalf("Registry[%d]: legacy name %q equals new name", i, legacy)
			}
			seenLegacy[legacy]++
		}
		seenNew[alias.New]++
	}
	for name, count := range seenNew {
		if count > 1 {
			t.Errorf("New name %q appears %d times in Registry", name, count)
		}
	}
	for name, count := range seenLegacy {
		if count > 1 {
			t.Errorf("Legacy name %q appears %d times in Registry", name, count)
		}
	}
}

// TestRegistryLookupRoundTrip verifies every registered alias actually resolves
// via Lookup when only the legacy name is set.
func TestRegistryLookupRoundTrip(t *testing.T) {
	for _, alias := range Registry {
		// Don't run in parallel — t.Setenv is not safe across parallel subtests
		// sharing process env.
		t.Run(alias.New, func(t *testing.T) {
			os.Unsetenv(alias.New)
			for _, other := range alias.Legacy {
				os.Unsetenv(other)
			}
			primary := alias.Legacy[0]
			t.Setenv(primary, "value-from-legacy")

			v, src, ok := Lookup(alias.New, alias.Legacy...)
			if !ok {
				t.Fatalf("Lookup returned ok=false; expected legacy %q to satisfy", primary)
			}
			if v != "value-from-legacy" {
				t.Fatalf("Lookup value = %q, want value-from-legacy", v)
			}
			if src != primary {
				t.Fatalf("Lookup source = %q, want %q", src, primary)
			}
		})
	}
}

// TestRegistryPreferredBeatsLegacy verifies the new name wins when both are set.
func TestRegistryPreferredBeatsLegacy(t *testing.T) {
	for _, alias := range Registry {
		t.Run(alias.New, func(t *testing.T) {
			for _, legacy := range alias.Legacy {
				os.Unsetenv(legacy)
			}
			t.Setenv(alias.New, "from-new")
			t.Setenv(alias.Legacy[0], "from-legacy")

			v, src, ok := Lookup(alias.New, alias.Legacy...)
			if !ok || v != "from-new" || src != alias.New {
				t.Fatalf("Lookup = %q,%q,%v; want from-new,%s,true", v, src, ok, alias.New)
			}
		})
	}
}

// TestLookupNoLegacyArgs verifies Lookup still works without any legacy names.
func TestLookupNoLegacyArgs(t *testing.T) {
	os.Unsetenv("ENVCOMPAT_NOLEGACY")
	if _, _, ok := Lookup("ENVCOMPAT_NOLEGACY"); ok {
		t.Fatal("Lookup with no legacy args should return ok=false when unset")
	}

	t.Setenv("ENVCOMPAT_NOLEGACY", "x")
	v, src, ok := Lookup("ENVCOMPAT_NOLEGACY")
	if !ok || v != "x" || src != "ENVCOMPAT_NOLEGACY" {
		t.Fatalf("Lookup = %q,%q,%v; want x,ENVCOMPAT_NOLEGACY,true", v, src, ok)
	}
}

// TestLookupTrimsWhitespace verifies Lookup trims surrounding whitespace.
func TestLookupTrimsWhitespace(t *testing.T) {
	t.Setenv("ENVCOMPAT_TRIM", "  padded  ")
	v, _, ok := Lookup("ENVCOMPAT_TRIM")
	if !ok || v != "padded" {
		t.Fatalf("Lookup = %q,%v; want padded,true", v, ok)
	}
}

// TestBoolAcceptsCaseInsensitive verifies Bool tolerates TRUE / False / etc.
func TestBoolAcceptsCaseInsensitive(t *testing.T) {
	cases := []struct {
		raw   string
		value bool
	}{
		{"true", true},
		{"TRUE", true},
		{"False", false},
		{"false", false},
	}
	for _, tc := range cases {
		t.Run(tc.raw, func(t *testing.T) {
			t.Setenv("ENVCOMPAT_BOOL_CASE", tc.raw)
			got, err := Bool("ENVCOMPAT_BOOL_CASE", !tc.value)
			if err != nil {
				t.Fatalf("Bool returned err for %q: %v", tc.raw, err)
			}
			if got != tc.value {
				t.Fatalf("Bool(%q) = %v, want %v", tc.raw, got, tc.value)
			}
		})
	}
}

// TestStringEmptyPreferredFallsBackToLegacyWhenOnlyLegacySet verifies an empty
// preferred value does not short-circuit Lookup (matches trim-empty rule).
func TestStringEmptyPreferredFallsBackToLegacyWhenOnlyLegacySet(t *testing.T) {
	t.Setenv("ENVCOMPAT_EMPTY_NEW", "")
	t.Setenv("ENVCOMPAT_EMPTY_LEGACY", "legacy")

	got := String("ENVCOMPAT_EMPTY_NEW", "default", "ENVCOMPAT_EMPTY_LEGACY")
	if got != "legacy" {
		t.Fatalf("String = %q, want legacy (empty preferred should not win)", got)
	}
}

// TestWarnLegacyEmitsForEveryLegacyUsed covers the multi-legacy case.
func TestWarnLegacyEmitsForEveryLegacyUsed(t *testing.T) {
	orig := Registry
	t.Cleanup(func() { Registry = orig })

	Registry = []Alias{
		{
			New:    "ENVCOMPAT_MULTI_NEW",
			Legacy: []string{"ENVCOMPAT_MULTI_LEGACY_A", "ENVCOMPAT_MULTI_LEGACY_B"},
		},
	}
	os.Unsetenv("ENVCOMPAT_MULTI_NEW")
	t.Setenv("ENVCOMPAT_MULTI_LEGACY_A", "a")
	t.Setenv("ENVCOMPAT_MULTI_LEGACY_B", "b")

	var reported []string
	WarnLegacy(func(legacy, newName string) {
		reported = append(reported, legacy)
	})

	if len(reported) != 2 {
		t.Fatalf("WarnLegacy reported %d legacies, want 2", len(reported))
	}
}
