package tui

import (
	"strings"
	"testing"

	tea "charm.land/bubbletea/v2"

	"github.com/smallnest/pigo/internal/agentcore"
)

// typeCommand feeds "/name" into the model and presses Enter, mirroring how a
// user runs a slash command from the composer (the popup is open at Enter, so it
// routes through submitSlashSelected, exactly like the REPL path).
func typeCommand(t *testing.T, m Model, cmd string) Model {
	t.Helper()
	m = typeInto(t, m, cmd).(Model)
	got, c := m.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
	if c != nil {
		if msg := c(); msg != nil {
			if _, isQuit := msg.(tea.QuitMsg); isQuit {
				t.Fatalf("%s should not quit", cmd)
			}
		}
	}
	return got.(Model)
}

// TestStatusWithSessionRendersSections drives /status on a session-bound model
// and asserts every report section appears in the transcript, with the model
// staying idle (no run is started).
func TestStatusWithSessionRendersSections(t *testing.T) {
	store := newTestStore(t)
	s, _, err := newRunSessionWithStore(store, Options{
		Model:         "status-model",
		ProviderName:  "status-provider",
		ThinkingLevel: agentcore.ThinkingLevel("low"),
	})
	if err != nil {
		t.Fatalf("newRunSessionWithStore: %v", err)
	}
	// The /status environment section keys off the launch directory; keep it
	// deterministic rather than depending on the test's cwd.
	s.cwd = "/tmp/tui-status"

	m := NewModel(Options{}).withSession(s, nil)
	m = typeCommand(t, m, "/status")

	if m.running {
		t.Error("/status is an action command; model should stay idle")
	}
	if m.input.Value() != "" {
		t.Errorf("after executing /status, input = %q, want cleared", m.input.Value())
	}
	joined := strings.Join(blockTexts(m.transcript), "\n")
	for _, want := range []string{
		"runtime config:",
		"model: status-model",
		"provider: status-provider",
		"context:",
		"project & environment:",
		"cwd: /tmp/tui-status",
		"credentials & connectivity:",
		"telemetry:",
		"no telemetry yet",
	} {
		if !strings.Contains(joined, want) {
			t.Errorf("/status output missing %q; transcript:\n%s", want, joined)
		}
	}
}

// TestStatusWithoutSessionNotice verifies a session-less model reports the
// unavailable notice rather than panicking on nil collaborators, and still
// clears the input.
func TestStatusWithoutSessionNotice(t *testing.T) {
	m := NewModel(Options{})
	m = typeCommand(t, m, "/status")

	if m.running {
		t.Error("/status must not start a run on a session-less model")
	}
	if m.input.Value() != "" {
		t.Errorf("after executing /status, input = %q, want cleared", m.input.Value())
	}
	joined := strings.Join(blockTexts(m.transcript), "\n")
	if !strings.Contains(joined, "status unavailable: no active session") {
		t.Errorf("expected an unavailable notice in transcript, got:\n%s", joined)
	}
}

// TestSessionCommandRendersSummary drives /session on a session-bound model and
// asserts the summary lines (session id, message count, tokens, model/provider,
// compactions) match the REPL's /session format.
func TestSessionCommandRendersSummary(t *testing.T) {
	store := newTestStore(t)
	s, _, err := newRunSessionWithStore(store, Options{
		Model:        "session-model",
		ProviderName: "session-provider",
	})
	if err != nil {
		t.Fatalf("newRunSessionWithStore: %v", err)
	}
	// Simulate two user/assistant turns in the live context (unsaved messages are
	// counted too, mirroring the REPL's in-memory source of truth).
	s.agentCtx.Messages = agentcore.MessageList{
		agentcore.UserMessage{RoleField: agentcore.RoleUser, Content: agentcore.ContentList{agentcore.NewTextContent("q1")}},
		agentcore.AssistantMessage{RoleField: agentcore.RoleAssistant, Content: agentcore.ContentList{agentcore.NewTextContent("a1")}},
		agentcore.UserMessage{RoleField: agentcore.RoleUser, Content: agentcore.ContentList{agentcore.NewTextContent("q2")}},
		agentcore.AssistantMessage{RoleField: agentcore.RoleAssistant, Content: agentcore.ContentList{agentcore.NewTextContent("a2")}},
	}

	m := NewModel(Options{}).withSession(s, nil)
	m = typeCommand(t, m, "/session")

	if m.running {
		t.Error("/session is an action command; model should stay idle")
	}
	joined := strings.Join(blockTexts(m.transcript), "\n")
	for _, want := range []string{
		"session:      " + s.header.ID,
		"messages:     4",
		"tokens (est):",
		"model:        session-model (provider: session-provider)",
		"compactions:  0",
	} {
		if !strings.Contains(joined, want) {
			t.Errorf("/session output missing %q; transcript:\n%s", want, joined)
		}
	}
}

