package ui

import (
	"strings"
	"testing"
	"time"

	"github.com/Armur-Ai/Pentest-Swarm-AI/internal/pipeline"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

// TestResponsiveNoOverflow drives the campaign model at a spread of window
// sizes (from a tiny pane up to a wide one), feeds it a stream of events, and
// asserts that no rendered line is wider than the window — i.e. the art/panels
// degrade instead of wrapping into noise — and that the viewport keeps at least
// a few rows and a scrollbar gutter renders.
func TestResponsiveNoOverflow(t *testing.T) {
	sizes := [][2]int{
		{24, 10}, {30, 14}, {40, 18}, {56, 24}, {72, 30}, {100, 40}, {160, 50},
	}
	for _, s := range sizes {
		w, h := s[0], s[1]
		var tm tea.Model = NewModel("live", "http://target.example/very/long/path/that/could/overflow", "find and prove everything")
		tm, _ = tm.Update(tea.WindowSizeMsg{Width: w, Height: h})
		// Stream a realistic burst of events so the log, findings and errors fill.
		for i := 0; i < 60; i++ {
			tm, _ = tm.Update(EventMsg(pipeline.CampaignEvent{
				EventType: pipeline.EventToolResult,
				AgentName: "recon",
				Detail:    strings.Repeat("nmap/httpx output segment ", 4),
				Timestamp: time.Now(),
			}))
		}
		tm, _ = tm.Update(EventMsg(pipeline.CampaignEvent{
			EventType: pipeline.EventError, AgentName: "exploit",
			Detail: "LLM provider returned 401 unauthorized — check your API key", Timestamp: time.Now(),
		}))
		// A chain with long reasoning + steps, so the chain panel is width-checked.
		tm, _ = tm.Update(EventMsg(pipeline.CampaignEvent{
			EventType: pipeline.EventChainStarted, AgentName: "exploit", Timestamp: time.Now(),
			Data: []byte(`{"id":"ch1","name":"BOLA to account takeover via leaked reset token","reasoning":"` +
				strings.Repeat("the recon surface exposed an IDOR on /vehicle/{id}/location which leaks owner email; ", 3) +
				`","steps":[{"name":"enumerate vehicle ids","technique":"T1595"},{"name":"leak owner email","technique":"T1213"},{"name":"reset password","technique":"T1110"}]}`),
		}))
		tm, _ = tm.Update(EventMsg(pipeline.CampaignEvent{
			EventType: pipeline.EventChainStep, AgentName: "exploit", Timestamp: time.Now(),
			Data: []byte(`{"chain_id":"ch1","step":"enumerate vehicle ids","success":true,"proof":"200 OK ids 1..50"}`),
		}))
		// Fire probes so the animated EXPLOIT SWARM fan renders and is width-checked.
		for i := 0; i < 30; i++ {
			tm, _ = tm.Update(EventMsg(pipeline.CampaignEvent{
				EventType: pipeline.EventProbe, AgentName: "exploit", Timestamp: time.Now(),
				Detail: "/identity/api/v2/vehicle/x/location",
				Data:   []byte(`{"target":"/identity/api/v2/vehicle/x/location","ok":false}`),
			}))
		}
		// A medium + a CRITICAL finding, so the findings reel and the critical
		// strobe banner both render and get width-checked at every size.
		tm, _ = tm.Update(EventMsg(pipeline.CampaignEvent{
			EventType: pipeline.EventFindingDiscovered, AgentName: "exploit", Timestamp: time.Now(),
			Data: []byte(`{"severity":"medium","title":"Excessive data exposure in the community feed leaking long PII records","verified":true}`),
		}))
		tm, _ = tm.Update(EventMsg(pipeline.CampaignEvent{
			EventType: pipeline.EventFindingDiscovered, AgentName: "exploit", Timestamp: time.Now(),
			Data: []byte(`{"severity":"critical","title":"JWT alg:none accepted — full account takeover of any user including admin","verified":true}`),
		}))
		out := tm.(Model).View()
		for i, line := range strings.Split(out, "\n") {
			if lw := lipgloss.Width(line); lw > w {
				t.Errorf("size %dx%d: line %d width %d > window width %d: %q", w, h, i, lw, w, line)
			}
		}
		m := tm.(Model)
		if m.viewport.Height < 3 {
			t.Errorf("size %dx%d: viewport height %d < 3", w, h, m.viewport.Height)
		}
	}
}

// TestScrollingAndScrollbar verifies the event-log viewport scrolls on key
// input and that the scrollbar reflects position (thumb not pinned at bottom
// after scrolling up, and the column is exactly viewport.Height rows tall).
func TestScrollingAndScrollbar(t *testing.T) {
	var tm tea.Model = NewModel("live", "http://t", "obj")
	tm, _ = tm.Update(tea.WindowSizeMsg{Width: 100, Height: 40})
	for i := 0; i < 200; i++ {
		tm, _ = tm.Update(EventMsg(pipeline.CampaignEvent{
			EventType: pipeline.EventToolResult, AgentName: "recon",
			Detail: "line " + strings.Repeat("x", 10), Timestamp: time.Now(),
		}))
	}
	m := tm.(Model)
	if !m.viewport.AtBottom() {
		t.Fatalf("expected to follow tail (AtBottom) after streaming events")
	}
	bottomPct := m.viewport.ScrollPercent()
	// Scroll up a page.
	tm, _ = m.Update(tea.KeyMsg{Type: tea.KeyPgUp})
	m = tm.(Model)
	if m.viewport.ScrollPercent() >= bottomPct {
		t.Errorf("PgUp did not scroll up: before=%.2f after=%.2f", bottomPct, m.viewport.ScrollPercent())
	}
	// Scrollbar column must be exactly viewport.Height rows.
	sb := m.scrollbarCol()
	if got := strings.Count(sb, "\n") + 1; got != m.viewport.Height {
		t.Errorf("scrollbar rows = %d, want viewport height %d", got, m.viewport.Height)
	}
	if !strings.Contains(sb, "█") {
		t.Errorf("scrollbar has no thumb when content overflows")
	}
}

// TestChainPanelShowsReasoningAndSteps verifies the dedicated chain panel
// surfaces the planner's reasoning AND each step's live status — the feature
// that was previously generated and then dropped before reaching any screen.
func TestChainPanelShowsReasoningAndSteps(t *testing.T) {
	var tm tea.Model = NewModel("live", "http://t", "obj")
	tm, _ = tm.Update(tea.WindowSizeMsg{Width: 120, Height: 44})
	tm, _ = tm.Update(EventMsg(pipeline.CampaignEvent{
		EventType: pipeline.EventChainStarted, AgentName: "exploit", Timestamp: time.Now(),
		Data: []byte(`{"id":"ch1","name":"BOLA to ATO","reasoning":"chain the IDOR leak into a password reset",` +
			`"steps":[{"name":"enumerate ids","technique":"T1595"},{"name":"leak email","technique":"T1213"}]}`),
	}))
	tm, _ = tm.Update(EventMsg(pipeline.CampaignEvent{
		EventType: pipeline.EventChainStep, AgentName: "exploit", Timestamp: time.Now(),
		Data: []byte(`{"chain_id":"ch1","step":"enumerate ids","success":true,"proof":"ok"}`),
	}))
	tm, _ = tm.Update(EventMsg(pipeline.CampaignEvent{
		EventType: pipeline.EventChainStep, AgentName: "exploit", Timestamp: time.Now(),
		Data: []byte(`{"chain_id":"ch1","step":"leak email","success":false,"proof":"403"}`),
	}))

	m := tm.(Model)
	if len(m.chainList) != 1 {
		t.Fatalf("chainList = %d, want 1", len(m.chainList))
	}
	if got := []string{m.chainList[0].steps[0].status, m.chainList[0].steps[1].status}; got[0] != "ok" || got[1] != "fail" {
		t.Fatalf("step statuses = %v, want [ok fail]", got)
	}
	out := stripANSI(m.View())
	for _, want := range []string{"CHAINS", "BOLA to ATO", "chain the IDOR leak into a password reset", "T1595", "enumerate ids"} {
		if !strings.Contains(out, want) {
			t.Errorf("chain panel missing %q in:\n%s", want, out)
		}
	}
}

// TestCriticalFlash verifies the attention strobe: a critical finding arms a
// multi-frame flash banner naming the finding, which then decays to nothing
// over successive ticks; a high finding arms a shorter flash.
func TestCriticalFlash(t *testing.T) {
	var tm tea.Model = NewModel("live", "http://t", "obj")
	tm, _ = tm.Update(tea.WindowSizeMsg{Width: 120, Height: 44})
	tm, _ = tm.Update(EventMsg(pipeline.CampaignEvent{
		EventType: pipeline.EventFindingDiscovered, AgentName: "exploit", Timestamp: time.Now(),
		Data: []byte(`{"severity":"critical","title":"JWT alg:none → account takeover"}`),
	}))
	m := tm.(Model)
	if m.flash <= 0 || m.flashSev != pipeline.SeverityCritical {
		t.Fatalf("critical finding should arm the flash: flash=%d sev=%q", m.flash, m.flashSev)
	}
	if out := stripANSI(m.View()); !strings.Contains(out, "CRITICAL FINDING") || !strings.Contains(out, "account takeover") {
		t.Fatalf("flash banner not rendered:\n%s", out)
	}
	// Decay: enough ticks must clear the banner.
	for i := 0; i < 8; i++ {
		tm, _ = tm.Update(TickMsg(time.Now()))
	}
	m = tm.(Model)
	if m.flash != 0 {
		t.Errorf("flash should have decayed to 0, got %d", m.flash)
	}
	if strings.Contains(stripANSI(m.View()), "CRITICAL FINDING") {
		t.Errorf("flash banner should be gone after decay")
	}
	// A high finding arms a (shorter) flash too.
	tm, _ = tm.Update(EventMsg(pipeline.CampaignEvent{
		EventType: pipeline.EventFindingDiscovered, AgentName: "exploit", Timestamp: time.Now(),
		Data: []byte(`{"severity":"high","title":"BOLA cross-user read"}`),
	}))
	if m2 := tm.(Model); m2.flash <= 0 || m2.flashSev != pipeline.SeverityHigh {
		t.Errorf("high finding should arm the flash, got flash=%d sev=%q", m2.flash, m2.flashSev)
	}
}

// TestAttackPathDrawsStepStates verifies the ASCII attack-path reflects per-step
// status (green completed, red fail, amber running, faint pending, ◆ objective)
// and never exceeds the width.
func TestAttackPathDrawsStepStates(t *testing.T) {
	steps := []chainStepView{
		{name: "auth", status: "ok"},
		{name: "harvest id", status: "ok"},
		{name: "replay", status: "pending"}, // the running node
		{name: "exfil", status: "pending"},  // objective, still pending
	}
	const w = 80
	line := attackPathLine(steps, 0, w)
	if line == "" {
		t.Fatal("expected a non-empty attack-path line")
	}
	if lipgloss.Width(line) > w {
		t.Errorf("attack-path width %d > %d: %q", lipgloss.Width(line), w, line)
	}
	plain := stripANSI(line)
	for _, want := range []string{"path ▸", "●", "◉", "◇"} { // completed, running, pending-objective
		if !strings.Contains(plain, want) {
			t.Errorf("attack-path missing %q in %q", want, plain)
		}
	}
	// A completed objective shows the solid ◆.
	done := []chainStepView{{name: "a", status: "ok"}, {name: "b", status: "ok"}}
	if !strings.Contains(stripANSI(attackPathLine(done, 0, w)), "◆") {
		t.Errorf("completed objective should render ◆")
	}
	// Too-narrow → empty, never an overflow.
	if attackPathLine(steps, 0, 10) != "" {
		t.Errorf("expected empty path when there's no room")
	}
}
