package app

import (
	"bufio"
	"os"
	"regexp"
	"strings"
	"testing"

	"github.com/correx/tui-go/internal/protocol"
	"github.com/correx/tui-go/internal/ws"
)

// render_matrix_test.go is the per-event-type golden render matrix (BACKLOG §E §2):
// one case per server event/entry kind the TUI handles. Each case drives a
// representative protocol.ServerMessage through the *production* path (applyServer),
// renders the surface it lands on (View / a card / a modal / the router transcript),
// strips ANSI, and asserts the render carries that kind's defining, stable text.
//
// Goldens are inline expected substrings rather than golden files: it matches the
// module's existing render-test convention (clarcard_test, ideacard_test, …), needs no
// -update dance, and pins the *meaning* of each render (the tool name, stage id, status
// label, card title) without coupling to exact lipgloss styling or timestamps.
//
// The coverage guard (TestRenderMatrixCoversEveryEventType) is the load-bearing part:
// it parses every protocol.Type* constant and fails if a new event kind is added
// without either a matrix case or an explicit non-rendering classification.

// matrixSurface selects which render surface a case asserts against — each is a real
// production render entry point, fed off the same Model that applyServer mutated.
type matrixSurface int

const (
	surfaceView           matrixSurface = iota // full immediate-mode frame, View()
	surfaceEvents                              // right-panel event-stream rows (eventRows)
	surfaceRouter                              // left transcript rows (routerRows)
	surfaceApprovalBand                        // docked approval band (renderApprovalBand)
	surfaceClarModal                           // clarification modal
	surfaceProposeModal                        // workflow-propose modal
	surfaceIdeasModal                          // idea board overlay
	surfaceStatsModal                          // session-stats overlay
	surfaceConfigModal                         // config editor overlay
	surfaceArtifactsModal                      // artifact viewer overlay
	surfaceModelsModal                         // model swap overlay
	surfaceStatusBar                           // top status bar (renderStatus)
)

// fixedNow is a deterministic timestamp used for every event carrying OccurredAt. The
// matrix asserts only event type/detail text, never the rendered clock — formatTime is
// local (machine-TZ dependent), so the time string is intentionally not part of any golden.
const fixedNow int64 = 1_700_000_000_000

// matrixCase is one event/entry kind in the matrix.
type matrixCase struct {
	name    string                        // human label / subtest name
	kinds   []string                      // protocol.Type* values this case exercises
	build   func() protocol.ServerMessage // a representative frame
	surface matrixSurface                 // where its render lands
	// prep mutates the model before applyServer (e.g. select + enter a session so an
	// in-session surface is reachable, or open the overlay an *.list reply fills).
	prep func(m *Model)
	want []string // ANSI-stripped substrings the render must contain
}

// newMatrixModel builds a deterministic Model: fixed width/theme, an entered session,
// an unconnected ws client (Send buffers, never dials).
func newMatrixModel() Model {
	m := NewModel(ws.New("", 0))
	m.width, m.height = 120, 40
	m.theme = NewTheme(SoftBlue)
	m.connected = true
	m.selectedID = "s1"
	m.sessionEntered = true
	m.sessions = []Session{{ID: "s1", Status: "ACTIVE", Name: "healthcheck", LastEventAt: fixedNow}}
	return m
}

// renderSurface renders the requested surface off m and strips ANSI.
func renderSurface(m Model, s matrixSurface) string {
	var raw string
	switch s {
	case surfaceView:
		raw = m.render()
	case surfaceEvents:
		// Render the event-stream rows at full panel width so a row's detail isn't
		// clipped at the slim right-panel edge (the production builder, just wide).
		raw = strings.Join(m.eventRows(110, 40), "\n")
	case surfaceRouter:
		raw = strings.Join(m.routerRows(100, 30), "\n")
	case surfaceApprovalBand:
		raw = m.renderApprovalBand(m.approvalBandHeight())
	case surfaceClarModal:
		raw = m.clarificationModal()
	case surfaceProposeModal:
		raw = m.proposeModal()
	case surfaceIdeasModal:
		raw = m.ideasModal()
	case surfaceStatsModal:
		raw = m.statsModal()
	case surfaceConfigModal:
		raw = m.configModal()
	case surfaceArtifactsModal:
		raw = m.artifactsModal()
	case surfaceModelsModal:
		raw = m.modelsModal()
	case surfaceStatusBar:
		raw = m.renderStatus()
	}
	return stripANSI(raw)
}

