package app

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

	"charm.land/bubbles/v2/spinner"
	"charm.land/bubbles/v2/textinput"
	tea "charm.land/bubbletea/v2"
	"github.com/creack/pty"
	xterm "github.com/gitpod-io/xterm-go"
	"github.com/samber/lo"
)

// with more hosts than the terminal has rows, typing and moving must still
// show on screen: an overlong view loses its top to the renderer, so the
// keys land in the model while only the spinners visibly change
func TestLongTablesKeepTheFilterAndCursorOnScreen(t *testing.T) {
	const hosts, rows, cols = 40, 24, 80
	f := textinput.New()
	f.Prompt = "› "
	f.Focus()
	f.SetWidth(cols - 8)
	m := pickerModel{width: cols, filter: f, th: newTheme(true), spinner: spinner.New(spinner.WithSpinner(spinner.Dot))}
	m.rows = []hostRow{{name: "selfish", local: true}}
	for i := range hosts {
		m.rows = append(m.rows, hostRow{name: fmt.Sprintf("host%03d", i), probing: true, started: time.Now()})
	}
	m.probes = make(chan probeResult, hosts)
	m.move(0)

	master, tty, err := pty.Open()
	if err != nil {
		t.Fatal(err)
	}
	defer master.Close()
	defer tty.Close()
	if err := pty.Setsize(tty, &pty.Winsize{Rows: rows, Cols: cols}); err != nil {
		t.Fatal(err)
	}
	vt := xterm.New(xterm.WithCols(cols), xterm.WithRows(rows))
	vt.OnData(func(s string) { _, _ = master.Write([]byte(s)) }) // answer the program's queries
	var mu sync.Mutex
	go func() {
		buf := make([]byte, 64<<10)
		for {
			n, err := master.Read(buf)
			mu.Lock()
			_, _ = vt.Write(buf[:n])
			mu.Unlock()
			if err != nil {
				return
			}
		}
	}()
	screen := func() string { mu.Lock(); defer mu.Unlock(); return vt.String() }
	shows := func(want []string) bool {
		s := screen()
		return lo.EveryBy(want, func(w string) bool { return strings.Contains(s, w) })
	}
	waitFor := func(want ...string) {
		t.Helper()
		deadline := time.Now().Add(3 * time.Second)
		for !shows(want) {
			if time.Now().After(deadline) {
				t.Fatalf("the screen never showed %q:\n%s", want, screen())
			}
			time.Sleep(20 * time.Millisecond)
		}
	}

	p := tea.NewProgram(m, tea.WithInput(tty), tea.WithOutput(tty))
	done := make(chan error, 1)
	go func() { _, err := p.Run(); done <- err }()
	defer func() { p.Quit(); <-done }()

	waitFor("│ ›", "› selfish")
	_, _ = master.Write([]byte("hos\x1b[B\x1b[B\x1b[B")) // selfish drops out, so the cursor lands on the fourth host
	waitFor("│ › hos", "› host003")

	// a filter nothing matches shrinks the view; the renderer has to clear
	// the taller frame above it rather than leave its top behind
	_, _ = master.Write([]byte("qqq"))
	waitFor("nothing matches")
	if n := strings.Count(screen(), "tobi"); n != 1 {
		t.Fatalf("the shrunk view left %d headers on screen:\n%s", n, screen())
	}
	_, _ = master.Write([]byte("\x1b")) // esc clears the filter and grows it back
	waitFor("› selfish")
	if n := strings.Count(screen(), "tobi"); n != 1 {
		t.Fatalf("the regrown view left %d headers on screen:\n%s", n, screen())
	}
}
