package bundler

import (
	"strings"
	"testing"
)

func twTestCompile(t *testing.T, candidates ...string) string {
	t.Helper()
	css, _, err := twCompile(`@import "tailwindcss";`, ".", candidates)
	if err != nil {
		t.Fatalf("twCompile error: %v", err)
	}
	return css
}

// Edge cases that the modifier/fraction rewrite fixes.
func TestEngineEdgeCases(t *testing.T) {
	cases := []struct {
		candidate string
		contains  string
	}{
		// Improper fraction read as a fraction (not an opacity modifier).
		{"aspect-16/9", "aspect-ratio: 16/9"},
		// Proper fraction.
		{"w-1/2", "width: calc(1 / 2 * 100%)"},
		// Arbitrary opacity modifier decoded (not "[0.5]%").
		{"bg-white/[0.5]", "color-mix(in oklab, var(--color-white) 50%, transparent)"},
		// Bare spacing.
		{"m-4", "margin: calc(var(--spacing) * 4)"},
		{"p-4", "padding: calc(var(--spacing) * 4)"},
		// text-{size}/{leading}: font-size AND line-height (the dropped-modifier fix).
		{"text-sm/6", "line-height: calc(var(--spacing) * 6)"},
		// Theme color via @theme namespace.
		{"bg-red-500", "background-color: var(--color-red-500)"},
	}
	for _, c := range cases {
		css := twTestCompile(t, c.candidate)
		if !strings.Contains(css, c.contains) {
			t.Errorf("compile(%q): expected to contain %q\n---\n%s", c.candidate, c.contains, css)
		}
	}
}

func TestSegmentTopLevel(t *testing.T) {
	cases := []struct {
		in   string
		sep  string
		want []string
	}{
		{"a:b:c", ":", []string{"a", "b", "c"}},
		{"var(--a, 0 0 1px rgb(0, 0, 0)), 0 0 1px rgb(0, 0, 0)", ",",
			[]string{"var(--a, 0 0 1px rgb(0, 0, 0))", " 0 0 1px rgb(0, 0, 0)"}},
		{"display:grid", ":", []string{"display", "grid"}},
		{"[display:grid]", ":", []string{"[display:grid]"}},
		{"red-500/50", "/", []string{"red-500", "50"}},
		{"calc(1/2)/3", "/", []string{"calc(1/2)", "3"}},
	}
	for _, c := range cases {
		got := segment(c.in, c.sep)
		if len(got) != len(c.want) {
			t.Errorf("segment(%q,%q) = %v, want %v", c.in, c.sep, got, c.want)
			continue
		}
		for i := range got {
			if got[i] != c.want[i] {
				t.Errorf("segment(%q,%q)[%d] = %q, want %q", c.in, c.sep, i, got[i], c.want[i])
			}
		}
	}
}

func TestEscapeIdentifier(t *testing.T) {
	cases := []struct{ in, want string }{
		{"flex", "flex"},
		{"hover:bg-red-500", `hover\:bg-red-500`},
		{"w-1/2", `w-1\/2`},
		{"bg-[#fff]", `bg-\[\#fff\]`},
		{"2xl", `\32 xl`},
	}
	for _, c := range cases {
		if got := escape(c.in); got != c.want {
			t.Errorf("escape(%q) = %q, want %q", c.in, got, c.want)
		}
	}
}

func TestDecodeArbitraryValue(t *testing.T) {
	cases := []struct{ in, want string }{
		{"100%_!important", "100% !important"},
		{"calc(100dvh_-_5rem)", "calc(100dvh - 5rem)"},
		{`url(/a_b.png)`, "url(/a_b.png)"},
		{`var(--my_var)`, "var(--my_var)"},
		{"calc(var(--spacing)*4_+_env(safe-area-inset-bottom,0px))",
			"calc(var(--spacing) * 4 + env(safe-area-inset-bottom,0px))"},
	}
	for _, c := range cases {
		if got := decodeArbitraryValue(c.in); got != c.want {
			t.Errorf("decodeArbitraryValue(%q) = %q, want %q", c.in, got, c.want)
		}
	}
}

