package ugh_test

import (
	"fmt"
	"strings"
	"testing"
	"time"

	"charm.land/huh/v2"
	"git.sr.ht/~timofurrer/ugh"
)

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

	c := newTestConsole(t)
	c.Expect(ugh.String("Hello World"))

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewNote().
				Title("Hello World").
				Next(true).
				NextLabel("Next"),
		),
	)
}

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

	c := newTestConsole(t)
	c.Expect(ugh.Note("Welcome")).
		Do(ugh.PressEnter)

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewNote().
				Title("Welcome").
				Description("Press enter to continue").
				Next(true).
				NextLabel("Next"),
		),
	)
}

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

	var name string

	c := newTestConsole(t)
	c.Expect(ugh.Input("Name")).
		Do(ugh.Type("Alice"))

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewInput().
				Title("Name").
				Value(&name),
		),
	)

	if name != "Alice" {
		t.Fatalf("expected name %q, got %q", "Alice", name)
	}
}

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

	var choice string

	c := newTestConsole(t)
	c.Expect(ugh.Select("Pick a fruit")).
		Do(ugh.SelectIndex(1))

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewSelect[string]().
				Title("Pick a fruit").
				Options(huh.NewOptions("Apple", "Banana", "Cherry")...).
				Value(&choice),
		),
	)

	if choice != "Banana" {
		t.Fatalf("expected choice %q, got %q", "Banana", choice)
	}
}

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

	var choice string

	c := newTestConsole(t)
	c.Expect(ugh.Select("Pick a fruit")).
		Do(ugh.SelectOption("Cherry"))

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewSelect[string]().
				Title("Pick a fruit").
				Options(huh.NewOptions("Apple", "Banana", "Cherry")...).
				Value(&choice),
		),
	)

	if choice != "Cherry" {
		t.Fatalf("expected choice %q, got %q", "Cherry", choice)
	}
}

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

	var confirmed bool

	c := newTestConsole(t)
	c.Expect(ugh.Confirm("Are you sure?")).
		Do(ugh.Affirm)

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewConfirm().
				Title("Are you sure?").
				Value(&confirmed),
		),
	)

	if !confirmed {
		t.Fatal("expected confirmed to be true")
	}
}

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

	confirmed := true

	c := newTestConsole(t)
	c.Expect(ugh.Confirm("Are you sure?")).
		Do(ugh.Reject)

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewConfirm().
				Title("Are you sure?").
				Value(&confirmed),
		),
	)

	if confirmed {
		t.Fatal("expected confirmed to be false")
	}
}

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

	var text string

	c := newTestConsole(t)
	c.Expect(ugh.Text("Notes")).
		Do(ugh.TypeMultiline("Hello there"))

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewText().
				Title("Notes").
				Value(&text).
				Lines(3),
		),
	)

	if text != "Hello there" {
		t.Fatalf("expected text %q, got %q", "Hello there", text)
	}
}

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

	c := newTestConsole(t)
	c.Expect(ugh.Regexp(`Order #\d+`))

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewNote().
				Title("Order #12345").
				Next(true).
				NextLabel("Next"),
		),
	)
}

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

	var name string
	var confirmed bool

	c := newTestConsole(t)
	c.Expect(ugh.Note("Welcome")).
		Do(ugh.PressEnter).
		Expect(ugh.Input("Name")).
		Do(ugh.Type("Bob")).
		Expect(ugh.Confirm("Confirm?")).
		Do(ugh.Affirm)

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewNote().
				Title("Welcome").
				Next(true).
				NextLabel("Next"),
		),
		huh.NewGroup(
			huh.NewInput().
				Title("Name").
				Value(&name),
		),
		huh.NewGroup(
			huh.NewConfirm().
				Title("Confirm?").
				Value(&confirmed),
		),
	)

	if name != "Bob" {
		t.Fatalf("expected name %q, got %q", "Bob", name)
	}
	if !confirmed {
		t.Fatal("expected confirmed to be true")
	}
}

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

	var toppings []string

	c := newTestConsole(t)
	c.Expect(ugh.MultiSelect("Toppings")).
		Do(ugh.ToggleIndices(0, 2))

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewMultiSelect[string]().
				Title("Toppings").
				Options(
					huh.NewOption("Lettuce", "Lettuce"),
					huh.NewOption("Tomatoes", "Tomatoes"),
					huh.NewOption("Cheese", "Cheese"),
					huh.NewOption("Onions", "Onions"),
				).
				Value(&toppings),
		),
	)

	if len(toppings) != 2 {
		t.Fatalf("expected 2 toppings, got %d: %v", len(toppings), toppings)
	}
	if toppings[0] != "Lettuce" || toppings[1] != "Cheese" {
		t.Fatalf("expected [Lettuce, Cheese], got %v", toppings)
	}
}

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

	var toppings []string

	c := newTestConsole(t)
	c.Expect(ugh.MultiSelect("Toppings")).
		Do(ugh.ToggleOptions("Tomatoes", "Onions"))

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewMultiSelect[string]().
				Title("Toppings").
				Options(
					huh.NewOption("Lettuce", "Lettuce"),
					huh.NewOption("Tomatoes", "Tomatoes"),
					huh.NewOption("Cheese", "Cheese"),
					huh.NewOption("Onions", "Onions"),
				).
				Value(&toppings),
		),
	)

	if len(toppings) != 2 {
		t.Fatalf("expected 2 toppings, got %d: %v", len(toppings), toppings)
	}
	if toppings[0] != "Tomatoes" || toppings[1] != "Onions" {
		t.Fatalf("expected [Tomatoes, Onions], got %v", toppings)
	}
}

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

	c := newTestConsole(t)
	c.Expect(ugh.String("Hello"))

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewNote().
				Title("Hello").
				Next(true),
		),
	)
}

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

	var choice string

	c := newTestConsole(t)
	c.Expect(ugh.Select("Pick a color")).
		Do(ugh.SelectOption("Red"))

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewSelect[string]().
				Title("Pick a color").
				Options(huh.NewOptions("Red", "Green", "Blue")...).
				Value(&choice),
		),
	)

	if choice != "Red" {
		t.Fatalf("expected choice %q, got %q", "Red", choice)
	}
}

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

	var choice string

	c := newTestConsole(t)
	c.Expect(ugh.Select("Pick")).
		Do(ugh.SelectIndex(0))

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewSelect[string]().
				Title("Pick").
				Options(huh.NewOptions("A", "B", "C")...).
				Value(&choice),
		),
	)

	if choice != "A" {
		t.Fatalf("expected choice %q, got %q", "A", choice)
	}
}

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

	var items []string

	c := newTestConsole(t)
	c.Expect(ugh.MultiSelect("Items")).
		Do(ugh.ToggleIndices(0, 1))

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewMultiSelect[string]().
				Title("Items").
				Options(
					huh.NewOption("Alpha", "Alpha").Selected(true),
					huh.NewOption("Beta", "Beta"),
					huh.NewOption("Gamma", "Gamma").Selected(true),
				).
				Value(&items),
		),
	)

	if len(items) != 2 {
		t.Fatalf("expected 2 items, got %d: %v", len(items), items)
	}
	if items[0] != "Beta" || items[1] != "Gamma" {
		t.Fatalf("expected [Beta, Gamma], got %v", items)
	}
}

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

	c := newTestConsole(t)
	c.Expect(ugh.String("Screen Test"))

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewNote().
				Title("Screen Test").
				Next(true).
				NextLabel("Next"),
		),
	)

	screen := c.Screen()
	if screen == "" {
		t.Fatal("expected non-empty screen after Run")
	}
}

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

	var name string

	c := newTestConsole(t)
	c.Expect(ugh.String("Name")).
		SendText("Hello").
		Send(ugh.Enter)

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewInput().
				Title("Name").
				Value(&name),
		),
	)

	if name != "Hello" {
		t.Fatalf("expected %q, got %q", "Hello", name)
	}
}

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

	var confirmed bool

	c := newTestConsole(t)
	c.Expect(ugh.ConfirmRegexp(`Run \x60.*\x60\?`)).
		Do(ugh.Affirm)

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewConfirm().
				Title("Run `echo hello`?").
				Value(&confirmed),
		),
	)

	if !confirmed {
		t.Fatal("expected confirmed")
	}
}

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

	confirmed := true

	c := newTestConsole(t)
	c.Expect(ugh.ConfirmRegexp(`Delete item #\d+\?`)).
		Do(ugh.Reject)

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewConfirm().
				Title("Delete item #42?").
				Value(&confirmed),
		),
	)

	if confirmed {
		t.Fatal("expected rejected")
	}
}

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

	var choice string

	c := newTestConsole(t)
	c.Expect(ugh.SelectRegexp(`Pick from list #\d+`)).
		Do(ugh.SelectIndex(2))

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewSelect[string]().
				Title("Pick from list #7").
				Options(huh.NewOptions("X", "Y", "Z")...).
				Value(&choice),
		),
	)

	if choice != "Z" {
		t.Fatalf("expected %q, got %q", "Z", choice)
	}
}

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

	var name string

	c := newTestConsole(t)
	c.Expect(ugh.Input("Name")).Do(func(r *ugh.Response) {
		r.SendText("custom")
		r.Send(ugh.Enter)
	})

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewInput().
				Title("Name").
				Value(&name),
		),
	)

	if name != "custom" {
		t.Fatalf("expected %q, got %q", "custom", name)
	}
}

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

	var capturedScreen string

	c := newTestConsole(t)
	c.Expect(ugh.Confirm("Screen check?")).Do(func(r *ugh.Response) {
		capturedScreen = r.Screen()
		r.SendText("y")
	})

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewConfirm().
				Title("Screen check?").
				Value(new(bool)),
		),
	)

	if capturedScreen == "" {
		t.Fatal("expected non-empty screen in action")
	}
}

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

	var capturedText string

	c := newTestConsole(t)
	c.Expect(ugh.Confirm("My title?")).Do(func(r *ugh.Response) {
		capturedText = r.Text()
		r.SendText("y")
	})

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewConfirm().
				Title("My title?").
				Value(new(bool)),
		),
	)

	if capturedText != "My title?" {
		t.Fatalf("expected text %q, got %q", "My title?", capturedText)
	}
}

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

	var (
		burger   string
		toppings []string
		spice    string
		name     string
		notes    string
		discount bool
	)

	const (
		expectedBurger = "Chicken"
		expectedName   = "Alice"
		expectedSpice  = "Hot"
		expectedNotes  = "No pickles please"
	)

	c := ugh.New(t, ugh.WithTimeout(10*time.Second))
	c.Expect(ugh.Note("Welcome")).
		Do(ugh.PressEnter).
		Expect(ugh.Select("Choose your burger")).
		Do(ugh.SelectOption(expectedBurger)).
		Expect(ugh.MultiSelect("Toppings")).
		Do(ugh.ToggleIndices(2)).
		Expect(ugh.Select("Spice level")).
		Do(ugh.SelectIndex(2)).
		Expect(ugh.Input("Your name")).
		Do(ugh.Type(expectedName)).
		Expect(ugh.Text("Special notes")).
		Do(ugh.TypeMultiline(expectedNotes)).
		Expect(ugh.Confirm("Want a discount?")).
		Do(ugh.Affirm)

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewNote().
				Title("Welcome").
				Description("Place your order").
				Next(true).
				NextLabel("Next"),
		),
		huh.NewGroup(
			huh.NewSelect[string]().
				Title("Choose your burger").
				Options(huh.NewOptions("Classic", "Chicken", "Veggie", "Fish")...).
				Value(&burger),
		),
		huh.NewGroup(
			huh.NewMultiSelect[string]().
				Title("Toppings").
				Options(
					huh.NewOption("Lettuce", "Lettuce").Selected(true),
					huh.NewOption("Tomato", "Tomato").Selected(true),
					huh.NewOption("Cheese", "Cheese"),
					huh.NewOption("Onion", "Onion"),
				).
				Value(&toppings),
		),
		huh.NewGroup(
			huh.NewSelect[string]().
				Title("Spice level").
				Options(huh.NewOptions("Mild", "Medium", "Hot")...).
				Value(&spice),
		),
		huh.NewGroup(
			huh.NewInput().
				Title("Your name").
				Value(&name),
		),
		huh.NewGroup(
			huh.NewText().
				Title("Special notes").
				Value(&notes).
				Lines(3),
		),
		huh.NewGroup(
			huh.NewConfirm().
				Title("Want a discount?").
				Value(&discount),
		),
	)

	if burger != expectedBurger {
		t.Fatalf("burger: expected %q, got %q", expectedBurger, burger)
	}
	if len(toppings) != 3 {
		t.Fatalf("toppings: expected 3, got %d: %v", len(toppings), toppings)
	}
	if spice != expectedSpice {
		t.Fatalf("spice: expected %q, got %q", expectedSpice, spice)
	}
	if name != expectedName {
		t.Fatalf("name: expected %q, got %q", expectedName, name)
	}
	if notes != expectedNotes {
		t.Fatalf("notes: expected %q, got %q", expectedNotes, notes)
	}
	if !discount {
		t.Fatal("discount: expected true")
	}
}

// ---------------------------------------------------------------------------
// Custom Matcher interface
// ---------------------------------------------------------------------------

type containsAllMatcher struct {
	substrings []string
}

func (m containsAllMatcher) Match(screen string) bool {
	for _, s := range m.substrings {
		if !strings.Contains(screen, s) {
			return false
		}
	}
	return true
}

func (m containsAllMatcher) String() string {
	return fmt.Sprintf("all of %v", m.substrings)
}

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

	c := newTestConsole(t)
	c.Expect(containsAllMatcher{[]string{"Are you sure?", "Yes"}}).
		Do(ugh.Affirm)

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewConfirm().
				Title("Are you sure?").
				Value(new(bool)),
		),
	)
}

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

	var confirmed bool

	c := newTestConsole(t)
	c.Expect(ugh.MatchScreen("has Yes button", func(screen string) bool {
		return strings.Contains(screen, "Yes")
	})).Do(ugh.Affirm)

	runTestForm(t, c,
		huh.NewGroup(
			huh.NewConfirm().
				Title("Proceed?").
				Value(&confirmed),
		),
	)

	if !confirmed {
		t.Fatal("expected confirmed")
	}
}
