package tmux

import (
	"context"
	"encoding/json"
	"testing"

	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/runtime"
)

func newRTWithPane(pane *FakePane) (*Runtime, *FakePane) {
	r := New(func(ctx context.Context, spec runtime.SpawnSpec, chInfo *ChannelPromptInfo) (PaneIO, error) {
		return pane, nil
	})
	return r, pane
}

// TestSidecar_ChannelOverridesPaneReceive proves the injection contract:
// when Runtime.Channel is set, CallLLM reads the structured response
// from the channel instead of from PaneIO.Receive.
func TestSidecar_ChannelOverridesPaneReceive(t *testing.T) {
	pane := NewFakePane()
	pane.QueueResponse("```harness-out\n{\"text\":\"FROM PANE - should NOT see this\"}\n```")

	r, _ := newRTWithPane(pane)
	r.Channel = &stubChannel{body: `{"text":"FROM CHANNEL","tool_calls":[],"tokens":{"prompt":3,"completion":2}}`}

	ctx := context.Background()
	_, _ = r.Spawn(ctx, runtime.SpawnSpec{AgentID: "a"})
	resp, err := r.CallLLM(ctx, "a", runtime.LLMRequest{Prompt: "x"})
	if err != nil {
		t.Fatal(err)
	}
	if resp.Text != "FROM CHANNEL" {
		t.Errorf("text = %q, want FROM CHANNEL (channel injection did not take effect)", resp.Text)
	}
	if len(pane.Sent()) != 1 {
		t.Errorf("expected 1 outgoing pane Send, got %d", len(pane.Sent()))
	}
}

// TestSidecar_ChannelNilFallsBackToPane confirms backward compatibility.
func TestSidecar_ChannelNilFallsBackToPane(t *testing.T) {
	pane := NewFakePane()
	pane.QueueResponse("```harness-out\n{\"text\":\"from-pane\",\"tokens\":{\"prompt\":1,\"completion\":1}}\n```")
	r, _ := newRTWithPane(pane)
	if r.Channel != nil {
		t.Fatal("default Channel should be nil")
	}
	ctx := context.Background()
	_, _ = r.Spawn(ctx, runtime.SpawnSpec{AgentID: "a"})
	resp, err := r.CallLLM(ctx, "a", runtime.LLMRequest{Prompt: "x"})
	if err != nil {
		t.Fatal(err)
	}
	if resp.Text != "from-pane" {
		t.Errorf("text = %q, want from-pane", resp.Text)
	}
}

// stubChannel is a test SidecarChannel: returns one canned body, then
// blocks forever.
type stubChannel struct {
	body      string
	delivered bool
	opened    []string
	closed    []string
}

func (s *stubChannel) Open(agentID string) (ChannelPromptInfo, error) {
	s.opened = append(s.opened, agentID)
	return ChannelPromptInfo{}, nil
}
func (s *stubChannel) Receive(ctx context.Context, agentID string) (string, error) {
	if s.delivered {
		<-ctx.Done()
		return "", ctx.Err()
	}
	s.delivered = true
	return s.body, nil
}
func (s *stubChannel) Close(agentID string) error {
	s.closed = append(s.closed, agentID)
	return nil
}

func TestExtractBlock(t *testing.T) {
	in := "preamble prose\n```harness-out\n{\"text\":\"hi\"}\n```\ntrailing prose"
	body, ok := ExtractBlock(in)
	if !ok {
		t.Fatal("expected to find block")
	}
	if body != `{"text":"hi"}` {
		t.Fatalf("body = %q", body)
	}
}

// TestExtractBlockStripsANSI is the L1 root-cause regression test.
//
// In run_1779319392853494000 the harness was stuck for hours; each
// CallLLM attempt burned 25s of LLM tokens before throwing
// "malformed JSON: invalid character '\x1b'". The cause: claude-code's
// TUI emits ANSI color codes around its output, the pipe-pane log
// captures those raw bytes, and ExtractBlock returned a body that still
// contained escapes for json.Unmarshal to choke on.
//
// This test feeds a realistic claude-code-style stream (block surrounded
// by colored prompt prefixes, with cursor-move and OSC escapes
// interleaved) and asserts ExtractBlock returns the cleaned JSON.
func TestExtractBlockStripsANSI(t *testing.T) {
	// Real-world style: claude paints a green "⏺" prefix in 32m color,
	// then emits the harness-out fence, with intermittent cursor moves.
	in := "\x1b[32m⏺\x1b[0m \x1b[2K\r" +
		"```harness-out\n" +
		"\x1b[36m{\"text\":\"hi\",\x1b[0m\"tool_calls\":[],\"tokens\":{\"prompt\":1,\"completion\":1}}\n" +
		"```\n" +
		"\x1b]0;some-title\x07\x1b[?25h"
	body, ok := ExtractBlock(in)
	if !ok {
		t.Fatal("expected to find block")
	}
	if body == "" {
		t.Fatal("body is empty after strip")
	}
	// The body must round-trip as JSON now (the actual production
	// failure was that this Unmarshal returned the \x1b parse error).
	var wire struct {
		Text   string `json:"text"`
		Tokens struct {
			Prompt int `json:"prompt"`
		} `json:"tokens"`
	}
	if err := json.Unmarshal([]byte(body), &wire); err != nil {
		t.Fatalf("post-strip body is still not parseable JSON: %v\nbody=%q", err, body)
	}
	if wire.Text != "hi" {
		t.Errorf("text = %q, want \"hi\"", wire.Text)
	}
}

