// The picker's keyboard state machine (search versus browsing, per-row effort
// picks, cap toggles) needs catalog shapes the e2e fake provider cannot serve,
// so these tests check what enter sends for each state.
package tui

import (
	"strings"
	"testing"

	"albedo/cli/internal/config"
	"albedo/cli/internal/daemon"

	tea "charm.land/bubbletea/v2"
	"github.com/charmbracelet/x/ansi"
)

var pickerProfiles = config.Profiles{
	Active: "work",
	Providers: map[string]config.Settings{
		"work":  {Extension: "openai", BaseURL: "https://work.example/v1", Model: "gpt-5", Protocol: "responses"},
		"codex": {Extension: "codex", Model: "gpt-5-codex", Protocol: "responses"},
	},
}

var reasoning = []string{"low", "medium", "high"}

func loadedPicker(t *testing.T) ModelPickerModel {
	t.Helper()
	m := NewModelPickerModel(nil, pickerProfiles, "gpt-5", "work", "high")
	m.SetSize(120, 30)
	m, _ = m.Update(modelCatalogLoadedMsg{Profile: "work", Models: []daemon.Model{
		{ID: "gpt-4.1", Context: 1_047_576},
		{ID: "gpt-5", Efforts: reasoning, Context: 400_000, Output: 128_000, Input: []string{"text", "image"}},
		{ID: "o4-mini", Efforts: []string{"low", "medium"}},
	}})
	m, _ = m.Update(modelCatalogLoadedMsg{Profile: "codex", Models: []daemon.Model{
		{ID: "gpt-5-codex", Efforts: []string{"minimal", "low", "medium", "high"}},
	}})
	return m
}

func pickerKey(m ModelPickerModel, keys ...tea.KeyPressMsg) ModelPickerModel {
	for _, k := range keys {
		m, _ = m.Update(k)
	}
	return m
}

func typed(s string) tea.KeyPressMsg { return tea.KeyPressMsg{Code: tea.KeyExtended, Text: s} }

func chosen(t *testing.T, m ModelPickerModel) ModelPickerSelectMsg {
	t.Helper()
	_, cmd := m.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
	if cmd == nil {
		t.Fatal("enter chose nothing")
	}
	msg, ok := cmd().(ModelPickerSelectMsg)
	if !ok {
		t.Fatalf("enter sent %T", cmd())
	}
	return msg
}

func rowIDs(m ModelPickerModel) []string {
	ids := make([]string, len(m.rows))
	for i, r := range m.rows {
		ids[i] = r.profile + "/" + r.model.ID
		if r.typed {
			ids[i] = "typed:" + ids[i]
		}
	}
	return ids
}

func TestModelPickerArrowsStepEffortAndEnterSendsIt(t *testing.T) {
	m := loadedPicker(t)
	if got := chosen(t, m); got.Effort != "high" || got.Model != "gpt-5" || got.Provider != "work" {
		t.Fatalf("unchanged choice %+v, want the session's high effort", got)
	}
	m = pickerKey(m, tea.KeyPressMsg{Code: tea.KeyLeft}, tea.KeyPressMsg{Code: tea.KeyLeft}, tea.KeyPressMsg{Code: tea.KeyLeft})
	if got := chosen(t, m); got.Effort != "low" {
		t.Fatalf("effort %q after stepping down past the end, want low", got.Effort)
	}
	m = pickerKey(m, tea.KeyPressMsg{Code: tea.KeyRight})
	if got := chosen(t, m); got.Effort != "medium" {
		t.Fatalf("effort %q, want medium", got.Effort)
	}
	// Another row lacking the session's level shows the daemon's default,
	// and the first row keeps its pick.
	m = pickerKey(m, tea.KeyPressMsg{Code: tea.KeyDown}, tea.KeyPressMsg{Code: tea.KeyDown})
	if got := chosen(t, m); got.Model != "o4-mini" || got.Effort != "medium" {
		t.Fatalf("choice %+v, want o4-mini at medium", got)
	}
	m = pickerKey(m, tea.KeyPressMsg{Code: tea.KeyUp}, tea.KeyPressMsg{Code: tea.KeyUp})
	if got := chosen(t, m); got.Effort != "medium" {
		t.Fatalf("first row forgot its pick: %+v", got)
	}
	// A model without levels sends none.
	m = pickerKey(m, tea.KeyPressMsg{Code: tea.KeyDown}, tea.KeyPressMsg{Code: tea.KeyRight})
	if got := chosen(t, m); got.Model != "gpt-4.1" || got.Effort != "" {
		t.Fatalf("choice %+v, want gpt-4.1 without effort", got)
	}
}

func TestModelPickerArrowsEditASearchUntilYouBrowse(t *testing.T) {
	m := loadedPicker(t)
	m = pickerKey(m, typed("codex"))
	if got := strings.Join(rowIDs(m), " "); got != "codex/gpt-5-codex typed:work/codex typed:codex/codex" {
		t.Fatalf("search rows %q", got)
	}
	m = pickerKey(m, tea.KeyPressMsg{Code: tea.KeyLeft}, typed("x"))
	if m.search.Value() != "codexx" {
		t.Fatalf("left arrow did not reach the search: %q", m.search.Value())
	}
	m = pickerKey(m, tea.KeyPressMsg{Code: tea.KeyBackspace}, tea.KeyPressMsg{Code: tea.KeyUp}, tea.KeyPressMsg{Code: tea.KeyLeft})
	if got := chosen(t, m); got.Model != "gpt-5-codex" || got.Effort != "medium" {
		t.Fatalf("after browsing, left should lower high to medium: %+v", got)
	}
}

