package cldrlocale_test

import (
	"bytes"
	"encoding/json"
	"go/parser"
	"go/token"
	"io/fs"
	"maps"
	"os"
	"os/exec"
	"path/filepath"
	"slices"
	"strings"
	"testing"

	cldrdisplaynames "github.com/agentable/go-intl/internal/cldr/displaynames"
	cldrlist "github.com/agentable/go-intl/internal/cldr/list"
	cldrlocale "github.com/agentable/go-intl/internal/cldr/locale"
	cldrnumber "github.com/agentable/go-intl/internal/cldr/number"
	cldrplural "github.com/agentable/go-intl/internal/cldr/plural"
	"github.com/agentable/go-intl/internal/localematcher"
)

// repoRoot is the module root relative to this package directory.
const repoRoot = "../../.."

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

	version := cldrlocale.Version()
	if version.CLDR == "" || version.ICU == "" || version.TZData == "" {
		t.Fatalf("Version() = %#v, want all version fields populated", version)
	}
	manifest := cldrlocale.Manifest()
	if manifest.CLDR != version.CLDR || manifest.ICU != version.ICU || manifest.TZData != version.TZData {
		t.Fatalf("Manifest versions = %#v, Version = %#v", manifest, version)
	}
	if manifest.Generator != "tools/gen-cldr" {
		t.Fatalf("Manifest().Generator = %q, want tools/gen-cldr", manifest.Generator)
	}
	if len(manifest.LocaleProfile) == 0 {
		t.Fatal("Manifest().LocaleProfile is empty")
	}
	if len(manifest.InputHashes) == 0 {
		t.Fatal("Manifest().InputHashes is empty")
	}

	loc, ok := cldrnumber.ResolveLocale("en")
	if !ok {
		t.Fatal("ResolveLocale(en) failed")
	}
	if got := loc.CompactPattern("latn", "short", 3, "other"); got == "" {
		t.Fatal("CompactPattern(latn, 3, other) returned placeholder data")
	}
}

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

	manifest := cldrlocale.Manifest()
	if len(manifest.LocaleProfile) == 0 || len(manifest.InputHashes) == 0 {
		t.Fatal("Manifest() returned empty slices")
	}
	manifest.LocaleProfile[0] = "mutated"
	manifest.InputHashes[0].SHA256 = "mutated"

	next := cldrlocale.Manifest()
	if next.LocaleProfile[0] == "mutated" {
		t.Fatal("Manifest().LocaleProfile shares mutable backing storage")
	}
	if next.InputHashes[0].SHA256 == "mutated" {
		t.Fatal("Manifest().InputHashes shares mutable backing storage")
	}
}

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

	root := filepath.Clean(repoRoot)
	want := readLocaleProfile(t, root)
	got := cldrlocale.Manifest().LocaleProfile
	requireSameStrings(t, "Manifest().LocaleProfile", got, want)
}

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

	manifest := cldrlocale.Manifest()
	if manifest.CLDR != "48.1.0" || manifest.ICU != "78" || manifest.TZData != "2025b" {
		t.Fatalf("Manifest data pins = cldr=%s icu=%s tzdata=%s, want cldr=48.1.0 icu=78 tzdata=2025b", manifest.CLDR, manifest.ICU, manifest.TZData)
	}
	if got, want := len(manifest.LocaleProfile), 104; got != want {
		t.Fatalf("Manifest().LocaleProfile length = %d, want %d curated default locale tags", got, want)
	}
}

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

	profile := cldrlocale.Manifest().LocaleProfile
	for _, tc := range []struct {
		name string
		got  []string
	}{
		{name: "number.SupportedLocales", got: cldrnumber.SupportedLocales()},
		{name: "list.SupportedLocales", got: cldrlist.SupportedLocales()},
		{name: "plural.SupportedLocales", got: cldrplural.SupportedLocales()},
		{name: "displaynames.SupportedLocales", got: cldrdisplaynames.SupportedLocales()},
	} {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			requireSupportedByProfile(t, tc.name, tc.got, profile)
		})
	}
}

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

	root := filepath.Clean(repoRoot)
	files := generatedCLDRFiles(t, root)
	if len(files) == 0 {
		t.Fatal("no generated CLDR files discovered")
	}
	for _, path := range files {
		header := genCLDRHeader
		if strings.Contains(filepath.ToSlash(path), "/internal/cldr/plural/") {
			header = "// Code generated by tools/gen-plural-rules; DO NOT EDIT.\n"
		}
		t.Run(filepath.Base(path), func(t *testing.T) {
			t.Parallel()
			src, err := os.ReadFile(path)
			if err != nil {
				t.Fatalf("read file: %v", err)
			}
			if !bytes.HasPrefix(src, []byte(header)) {
				t.Fatalf("%s missing generated header", filepath.ToSlash(path))
			}
		})
	}
}

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

	root, err := filepath.Abs(repoRoot)
	if err != nil {
		t.Fatalf("resolve repo root: %v", err)
	}
	cldrDir := filepath.Join(root, "tools", "gen-cldr", ".cldr-json", "node_modules")
	if _, err := os.Stat(filepath.Join(cldrDir, "cldr-core")); err != nil {
		t.Skip("tools/gen-cldr/.cldr-json is absent; run task data to install the CLDR checkout")
	}

	out := filepath.Join(t.TempDir(), "cldr")
	if err := os.MkdirAll(out, 0o777); err != nil {
		t.Fatalf("mkdir output: %v", err)
	}
	profile := filepath.Join(root, "tools", "locale-profile.json")
	cmd := exec.Command("go", "-C", filepath.Join(root, "tools", "gen-cldr"), "run", ".", "-cldr-dir", ".cldr-json/node_modules", "-out", out, "-version-file", filepath.Join(root, "internal", "cldr", "VERSION"), "-profile", profile)
	cmd.Env = os.Environ()
	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("run gen-cldr: %v\n%s", err, output)
	}
	pluralCmd := exec.Command(
		"go",
		"-C", filepath.Join(root, "tools", "gen-plural-rules"),
		"run", ".",
		"-plurals", filepath.Join(root, "tools", "gen-cldr", ".cldr-json", "node_modules", "cldr-core", "supplemental", "plurals.json"),
		"-ranges", filepath.Join(root, "tools", "gen-cldr", ".cldr-json", "node_modules", "cldr-core", "supplemental", "pluralRanges.json"),
		"-out", filepath.Join(out, "plural"),
		"-profile", profile,
	)
	pluralCmd.Env = os.Environ()
	output, err = pluralCmd.CombinedOutput()
	if err != nil {
		t.Fatalf("run gen-plural-rules: %v\n%s", err, output)
	}

	files := generatedCLDRFiles(t, root)
	cldrRoot := filepath.Join(root, "internal", "cldr")
	for _, path := range files {
		t.Run(filepath.Base(filepath.Dir(path))+"/"+filepath.Base(path), func(t *testing.T) {
			t.Parallel()
			want, err := os.ReadFile(path)
			if err != nil {
				t.Fatalf("read committed file: %v", err)
			}
			rel, err := filepath.Rel(cldrRoot, path)
			if err != nil {
				t.Fatalf("relative path: %v", err)
			}
			got, err := os.ReadFile(filepath.Join(out, rel))
			if err != nil {
				t.Fatalf("read generated file: %v", err)
			}
			if !bytes.Equal(got, want) {
				t.Fatalf("%s drifted from generator output", filepath.ToSlash(rel))
			}
		})
	}
}

