package app

import (
	"strings"
	"testing"
)

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

// sampleSummaries is a deterministic two-row roster used across the tests.
func sampleSummaries() []SessionSummary {
	return []SessionSummary{
		{SessionID: "aaaaaaaa-1111", Status: "ACTIVE", WorkflowID: "refactor", StageCount: 2},
		{SessionID: "bbbbbbbb-2222", Status: "FAILED", WorkflowID: "healthcheck"},
	}
}

func TestSessionsOverlayToggle(t *testing.T) {
	m := sessionsModel()
	// `R` opens the browser from the idle list.
	updated, _ := m.handleNormalKey(keyMsg{Type: keyRunes, Runes: []rune("R")})
	m = updated.(Model)
	if m.overlay != OverlaySessions {
		t.Fatalf("expected OverlaySessions after R, got %d", m.overlay)
	}
	if !m.sessionListLoading {
		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 TestSessionsLoadedPopulatesAndRenders(t *testing.T) {
	m := sessionsModel()
	m.overlay = OverlaySessions
	m.sessionListLoading = true
	m.applySessionsLoaded(sessionsLoadedMsg{sessions: sampleSummaries()})
	if m.sessionListLoading {
		t.Fatalf("expected loading cleared after load")
	}
	if len(m.sessionList) != 2 {
		t.Fatalf("expected 2 sessions, got %d", len(m.sessionList))
	}
	out := m.sessionsModal()
	if !strings.Contains(out, "aaaaaaaa") || !strings.Contains(out, "bbbbbbbb") {
		t.Fatalf("modal missing session ids:\n%s", out)
	}
	if !strings.Contains(out, "ACTIVE") || !strings.Contains(out, "FAILED") {
		t.Fatalf("modal missing statuses:\n%s", out)
	}
}

func TestSessionsNavigationBoundsChecked(t *testing.T) {
	m := sessionsModel()
	m.overlay = OverlaySessions
	m.sessionList = sampleSummaries()
	// Up at the top is a no-op (clamped at 0).
	updated, _ := m.handleSessionsKey(keyMsg{Type: keyUp})
	m = updated.(Model)
	if m.sessionListIndex != 0 {
		t.Fatalf("expected index clamped to 0 at top, got %d", m.sessionListIndex)
	}
	// Down moves to row 1.
	updated, _ = m.handleSessionsKey(keyMsg{Type: keyDown})
	m = updated.(Model)
	if m.sessionListIndex != 1 {
		t.Fatalf("expected index 1 after down, got %d", m.sessionListIndex)
	}
	// Down again is clamped at the last row.
	updated, _ = m.handleSessionsKey(keyMsg{Type: keyDown})
	m = updated.(Model)
	if m.sessionListIndex != 1 {
		t.Fatalf("expected index clamped to 1 at bottom, got %d", m.sessionListIndex)
	}
	// `j` is the vim-style alias for down (still clamped here).
	updated, _ = m.handleSessionsKey(keyMsg{Type: keyRunes, Runes: []rune("j")})
	m = updated.(Model)
	if m.sessionListIndex != 1 {
		t.Fatalf("expected j clamped at bottom, got %d", m.sessionListIndex)
	}
}

func TestSessionsEnterFocusesAndEmitsResume(t *testing.T) {
	m := sessionsModel()
	m.overlay = OverlaySessions
	m.sessionList = sampleSummaries()
	m.sessionListIndex = 1 // the FAILED healthcheck row
	updated, cmd := m.handleSessionsKey(keyMsg{Type: keyEnter})
	m = updated.(Model)
	if m.overlay != OverlayNone {
		t.Fatalf("expected overlay closed after resume, got %d", m.overlay)
	}
	if m.selectedID != "bbbbbbbb-2222" {
		t.Fatalf("expected selected id bbbbbbbb-2222, got %q", m.selectedID)
	}
	if !m.sessionEntered {
		t.Fatalf("expected session entered after resume")
	}
	if s := m.session("bbbbbbbb-2222"); s == nil || s.WorkflowID != "healthcheck" {
		t.Fatalf("expected vivified session with workflow healthcheck, got %+v", s)
	}
	// A resume cmd is emitted; with a nil client (no server addr) it resolves to a
	// sessionResumeMsg carrying the error — exercised here so it can't panic.
	if cmd == nil {
		t.Fatalf("expected a resume cmd to be emitted")
	}
	if msg, ok := cmd().(sessionResumeMsg); !ok {
		t.Fatalf("expected sessionResumeMsg from resume cmd, got %T", cmd())
	} else if msg.sessionID != "bbbbbbbb-2222" {
		t.Fatalf("resume msg for wrong session: %q", msg.sessionID)
	}
}

func TestSessionsFetchErrorShownNoPanic(t *testing.T) {
	m := sessionsModel()
	m.overlay = OverlaySessions
	m.sessionListLoading = true
	m.applySessionsLoaded(sessionsLoadedMsg{err: "server returned 503 Service Unavailable"})
	if m.sessionListLoading {
		t.Fatalf("expected loading cleared on error")
	}
	if m.sessionListErr == "" {
		t.Fatalf("expected error recorded")
	}
	out := m.sessionsModal()
	if !strings.Contains(out, "503") {
		t.Fatalf("modal should surface the fetch error:\n%s", out)
	}
}

func TestRelativeAgeParsing(t *testing.T) {
	if got := relativeAge(""); got != "—" {
		t.Fatalf("empty age should be em-dash, got %q", got)
	}
	if got := relativeAge("not-a-timestamp"); got != "—" {
		t.Fatalf("unparseable age should be em-dash, got %q", got)
	}
	if got := relativeAge("2020-01-01T00:00:00Z"); !strings.HasSuffix(got, "ago") {
		t.Fatalf("a real instant should render an age, got %q", got)
	}
}