// matrixCases is the matrix: one entry per event/entry kind the renderer handles.
func matrixCases() []matrixCase {
	str := func(s string) *string { return &s }
	return []matrixCase{
		// --- session lifecycle (status bar + narration feed) ---
		{
			name:    "session.announced",
			kinds:   []string{protocol.TypeSessionAnnounced},
			surface: surfaceStatusBar,
			build:   func() protocol.ServerMessage { return msg(protocol.TypeSessionAnnounced, withWF("healthcheck")) },
			want:    []string{"healthcheck", "ACTIVE"},
		},
		{
			name:    "session.paused",
			kinds:   []string{protocol.TypeSessionPaused},
			surface: surfaceRouter,
			build:   func() protocol.ServerMessage { return msg(protocol.TypeSessionPaused) },
			want:    []string{"paused"},
		},
		{
			name:    "session.resumed",
			kinds:   []string{protocol.TypeSessionResumed},
			surface: surfaceRouter,
			build:   func() protocol.ServerMessage { return msg(protocol.TypeSessionResumed) },
			want:    []string{"resumed"},
		},
		{
			name:    "session.completed",
			kinds:   []string{protocol.TypeSessionCompleted},
			surface: surfaceRouter,
			build:   func() protocol.ServerMessage { return msg(protocol.TypeSessionCompleted) },
			want:    []string{"workflow complete"},
		},
		{
			name:    "session.failed",
			kinds:   []string{protocol.TypeSessionFailed},
			surface: surfaceRouter,
			build:   func() protocol.ServerMessage { return msg(protocol.TypeSessionFailed, withReason("schema mismatch")) },
			want:    []string{"workflow failed", "schema mismatch"},
		},

		// --- chat / router transcript ---
		{
			name:    "chat.turn (router)",
			kinds:   []string{protocol.TypeChatTurn},
			surface: surfaceRouter,
			build: func() protocol.ServerMessage {
				return msg(protocol.TypeChatTurn, func(m *protocol.ServerMessage) {
					m.Role, m.Content = "ROUTER", "running the healthcheck now"
				})
			},
			want: []string{"running the healthcheck now"},
		},
		{
			name:    "router.narration",
			kinds:   []string{protocol.TypeRouterNarration},
			surface: surfaceRouter,
			build: func() protocol.ServerMessage {
				return msg(protocol.TypeRouterNarration, func(m *protocol.ServerMessage) {
					m.Content = "moving on to validation"
				})
			},
			want: []string{"moving on to validation", "◆"},
		},

		// --- stage lifecycle (event stream rows) ---
		{
			name:    "stage.started",
			kinds:   []string{protocol.TypeStageStarted},
			surface: surfaceEvents,
			build:   func() protocol.ServerMessage { return msg(protocol.TypeStageStarted, withStage("write_script")) },
			want:    []string{"StageStarted", "write_script"},
		},
		{
			name:    "stage.completed",
			kinds:   []string{protocol.TypeStageCompleted},
			surface: surfaceEvents,
			build:   func() protocol.ServerMessage { return msg(protocol.TypeStageCompleted, withStage("write_script")) },
			want:    []string{"StageCompleted", "write_script"},
		},
		{
			name:    "stage.failed",
			kinds:   []string{protocol.TypeStageFailed},
			surface: surfaceEvents,
			build:   func() protocol.ServerMessage { return msg(protocol.TypeStageFailed, withStage("generate")) },
			want:    []string{"StageFailed", "generate"},
		},

		// --- inference lifecycle (event stream rows) ---
		{
			name:    "inference.started",
			kinds:   []string{protocol.TypeInferenceStarted},
			surface: surfaceEvents,
			build:   func() protocol.ServerMessage { return msg(protocol.TypeInferenceStarted, withStage("plan")) },
			want:    []string{"InferenceStarted", "plan"},
		},
		{
			name:    "inference.completed",
			kinds:   []string{protocol.TypeInferenceDone},
			surface: surfaceEvents,
			build: func() protocol.ServerMessage {
				return msg(protocol.TypeInferenceDone, withStage("plan"), func(m *protocol.ServerMessage) { m.Summary = "ok" })
			},
			want: []string{"InferenceCompleted", "plan"},
		},
		{
			name:    "inference.timed_out",
			kinds:   []string{protocol.TypeInferenceTimeout},
			surface: surfaceEvents,
			build:   func() protocol.ServerMessage { return msg(protocol.TypeInferenceTimeout, withStage("plan")) },
			want:    []string{"InferenceTimedOut", "plan"},
		},
		{
			name:    "inference.failed",
			kinds:   []string{protocol.TypeInferenceFailed},
			surface: surfaceEvents,
			build: func() protocol.ServerMessage {
				return msg(protocol.TypeInferenceFailed, withStage("plan"), withReason("oom"))
			},
			want: []string{"InferenceFailed", "plan", "oom"},
		},
		{
			name:    "inference.retry",
			kinds:   []string{protocol.TypeInferenceRetry},
			surface: surfaceEvents,
			build: func() protocol.ServerMessage {
				return msg(protocol.TypeInferenceRetry, withStage("plan"), func(m *protocol.ServerMessage) {
					m.AttemptNumber, m.MaxAttempts, m.FailureReason = 2, 3, "rate limited"
				})
			},
			want: []string{"RetryAttempted", "plan", "2/3", "rate limited"},
		},

		// --- tools ---
		{
			name:    "tool.started",
			kinds:   []string{protocol.TypeToolStarted},
			surface: surfaceStatusBar, // started shows as the active spinner; record lands in s.Tools
			build:   func() protocol.ServerMessage { return msg(protocol.TypeToolStarted, withTool("file_write")) },
			want:    []string{"active"},
		},
		{
			name:    "tool.completed",
			kinds:   []string{protocol.TypeToolCompleted},
			surface: surfaceEvents,
			build: func() protocol.ServerMessage {
				return msg(protocol.TypeToolCompleted, withTool("file_write"), func(m *protocol.ServerMessage) { m.Summary = "wrote 2 files" })
			},
			want: []string{"ToolCompleted", "file_write"},
		},
		{
			name:    "tool.failed",
			kinds:   []string{protocol.TypeToolFailed},
			surface: surfaceView,
			prep: func(m *Model) {
				// failed marks an already-started tool record; seed one so the status flips.
				s := m.session("s1")
				s.Tools = []ToolRecord{{Name: "file_write", Status: ToolStarted}}
			},
			build: func() protocol.ServerMessage { return msg(protocol.TypeToolFailed, withTool("file_write")) },
			// tool.failed is a state-only mutation (no event row, no card); assert it
			// renders the in-session frame without crashing and keeps the session header.
			want: []string{"healthcheck"},
		},
		{
			name:    "tool.rejected",
			kinds:   []string{protocol.TypeToolRejected},
			surface: surfaceView,
			prep: func(m *Model) {
				s := m.session("s1")
				s.Tools = []ToolRecord{{Name: "shell_exec", Status: ToolStarted}}
			},
			build: func() protocol.ServerMessage { return msg(protocol.TypeToolRejected, withTool("shell_exec")) },
			want:  []string{"healthcheck"},
		},
		{
			name:    "tool.assessed",
			kinds:   []string{protocol.TypeToolAssessed},
			surface: surfaceEvents,
			build: func() protocol.ServerMessage {
				return msg(protocol.TypeToolAssessed, withTool("file_write"), func(m *protocol.ServerMessage) {
					m.Disposition = "BLOCK"
					m.AssessedIssues = []protocol.AssessedIssueDto{{Code: "PATH_ESCAPE", Message: "outside workspace", Severity: "HIGH"}}
				})
			},
			want: []string{"ToolAssessed", "BLOCK", "PATH_ESCAPE"},
		},
		{
			name:    "review.findings",
			kinds:   []string{protocol.TypeReviewFindings},
			surface: surfaceEvents,
			build: func() protocol.ServerMessage {
				return msg(protocol.TypeReviewFindings, withStage("write_script"), func(m *protocol.ServerMessage) {
					m.ReviewVerdict = "FAIL"
					m.ReviewBlocked = true
					m.ReviewFindings = []protocol.ReviewFindingDto{
						{Severity: "CRITICAL", Confidence: 0.9, Category: "correctness", Target: "main.go:12", Message: "nil deref"},
					}
				})
			},
			want: []string{"ReviewFindings", "write_script", "FAIL"},
		},

		// --- artifacts / plan (event stream rows) ---
		{
			name:    "artifact.created",
			kinds:   []string{protocol.TypeArtifactCreated},
			surface: surfaceEvents,
			build: func() protocol.ServerMessage {
				return msg(protocol.TypeArtifactCreated, func(m *protocol.ServerMessage) { m.ArtifactID = "art-7" })
			},
			want: []string{"ArtifactCreated", "art-7"},
		},
		{
			name:    "artifact.validated",
			kinds:   []string{protocol.TypeArtifactValid},
			surface: surfaceEvents,
			build: func() protocol.ServerMessage {
				return msg(protocol.TypeArtifactValid, func(m *protocol.ServerMessage) { m.ArtifactID = "art-7" })
			},
			want: []string{"ArtifactValidated", "art-7"},
		},
		{
			name:    "plan.locked",
			kinds:   []string{protocol.TypePlanLocked},
			surface: surfaceEvents,
			build: func() protocol.ServerMessage {
				return msg(protocol.TypePlanLocked, func(m *protocol.ServerMessage) { m.StageIDs = []string{"plan", "build", "verify"} })
			},
			want: []string{"PlanLocked", "plan", "build", "verify"},
		},

		// --- workspace bind (status bar) ---
		{
			name:    "session.workspace_bound",
			kinds:   []string{protocol.TypeWorkspaceBound},
			surface: surfaceStatusBar,
			build: func() protocol.ServerMessage {
				return msg(protocol.TypeWorkspaceBound, func(m *protocol.ServerMessage) { m.WorkspaceRoot = "/tmp/corx-ws" })
			},
			want: []string{"corx-ws"},
		},

		// --- session rename (status bar shows intent-derived title) ---
		{
			name:    "session.renamed",
			kinds:   []string{protocol.TypeSessionRenamed},
			surface: surfaceStatusBar,
			build: func() protocol.ServerMessage {
				return msg(protocol.TypeSessionRenamed, func(m *protocol.ServerMessage) { m.Name = "Add Auth Layer" })
			},
			want: []string{"Add Auth Layer"},
		},

		// --- approval gate (docked band card) ---
		{
			name:    "approval.required",
			kinds:   []string{protocol.TypeApprovalRequired},
			surface: surfaceApprovalBand,
			build: func() protocol.ServerMessage {
				return msg(protocol.TypeApprovalRequired, withTool("file_write"), func(m *protocol.ServerMessage) {
					m.RequestID, m.Tier = "req-1", "T3"
					m.Preview = str("--- a/x\n+++ b/x\n@@ -1 +1 @@\n-old\n+new\n")
					m.RiskSummary = &protocol.RiskSummaryDto{Level: "HIGH", Rationale: []string{"[PATH_OUTSIDE_WORKSPACE] /etc/hosts"}}
				})
			},
			want: []string{"file_write", "T3", "HIGH", "PATH_OUTSIDE_WORKSPACE"},
		},
		{
			name:    "approval.resolved",
			kinds:   []string{protocol.TypeApprovalResolved},
			surface: surfaceEvents,
			build: func() protocol.ServerMessage {
				return msg(protocol.TypeApprovalResolved, func(m *protocol.ServerMessage) { m.Outcome, m.Reason = "APPROVED", "operator ok" })
			},
			want: []string{"ApprovalResolved", "APPROVED", "operator ok"},
		},

		// --- clarification (modal card) ---
		{
			name:    "clarification.required",
			kinds:   []string{protocol.TypeClarification},
			surface: surfaceClarModal,
			build: func() protocol.ServerMessage {
				return msg(protocol.TypeClarification, withStage("analyst"), func(m *protocol.ServerMessage) {
					m.RequestID = "req-1"
					m.Questions = []protocol.ClarificationQuestionDto{
						{ID: "stack", Prompt: "Which stack?", Options: []string{"React", "Vue"}, Header: "Stack"},
					}
				})
			},
			want: []string{"clarifying questions", "Which stack?", "React", "Vue", "Stack"},
		},

		// --- workflow proposal (modal card) ---
		{
			name:    "workflow.proposed",
			kinds:   []string{protocol.TypeWorkflowProposed},
			surface: surfaceProposeModal,
			build: func() protocol.ServerMessage {
				return msg(protocol.TypeWorkflowProposed, func(m *protocol.ServerMessage) {
					m.ProposalID, m.Prompt, m.OriginalRequest = "prop-1", "Run one of these?", "scan the repo"
					m.Candidates = []protocol.ProposedWorkflowDto{
						{WorkflowID: "research", Reason: "gather + report"},
						{WorkflowID: "role_pipeline", Reason: "full build"},
					}
				})
			},
			want: []string{"workflow suggestion", "Run one of these?", "research", "gather + report", "role_pipeline"},
		},

		// --- idea board (overlay; cross-session, no session scope) ---
		{
			name:    "idea.list",
			kinds:   []string{protocol.TypeIdeaList},
			surface: surfaceIdeasModal,
			prep:    func(m *Model) { m.overlay = OverlayIdeas },
			build: func() protocol.ServerMessage {
				return protocol.ServerMessage{Type: protocol.TypeIdeaList, Ideas: []protocol.IdeaDto{
					{IdeaID: "i1", Text: "cache the repo map", CapturedAtMs: 1000},
				}}
			},
			want: []string{"ideas", "cache the repo map"},
		},

		// --- session stats (overlay) ---
		{
			name:    "session.stats",
			kinds:   []string{protocol.TypeSessionStats},
			surface: surfaceStatsModal,
			prep:    func(m *Model) { m.overlay = OverlayStats; m.statsFor = "s1" },
			build: func() protocol.ServerMessage {
				return protocol.ServerMessage{Type: protocol.TypeSessionStats, SessionID: "s1", Stats: sampleStats()}
			},
			want: []string{"file_write", "read_file"},
		},

		// --- config snapshot (overlay) ---
		{
			name:    "config.snapshot",
			kinds:   []string{protocol.TypeConfigSnapshot},
			surface: surfaceConfigModal,
			prep:    func(m *Model) { m.overlay = OverlayConfig },
			build: func() protocol.ServerMessage {
				return protocol.ServerMessage{Type: protocol.TypeConfigSnapshot, ConfigFields: []protocol.ConfigFieldDto{
					{Key: "router.model", Type: "string", Value: "qwen2.5-coder"},
				}}
			},
			want: []string{"router.model", "qwen2.5-coder"},
		},

		// --- artifact list (overlay) ---
		{
			name:    "artifact.list",
			kinds:   []string{protocol.TypeArtifactList},
			surface: surfaceArtifactsModal,
			prep:    func(m *Model) { m.overlay = OverlayArtifacts; m.artifactsFor = "s1" },
			build: func() protocol.ServerMessage {
				return protocol.ServerMessage{Type: protocol.TypeArtifactList, SessionID: "s1", Artifacts: []protocol.ArtifactDto{
					{ArtifactID: "art-7", StageID: "plan", SchemaVersion: 1, Phase: "PLAN", Content: str("{\"ok\":true}")},
				}}
			},
			want: []string{"art-7"},
		},

		// --- model list / changed (overlay + status chrome) ---
		{
			name:    "model.list",
			kinds:   []string{protocol.TypeModelList},
			surface: surfaceModelsModal,
			prep:    func(m *Model) { m.overlay = OverlayModels },
			build: func() protocol.ServerMessage {
				return protocol.ServerMessage{Type: protocol.TypeModelList, Models: []string{"qwen2.5-coder", "llama3"}, Current: "qwen2.5-coder"}
			},
			want: []string{"qwen2.5-coder", "llama3"},
		},
		{
			name:    "model.changed",
			kinds:   []string{protocol.TypeModelChanged},
			surface: surfaceStatusBar,
			build: func() protocol.ServerMessage {
				return protocol.ServerMessage{Type: protocol.TypeModelChanged, ModelID: "llama3-swapped", Loaded: true}
			},
			want: []string{"llama3-swapped"},
		},

		// --- provider status (status bar model + locality) ---
		{
			name:    "provider.status_changed",
			kinds:   []string{protocol.TypeProviderStatus},
			surface: surfaceStatusBar,
			build: func() protocol.ServerMessage {
				return protocol.ServerMessage{Type: protocol.TypeProviderStatus, ProviderID: "llama-cpp:qwen"}
			},
			want: []string{"llama-cpp:qwen", "local"},
		},

		// --- resource gauge (status bar) ---
		{
			name:    "resource.status",
			kinds:   []string{protocol.TypeResourceStatus},
			surface: surfaceStatusBar,
			prep:    func(m *Model) { m.selectedID = ""; m.sessionEntered = false }, // gauge shows on the idle bar
			build: func() protocol.ServerMessage {
				used, total, util := int64(4096), int64(8192), 42
				return protocol.ServerMessage{Type: protocol.TypeResourceStatus,
					GpuMemoryUsedMb: &used, GpuMemoryTotalMb: &total, GpuUtilizationPct: &util}
			},
			want: []string{"VRAM 4096/8192M", "GPU 42%"},
		},

		// --- workflow list → idle launch target (Tab-selected, shown at the input) ---
		{
			name:    "workflow.list",
			kinds:   []string{protocol.TypeWorkflowList},
			surface: surfaceView,
			prep:    func(m *Model) { m.selectedID = ""; m.sessionEntered = false; m.launcherWf = 1 },
			build: func() protocol.ServerMessage {
				return protocol.ServerMessage{Type: protocol.TypeWorkflowList, Workflows: []protocol.WorkflowDto{
					{WorkflowID: "healthcheck", Description: "kick the tires"},
				}}
			},
			want: []string{"healthcheck"},
		},

		// --- session snapshot (reopen: rebuilds a session from the recent-events tail) ---
		{
			name:    "session_snapshot",
			kinds:   []string{protocol.TypeSessionSnapshot},
			surface: surfaceEvents,
			prep:    func(m *Model) { m.sessions = nil; m.selectedID = "snap1"; m.sessionEntered = true },
			build: func() protocol.ServerMessage {
				return protocol.ServerMessage{
					Type: protocol.TypeSessionSnapshot, SessionID: "snap1", WorkflowID: "refactor",
					State:        &protocol.SessionStateDto{Status: "PAUSED"},
					RecentEvents: []protocol.EventEntryDto{{Timestamp: fixedNow, Type: "StageStarted", Detail: "plan"}},
				}
			},
			want: []string{"StageStarted", "plan"},
		},

		// --- stage tool manifest (declared, not-yet-run tools — state only) ---
		{
			name:    "stage.tool_manifest",
			kinds:   []string{protocol.TypeStageManifest},
			surface: surfaceView,
			build: func() protocol.ServerMessage {
				return protocol.ServerMessage{Type: protocol.TypeStageManifest, SessionID: "s1", Stages: []protocol.StageToolDecl{
					{StageID: "plan", Tools: []protocol.ToolDecl{{Name: "read_file", Tier: 1}}},
				}}
			},
			// manifest seeds s.ToolsByStage (surfaced by the tool palette, not the main
			// frame); assert the in-session frame still renders cleanly with the session.
			want: []string{"healthcheck"},
		},

		// --- unknown / future event: raw fallback row, never dropped (§2) ---
		{
			name:    "unknown event (raw fallback)",
			kinds:   []string{"some.future.event"},
			surface: surfaceEvents,
			build: func() protocol.ServerMessage {
				return protocol.ServerMessage{Type: "some.future.event", SessionID: "s1", OccurredAt: fixedNow}
			},
			want: []string{"some.future.event", "raw"},
		},
	}
}

