package main

import "testing"

// newTestResolver compiles the kjol theme layer (no app brand) against a fixed token
// set. baseDir "." is fine — nothing in the entry @imports a local file.
func newTestResolver(t *testing.T, tokens ...string) *Resolver {
	t.Helper()
	r, err := NewResolver("", ".", tokens)
	if err != nil {
		t.Fatalf("NewResolver: %v", err)
	}
	return r
}

func TestResolverSemanticTokens(t *testing.T) {
	r := newTestResolver(t, "bg-surface", "text-ink", "text-white", "bg-red-500")

	// Semantic surface: white in light, near-black in dark.
	if c, ok := r.resolveToken("bg-surface", light); !ok || hexOf(c) != "#ffffff" {
		t.Errorf("bg-surface light = %v (%s), want #ffffff", ok, hexOf(c))
	}
	if c, ok := r.resolveToken("bg-surface", dark); !ok || hexOf(c) != "#101013" {
		t.Errorf("bg-surface dark = %v (%s), want #101013", ok, hexOf(c))
	}
	// text-white chases var(--color-white) → #fff.
	if c, ok := r.resolveToken("text-white", light); !ok || hexOf(c) != "#ffffff" {
		t.Errorf("text-white = %v (%s), want #ffffff", ok, hexOf(c))
	}
	// The palette OKLCH resolves to Tailwind's published hex.
	if c, ok := r.resolveToken("bg-red-500", light); !ok || hexOf(c) != "#fb2c36" {
		t.Errorf("bg-red-500 = %v (%s), want #fb2c36", ok, hexOf(c))
	}
}

func TestResolverSideClassification(t *testing.T) {
	r := newTestResolver(t, "bg-red-500", "text-ink", "flex", "text-sm", "p-4")
	cases := map[string]string{
		"bg-red-500": "bg",
		"text-ink":   "fg",
		"flex":       "", // not a colour utility
		"text-sm":    "", // font-size, not a colour
		"p-4":        "",
	}
	for tok, want := range cases {
		if got := r.side(tok); got != want {
			t.Errorf("side(%q) = %q, want %q", tok, got, want)
		}
	}
}

func TestResolverOpacityModifier(t *testing.T) {
	r := newTestResolver(t, "bg-white/50")
	c, ok := r.resolveToken("bg-white/50", light)
	if !ok {
		t.Fatal("bg-white/50 did not resolve")
	}
	approx(t, "bg-white/50 alpha", c.A, 0.5, 0.02)
}

func TestResolverArbitraryValue(t *testing.T) {
	r := newTestResolver(t, "bg-[#123456]")
	c, ok := r.resolveToken("bg-[#123456]", light)
	if !ok || hexOf(c) != "#123456" {
		t.Errorf("bg-[#123456] = %v (%s), want #123456", ok, hexOf(c))
	}
}

func TestResolverDarkVariantToken(t *testing.T) {
	r := newTestResolver(t, "dark:bg-surface")
	// A dark: token resolves against the dark environment.
	if c, ok := r.resolveToken("dark:bg-surface", dark); !ok || hexOf(c) != "#101013" {
		t.Errorf("dark:bg-surface (dark) = %v (%s), want #101013", ok, hexOf(c))
	}
}

// End to end: white-on-white fails; the semantic surface/ink pair passes in both
// themes (the two tokens move together, so dark mode stays legible).
func TestCheckGroupContrast(t *testing.T) {
	r := newTestResolver(t, "bg-white", "text-white", "bg-surface", "text-ink")

	// White on white: identical in both themes, so it collapses to one finding.
	fail := checkGroup(ClassGroup{
		File: "x.tsx", Line: 1, Tokens: []string{"bg-white", "text-white"},
	}, r, Options{Level: "AA"})
	if len(fail) != 1 || fail[0].Pass {
		t.Fatalf("white-on-white should fail once, got %+v", fail)
	}

	// Surface/ink: light and dark are both checked (colours differ per theme) and
	// both must pass.
	pass := checkGroup(ClassGroup{
		File: "x.tsx", Line: 2, Tokens: []string{"bg-surface", "text-ink"},
	}, r, Options{Level: "AA"})
	if len(pass) == 0 {
		t.Fatal("surface/ink produced no findings")
	}
	for _, f := range pass {
		if !f.Pass {
			t.Errorf("surface/ink should pass in %s, got %.2f:1", f.ThemeName(), f.Ratio)
		}
	}
}

// A fixed palette background paired with an inverting semantic text token is a real
// dark-mode trap: bg-white stays white while text-ink climbs to near-white.
func TestCheckGroupFixedVsSemanticDarkTrap(t *testing.T) {
	r := newTestResolver(t, "bg-white", "text-ink")
	got := checkGroup(ClassGroup{
		File: "x.tsx", Line: 1, Tokens: []string{"bg-white", "text-ink"},
	}, r, Options{Level: "AA"})

	var lightPass, darkFail bool
	for _, f := range got {
		if f.Theme == light && f.Pass {
			lightPass = true
		}
		if f.Theme == dark && !f.Pass {
			darkFail = true
		}
	}
	if !lightPass || !darkFail {
		t.Errorf("expected light pass + dark fail, got %+v", got)
	}
}

// Scanner: a comment apostrophe ("panel's") must not open a string literal and
// swallow the class constants below it — the desync that produced dozens of bogus
// cross-paired findings.
func TestLexerSkipsCommentApostrophe(t *testing.T) {
	src := []byte("// ModalSize selects the panel's max width.\n" +
		"const a = \"text-white\"\n" +
		"const b = \"bg-surface\"\n")
	lits := extractLiterals(src)
	if len(lits) != 2 {
		t.Fatalf("expected 2 literals, got %d: %+v", len(lits), lits)
	}
	if lits[0].content != "text-white" || lits[1].content != "bg-surface" {
		t.Errorf("unexpected literal contents: %+v", lits)
	}
}

func TestLexerSingleQuoteExpressionPosition(t *testing.T) {
	// Apostrophe in JSX text is not a string; a single-quoted attribute value is.
	src := []byte("<p>don't click</p>\n<a class='bg-red-500 text-white'>x</a>\n")
	lits := extractLiterals(src)
	found := false
	for _, l := range lits {
		if l.content == "bg-red-500 text-white" {
			found = true
		}
	}
	if !found {
		t.Errorf("single-quoted class attribute not extracted: %+v", lits)
	}
}

func TestSplitVariants(t *testing.T) {
	cases := []struct {
		token string
		base  string
		nvar  int
	}{
		{"bg-red-500", "bg-red-500", 0},
		{"dark:bg-surface", "bg-surface", 1},
		{"dark:hover:text-ink", "text-ink", 2},
		{"text-[color:red]", "text-[color:red]", 0}, // ':' inside [] is not a variant sep
	}
	for _, c := range cases {
		v, base := splitVariants(c.token)
		if base != c.base || len(v) != c.nvar {
			t.Errorf("splitVariants(%q) = %v,%q; want %d variants, base %q", c.token, v, base, c.nvar, c.base)
		}
	}
}