func TestInferDataType(t *testing.T) {
	cases := []struct {
		value string
		types []string
		want  string
	}{
		{"#ff0000", []string{dtColor, dtLength}, dtColor},
		{"10rem", []string{dtColor, dtLength}, dtLength},
		{"50%", []string{dtLength, dtPercentage}, dtPercentage},
		{"16/9", []string{dtRatio, dtColor}, dtRatio},
		{"var(--x)", []string{dtColor, dtLength}, ""},
		{"calc(1px+2px)", []string{dtLength}, dtLength},
		{"red", []string{dtColor}, dtColor},
	}
	for _, c := range cases {
		if got := inferDataType(c.value, c.types); got != c.want {
			t.Errorf("inferDataType(%q,%v) = %q, want %q", c.value, c.types, got, c.want)
		}
	}
}

func TestNumericPredicates(t *testing.T) {
	if !isValidSpacingMultiplier("0.5") {
		t.Error("0.5 should be a valid spacing multiplier")
	}
	if isValidSpacingMultiplier("0.3") {
		t.Error("0.3 should NOT be a valid spacing multiplier")
	}
	if !isPositiveInteger("3") || isPositiveInteger("3.0") || isPositiveInteger("03") {
		t.Error("isPositiveInteger canonical-form check failed")
	}
}

// Apostrophes inside JS comments (e.g. "don't", "button's") must not desync the
// quote-based class scanner. Before comments were skipped, a stray apostrophe
// flipped quote parity and swallowed every class literal until the next quote —
// silently dropping singly-used utilities like the lineup card's switch and
// drag styles.
func TestExtractCandidatesSkipsComments(t *testing.T) {
	src := "// we don't need reactivity here\n" +
		"const cur = locked ? \"cursor-not-allowed\" : \"cursor-move\";\n" +
		"/* the button's thumb: it's offset */\n" +
		"const thumb = on ? \"translate-x-3\" : \"\";\n" +
		"const tpl = `<input class=\"sr-only\"/>`;\n"

	got := map[string]bool{}
	for _, c := range extractCandidates(src) {
		got[c] = true
	}
	for _, want := range []string{"cursor-not-allowed", "cursor-move", "translate-x-3", "sr-only"} {
		if !got[want] {
			t.Errorf("candidate %q was not extracted past comment apostrophes", want)
		}
	}
}

// scan returns the set of candidates extracted from src.
func scan(src string) map[string]bool {
	got := map[string]bool{}
	for _, c := range extractCandidates(src) {
		got[c] = true
	}
	return got
}

// Quotes inside a regex literal must not be read as a string (which would
// desync the scanner and drop following class literals), and a division `/`
// must not be mistaken for a regex (which would swallow the code after it).
func TestExtractCandidatesRegexLiterals(t *testing.T) {
	cases := []struct {
		name string
		src  string
		want []string
	}{
		{
			"apostrophe in regex",
			`const r = /it's/; const c = "cursor-move";`,
			[]string{"cursor-move"},
		},
		{
			"regex after return keyword",
			`function f(){ return /a"b/; } const c = "p-4";`,
			[]string{"p-4"},
		},
		{
			"char class containing slash and quote",
			`const r = /[/']/g; const c = "block";`,
			[]string{"block"},
		},
		{
			"division is not a regex (string between divisions survives)",
			`const w = a / 2; cls = "text-lg"; const h = b / 4; cls2 = "m-2";`,
			[]string{"text-lg", "m-2"},
		},
		{
			"regex inside a template interpolation, class after it",
			"const t = html`<a class=${x.replace(/'/g, \"\")}>${y ? \"p-5\" : \"p-6\"}</a>`;",
			[]string{"p-5", "p-6"},
		},
		{
			"regex with braces in template interpolation keeps ${} balanced",
			"const t = html`<a class=${s.match(/[{}]/) ? \"flex\" : \"hidden\"}></a>`;",
			[]string{"flex", "hidden"},
		},
	}
	for _, tc := range cases {
		got := scan(tc.src)
		for _, w := range tc.want {
			if !got[w] {
				t.Errorf("%s: candidate %q not extracted (src=%q)", tc.name, w, tc.src)
			}
		}
	}
}
