package repl

// Tests for the /btw side-thread command (#279): a side question must run an
// agent stream but MUST NOT mutate or persist the main conversation. These
// drive the whole runREPL loop with the fake replProvider, then assert the main
// context and persistence state are unchanged.

import (
	"bytes"
	"context"
	"strings"
	"testing"

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

// TestBtwDoesNotPolluteMainContext verifies that "/btw <q>" launches a run
// (provider called) yet appends nothing to deps.agentCtx.Messages.
func TestBtwDoesNotPolluteMainContext(t *testing.T) {
	p := &replProvider{reply: "side answer"}
	deps, _ := newTestDeps(t, p)

	var out bytes.Buffer
	if err := runREPL(strings.NewReader("/btw why pointers?\n/exit\n"), &out, deps); err != nil {
		t.Fatalf("runREPL: %v", err)
	}
	if p.calls != 1 {
		t.Fatalf("expected side question to launch exactly 1 run, got %d", p.calls)
	}
	if len(deps.agentCtx.Messages) != 0 {
		t.Fatalf("main context must be untouched by /btw, got %d messages", len(deps.agentCtx.Messages))
	}
	if !strings.Contains(out.String(), btw.BtwHeader) {
		t.Errorf("expected side-thread header %q in output", btw.BtwHeader)
	}
	if !strings.Contains(out.String(), "side answer") {
		t.Errorf("expected the side answer to be printed, got: %q", out.String())
	}
}

// TestBtwDoesNotPersist verifies /btw writes nothing to disk: deps.persisted and
// deps.curLeaf are unchanged, and no session entries were appended.
func TestBtwDoesNotPersist(t *testing.T) {
	p := &replProvider{reply: "answer"}
	deps, store := newTestDeps(t, p)

	var out bytes.Buffer
	if err := runREPL(strings.NewReader("/btw quick q\n/exit\n"), &out, deps); err != nil {
		t.Fatalf("runREPL: %v", err)
	}
	if deps.persisted != 0 {
		t.Fatalf("deps.persisted must stay 0 after /btw, got %d", deps.persisted)
	}
	if deps.curLeaf != "" {
		t.Fatalf("deps.curLeaf must stay empty after /btw, got %q", deps.curLeaf)
	}
	if _, entries, err := store.LoadEntries(deps.header.ID); err == nil && len(entries) != 0 {
		t.Fatalf("no session entries should be persisted by /btw, got %d", len(entries))
	}
}

// TestBtwBareUsage verifies bare "/btw" with no prior side thread does not
// launch a run and prints usage guidance.
func TestBtwBareUsage(t *testing.T) {
	p := &replProvider{reply: "unused"}
	deps, _ := newTestDeps(t, p)

	var out bytes.Buffer
	if err := runREPL(strings.NewReader("/btw\n/exit\n"), &out, deps); err != nil {
		t.Fatalf("runREPL: %v", err)
	}
	if p.calls != 0 {
		t.Fatalf("bare /btw must not launch a run, got %d calls", p.calls)
	}
	if !strings.Contains(out.String(), "usage: /btw") {
		t.Errorf("expected usage hint for bare /btw, got: %q", out.String())
	}
}

// TestBtwBareReopensLastThread verifies that after a side thread exists, a bare
// "/btw" reopens it, replays the prior side Q&A, and lets the user keep asking
// in the SAME thread. The main context stays untouched throughout.
func TestBtwBareReopensLastThread(t *testing.T) {
	p := &replProvider{reply: "side answer"}
	deps, _ := newTestDeps(t, p)

	var out bytes.Buffer
	// Open a side thread and ask once, leave it, then bare /btw reopens it and
	// asks a follow-up, then leave again and exit the REPL.
	in := strings.NewReader("/btw first question?\n/exit\n/btw\nsecond question?\n/exit\n/exit\n")
	if err := runREPL(in, &out, deps); err != nil {
		t.Fatalf("runREPL: %v", err)
	}
	if p.calls != 2 {
		t.Fatalf("expected 2 side runs (1 initial + 1 after reopen), got %d", p.calls)
	}
	if len(deps.agentCtx.Messages) != 0 {
		t.Fatalf("main context must stay untouched, got %d messages", len(deps.agentCtx.Messages))
	}
	s := out.String()
	// The reopen must not print the bare-/btw usage hint (a thread existed).
	if strings.Contains(s, "usage: /btw") {
		t.Errorf("bare /btw with an existing thread must not print usage, got: %q", s)
	}
	// The replay must echo the earlier question.
	if !strings.Contains(s, "first question?") {
		t.Errorf("reopen should replay the earlier side question, got: %q", s)
	}
}

// TestBtwFollowUpsShareThread verifies that after "/btw <q>" the user can ask
// follow-ups at the btw prompt (without retyping /btw), each launching a run,
// and that none of them pollute the main context. "/exit" leaves the thread.
func TestBtwFollowUpsShareThread(t *testing.T) {
	p := &replProvider{reply: "ok"}
	deps, _ := newTestDeps(t, p)

	var out bytes.Buffer
	// First /btw asks once; then two bare follow-ups; then /exit leaves the side
	// thread; then /exit ends the REPL.
	in := strings.NewReader("/btw first?\nsecond?\nthird?\n/exit\n/exit\n")
	if err := runREPL(in, &out, deps); err != nil {
		t.Fatalf("runREPL: %v", err)
	}
	if p.calls != 3 {
		t.Fatalf("expected 3 side runs (1 initial + 2 follow-ups), got %d", p.calls)
	}
	if len(deps.agentCtx.Messages) != 0 {
		t.Fatalf("follow-ups must not pollute main context, got %d messages", len(deps.agentCtx.Messages))
	}
	if !strings.Contains(out.String(), "left side thread") {
		t.Errorf("expected 'left side thread' on /exit from the side thread")
	}
}

// TestBtwFollowUpLoopAccumulates verifies the side context grows across
// follow-ups so a later question sees the earlier Q&A.
func TestBtwFollowUpLoopAccumulates(t *testing.T) {
	side := &agentcore.AgentContext{}
	deps, _ := newTestDeps(t, &replProvider{reply: "a"})
	setCancel := func(context.CancelFunc) {}
	settings := btw.ResolveBtwSettings(&bytes.Buffer{}, &deps)
	btw.AskSide(setCancel, &bytes.Buffer{}, &deps, side, settings, "q1")
	n1 := len(side.Messages)
	btw.AskSide(setCancel, &bytes.Buffer{}, &deps, side, settings, "q2")
	if len(side.Messages) <= n1 {
		t.Fatalf("side context should accumulate across follow-ups: %d then %d", n1, len(side.Messages))
	}
}

// appending to the side thread cannot reach the main slice.
func TestNewSideContextIsolated(t *testing.T) {
	main := &agentcore.AgentContext{
		SystemPrompt: "sys",
		Messages: agentcore.MessageList{
			agentcore.UserMessage{RoleField: agentcore.RoleUser, Content: agentcore.ContentList{agentcore.NewTextContent("hi")}},
		},
	}
	side := btw.NewSideContext(main)
	if side.SystemPrompt != "sys" {
		t.Errorf("side thread should inherit the system prompt")
	}
	if len(side.Messages) != 1 {
		t.Fatalf("side thread should be seeded with the main messages, got %d", len(side.Messages))
	}
	side.Messages = append(side.Messages, agentcore.UserMessage{RoleField: agentcore.RoleUser})
	if len(main.Messages) != 1 {
		t.Fatalf("appending to the side thread must not grow the main context, got %d", len(main.Messages))
	}
}