// --- frame builders (keep cases terse) ---

type msgOpt func(*protocol.ServerMessage)

func msg(typ string, opts ...msgOpt) protocol.ServerMessage {
	m := protocol.ServerMessage{Type: typ, SessionID: "s1", OccurredAt: fixedNow}
	for _, o := range opts {
		o(&m)
	}
	return m
}

func withWF(id string) msgOpt     { return func(m *protocol.ServerMessage) { m.WorkflowID = id } }
func withStage(id string) msgOpt  { return func(m *protocol.ServerMessage) { m.StageID = id } }
func withTool(name string) msgOpt { return func(m *protocol.ServerMessage) { m.ToolName = name } }
func withReason(r string) msgOpt  { return func(m *protocol.ServerMessage) { m.Reason = r } }

// TestRenderMatrix is the table-driven golden render matrix: every event/entry kind,
// driven through applyServer, asserts its render carries its defining stable text.
func TestRenderMatrix(t *testing.T) {
	for _, c := range matrixCases() {
		t.Run(c.name, func(t *testing.T) {
			m := newMatrixModel()
			if c.prep != nil {
				c.prep(&m)
			}
			m.applyServer(c.build())
			out := renderSurface(m, c.surface)
			for _, want := range c.want {
				if !strings.Contains(out, want) {
					t.Fatalf("render for %q missing %q\n--- visible ---\n%s", c.name, want, out)
				}
			}
		})
	}
}

