package orchestrator_test

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

	"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/store"
)

// TestPhaseJ_OrgBuilderProposesFromHistory exercises the end-to-end pipeline:
//
//  1. Seed: a baseline org yaml on disk + several "historical" runs attributed
//     to it, with thrash events recorded.
//  2. OrgBuilder.Propose reads that history, derives a structural change
//     (lower thrash_max_exchanges), writes a proposed yaml.
//  3. EvaluateProposal runs the falsifier on *held-out* benchmark scores —
//     scores that the proposer did NOT see. Pass → status=accepted.
//
// This satisfies plan §17 Phase J done-criterion: "a self-proposed change
// passes the falsifier on benchmarks unseen during proposal."
func TestPhaseJ_OrgBuilderProposesFromHistory(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)
	orch := orchestrator.New(st, bus)
	ctx := context.Background()

	// (1) Baseline org yaml on disk.
	baselineYAML := `name: hierarchical-v1
version: 1
roles:
  - id: master
    delegates_to: []
policies:
  max_depth: 4
  max_fanout: 3
  thrash_max_exchanges: 4
`
	baselinePath := filepath.Join(tmp, "hierarchical-v1.yaml")
	if err := os.WriteFile(baselinePath, []byte(baselineYAML), 0o644); err != nil {
		t.Fatal(err)
	}

	// (2) Seed history: two runs of this org with thrash events recorded.
	for i, runID := range []string{"hist-run-1", "hist-run-2"} {
		if err := orch.CreateRun(ctx, runID, ""); err != nil {
			t.Fatal(err)
		}
		if err := orch.SetRunOrg(ctx, runID, "hierarchical-v1", 1, "roles/v1/"); err != nil {
			t.Fatal(err)
		}
		// One completed task per run; one thrash event per run.
		_, _ = st.DB().Exec(`INSERT INTO agents(id, run_id, status, spawned_at) VALUES(?, ?, 'running', ?)`,
			"hist-a-"+itoa(i), runID, store.FmtTime(time.Now().UTC()))
		tID := "hist-t-" + itoa(i)
		_ = orch.CreateTask(ctx, orchestrator.Task{ID: tID, RunID: runID, Title: "x"})
		_ = orch.AssignOwner(ctx, tID, "hist-a-"+itoa(i))
		_ = orch.Transition(ctx, tID, orchestrator.StateInProgress)
		_ = orch.Transition(ctx, tID, orchestrator.StateCompleted)
		_, _ = bus.Emit(ctx, event.Event{
			Kind: event.KindCoordinationThrash, RunID: runID,
			Payload: map[string]any{"exchanges": 5},
		})
	}

	// (3) OrgBuilder proposes.
	builder := orchestrator.NewOrgBuilder(st, bus, filepath.Join(tmp, "proposals"))
	prop, err := builder.Propose(ctx, baselinePath)
	if err != nil {
		t.Fatalf("Propose: %v", err)
	}
	if prop.BaselineOrgID != "hierarchical-v1" {
		t.Errorf("proposal baseline = %s", prop.BaselineOrgID)
	}
	if !strings.Contains(prop.Rationale, "thrash") {
		t.Errorf("rationale should mention thrash; got: %s", prop.Rationale)
	}

	// Verify the proposed yaml has the change applied.
	body, err := os.ReadFile(prop.ProposedPath)
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(string(body), "thrash_max_exchanges: 3") {
		t.Errorf("proposed yaml didn't lower thrash_max_exchanges to 3:\n%s", string(body))
	}

	// (4) Run falsifier on held-out scores: candidate beats baseline by 20%
	// on the primary benchmark, no regression on secondaries, agreement 0.9,
	// 4 sample runs. These scores are produced by the test (held out from
	// what the builder saw — the builder only saw event counts, never these
	// benchmark scores).
	crit := orchestrator.DefaultFalsifier()
	crit.PrimaryBenchmark = "decomposable-v1"
	res, err := builder.EvaluateProposal(ctx, prop, crit,
		0.50, 0.62, // primary: +24%
		map[string]float64{"cross-cutting-v1": 0.55, "underspecified-v1": 0.40},
		map[string]float64{"cross-cutting-v1": 0.56, "underspecified-v1": 0.39},
		4,
		0.90,
	)
	if err != nil {
		t.Fatalf("EvaluateProposal: %v", err)
	}
	if !res.Pass {
		t.Errorf("expected proposal to pass falsifier; rationale: %s", res.Rationale)
	}
	// Verify status updated to accepted in DB.
	var status string
	_ = st.DB().QueryRow(`SELECT status FROM proposals WHERE id=?`, prop.ID).Scan(&status)
	if status != "accepted" {
		t.Errorf("DB status = %s, want accepted", status)
	}
}