const genCLDRHeader = `// Code generated by tools/gen-cldr; see SPECS/50-cldr-data.md#cldr-pin-rationale; DO NOT EDIT.

// Source: CLDR v48.1.0 / ICU 78 / tzdata 2025b (input hashes in internal/cldr/locale/manifest.go)
// Generated: reproducible from tools/gen-cldr with pinned inputs; no timestamps or machine-local paths
// Schema: SPECS/50-cldr-data.md#cldr-data-package-convention
`

// generatedCLDRFiles walks internal/cldr and returns every generated Go file,
// identified by the generator header comment. The drift and header gates need
// the full set across all domain packages, not a fixed file list.
func generatedCLDRFiles(t *testing.T, root string) []string {
	t.Helper()

	cldrRoot := filepath.Join(root, "internal", "cldr")
	var files []string
	err := filepath.WalkDir(cldrRoot, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() {
			return nil
		}
		name := d.Name()
		if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
			return nil
		}
		if isGeneratedCLDRSnapshotFile(t, path) {
			files = append(files, path)
		}
		return nil
	})
	if err != nil {
		t.Fatalf("walk internal/cldr: %v", err)
	}
	slices.Sort(files)
	return files
}

// isGeneratedCLDRSnapshotFile reports whether the file carries a recognized
// generator header comment. Hand-written decode.go / accessors.go / doc.go and
// the codec package are excluded by lacking the header.
func isGeneratedCLDRSnapshotFile(t *testing.T, path string) bool {
	t.Helper()

	fset := token.NewFileSet()
	f, err := parser.ParseFile(fset, path, nil, parser.PackageClauseOnly|parser.ParseComments)
	if err != nil {
		t.Fatalf("parse comments %s: %v", path, err)
	}
	for _, group := range f.Comments {
		for _, c := range group.List {
			if strings.HasPrefix(c.Text, "// Code generated by tools/gen-cldr") ||
				strings.HasPrefix(c.Text, "// Code generated by tools/gen-plural-rules") {
				return true
			}
		}
	}
	return false
}