// TestSessionWithoutSessionNotice verifies a session-less model reports the
// unavailable notice for /session.
func TestSessionWithoutSessionNotice(t *testing.T) {
	m := NewModel(Options{})
	m = typeCommand(t, m, "/session")

	joined := strings.Join(blockTexts(m.transcript), "\n")
	if !strings.Contains(joined, "session unavailable: no active session") {
		t.Errorf("expected an unavailable notice in transcript, got:\n%s", joined)
	}
}

// TestTelemetryFoldingFeedsStatus verifies a telemetryMsg from the bridge is
// folded into the session's telemetry holder, so /status renders the cumulative
// and last-run telemetry blocks instead of "no telemetry yet".
func TestTelemetryFoldingFeedsStatus(t *testing.T) {
	store := newTestStore(t)
	s, _, err := newRunSessionWithStore(store, Options{Model: "m", ProviderName: "p"})
	if err != nil {
		t.Fatalf("newRunSessionWithStore: %v", err)
	}
	if s.telemetry == nil {
		t.Fatal("session telemetry holder should be initialized")
	}

	m := NewModel(Options{}).withSession(s, nil)
	// A telemetryMsg is bridged while a run is pumping; set up that state so
	// Update keeps the pump running (pumpNext) exactly as during a real run.
	m.running = true
	m.runCh = make(chan tea.Msg)
	next, cmd := m.Update(telemetryMsg{ev: agentcore.TelemetryEvent{
		Turns:              3,
		TruncationCount:    1,
		CompactionCount:    1,
		ContextTokens:      1000,
		ContextWindow:      200000,
		ContextUtilization: 0.005,
	}})
	if cmd == nil {
		t.Fatal("telemetryMsg should return a pump cmd while a run is in flight")
	}
	m = next.(Model)

	// The event must be retained on the session's holder (not just the status
	// bar), otherwise /status could not render the telemetry section.
	if !s.telemetry.HasTelemetry() {
		t.Fatal("telemetry event should be folded into the session holder")
	}
	if s.telemetry.CumulativeTurns() != 3 {
		t.Errorf("CumulativeTurns = %d, want 3", s.telemetry.CumulativeTurns())
	}

	// The run has since ended, returning the model to idle so the user can
	// issue /status (key presses are dropped while a run is in flight).
	m.running = false
	m.runCh = nil

	m = typeCommand(t, m, "/status")
	joined := strings.Join(blockTexts(m.transcript), "\n")
	for _, want := range []string{
		"telemetry:",
		"since session start:",
		"turns: 3",
		"truncations: 1",
		"last run:",
	} {
		if !strings.Contains(joined, want) {
			t.Errorf("/status output missing %q after telemetry fold; transcript:\n%s", want, joined)
		}
	}
	if strings.Contains(joined, "no telemetry yet") {
		t.Error("/status should render real telemetry after a fold, not 'no telemetry yet'")
	}
}

// TestStatusNotInterceptedForStatusFoo verifies "/statusfoo" is NOT intercepted
// as /status (mirroring the REPL's guard), so it resolves as an unknown command
// rather than rendering the status report.
func TestStatusNotInterceptedForStatusFoo(t *testing.T) {
	m := NewModel(Options{})
	m = typeCommand(t, m, "/statusfoo")

	joined := strings.Join(blockTexts(m.transcript), "\n")
	if strings.Contains(joined, "runtime config:") {
		t.Errorf("/statusfoo must not run the status command; transcript:\n%s", joined)
	}
}