func TestModelPickerOffersATypedIDToEveryProfileMissingIt(t *testing.T) {
	m := pickerKey(loadedPicker(t), typed("gpt-5-codex"))
	got := strings.Join(rowIDs(m), " ")
	if got != "codex/gpt-5-codex typed:work/gpt-5-codex" {
		t.Fatalf("rows %q", got)
	}
	m = pickerKey(m, tea.KeyPressMsg{Code: tea.KeyDown})
	if got := chosen(t, m); got.Model != "gpt-5-codex" || got.Provider != "work" || got.Effort != "" {
		t.Fatalf("typed choice %+v", got)
	}
}

func TestModelPickerViewFitsEveryWidth(t *testing.T) {
	for _, size := range [][2]int{{30, 10}, {60, 20}, {80, 24}, {100, 30}, {160, 40}} {
		m := loadedPicker(t)
		m.SetSize(size[0], size[1])
		view := m.View()
		lines := strings.Split(view, "\n")
		if len(lines) > size[1] {
			t.Fatalf("%v: %d lines", size, len(lines))
		}
		for _, line := range lines {
			if w := ansi.StringWidth(line); w > size[0] {
				t.Fatalf("%v: line is %d cells wide: %q", size, w, ansi.Strip(line))
			}
		}
	}
}

func TestModelPickerTabRaisesAModelsContextCap(t *testing.T) {
	m := NewModelPickerModel(nil, pickerProfiles, "gpt-5-codex", "codex", "")
	m.SetSize(140, 30)
	m, _ = m.Update(modelCatalogLoadedMsg{Profile: "codex", Models: []daemon.Model{
		{ID: "gpt-6-astra", Context: 272_000, MaxContext: 872_000, Efforts: reasoning},
		{ID: "gpt-6-sol", Context: 272_000, MaxContext: 872_000, Raised: true},
		{ID: "gpt-5.5", Context: 272_000},
	}})
	m = pickerKey(m, typed("astra"))
	if view := ansi.Strip(m.View()); !strings.Contains(view, "tab raises to 872k") || !strings.Contains(view, "272k tokens") {
		t.Fatalf("astra should offer to raise its cap:\n%s", view)
	}
	m = pickerKey(m, tea.KeyPressMsg{Code: tea.KeyTab})
	if view := ansi.Strip(m.View()); !strings.Contains(view, "872k tokens") || !strings.Contains(view, "tab restores 272k") {
		t.Fatalf("a raised cap should show the larger window:\n%s", view)
	}
	if got := chosen(t, m).RaiseCap; got == nil || !*got {
		t.Fatalf("enter should raise astra's cap, got %v", got)
	}
	// Flipping back leaves the saved cap alone.
	m = pickerKey(m, tea.KeyPressMsg{Code: tea.KeyTab})
	if got := chosen(t, m).RaiseCap; got != nil {
		t.Fatalf("an unchanged cap should send nothing, got %v", *got)
	}

	// A saved raise shows as raised, and tab restores the default.
	sol := pickerKey(m, tea.KeyPressMsg{Code: tea.KeyBackspace}, tea.KeyPressMsg{Code: tea.KeyBackspace},
		tea.KeyPressMsg{Code: tea.KeyBackspace}, tea.KeyPressMsg{Code: tea.KeyBackspace},
		tea.KeyPressMsg{Code: tea.KeyBackspace}, typed("6-sol"))
	if view := ansi.Strip(sol.View()); !strings.Contains(view, "tab restores 272k") {
		t.Fatalf("sol's saved raise should show:\n%s", view)
	}
	sol = pickerKey(sol, tea.KeyPressMsg{Code: tea.KeyTab})
	if got := chosen(t, sol).RaiseCap; got == nil || *got {
		t.Fatalf("tab should restore sol's default window, got %v", got)
	}

	// Nothing to raise, nothing to toggle.
	older := pickerKey(sol, tea.KeyPressMsg{Code: tea.KeyBackspace}, tea.KeyPressMsg{Code: tea.KeyBackspace},
		tea.KeyPressMsg{Code: tea.KeyBackspace}, tea.KeyPressMsg{Code: tea.KeyBackspace},
		tea.KeyPressMsg{Code: tea.KeyBackspace}, typed("5.5"), tea.KeyPressMsg{Code: tea.KeyTab})
	if view := ansi.Strip(older.View()); strings.Contains(view, "tab raises") || strings.Contains(view, "tab cap") {
		t.Fatalf("gpt-5.5 has no cap to raise:\n%s", view)
	}
	if got := chosen(t, older).RaiseCap; got != nil {
		t.Fatalf("gpt-5.5 should send no cap, got %v", *got)
	}
}