func readLocaleProfile(t *testing.T, root string) []string {
	t.Helper()

	raw, err := os.ReadFile(filepath.Join(root, "tools", "locale-profile.json"))
	if err != nil {
		t.Fatalf("read locale profile: %v", err)
	}
	var fields map[string]json.RawMessage
	if err := json.Unmarshal(raw, &fields); err != nil {
		t.Fatalf("parse locale profile fields: %v", err)
	}
	for key := range fields {
		if key != "locales" {
			t.Fatalf("tools/locale-profile.json contains unsupported key %q", key)
		}
	}
	var profile struct {
		Locales []string `json:"locales"`
	}
	if err := json.Unmarshal(raw, &profile); err != nil {
		t.Fatalf("parse locale profile: %v", err)
	}
	return sortedUniqueLocales(profile.Locales)
}

func sortedUniqueLocales(locales []string) []string {
	set := map[string]bool{}
	for _, locale := range locales {
		if locale != "" && locale != "und" {
			set[locale] = true
		}
	}
	return slices.Sorted(maps.Keys(set))
}

func requireSameStrings(t *testing.T, name string, got, want []string) {
	t.Helper()

	if slices.Equal(got, want) {
		return
	}
	t.Fatalf("%s = [%s], want [%s]", name, strings.Join(got, ", "), strings.Join(want, ", "))
}

func requireSupportedByProfile(t *testing.T, name string, supported, profile []string) {
	t.Helper()

	if len(supported) == 0 {
		t.Fatalf("%s is empty", name)
	}
	if !slices.IsSorted(supported) {
		t.Fatalf("%s = [%s], want sorted locale tags", name, strings.Join(supported, ", "))
	}
	if got := slices.Compact(slices.Clone(supported)); !slices.Equal(got, supported) {
		t.Fatalf("%s = [%s], want unique locale tags", name, strings.Join(supported, ", "))
	}

	profileSet := map[string]bool{}
	for _, locale := range profile {
		profileSet[locale] = true
	}
	for _, locale := range supported {
		if !profileSet[locale] {
			t.Fatalf("%s contains %q outside Manifest().LocaleProfile", name, locale)
		}
	}
	for _, locale := range profile {
		if localematcher.BestAvailableLocale(supported, locale) == "" {
			t.Fatalf("%s cannot support profile locale %q through ECMA-402 lookup", name, locale)
		}
	}
}