// nonRenderingEventTypes are protocol.Type* constants the TUI handles but that have no
// per-kind render of their own — they are explicitly classified here so the coverage
// guard stays exhaustive. Adding a new Type* constant forces a choice: give it a matrix
// case, or list it here with a reason. Either way the guard can't silently pass.
var nonRenderingEventTypes = map[string]string{
	protocol.TypeSnapshotComplete: "control frame: ends the snapshot-buffering phase; no render",
	protocol.TypeRouterResponse:   "recognized no-op: superseded by chat.turn on the global stream",
	protocol.TypeProtocolError:    "transport diagnostic: explicit no-op, not surfaced",
	protocol.TypeFileList:           "query reply: populates the @ file picker overlay, not the transcript",
	protocol.TypeGrantList:          "query reply: populates the standing-grants overlay, not the transcript",
	protocol.TypeHealthChecks:       "query reply: populates the health-checks overlay, not the transcript",
	protocol.TypeProjectProfile:     "query reply: populates the project profile overlay, not the transcript",
	protocol.TypeOperatorProfile:    "query reply: populates the operator profile overlay, not the transcript",
}

// TestRenderMatrixCoversEveryEventType is the load-bearing matrix guarantee: it scans
// every protocol.Type* constant declared in protocol.go and fails if any is neither
// covered by a matrix case nor explicitly classified as non-rendering. A new event kind
// added to the protocol without a matching render assertion breaks this test.
func TestRenderMatrixCoversEveryEventType(t *testing.T) {
	declared := declaredEventTypes(t)
	if len(declared) < 30 {
		t.Fatalf("expected to scan ~40 protocol Type* constants, found only %d — parser drift?", len(declared))
	}

	covered := map[string]bool{}
	for _, c := range matrixCases() {
		for _, k := range c.kinds {
			covered[k] = true
		}
	}

	var missing []string
	for typ := range declared {
		if covered[typ] {
			continue
		}
		if _, ok := nonRenderingEventTypes[typ]; ok {
			continue
		}
		missing = append(missing, typ)
	}
	if len(missing) > 0 {
		t.Fatalf("event types with no matrix case and no non-rendering classification: %v\n"+
			"add a case to matrixCases() (preferred) or list it in nonRenderingEventTypes with a reason", missing)
	}

	// Reverse guard: every classified non-rendering type must still be a real constant
	// (catches a typo or a removed constant rotting the allowlist).
	for typ := range nonRenderingEventTypes {
		if !declared[typ] {
			t.Errorf("nonRenderingEventTypes lists %q which is not a declared protocol Type* value", typ)
		}
	}
}

// declaredEventTypes parses protocol.go and returns the value of every Type* string
// constant (e.g. "stage.started"). Deriving the authoritative set from source — rather
// than a hand-kept list — is what makes the coverage guard catch *new* event kinds.
func declaredEventTypes(t *testing.T) map[string]bool {
	t.Helper()
	const src = "../protocol/protocol.go"
	f, err := os.Open(src)
	if err != nil {
		t.Fatalf("open %s: %v", src, err)
	}
	defer f.Close()

	// Matches lines like:  TypeStageStarted     = "stage.started"
	re := regexp.MustCompile(`^\s*Type[A-Za-z0-9]+\s*=\s*"([^"]+)"`)
	out := map[string]bool{}
	sc := bufio.NewScanner(f)
	for sc.Scan() {
		if mm := re.FindStringSubmatch(sc.Text()); mm != nil {
			out[mm[1]] = true
		}
	}
	if err := sc.Err(); err != nil {
		t.Fatalf("scan %s: %v", src, err)
	}
	return out
}
