package app

import (
	"encoding/json"
	"strings"
	"testing"

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

// approval_test.go covers the approval-ergonomics gaps (BACKLOG §E §3): single-key
// approve/reject/steer for low tiers, an arm→confirm gate for T3+, and a navigable
// pending-approval queue (never modal-stacked). Deterministic: an unconnected ws
// client buffers Sent frames, Drain reads them back.

// approvalModel builds an entered session and queues each given approval through the
// production applyServer path, so the queue/Pending state matches real event flow.
func approvalModel(approvals ...protocol.ServerMessage) (Model, *ws.Client) {
	client := ws.New("", 0) // unconnected; Send buffers, Drain reads it back
	m := NewModel(client)
	m.width, m.height = 120, 40
	m.theme = NewTheme(SoftBlue)
	m.selectedID = "s1"
	m.sessionEntered = true
	m.sessions = []Session{{ID: "s1", Status: "ACTIVE", Name: "healthcheck", LastEventAt: fixedNow}}
	for _, a := range approvals {
		m.applyServer(a)
	}
	return m, client
}

// apprReq builds an approval.required frame for the given request id and tier.
func apprReq(reqID, tier string) protocol.ServerMessage {
	return msg(protocol.TypeApprovalRequired, withTool("file_write"), func(m *protocol.ServerMessage) {
		m.RequestID, m.Tier = reqID, tier
		m.RiskSummary = &protocol.RiskSummaryDto{Level: "LOW"}
	})
}

// applyKey drives a key through the production handleKey entry and returns the model.
func applyKey(m Model, k keyMsg) Model {
	updated, _ := m.handleKey(k)
	return updated.(Model)
}

func runeKey(r rune) keyMsg { return keyMsg{Type: keyRunes, Runes: []rune{r}} }

// decodeApproval pulls the single ApprovalResponse frame off the client and returns
// its request id + decision. Fails if there isn't exactly one decision frame.
func decodeApproval(t *testing.T, client *ws.Client) (requestID, decision string) {
	t.Helper()
	var resp map[string]any
	n := 0
	for _, f := range client.Drain() {
		var raw map[string]any
		if err := json.Unmarshal(f, &raw); err != nil {
			t.Fatalf("unmarshal: %v", err)
		}
		if raw["type"] == "com.correx.apps.server.protocol.ClientMessage.ApprovalResponse" {
			resp = raw
			n++
		}
	}
	if n != 1 {
		t.Fatalf("expected exactly one ApprovalResponse frame, got %d", n)
	}
	rid, _ := resp["requestId"].(string)
	dec, _ := resp["decision"].(string)
	return rid, dec
}

// noApprovalSent asserts the client buffered no ApprovalResponse frame.
func noApprovalSent(t *testing.T, client *ws.Client) {
	t.Helper()
	for _, f := range client.Drain() {
		var raw map[string]any
		_ = json.Unmarshal(f, &raw)
		if raw["type"] == "com.correx.apps.server.protocol.ClientMessage.ApprovalResponse" {
			t.Fatalf("did not expect an ApprovalResponse frame yet")
		}
	}
}

func TestApprovalLowTierApprovesImmediately(t *testing.T) {
	m, client := approvalModel(apprReq("req-1", "T1"))
	if m.displayState() != StateApproval {
		t.Fatalf("want StateApproval, got %v", m.displayState())
	}
	m = applyKey(m, runeKey('y'))
	rid, dec := decodeApproval(t, client)
	if rid != "req-1" || dec != "APPROVE" {
		t.Fatalf("want approve req-1, got %s/%s", rid, dec)
	}
	if s := m.session("s1"); s == nil || s.Pending != nil {
		t.Fatalf("pending gate should be cleared after a low-tier approve")
	}
	if m.approvalArmed {
		t.Fatalf("low-tier approve must not leave an armed confirm")
	}
}

func TestApprovalHighTierRequiresSecondConfirm(t *testing.T) {
	m, client := approvalModel(apprReq("req-1", "T3"))
	// First `y` arms; nothing is sent yet.
	m = applyKey(m, runeKey('y'))
	if !m.approvalArmed {
		t.Fatalf("T3 approve should arm a confirm, not send")
	}
	noApprovalSent(t, client)
	if s := m.session("s1"); s == nil || s.Pending == nil {
		t.Fatalf("gate must remain pending while armed")
	}
	// Second `y` confirms and sends.
	m = applyKey(m, runeKey('y'))
	rid, dec := decodeApproval(t, client)
	if rid != "req-1" || dec != "APPROVE" {
		t.Fatalf("want approve req-1 after confirm, got %s/%s", rid, dec)
	}
	if m.approvalArmed {
		t.Fatalf("arm should clear after the confirming keypress")
	}
}

func TestApprovalHighTierArmCancelledByOtherKey(t *testing.T) {
	m, client := approvalModel(apprReq("req-1", "T3"))
	m = applyKey(m, runeKey('y')) // arm
	if !m.approvalArmed {
		t.Fatalf("expected armed after first y")
	}
	// A non-confirm key (here `n` reject) cancels the arm. The arm itself must be
	// gone, and no spurious APPROVE may slip through.
	m = applyKey(m, runeKey('n'))
	if m.approvalArmed {
		t.Fatalf("arm should be cancelled by a non-confirm key")
	}
	rid, dec := decodeApproval(t, client)
	if dec != "REJECT" || rid != "req-1" {
		t.Fatalf("non-confirm key should route to its own action (reject), got %s/%s", rid, dec)
	}
}

func TestApprovalHighTierArmCancelledByNavKey(t *testing.T) {
	// Two T3 gates: arming then pressing ↓ must disarm and only move the cursor —
	// no decision may be emitted.
	m, client := approvalModel(apprReq("req-1", "T3"), apprReq("req-2", "T3"))
	m = applyKey(m, runeKey('y')) // arm req-1
	m = applyKey(m, keyMsg{Type: keyDown})
	if m.approvalArmed {
		t.Fatalf("nav key should disarm")
	}
	noApprovalSent(t, client)
	if s := m.session("s1"); s == nil || s.PendingIdx != 1 {
		t.Fatalf("expected cursor advanced to index 1, got %d", s.PendingIdx)
	}
}

func TestApprovalQueueNavigatesAndCounts(t *testing.T) {
	m, _ := approvalModel(apprReq("req-1", "T1"), apprReq("req-2", "T1"), apprReq("req-3", "T1"))
	s := m.session("s1")
	if s == nil || len(s.PendingQueue) != 3 {
		t.Fatalf("expected 3 queued approvals, got %d", len(s.PendingQueue))
	}
	// The band shows a "1/3" count and the current request, never stacked modals.
	out := stripANSI(m.renderApprovalBand(m.approvalBandHeight()))
	if !strings.Contains(out, "1/3") {
		t.Fatalf("band should show queue count 1/3:\n%s", out)
	}
	// ↓ advances the selection; Pending follows the cursor.
	m = applyKey(m, keyMsg{Type: keyDown})
	s = m.session("s1")
	if s.PendingIdx != 1 || s.Pending.RequestID != "req-2" {
		t.Fatalf("↓ should select req-2, got idx=%d id=%s", s.PendingIdx, s.Pending.RequestID)
	}
	out = stripANSI(m.renderApprovalBand(m.approvalBandHeight()))
	if !strings.Contains(out, "2/3") {
		t.Fatalf("band should show 2/3 after ↓:\n%s", out)
	}
	// ↑ wraps back.
	m = applyKey(m, keyMsg{Type: keyUp})
	if m.session("s1").PendingIdx != 0 {
		t.Fatalf("↑ should return to index 0")
	}
}

func TestApprovalResolveFrontAdvancesQueue(t *testing.T) {
	m, client := approvalModel(apprReq("req-1", "T1"), apprReq("req-2", "T1"))
	// Approve the front gate (low tier → immediate).
	m = applyKey(m, runeKey('y'))
	if rid, dec := decodeApproval(t, client); rid != "req-1" || dec != "APPROVE" {
		t.Fatalf("want approve req-1, got %s/%s", rid, dec)
	}
	s := m.session("s1")
	if s == nil || len(s.PendingQueue) != 1 || s.Pending.RequestID != "req-2" {
		t.Fatalf("resolving the front gate should advance to req-2, got %+v", s.PendingQueue)
	}
	// A server-side resolve of the remaining gate empties the queue and exits approval.
	m.applyServer(msg(protocol.TypeApprovalResolved, func(mm *protocol.ServerMessage) {
		mm.RequestID, mm.Outcome = "req-2", "APPROVED"
	}))
	if s := m.session("s1"); s == nil || s.Pending != nil || len(s.PendingQueue) != 0 {
		t.Fatalf("server resolve should clear the last gate")
	}
	if m.displayState() != StateInSession {
		t.Fatalf("empty queue should leave approval state, got %v", m.displayState())
	}
}

func TestApprovalServerResolveRemovesSpecificGate(t *testing.T) {
	// A resolve for the *back* gate must drop only that one and keep the front shown.
	m, _ := approvalModel(apprReq("req-1", "T1"), apprReq("req-2", "T1"))
	m.applyServer(msg(protocol.TypeApprovalResolved, func(mm *protocol.ServerMessage) {
		mm.RequestID, mm.Outcome = "req-2", "APPROVED"
	}))
	s := m.session("s1")
	if s == nil || len(s.PendingQueue) != 1 || s.Pending.RequestID != "req-1" {
		t.Fatalf("resolving req-2 should leave req-1 shown, got %+v", s.PendingQueue)
	}
}

func TestApprovalRejectAndSteerRoute(t *testing.T) {
	// Reject is single-key for a low tier.
	m, client := approvalModel(apprReq("req-1", "T1"))
	m = applyKey(m, runeKey('n'))
	if rid, dec := decodeApproval(t, client); rid != "req-1" || dec != "REJECT" {
		t.Fatalf("want reject req-1, got %s/%s", rid, dec)
	}

	// Steer enters the existing note-input path (no decision sent yet).
	m2, client2 := approvalModel(apprReq("req-2", "T1"))
	m2 = applyKey(m2, runeKey('e'))
	if !m2.steering || m2.editMode != ModeInsert {
		t.Fatalf("steer key should enter the steering-note input path")
	}
	noApprovalSent(t, client2)
	// Typing a note then Enter approves with the note riding along.
	for _, r := range "use sudo" {
		m2 = applyKey(m2, runeKey(r))
	}
	m2 = applyKey(m2, keyMsg{Type: keyEnter})
	frames := client2.Drain()
	var note string
	for _, f := range frames {
		var raw map[string]any
		_ = json.Unmarshal(f, &raw)
		if raw["type"] == "com.correx.apps.server.protocol.ClientMessage.ApprovalResponse" {
			note, _ = raw["steeringNote"].(string)
			if raw["decision"] != "APPROVE" {
				t.Fatalf("steer+enter should approve, got %v", raw["decision"])
			}
		}
	}
	if note != "use sudo" {
		t.Fatalf("steering note should ride along with the decision, got %q", note)
	}
}
