package webui

import (
	"strings"
	"testing"

	"kjol/vdom"
)

// ---- input masks ----

func TestMaskTaxID(t *testing.T) {
	for _, tc := range []struct{ in, want string }{
		{"", ""},
		{"1", "1"},
		{"12", "12"},
		{"123", "12-3"},
		{"123456789", "12-3456789"},
		{"12-3456789", "12-3456789"},    // idempotent: re-masking its own output
		{"1234567890123", "12-3456789"}, // nine digits, no more
		{"ab12cd3456789xy", "12-3456789"},
	} {
		if got := MaskTaxID(tc.in); got != tc.want {
			t.Errorf("MaskTaxID(%q) = %q, want %q", tc.in, got, tc.want)
		}
	}
}

func TestMaskRate(t *testing.T) {
	for _, tc := range []struct{ in, want string }{
		{"", ""},
		{"5", "5"},
		{"5.25", "5.25"},
		{"5.2555", "5.255"}, // three decimals, no more
		{"5.2.5", "5.25"},   // one decimal point, no more
		{"007", "7"},        // no leading zeros...
		{"0", "0"},          // ...but a lone zero is a number
		{"0.5", "0.5"},      // and so is a leading zero before a point
		{"$1,2a3.45", "123.45"},
		{"5.25", "5.25"}, // idempotent
	} {
		if got := MaskRate(tc.in); got != tc.want {
			t.Errorf("MaskRate(%q) = %q, want %q", tc.in, got, tc.want)
		}
	}
}

// A mask is applied to its own output on every keystroke, so anything that is not
// idempotent corrupts the field as you type.
func TestMasksAreIdempotent(t *testing.T) {
	for _, in := range []string{"123456789", "12-345", "1", ""} {
		once := MaskTaxID(in)
		if twice := MaskTaxID(once); twice != once {
			t.Errorf("MaskTaxID is not idempotent: %q -> %q -> %q", in, once, twice)
		}
	}
	for _, in := range []string{"5.255", "0.5", "007", "12"} {
		once := MaskRate(in)
		if twice := MaskRate(once); twice != once {
			t.Errorf("MaskRate is not idempotent: %q -> %q -> %q", in, once, twice)
		}
	}
}

// ---- async combobox ----

func asyncOpts(labels ...string) []FormSelectOption {
	out := make([]FormSelectOption, len(labels))
	for i, l := range labels {
		out[i] = FormSelectOption{Value: strings.ToLower(l), Label: l}
	}
	return out
}

// The classic bug in a search-as-you-type box: a SLOW answer to an early query lands
// after a FAST answer to the query the user actually finished typing, and overwrites it.
// The user is then reading results for "ab" while the box says "abcdef".
//
// Every response carries the query it was for; one that no longer matches is dropped.
func TestAsyncComboboxDropsStaleResponses(t *testing.T) {
	var pending []func([]FormSelectOption) // answers we have not given yet
	var queries []string

	c := NewAsyncCombobox(AsyncComboboxOptions{
		MinChars: 2,
		Search: func(q string, done func([]FormSelectOption)) {
			queries = append(queries, q)
			pending = append(pending, done)
		},
	})

	// Type "ab", then "abcdef". (run() is called directly: the debounce timer is a
	// browser timer, and this test has no browser.)
	c.query.Set("ab")
	c.run("ab")
	c.query.Set("abcdef")
	c.run("abcdef")

	if len(pending) != 2 {
		t.Fatalf("expected 2 searches, got %d (%v)", len(pending), queries)
	}

	// The answer to "abcdef" comes back first...
	pending[1](asyncOpts("Abcdef Ltd"))
	if got := c.results.Get(); len(got) != 1 || got[0].Label != "Abcdef Ltd" {
		t.Fatalf("the current query's results were not shown: %v", got)
	}

	// ...and the slow answer to "ab" arrives afterwards. It must be DISCARDED.
	pending[0](asyncOpts("Ab Corp", "Abacus"))

	got := c.results.Get()
	if len(got) != 1 || got[0].Label != "Abcdef Ltd" {
		t.Errorf("a stale response overwrote the current results: %v", got)
	}
}

// Below MinChars there is no search at all — a one-character query against a big table
// is a scan, and every user who ever types starts by typing one character.
func TestAsyncComboboxRespectsMinChars(t *testing.T) {
	searched := 0
	c := NewAsyncCombobox(AsyncComboboxOptions{
		MinChars: 3,
		Search:   func(string, func([]FormSelectOption)) { searched++ },
	})

	c.onInput("a")
	c.onInput("ab")
	if searched != 0 {
		t.Errorf("searched %d times below MinChars", searched)
	}
	if c.IsOpen() {
		t.Error("the panel opened with nothing to show")
	}
}

// Choosing an option closes the box and reports the whole option, not just its value —
// the caller usually needs the label back to display it.
func TestAsyncComboboxSelect(t *testing.T) {
	var picked FormSelectOption
	c := NewAsyncCombobox(AsyncComboboxOptions{
		Search: func(_ string, done func([]FormSelectOption)) { done(asyncOpts("Ada Lovelace")) },
	})

	c.query.Set("ada")
	c.run("ada")
	c.f.Show()

	node := c.Render(FormAsyncComboboxProps{OnSelect: func(o FormSelectOption) { picked = o }})
	if !findAndClick(node, "Ada Lovelace") {
		t.Fatalf("no result row to click:\n%s", vdom.RenderHTML(node))
	}

	if picked.Value != "ada lovelace" {
		t.Errorf("OnSelect got %+v", picked)
	}
	if c.IsOpen() {
		t.Error("choosing an option should close the panel")
	}
	if c.query.Get() != "Ada Lovelace" {
		t.Errorf("the field should show the chosen label, got %q", c.query.Get())
	}
}
