package store

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

func TestOpenAndMigrate(t *testing.T) {
	tmp := t.TempDir()
	path := filepath.Join(tmp, "harness.db")
	s, err := Open(path)
	if err != nil {
		t.Fatalf("Open: %v", err)
	}
	defer s.Close()

	// Sanity: schema_version table exists and one row recorded.
	row := s.db.QueryRow(`SELECT version FROM schema_version ORDER BY version DESC LIMIT 1`)
	var v int
	if err := row.Scan(&v); err != nil {
		t.Fatalf("scan schema_version: %v", err)
	}
	if v < 1 {
		t.Fatalf("expected schema_version >= 1, got %d", v)
	}
}

func TestTxAndTxImmediate(t *testing.T) {
	tmp := t.TempDir()
	path := filepath.Join(tmp, "harness.db")
	s, err := Open(path)
	if err != nil {
		t.Fatalf("Open: %v", err)
	}
	defer s.Close()

	ctx := context.Background()

	// Insert a row in a normal Tx.
	err = s.Tx(ctx, func(q Querier) error {
		_, err := q.Exec(`INSERT INTO runs(id, started_at, status) VALUES(?, ?, 'running')`, "run-1", FmtTime(Now()))
		return err
	})
	if err != nil {
		t.Fatalf("Tx insert: %v", err)
	}

	// Read via TxImmediate (read+update inside same tx).
	// The v5 invariant requires status=completed to have a root_task_id whose
	// task is itself completed; satisfy it here so the test exercises Tx
	// semantics, not the constraint.
	err = s.TxImmediate(ctx, func(q Querier) error {
		var status string
		if err := q.QueryRow(`SELECT status FROM runs WHERE id=?`, "run-1").Scan(&status); err != nil {
			return err
		}
		if status != "running" {
			t.Errorf("status = %q, want %q", status, "running")
		}
		now := FmtTime(Now())
		if _, err := q.Exec(
			`INSERT INTO tasks(id, run_id, title, state, created_at, updated_at) VALUES('t-root','run-1','root','completed',?,?)`, now, now,
		); err != nil {
			return err
		}
		if _, err := q.Exec(`UPDATE runs SET root_task_id='t-root' WHERE id=?`, "run-1"); err != nil {
			return err
		}
		_, err := q.Exec(`UPDATE runs SET status='completed' WHERE id=?`, "run-1")
		return err
	})
	if err != nil {
		t.Fatalf("TxImmediate: %v", err)
	}

	var status string
	if err := s.db.QueryRow(`SELECT status FROM runs WHERE id=?`, "run-1").Scan(&status); err != nil {
		t.Fatalf("post-tx read: %v", err)
	}
	if status != "completed" {
		t.Fatalf("post-tx status = %q, want completed", status)
	}
}