// TestPhaseJ_FalsifierRejectsBadProposal: builder produces a proposal that
// makes things worse on held-out → status=rejected.
func TestPhaseJ_FalsifierRejectsBadProposal(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)
	orch := orchestrator.New(st, bus)
	ctx := context.Background()

	baselineYAML := `name: hierarchical-v1
version: 1
roles:
  - id: master
    delegates_to: []
policies:
  max_depth: 4
  max_fanout: 3
  thrash_max_exchanges: 4
`
	baselinePath := filepath.Join(tmp, "hier.yaml")
	_ = os.WriteFile(baselinePath, []byte(baselineYAML), 0o644)

	// Seed: one historical run with thrash so Propose generates a proposal.
	_ = orch.CreateRun(ctx, "h1", "")
	_ = orch.SetRunOrg(ctx, "h1", "hierarchical-v1", 1, "roles/v1/")
	_, _ = bus.Emit(ctx, event.Event{Kind: event.KindCoordinationThrash, RunID: "h1"})

	builder := orchestrator.NewOrgBuilder(st, bus, filepath.Join(tmp, "props"))
	prop, err := builder.Propose(ctx, baselinePath)
	if err != nil {
		t.Fatal(err)
	}
	crit := orchestrator.DefaultFalsifier()
	crit.PrimaryBenchmark = "decomposable-v1"
	// Held-out scores show candidate regresses on a secondary.
	res, err := builder.EvaluateProposal(ctx, prop, crit,
		0.50, 0.60,
		map[string]float64{"cross-cutting-v1": 0.70},
		map[string]float64{"cross-cutting-v1": 0.40}, // -43% — catastrophic regression
		4, 0.90,
	)
	if err != nil {
		t.Fatal(err)
	}
	if res.Pass {
		t.Errorf("expected reject; rationale: %s", res.Rationale)
	}
	var status string
	_ = st.DB().QueryRow(`SELECT status FROM proposals WHERE id=?`, prop.ID).Scan(&status)
	if status != "rejected" {
		t.Errorf("status = %s, want rejected", status)
	}
}

// TestPhaseJ_PromoteTeamTemplate: battle-tested team promotion.
func TestPhaseJ_PromoteTeamTemplate(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)
	ctx := context.Background()

	// Insert a template directly.
	_, _ = st.DB().Exec(`INSERT INTO teams_templates(id, snapshot_path, created_at) VALUES('tmpl1', '/x/snap.md', ?)`,
		store.FmtTime(store.Now()))
	builder := orchestrator.NewOrgBuilder(st, bus, tmp)
	if err := builder.PromoteTeamTemplate(ctx, "tmpl1", 0.92); err != nil {
		t.Fatal(err)
	}
	var score float64
	_ = st.DB().QueryRow(`SELECT score FROM teams_templates WHERE id='tmpl1'`).Scan(&score)
	if score < 0.91 {
		t.Errorf("score after promotion = %v, want ≥ 0.92", score)
	}
	if err := builder.PromoteTeamTemplate(ctx, "missing", 0.5); err == nil {
		t.Errorf("expected error promoting nonexistent template")
	}
}

func itoa(n int) string {
	if n == 0 {
		return "0"
	}
	neg := n < 0
	if neg {
		n = -n
	}
	var buf [20]byte
	i := len(buf)
	for n > 0 {
		i--
		buf[i] = byte('0' + n%10)
		n /= 10
	}
	if neg {
		i--
		buf[i] = '-'
	}
	return string(buf[i:])
}
