// Scrollback fetching must preserve position and resume after eviction in the TUI.
package tui

import (
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"albedo/cli/internal/daemon"
	tea "charm.land/bubbletea/v2"
	"github.com/charmbracelet/x/ansi"
)

// turn is one user message and its reply, committed as rows seq and seq+1.
func turn(name string, seq int64) []daemon.StreamEvent {
	return []daemon.StreamEvent{
		{Type: daemon.EventUser, Text: name, Source: "chat", Replayed: true},
		{Type: daemon.EventCommitted, Seq: seq, Replayed: true},
		{Type: daemon.EventMessage, Role: "assistant", Text: "reply to " + name + "\n" + strings.Repeat("more\n", 8), Replayed: true},
		{Type: daemon.EventCommitted, Seq: seq + 1, Replayed: true},
	}
}

func rowOf(view, text string) int {
	for i, line := range strings.Split(ansi.Strip(view), "\n") {
		if strings.Contains(line, text) {
			return i
		}
	}
	return -1
}

func TestScrollingToTheTopLoadsOlderHistoryInPlace(t *testing.T) {
	var asked []string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		asked = append(asked, r.URL.RawQuery)
		_ = json.NewEncoder(w).Encode(map[string]any{
			"events": []any{
				map[string]any{"type": "user", "text": "older question", "source": "chat", "triggeredAt": ""},
				map[string]any{"type": "committed", "seq": 8},
				map[string]any{"type": "message", "role": "assistant", "text": "older answer"},
				map[string]any{"type": "committed", "seq": 9},
			},
			"before": 8,
			"more":   false,
		})
	}))
	defer server.Close()
	conn := daemon.NewConnection(daemon.ConnectionSnapshot{Port: server.Listener.Addr().(*net.TCPAddr).Port}, nil)
	client := daemon.NewChatClient(conn, "s")
	m := NewChatModel(&daemon.Session{ID: "s"}, client)
	t.Cleanup(m.Close)
	m.SetSize(80, 20)
	m.handleStreamEvent(daemon.StreamEvent{Type: daemon.EventReset, Before: 10, More: true, Replayed: true})
	for i, name := range []string{"first", "second", "third"} {
		for _, evt := range turn(name, int64(10+2*i)) {
			m.handleStreamEvent(evt)
		}
	}
	m.refreshViewportContent()

	m.Follow = false
	m.scrollOffset = 1
	m.refreshViewportContent()
	m, cmd := m.Update(tea.KeyPressMsg{Code: tea.KeyUp})
	if cmd == nil || !m.loadingOlder {
		t.Fatal("reaching the top did not ask for older history")
	}
	before := rowOf(m.Viewport.View(), "first")
	if before < 0 {
		t.Fatal("the first turn is not on screen before loading")
	}
	m, _ = m.Update(cmd())
	if len(asked) != 1 || !strings.Contains(asked[0], "before=10") {
		t.Fatalf("asked %v", asked)
	}
	if got := rowOf(m.Viewport.View(), "first"); got != before {
		t.Fatalf("loading older history moved the view: row %d → %d", before, got)
	}
	entries := m.History.Entries()
	if entries[0].Text != "older question" || entries[0].Seq != 8 {
		t.Fatalf("older page not prepended: %+v", entries[0])
	}
	if _, more := m.olderCursor(); more {
		t.Fatal("history start not recorded")
	}
	m.scrollOffset = 0
	m.refreshViewportContent()
	if !strings.Contains(ansi.Strip(m.Viewport.View()), "older question") {
		t.Fatal("older page not shown above")
	}
	if _, cmd := m.Update(tea.KeyPressMsg{Code: tea.KeyUp}); cmd != nil {
		t.Fatal("asked again with nothing older")
	}
}

func TestEvictedHistoryResumesAfterItsNewestRow(t *testing.T) {
	h := NewBoundedHistory(3, 1<<20)
	for seq := int64(1); seq <= 3; seq++ {
		h.Append(HistoryEntry{Kind: EntryAssistant, Text: fmt.Sprint(seq)})
		h.Stamp(seq)
	}
	// Two entries from row 4: the first eviction takes row 1 only.
	h.Append(HistoryEntry{Kind: EntryThinking, Text: "4a"})
	h.Append(HistoryEntry{Kind: EntryAssistant, Text: "4b"})
	h.Stamp(4)
	if h.EvictedThrough() != 2 || h.Entries()[0].Text != "3" {
		t.Fatalf("evicted through %d, first %q", h.EvictedThrough(), h.Entries()[0].Text)
	}
	// Evicting into row 4 takes both of its entries.
	h.Append(HistoryEntry{Kind: EntryAssistant, Text: "5"})
	h.Append(HistoryEntry{Kind: EntryAssistant, Text: "6"})
	if h.EvictedThrough() != 4 || h.Entries()[0].Text != "5" {
		t.Fatalf("row 4 split: evicted through %d, first %q", h.EvictedThrough(), h.Entries()[0].Text)
	}
	h.Prepend([]HistoryEntry{{Kind: EntryUser, Text: "old", Seq: 4}})
	if h.EvictedThrough() != 0 || h.Entries()[0].Text != "old" {
		t.Fatal("prepend did not take over the cursor")
	}
}
