package app

import (
	"strings"
	"testing"
)

func tasksModel() Model {
	m := NewModel(nil)
	m.width, m.height = 120, 40
	m.theme = NewTheme(SoftBlue)
	return m
}

// sampleTasks is a deterministic two-row board used across the tests.
func sampleTasks() []TaskSummary {
	return []TaskSummary{
		{
			ID: "auth-1", Key: "auth-1", Status: "IN_PROGRESS", Title: "JWT refresh",
			Goal: "users stay authenticated", AcceptanceCriteria: []string{"rotates"},
			Claimant: "aaaaaaaa-1111",
			Links:    []TaskLinkWire{{TargetID: "adr-7", Type: "IMPLEMENTS", TargetKind: "DOC"}},
			Notes:    []TaskNoteWire{{Author: "AGENT", Body: "kickoff"}},
		},
		{ID: "billing-3", Key: "billing-3", Status: "DONE", Title: "Invoice export"},
	}
}

func TestTasksOverlayToggle(t *testing.T) {
	m := tasksModel()
	// `T` opens the board from the idle list.
	updated, _ := m.handleNormalKey(keyMsg{Type: keyRunes, Runes: []rune("T")})
	m = updated.(Model)
	if m.overlay != OverlayTasks {
		t.Fatalf("expected OverlayTasks after T, got %d", m.overlay)
	}
	if !m.taskListLoading {
		t.Fatalf("expected loading state right after open")
	}
	// `esc` closes it (routed through the overlay key handler).
	updated, _ = m.handleOverlayKey(keyMsg{Type: keyEsc})
	m = updated.(Model)
	if m.overlay != OverlayNone {
		t.Fatalf("expected overlay closed after esc, got %d", m.overlay)
	}
}

func TestTasksLoadedPopulatesAndRenders(t *testing.T) {
	m := tasksModel()
	m.overlay = OverlayTasks
	m.taskListLoading = true
	m.applyTasksLoaded(tasksLoadedMsg{tasks: sampleTasks()})
	if m.taskListLoading {
		t.Fatalf("expected loading cleared after load")
	}
	if len(m.taskList) != 2 {
		t.Fatalf("expected 2 tasks, got %d", len(m.taskList))
	}
	out := m.tasksModal()
	if !strings.Contains(out, "auth-1") || !strings.Contains(out, "billing-3") {
		t.Fatalf("modal missing task ids:\n%s", out)
	}
	if !strings.Contains(out, "IN_PROGRESS") || !strings.Contains(out, "DONE") {
		t.Fatalf("modal missing statuses:\n%s", out)
	}
}

func TestTasksEnterOpensDetailWithBundleFields(t *testing.T) {
	m := tasksModel()
	m.overlay = OverlayTasks
	m.taskList = sampleTasks()
	m.taskListIndex = 0

	updated, _ := m.handleTasksKey(keyMsg{Type: keyEnter})
	m = updated.(Model)
	if !m.taskDetail {
		t.Fatalf("expected detail pane open after enter")
	}
	out := m.tasksModal()
	for _, want := range []string{"users stay authenticated", "rotates", "adr-7", "kickoff"} {
		if !strings.Contains(out, want) {
			t.Fatalf("detail missing %q:\n%s", want, out)
		}
	}
	// esc returns to the list, not all the way out.
	updated, _ = m.handleTasksKey(keyMsg{Type: keyEsc})
	m = updated.(Model)
	if m.taskDetail {
		t.Fatalf("expected detail closed after esc")
	}
	if m.overlay != OverlayTasks {
		t.Fatalf("expected to stay on the board after esc from detail, got %d", m.overlay)
	}
}

func TestTasksNavigationBoundsChecked(t *testing.T) {
	m := tasksModel()
	m.overlay = OverlayTasks
	m.taskList = sampleTasks()
	// Up at the top is a no-op (clamped at 0).
	updated, _ := m.handleTasksKey(keyMsg{Type: keyUp})
	m = updated.(Model)
	if m.taskListIndex != 0 {
		t.Fatalf("expected index clamped to 0 at top, got %d", m.taskListIndex)
	}
	// Down then clamped at the last row.
	updated, _ = m.handleTasksKey(keyMsg{Type: keyDown})
	m = updated.(Model)
	updated, _ = m.handleTasksKey(keyMsg{Type: keyDown})
	m = updated.(Model)
	if m.taskListIndex != 1 {
		t.Fatalf("expected index clamped to 1 at bottom, got %d", m.taskListIndex)
	}
}

func TestTasksFilterNarrowsList(t *testing.T) {
	m := tasksModel()
	m.overlay = OverlayTasks
	m.taskList = sampleTasks()

	// `/` enters filter typing; a query narrows the board to matching rows.
	updated, _ := m.handleTasksKey(keyMsg{Type: keyRunes, Runes: []rune("/")})
	m = updated.(Model)
	if !m.taskFilterTyping {
		t.Fatalf("expected filter typing after /")
	}
	for _, r := range "invoice" {
		updated, _ = m.handleTasksKey(keyMsg{Type: keyRunes, Runes: []rune{r}})
		m = updated.(Model)
	}
	if got := m.filteredTasks(); len(got) != 1 || got[0].ID != "billing-3" {
		t.Fatalf("expected only billing-3 to match 'invoice', got %+v", got)
	}
	// enter stops editing but keeps the query; the selected task is the filtered one.
	updated, _ = m.handleTasksKey(keyMsg{Type: keyEnter})
	m = updated.(Model)
	if m.taskFilterTyping {
		t.Fatalf("expected typing to stop on enter")
	}
	if sel, ok := m.selectedTask(); !ok || sel.ID != "billing-3" {
		t.Fatalf("expected selected task billing-3 under filter, got %+v ok=%v", sel, ok)
	}
	// `c` clears the filter back to the full board.
	updated, _ = m.handleTasksKey(keyMsg{Type: keyRunes, Runes: []rune("c")})
	m = updated.(Model)
	if len(m.filteredTasks()) != 2 {
		t.Fatalf("expected full board after clear, got %d", len(m.filteredTasks()))
	}
}

func TestTasksReadyAndBlockedSurfaced(t *testing.T) {
	m := tasksModel()
	m.overlay = OverlayTasks
	m.taskList = []TaskSummary{
		{ID: "webui-2", Key: "webui-2", Status: "TODO", Title: "Scaffold app", Ready: true},
		{ID: "webui-3", Key: "webui-3", Status: "TODO", Title: "Auth view", BlockedBy: []string{"webui-2"}},
	}

	// A ready task's detail says so; a blocked task's detail names what it waits on.
	m.taskDetail = true
	m.taskListIndex = 0
	if out := m.tasksModal(); !strings.Contains(out, "ready to work") {
		t.Fatalf("expected ready marker in detail:\n%s", out)
	}
	m.taskListIndex = 1
	if out := m.tasksModal(); !strings.Contains(out, "waiting on webui-2") {
		t.Fatalf("expected blocked-by detail naming the blocker:\n%s", out)
	}
}

func TestTasksFetchErrorShownNoPanic(t *testing.T) {
	m := tasksModel()
	m.overlay = OverlayTasks
	m.taskListLoading = true
	m.applyTasksLoaded(tasksLoadedMsg{err: "server returned 503 Service Unavailable"})
	if m.taskListLoading {
		t.Fatalf("expected loading cleared on error")
	}
	if m.taskListErr == "" {
		t.Fatalf("expected error recorded")
	}
	out := m.tasksModal()
	if !strings.Contains(out, "503") {
		t.Fatalf("modal should surface the fetch error:\n%s", out)
	}
}
