package headless

// Tests for headless session persistence and resume (session id in stream-json
// + --resume for headless runs). openHeadlessSession/persist are exercised
// directly against an isolated PIGO_HOME so a headless run's session round-trips
// without spawning a provider.

import (
	"testing"

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

func textUser(s string) agentcore.UserMessage {
	return agentcore.UserMessage{RoleField: agentcore.RoleUser, Content: agentcore.ContentList{agentcore.NewTextContent(s)}}
}

func textAssistant(s string) agentcore.AssistantMessage {
	return agentcore.AssistantMessage{RoleField: agentcore.RoleAssistant, Content: agentcore.ContentList{agentcore.NewTextContent(s)}}
}

// TestOpenHeadlessSessionFresh verifies a fresh headless session gets a new id
// and empty prior messages, and that persist writes the run's messages so they
// can be resumed.
func TestOpenHeadlessSessionFresh(t *testing.T) {
	t.Setenv("PIGO_HOME", t.TempDir())

	prior, hs, err := openHeadlessSession("", "faux-model", "faux", "sys prompt")
	if err != nil {
		t.Fatalf("openHeadlessSession fresh: %v", err)
	}
	if len(prior) != 0 {
		t.Errorf("fresh session must have no prior messages, got %d", len(prior))
	}
	if hs.header.ID == "" {
		t.Fatal("fresh session must have a non-empty id")
	}
	if hs.header.SystemPrompt != "sys prompt" {
		t.Errorf("header SystemPrompt = %q, want the passed prompt", hs.header.SystemPrompt)
	}

	// Simulate a completed run: prompt + assistant reply.
	agentCtx := &agentcore.AgentContext{Messages: agentcore.MessageList{textUser("1+1=?"), textAssistant("2")}}
	if err := hs.persist(agentCtx); err != nil {
		t.Fatalf("persist: %v", err)
	}

	// The session must now be loadable with both messages.
	_, msgs, err := hs.store.Load(hs.header.ID)
	if err != nil {
		t.Fatalf("Load persisted session: %v", err)
	}
	if len(msgs) != 2 {
		t.Fatalf("persisted session has %d messages, want 2", len(msgs))
	}
}

// TestOpenHeadlessSessionResume verifies that resuming seeds the prior messages
// and that a subsequent run appends only the new tail as a branch, so the
// session grows rather than being rewritten.
func TestOpenHeadlessSessionResume(t *testing.T) {
	t.Setenv("PIGO_HOME", t.TempDir())

	// First run: create and persist a session.
	_, hs1, err := openHeadlessSession("", "faux-model", "faux", "sys")
	if err != nil {
		t.Fatalf("first openHeadlessSession: %v", err)
	}
	ctx1 := &agentcore.AgentContext{Messages: agentcore.MessageList{textUser("first"), textAssistant("reply1")}}
	if err := hs1.persist(ctx1); err != nil {
		t.Fatalf("first persist: %v", err)
	}
	sessID := hs1.header.ID

	// Second run: resume the session id.
	prior, hs2, err := openHeadlessSession(sessID, "faux-model", "faux", "sys")
	if err != nil {
		t.Fatalf("resume openHeadlessSession: %v", err)
	}
	if len(prior) != 2 {
		t.Fatalf("resume must seed %d prior messages, got %d", 2, len(prior))
	}
	if hs2.header.ID != sessID {
		t.Errorf("resumed session id = %q, want %q", hs2.header.ID, sessID)
	}
	if hs2.persisted != 2 {
		t.Errorf("resumed persisted cursor = %d, want 2", hs2.persisted)
	}

	// A second turn appends its new tail.
	ctx2 := &agentcore.AgentContext{Messages: append(prior, textUser("second"), textAssistant("reply2"))}
	if err := hs2.persist(ctx2); err != nil {
		t.Fatalf("second persist: %v", err)
	}
	_, msgs, err := hs2.store.Load(sessID)
	if err != nil {
		t.Fatalf("Load after second turn: %v", err)
	}
	if len(msgs) != 4 {
		t.Fatalf("session after two turns has %d messages, want 4", len(msgs))
	}
}

// TestHeadlessPersistNoop verifies persist is a no-op (no error, no growth) when
// the run produced nothing new past what was already persisted.
func TestHeadlessPersistNoop(t *testing.T) {
	t.Setenv("PIGO_HOME", t.TempDir())
	_, hs, err := openHeadlessSession("", "m", "p", "s")
	if err != nil {
		t.Fatalf("openHeadlessSession: %v", err)
	}
	ctx := &agentcore.AgentContext{Messages: agentcore.MessageList{textUser("x"), textAssistant("y")}}
	if err := hs.persist(ctx); err != nil {
		t.Fatalf("first persist: %v", err)
	}
	// Persisting again with no new messages must not error and must not duplicate.
	if err := hs.persist(ctx); err != nil {
		t.Fatalf("noop persist: %v", err)
	}
	_, msgs, err := hs.store.Load(hs.header.ID)
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	if len(msgs) != 2 {
		t.Fatalf("noop persist changed message count to %d, want 2", len(msgs))
	}
}

// TestHeadlessPersistCompactionShrink verifies persist tolerates the context
// being rebuilt to fewer messages than were on disk before the run (mid-run
// compaction replaces agentCtx.Messages). The persisted cursor is clamped so
// the tail slice stays in bounds rather than panicking.
func TestHeadlessPersistCompactionShrink(t *testing.T) {
	t.Setenv("PIGO_HOME", t.TempDir())
	_, hs, err := openHeadlessSession("", "m", "p", "s")
	if err != nil {
		t.Fatalf("openHeadlessSession: %v", err)
	}
	// Persist four messages, advancing the cursor to 4.
	ctx := &agentcore.AgentContext{Messages: agentcore.MessageList{textUser("a"), textAssistant("b"), textUser("c"), textAssistant("d")}}
	if err := hs.persist(ctx); err != nil {
		t.Fatalf("first persist: %v", err)
	}
	if hs.persisted != 4 {
		t.Fatalf("cursor = %d, want 4", hs.persisted)
	}
	// Simulate compaction: the context is rebuilt to fewer messages than the
	// cursor. persist must not panic on the out-of-range slice.
	ctx.Messages = agentcore.MessageList{textAssistant("summary"), textUser("e")}
	if err := hs.persist(ctx); err != nil {
		t.Fatalf("persist after compaction shrink: %v", err)
	}
	if hs.persisted != 2 {
		t.Errorf("cursor after clamp = %d, want 2", hs.persisted)
	}
}