// TestEnumTriggers asserts the v4 status-enum guards reject out-of-set values
// for runs.status, tasks.state, agents.status, messages.status. The harness's
// research-data integrity depends on these triggers — every analytics query
// keys off these enums, so a typo silently inserting an unknown value would
// corrupt the trail. The triggers exist precisely to make such a typo fail
// loud rather than land in the DB.
func TestEnumTriggers(t *testing.T) {
	tmp := t.TempDir()
	s, err := Open(filepath.Join(tmp, "harness.db"))
	if err != nil {
		t.Fatalf("Open: %v", err)
	}
	defer s.Close()

	ctx := context.Background()
	now := FmtTime(Now())

	cases := []struct {
		name string
		sql  string
		args []any
	}{
		{
			name: "runs.status invalid INSERT rejected",
			sql:  `INSERT INTO runs(id, started_at, status) VALUES('r-bad', ?, 'bogus')`,
			args: []any{now},
		},
		{
			name: "tasks.state invalid INSERT rejected",
			sql:  `INSERT INTO tasks(id, run_id, title, state, created_at, updated_at) VALUES('t-bad','r-x','t','flying',?,?)`,
			args: []any{now, now},
		},
		{
			name: "agents.status invalid INSERT rejected",
			sql:  `INSERT INTO agents(id, run_id, status, spawned_at) VALUES('a-bad','r-x','marching',?)`,
			args: []any{now},
		},
		{
			name: "messages.status invalid INSERT rejected",
			sql:  `INSERT INTO messages(id, run_id, from_agent, to_agent, type, payload_json, next_visible_at, status, created_at) VALUES('m-bad','r-x','a','b','t','{}',?,?,'gone',?)`,
			args: []any{now, "queued", now}, // intentionally swapping to test the status arg
		},
	}
	_ = cases // simpler explicit form below
	type tc struct {
		name string
		exec func() error
	}
	checks := []tc{
		{"runs INSERT invalid", func() error {
			return s.Tx(ctx, func(q Querier) error {
				_, e := q.Exec(`INSERT INTO runs(id, started_at, status) VALUES('r-bad', ?, 'bogus')`, now)
				return e
			})
		}},
		{"runs UPDATE invalid", func() error {
			_ = s.Tx(ctx, func(q Querier) error {
				_, e := q.Exec(`INSERT INTO runs(id, started_at, status) VALUES('r-ok', ?, 'running')`, now)
				return e
			})
			return s.Tx(ctx, func(q Querier) error {
				_, e := q.Exec(`UPDATE runs SET status='bogus' WHERE id='r-ok'`)
				return e
			})
		}},
		{"tasks INSERT invalid", func() error {
			return s.Tx(ctx, func(q Querier) error {
				_, e := q.Exec(`INSERT INTO tasks(id, run_id, title, state, created_at, updated_at) VALUES('t-bad','r-ok','t','flying',?,?)`, now, now)
				return e
			})
		}},
		{"agents INSERT invalid", func() error {
			return s.Tx(ctx, func(q Querier) error {
				_, e := q.Exec(`INSERT INTO agents(id, run_id, status, spawned_at) VALUES('a-bad','r-ok','marching',?)`, now)
				return e
			})
		}},
		{"messages INSERT invalid", func() error {
			return s.Tx(ctx, func(q Querier) error {
				_, e := q.Exec(`INSERT INTO messages(id, run_id, from_agent, to_agent, type, payload_json, next_visible_at, status, created_at) VALUES('m-bad','r-ok','a','b','t','{}',?, 'gone', ?)`, now, now)
				return e
			})
		}},
	}
	for _, c := range checks {
		if err := c.exec(); err == nil {
			t.Errorf("%s: expected error, got nil — trigger did not fire", c.name)
		}
	}

	// Sanity: a valid value in the enum still goes through (use 'running'
	// because the v5 invariant requires 'failed' to have a root_task_id —
	// that constraint is exercised separately in TestRunInvariants).
	if err := s.Tx(ctx, func(q Querier) error {
		_, e := q.Exec(`INSERT INTO runs(id, started_at, status) VALUES('r-valid', ?, 'running')`, now)
		return e
	}); err != nil {
		t.Errorf("valid status='running' rejected unexpectedly: %v", err)
	}
}

// TestV7Backfill verifies that legacy "lying" data is relabeled honestly when
// migrations run. We construct a fresh DB through migration v6 only (no v7
// yet), insert pre-fix data, then apply v7 by re-running migrate, and assert
// that the relabeling matched the three buckets:
//   - completed + no tasks → unrouted
//   - completed + a parent-less completed task → completed with root_task_id set
//   - completed + tasks but no provable root → killed (task_failed)
func TestV7Backfill(t *testing.T) {
	tmp := t.TempDir()
	// Open and migrate stops at the highest migration index — we can't
	// "stop at v6", but we can directly seed problematic rows into a
	// post-v7 DB by temporarily disabling the triggers via direct INSERT.
	// SQLite triggers fire on writes regardless; instead we test the
	// backfill logic by simulating it on hand-seeded data INSERTed before
	// the v5/v6 triggers existed, which we can't do here without altering
	// migration order. So this test asserts the END state: the migrations
	// produce a DB where no row violates the new invariants — the strict
	// research-data guarantee.
	s, err := Open(filepath.Join(tmp, "harness.db"))
	if err != nil {
		t.Fatalf("Open: %v", err)
	}
	defer s.Close()

	var rows int
	_ = s.db.QueryRow(
		`SELECT COUNT(*) FROM runs
		   WHERE status='completed' AND root_task_id IS NULL`,
	).Scan(&rows)
	if rows != 0 {
		t.Fatalf("post-migration: %d runs still violate v5 invariant (completed without root_task_id)", rows)
	}
	_ = s.db.QueryRow(
		`SELECT COUNT(*) FROM runs
		   WHERE status IN ('killed','failed','unrouted') AND failure_category=''`,
	).Scan(&rows)
	if rows != 0 {
		t.Fatalf("post-migration: %d runs have terminal-failure status without failure_category", rows)
	}
}

