package orchestrator_test

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

	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/deterministic"
	"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/prompt"
	"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"
)

// TestPhaseC_MasterWorkerRoundTrip is the Phase C done-criterion: user input
// → master classifies → master delegates to worker → worker reports →
// master transitions task to completed → done event emitted.
func TestPhaseC_MasterWorkerRoundTrip(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()

	_ = orch.CreateRun(ctx, "run-c", "phase C smoke")

	// Insert master + worker rows.
	for _, id := range []string{"master", "worker-1"} {
		_, _ = st.DB().Exec(`INSERT INTO agents(id, run_id, status, spawned_at, heartbeat_at) VALUES(?,'run-c','running',?,?)`,
			id, store.FmtTime(store.Now()), store.FmtTime(store.Now()))
	}

	classer := prompt.NewRules()
	m := orchestrator.NewMaster(orch, q, classer, "run-c")

	// Worker script: respond to delegate with a report envelope.
	rt.SetScript("worker-1", []*runtime.LLMResponse{
		{
			Text: "doing it",
			Tokens: runtime.TokenUsage{Prompt: 100, Completion: 50},
			ToolCalls: []runtime.ToolCall{
				{Name: "send_message", Args: map[string]any{
					"to":   "master",
					"type": "report",
					"task_id": "", // filled below
					"ttl_ms": 60000,
					"id": "msg_report_c",
					"run_id": "run-c",
					"payload": map[string]any{
						"intent": "implemented",
						"expects": "none",
					},
				}},
			},
		},
	})
	_, _ = rt.Spawn(ctx, runtime.SpawnSpec{
		AgentID: "worker-1", RunID: "run-c",
		Tools:     []string{"send_message"},
		ZoneScope: []string{"client/**/*"},
	})
	agent := runtime.NewAgent(rt, st, bus, q, "worker-1", "fe-worker", "run-c",
		[]string{"client/**/*"}, []string{"send_message"}, "scripted", 0.005)

	// User prompt to the master.
	kind, _, err := determDispatch(t, m, "Add a tooltip to the dashboard")
	if err != nil {
		t.Fatalf("master dispatch: %v", err)
	}
	if kind != prompt.KindImplementNew {
		t.Errorf("classification = %s, want implement.new-feature", kind)
	}

	// Look up the task the master created.
	var taskID string
	if err := st.DB().QueryRow(`SELECT id FROM tasks WHERE run_id='run-c' LIMIT 1`).Scan(&taskID); err != nil {
		t.Fatal(err)
	}
	// Patch the worker's report task_id since we couldn't know it ahead of time.
	// (Scripted runtime doesn't see the inbound envelope's task_id — we'd need
	//  a richer scripted Step. For test purposes we manually fix the script
	//  by re-setting it with the correct task_id.)
	rt.SetScript("worker-1", []*runtime.LLMResponse{
		{
			Text: "doing it",
			Tokens: runtime.TokenUsage{Prompt: 100, Completion: 50},
			ToolCalls: []runtime.ToolCall{
				{Name: "send_message", Args: map[string]any{
					"to":   "master",
					"type": "report",
					"task_id": taskID,
					"ttl_ms": 60000,
					"id": "msg_report_c",
					"run_id": "run-c",
					"payload": map[string]any{
						"intent": "implemented",
						"expects": "none",
					},
				}},
			},
		},
	})

	// Worker turn.
	if h, err := agent.HandleOne(ctx); err != nil || !h {
		t.Fatalf("worker.HandleOne: %v / %v", h, err)
	}

	// Master receives report and completes.
	report, err := q.Receive(ctx, "master")
	if err != nil {
		t.Fatal(err)
	}
	if report == nil || report.Type != envelope.TypeReport {
		t.Fatalf("report missing or wrong type: %+v", report)
	}
	if err := orch.Transition(ctx, report.TaskID, orchestrator.StateCompleted); err != nil {
		t.Fatal(err)
	}
	_ = q.Ack(ctx, report.ID)

	// Verify task completed.
	var state string
	_ = st.DB().QueryRow(`SELECT state FROM tasks WHERE id=?`, taskID).Scan(&state)
	if state != string(orchestrator.StateCompleted) {
		t.Errorf("task state = %s, want completed", state)
	}
}

// TestPhaseC_SteeringReachesWorker exercises the master's steering path:
// user sends a redirect; master finds the active task; sends `steering`
// envelope to its owner; receiver must ack.
func TestPhaseC_SteeringReachesWorker(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)
	ctx := context.Background()

	_ = orch.CreateRun(ctx, "run-cs", "")
	for _, id := range []string{"master", "worker-1"} {
		_, _ = st.DB().Exec(`INSERT INTO agents(id, run_id, status, spawned_at) VALUES(?,'run-cs','running',?)`,
			id, store.FmtTime(store.Now()))
	}
	// Pre-existing in-progress task owned by worker.
	_ = orch.CreateTask(ctx, orchestrator.Task{ID: "t-existing", RunID: "run-cs", Title: "x"})
	_ = orch.AssignOwner(ctx, "t-existing", "worker-1")
	_ = orch.Transition(ctx, "t-existing", orchestrator.StateInProgress)

	m := orchestrator.NewMaster(orch, q, prompt.NewRules(), "run-cs")
	kind, taskID, err := determDispatch(t, m, "actually use a popover instead")
	if err != nil {
		t.Fatal(err)
	}
	if kind != prompt.KindSteering {
		t.Errorf("classification = %s, want steering", kind)
	}
	if taskID != "t-existing" {
		t.Errorf("target = %s, want t-existing", taskID)
	}

	// Worker receives the steering envelope.
	got, err := q.Receive(ctx, "worker-1")
	if err != nil {
		t.Fatal(err)
	}
	if got == nil {
		t.Fatal("no steering message at worker")
	}
	if got.Type != envelope.TypeSteering {
		t.Errorf("type = %s, want steering", got.Type)
	}
	if got.Payload.Expects != envelope.ExpectsAck {
		t.Errorf("expects = %s, want ack", got.Payload.Expects)
	}
}

// determDispatch wraps the deterministic coordinator for tests that
// were written against the old Master.Dispatch (classifier→switch)
// behavior. Master.Dispatch is now LLM-mode (shovel-to-inbox); the
// hardcoded routing lives in internal/harness/deterministic.
func determDispatch(t *testing.T, m *orchestrator.Master, userText string) (prompt.Kind, string, error) {
	t.Helper()
	coord := &deterministic.Coordinator{
		Orch: m.Orch, Queue: m.Queue, Runner: m.Runner, Classer: m.Classer,
		RunID: m.RunID, MasterRole: m.MasterRole,
		AutoEvaluator: m.AutoEvaluator,
		NotifyUser:    m.NotifyUser,
		RecordPrompt:  m.RecordPrompt,
		OpenTasks:     m.OpenTasks,
	}
	return coord.Dispatch(context.Background(), userText)
}
