package runtime_test

import (
	"context"
	"path/filepath"
	"testing"
	"time"

	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/envelope"
	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/event"
	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/orchestrator"
	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/runtime"
	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/runtime/scripted"
	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/store"
	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/transport"
)

// TestPhaseB_SingleAgentEndToEnd is the Phase B done-criterion: one headless
// agent receives a task, calls introspect, writes a report artifact, the run
// shows up in the dashboard's authoritative table (the events log here).
//
// We use the scripted runtime (deterministic) but the agent loop is the
// same code path the real headless runtime drives. Phase B success is about
// the loop integration, not about the specific provider.
func TestPhaseB_SingleAgentEndToEnd(t *testing.T) {
	tmp := t.TempDir()
	st, err := store.Open(filepath.Join(tmp, "harness.db"))
	if err != nil {
		t.Fatal(err)
	}
	defer st.Close()
	bus := event.NewBus(st)
	q := transport.New(st, bus)
	orch := orchestrator.New(st, bus)
	rt := scripted.New()
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// Scripted agent: turn 1 calls introspect + send_message(report) to master.
	rt.SetScript("worker-1", []*runtime.LLMResponse{
		{
			Text: "Calling introspect then reporting back.",
			Tokens: runtime.TokenUsage{Prompt: 200, Completion: 100},
			ToolCalls: []runtime.ToolCall{
				{Name: "introspect", Args: map[string]any{}},
				{Name: "send_message", Args: map[string]any{
					"to":   "master",
					"type": "report",
					"task_id": "task-b1",
					"ttl_ms": 60000,
					"id": "msg_report_b1",
					"run_id": "run-b",
					"payload": map[string]any{
						"intent": "done",
						"expects": "none",
					},
				}},
			},
		},
	})

	_ = orch.CreateRun(ctx, "run-b", "phase B smoke")
	_, _ = st.DB().Exec(`INSERT INTO agents(id, run_id, status, spawned_at, heartbeat_at) VALUES('worker-1','run-b','running',?,?)`,
		store.FmtTime(store.Now()), store.FmtTime(store.Now()))
	_, _ = rt.Spawn(ctx, runtime.SpawnSpec{
		AgentID: "worker-1", RunID: "run-b",
		Tools:     []string{"introspect", "send_message", "write_file"},
		ZoneScope: []string{"client/**/*"},
	})

	agent := runtime.NewAgent(rt, st, bus, q, "worker-1", "tester", "run-b",
		[]string{"client/**/*"}, []string{"introspect", "send_message", "write_file"}, "scripted", 0.005)

	// Create task and assign owner.
	_ = orch.CreateTask(ctx, orchestrator.Task{
		ID: "task-b1", RunID: "run-b", Title: "smoke",
		Deadline: ptrTime(time.Now().UTC().Add(60 * time.Second)),
	})
	_ = orch.AssignOwner(ctx, "task-b1", "worker-1")
	_ = orch.Transition(ctx, "task-b1", orchestrator.StateInProgress)

	// Master sends a delegate.
	delegate := &envelope.Envelope{
		ID: "msg_delegate_b1", RunID: "run-b",
		From: "master", To: "worker-1", Type: envelope.TypeDelegate,
		TaskID: "task-b1", TTLMs: 60000,
		Payload: envelope.Payload{Intent: "do the smoke task", Expects: envelope.ExpectsReport},
	}
	if err := q.Send(ctx, delegate); err != nil {
		t.Fatal(err)
	}

	// Run one cycle on worker.
	if h, err := agent.HandleOne(ctx); err != nil || !h {
		t.Fatalf("worker.HandleOne: handled=%v err=%v", h, err)
	}

	// Master receives the report and completes the task.
	report, err := q.Receive(ctx, "master")
	if err != nil {
		t.Fatal(err)
	}
	if report == nil {
		t.Fatal("expected report at master inbox")
	}
	if report.Type != envelope.TypeReport {
		t.Fatalf("report type = %s, want report", report.Type)
	}
	if err := orch.Transition(ctx, report.TaskID, orchestrator.StateCompleted); err != nil {
		t.Fatal(err)
	}
	_ = q.Ack(ctx, report.ID)

	// Verify: task completed, introspect was called, tokens recorded.
	if agent.IntrospectCalls() != 1 {
		t.Errorf("introspect calls = %d, want 1", agent.IntrospectCalls())
	}
	if agent.TokensTotal() < 300 {
		t.Errorf("tokens recorded = %d, want >=300", agent.TokensTotal())
	}
	var state string
	_ = st.DB().QueryRow(`SELECT state FROM tasks WHERE id='task-b1'`).Scan(&state)
	if state != string(orchestrator.StateCompleted) {
		t.Errorf("task state = %s, want completed", state)
	}
	// Run-level token accounting incremented.
	var runTokens int64
	_ = st.DB().QueryRow(`SELECT tokens_total FROM runs WHERE id='run-b'`).Scan(&runTokens)
	if runTokens < 300 {
		t.Errorf("runs.tokens_total = %d, want >=300", runTokens)
	}
}

func ptrTime(t time.Time) *time.Time { return &t }