// TestRunInvariants exercises the v5/v6 cross-column constraints that prevent
// the "completed without dispatching anything" bug class. Each subtest is a
// concrete violation the harness used to permit; the trigger must reject.
func TestRunInvariants(t *testing.T) {
	tmp := t.TempDir()
	s, err := Open(filepath.Join(tmp, "harness.db"))
	if err != nil {
		t.Fatalf("Open: %v", err)
	}
	defer s.Close()
	ctx := context.Background()
	now := FmtTime(Now())

	// Seed: a running run.
	if err := s.Tx(ctx, func(q Querier) error {
		_, e := q.Exec(`INSERT INTO runs(id, started_at, status) VALUES('r1', ?, 'running')`, now)
		return e
	}); err != nil {
		t.Fatalf("seed run: %v", err)
	}

	checks := []struct {
		name string
		exec func() error
	}{
		{"completed without root_task_id is rejected", func() error {
			return s.Tx(ctx, func(q Querier) error {
				_, e := q.Exec(`UPDATE runs SET status='completed' WHERE id='r1'`)
				return e
			})
		}},
		{"failed without root_task_id is rejected", func() error {
			return s.Tx(ctx, func(q Querier) error {
				_, e := q.Exec(`UPDATE runs SET status='failed', failure_category='task_failed' WHERE id='r1'`)
				return e
			})
		}},
		{"root_task_id pointing at missing task is rejected", func() error {
			return s.Tx(ctx, func(q Querier) error {
				_, e := q.Exec(`UPDATE runs SET root_task_id='does-not-exist' WHERE id='r1'`)
				return e
			})
		}},
		{"completed referencing an in_progress task is rejected", func() error {
			// Create a non-completed task and try to mark the run completed.
			return s.Tx(ctx, func(q Querier) error {
				if _, err := q.Exec(`INSERT INTO tasks(id, run_id, title, state, created_at, updated_at) VALUES('t-open','r1','t','in_progress',?,?)`, now, now); err != nil {
					return err
				}
				if _, err := q.Exec(`UPDATE runs SET root_task_id='t-open' WHERE id='r1'`); err != nil {
					return err
				}
				_, e := q.Exec(`UPDATE runs SET status='completed' WHERE id='r1'`)
				return e
			})
		}},
		{"unrouted with a root_task_id is rejected", func() error {
			// Previous failing subtests roll back, so r1 still has
			// root_task_id=NULL. Set it inside the same tx so we exercise
			// the unrouted-forbids-root rule, not the completed-needs-root one.
			return s.Tx(ctx, func(q Querier) error {
				if _, err := q.Exec(`INSERT INTO tasks(id, run_id, title, state, created_at, updated_at) VALUES('t-x','r1','t','in_progress',?,?)`, now, now); err != nil {
					return err
				}
				if _, err := q.Exec(`UPDATE runs SET root_task_id='t-x' WHERE id='r1'`); err != nil {
					return err
				}
				_, e := q.Exec(`UPDATE runs SET status='unrouted', failure_category='unrouted' WHERE id='r1'`)
				return e
			})
		}},
		{"terminal-failure status without failure_category is rejected", func() error {
			// Set up a completed task so the root-task constraint is satisfied,
			// then try to mark the run failed without a category.
			return s.Tx(ctx, func(q Querier) error {
				if _, err := q.Exec(`INSERT INTO tasks(id, run_id, title, state, created_at, updated_at) VALUES('t-done2','r1','t','completed',?,?)`, now, now); err != nil {
					return err
				}
				if _, err := q.Exec(`UPDATE runs SET root_task_id='t-done2' WHERE id='r1'`); err != nil {
					return err
				}
				_, e := q.Exec(`UPDATE runs SET status='failed' WHERE id='r1'`) // no failure_category set
				return e
			})
		}},
	}
	for _, c := range checks {
		if err := c.exec(); err == nil {
			t.Errorf("%s: expected constraint failure, got nil", c.name)
		}
	}

	// Positive path: end-to-end success.
	if err := s.Tx(ctx, func(q Querier) error {
		_, e := q.Exec(`INSERT INTO runs(id, started_at, status) VALUES('r2', ?, 'running')`, now)
		return e
	}); err != nil {
		t.Fatalf("seed r2: %v", err)
	}
	if err := s.Tx(ctx, func(q Querier) error {
		if _, err := q.Exec(`INSERT INTO tasks(id, run_id, title, state, created_at, updated_at) VALUES('t-good','r2','root','completed',?,?)`, now, now); err != nil {
			return err
		}
		if _, err := q.Exec(`UPDATE runs SET root_task_id='t-good' WHERE id='r2'`); err != nil {
			return err
		}
		_, e := q.Exec(`UPDATE runs SET status='completed' WHERE id='r2'`)
		return e
	}); err != nil {
		t.Fatalf("legitimate completion path was rejected: %v", err)
	}
}