// TestExtractBlockHandlesFenceSplitByEscape guards the case where an ANSI
// escape lands INSIDE the opening fence — without stripping FIRST, the
// fence string-match wouldn't find "```harness-out" and we'd report no
// block (silently dropping the agent's response).
func TestExtractBlockHandlesFenceSplitByEscape(t *testing.T) {
	in := "```har\x1b[0mness-out\n{\"text\":\"split\"}\n```"
	body, ok := ExtractBlock(in)
	if !ok {
		t.Fatal("expected to find block after splitting fence by escape")
	}
	if body != `{"text":"split"}` {
		t.Errorf("body = %q", body)
	}
}

func TestSidecarHappyPath(t *testing.T) {
	pane := NewFakePane()
	pane.QueueResponse("noise prose ```harness-out\n{\"text\":\"hi\",\"tokens\":{\"prompt\":10,\"completion\":5}}\n```")
	r, _ := newRTWithPane(pane)
	ctx := context.Background()
	_, _ = r.Spawn(ctx, runtime.SpawnSpec{AgentID: "a"})
	resp, err := r.CallLLM(ctx, "a", runtime.LLMRequest{Prompt: "x"})
	if err != nil {
		t.Fatal(err)
	}
	if resp.Text != "hi" {
		t.Errorf("text = %q", resp.Text)
	}
	if resp.Tokens.Prompt != 10 || resp.Tokens.Completion != 5 {
		t.Errorf("tokens = %+v", resp.Tokens)
	}
	if resp.ParseFailures != 0 {
		t.Errorf("parse failures = %d, want 0", resp.ParseFailures)
	}
}

func TestSidecarReformatRetryRecovers(t *testing.T) {
	pane := NewFakePane()
	// First response: malformed (no fences).
	pane.QueueResponse("here is my answer: just text, no fences")
	// Second response: correct.
	pane.QueueResponse("```harness-out\n{\"text\":\"reformatted\",\"tokens\":{\"prompt\":1,\"completion\":1}}\n```")
	r, _ := newRTWithPane(pane)
	ctx := context.Background()
	_, _ = r.Spawn(ctx, runtime.SpawnSpec{AgentID: "a"})

	resp, err := r.CallLLM(ctx, "a", runtime.LLMRequest{Prompt: "x"})
	if err != nil {
		t.Fatal(err)
	}
	if resp.Text != "reformatted" {
		t.Errorf("text = %q", resp.Text)
	}
	if resp.ParseFailures != 1 {
		t.Errorf("parse failures = %d, want 1", resp.ParseFailures)
	}
	// Verify the second Send was a reformat ask.
	sent := pane.Sent()
	if len(sent) != 2 {
		t.Fatalf("sent %d, want 2", len(sent))
	}
	if !contains(sent[1], "reformat") {
		t.Errorf("second send didn't ask for reformat: %s", sent[1])
	}
}

func TestSidecarHardFailAfterRetries(t *testing.T) {
	pane := NewFakePane()
	// Always malformed.
	for i := 0; i < 10; i++ {
		pane.QueueResponse("never any fences here")
	}
	r, _ := newRTWithPane(pane)
	r.MaxParseRetries = 2
	ctx := context.Background()
	_, _ = r.Spawn(ctx, runtime.SpawnSpec{AgentID: "a"})

	resp, err := r.CallLLM(ctx, "a", runtime.LLMRequest{Prompt: "x"})
	if err == nil {
		t.Fatal("expected hard-fail error")
	}
	// Total attempts = MaxParseRetries + 1 (first try) → 3 parse failures.
	if resp.ParseFailures != 3 {
		t.Errorf("parse failures = %d, want 3", resp.ParseFailures)
	}
}

func contains(s, sub string) bool {
	for i := 0; i+len(sub) <= len(s); i++ {
		if s[i:i+len(sub)] == sub {
			return true
		}
	}
	return false
}
