"""Tests for architect DAGStore."""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from cleave.architect.dag import Checkpoint, DAGStore, Phase


class TestPhase:
    def test_defaults(self) -> None:
        p = Phase(id="phase-1", label="Test Phase")
        assert p.status == "pending"
        assert p.phase_type == "cleave_run"
        assert p.cost_usd == 0.0
        assert p.created_at != ""
        assert p.updated_at != ""

    def test_to_row_roundtrip(self, tmp_path: Path) -> None:
        p = Phase(id="phase-1", label="Test", description="Desc", wave=2)
        row = p.to_row()
        assert row[0] == "phase-1"
        assert row[1] == "Test"


class TestCheckpoint:
    def test_defaults(self) -> None:
        cp = Checkpoint(gate_type="budget", decision="fail", reason="Over budget")
        assert cp.created_at != ""
        assert cp.id is None


class TestDAGStore:
    def _store(self, tmp_path: Path) -> DAGStore:
        db_path = tmp_path / "test.db"
        store = DAGStore(db_path)
        store.open()
        return store

    def test_open_close(self, tmp_path: Path) -> None:
        store = self._store(tmp_path)
        store.close()
        assert (tmp_path / "test.db").exists()

    def test_context_manager(self, tmp_path: Path) -> None:
        db_path = tmp_path / "test.db"
        with DAGStore(db_path) as store:
            store.add_phase(Phase(id="p1", label="Phase 1"))
        # Closed after exit
        with DAGStore(db_path) as store:
            p = store.get_phase("p1")
            assert p is not None
            assert p.label == "Phase 1"

    def test_add_get_phase(self, tmp_path: Path) -> None:
        store = self._store(tmp_path)
        store.add_phase(Phase(id="p1", label="Phase 1", description="Do something"))
        p = store.get_phase("p1")
        assert p is not None
        assert p.label == "Phase 1"
        assert p.description == "Do something"
        store.close()

    def test_get_nonexistent_phase(self, tmp_path: Path) -> None:
        store = self._store(tmp_path)
        assert store.get_phase("nonexistent") is None
        store.close()

    def test_update_phase(self, tmp_path: Path) -> None:
        store = self._store(tmp_path)
        store.add_phase(Phase(id="p1", label="Phase 1"))
        store.update_phase("p1", status="running", cost_usd=5.0)
        p = store.get_phase("p1")
        assert p is not None
        assert p.status == "running"
        assert p.cost_usd == 5.0
        store.close()

    def test_list_phases(self, tmp_path: Path) -> None:
        store = self._store(tmp_path)
        store.add_phase(Phase(id="p1", label="Phase 1", status="pending"))
        store.add_phase(Phase(id="p2", label="Phase 2", status="completed"))
        store.add_phase(Phase(id="p3", label="Phase 3", status="pending"))

        all_phases = store.list_phases()
        assert len(all_phases) == 3

        pending = store.list_phases(status="pending")
        assert len(pending) == 2

        completed = store.list_phases(status="completed")
        assert len(completed) == 1
        store.close()

    def test_remove_phase(self, tmp_path: Path) -> None:
        store = self._store(tmp_path)
        store.add_phase(Phase(id="p1", label="Phase 1"))
        store.add_phase(Phase(id="p2", label="Phase 2"))
        store.add_dependency("p2", "p1")
        store.remove_phase("p1")
        assert store.get_phase("p1") is None
        assert store.get_dependencies("p2") == []
        store.close()

    def test_dependencies(self, tmp_path: Path) -> None:
        store = self._store(tmp_path)
        store.add_phase(Phase(id="p1", label="Phase 1"))
        store.add_phase(Phase(id="p2", label="Phase 2"))
        store.add_phase(Phase(id="p3", label="Phase 3"))
        store.add_dependency("p2", "p1")
        store.add_dependency("p3", "p1")
        store.add_dependency("p3", "p2")

        assert store.get_dependencies("p2") == ["p1"]
        assert sorted(store.get_dependencies("p3")) == ["p1", "p2"]
        assert sorted(store.get_dependents("p1")) == ["p2", "p3"]
        store.close()

    def test_duplicate_dependency_ignored(self, tmp_path: Path) -> None:
        store = self._store(tmp_path)
        store.add_phase(Phase(id="p1", label="Phase 1"))
        store.add_phase(Phase(id="p2", label="Phase 2"))
        store.add_dependency("p2", "p1")
        store.add_dependency("p2", "p1")  # Should not raise
        assert store.get_dependencies("p2") == ["p1"]
        store.close()


class TestReadySet:
    def _setup_dag(self, tmp_path: Path) -> DAGStore:
        store = DAGStore(tmp_path / "test.db")
        store.open()
        store.add_phase(Phase(id="p1", label="P1", status="completed"))
        store.add_phase(Phase(id="p2", label="P2", status="pending"))
        store.add_phase(Phase(id="p3", label="P3", status="pending"))
        store.add_dependency("p2", "p1")
        store.add_dependency("p3", "p2")
        return store

    def test_ready_set_after_first_phase(self, tmp_path: Path) -> None:
        store = self._setup_dag(tmp_path)
        ready = store.ready_set()
        assert len(ready) == 1
        assert ready[0].id == "p2"
        store.close()

    def test_ready_set_all_deps_completed(self, tmp_path: Path) -> None:
        store = self._setup_dag(tmp_path)
        store.update_phase("p2", status="completed")
        ready = store.ready_set()
        assert len(ready) == 1
        assert ready[0].id == "p3"
        store.close()

    def test_ready_set_no_deps(self, tmp_path: Path) -> None:
        store = DAGStore(tmp_path / "test.db")
        store.open()
        store.add_phase(Phase(id="p1", label="P1", status="pending"))
        store.add_phase(Phase(id="p2", label="P2", status="pending"))
        ready = store.ready_set()
        assert len(ready) == 2
        store.close()


class TestGraphAlgorithms:
    def test_topological_sort_linear(self, tmp_path: Path) -> None:
        store = DAGStore(tmp_path / "test.db")
        store.open()
        store.add_phase(Phase(id="p1", label="P1"))
        store.add_phase(Phase(id="p2", label="P2"))
        store.add_phase(Phase(id="p3", label="P3"))
        store.add_dependency("p2", "p1")
        store.add_dependency("p3", "p2")
        result = store.topological_sort()
        assert result == ["p1", "p2", "p3"]
        store.close()

    def test_topological_sort_diamond(self, tmp_path: Path) -> None:
        store = DAGStore(tmp_path / "test.db")
        store.open()
        store.add_phase(Phase(id="p1", label="P1"))
        store.add_phase(Phase(id="p2", label="P2"))
        store.add_phase(Phase(id="p3", label="P3"))
        store.add_phase(Phase(id="p4", label="P4"))
        store.add_dependency("p2", "p1")
        store.add_dependency("p3", "p1")
        store.add_dependency("p4", "p2")
        store.add_dependency("p4", "p3")
        result = store.topological_sort()
        assert result[0] == "p1"
        assert result[-1] == "p4"
        assert result.index("p2") < result.index("p4")
        assert result.index("p3") < result.index("p4")
        store.close()

    def test_detect_cycles_none(self, tmp_path: Path) -> None:
        store = DAGStore(tmp_path / "test.db")
        store.open()
        store.add_phase(Phase(id="p1", label="P1"))
        store.add_phase(Phase(id="p2", label="P2"))
        store.add_dependency("p2", "p1")
        cycles = store.detect_cycles()
        assert cycles == []
        store.close()

    def test_compute_waves_linear(self, tmp_path: Path) -> None:
        store = DAGStore(tmp_path / "test.db")
        store.open()
        store.add_phase(Phase(id="p1", label="P1"))
        store.add_phase(Phase(id="p2", label="P2"))
        store.add_phase(Phase(id="p3", label="P3"))
        store.add_dependency("p2", "p1")
        store.add_dependency("p3", "p2")
        waves = store.compute_waves()
        assert waves == [["p1"], ["p2"], ["p3"]]
        store.close()

    def test_compute_waves_parallel(self, tmp_path: Path) -> None:
        store = DAGStore(tmp_path / "test.db")
        store.open()
        store.add_phase(Phase(id="p1", label="P1"))
        store.add_phase(Phase(id="p2", label="P2"))
        store.add_phase(Phase(id="p3", label="P3"))
        # p2 and p3 are independent, both depend on p1
        store.add_dependency("p2", "p1")
        store.add_dependency("p3", "p1")
        waves = store.compute_waves()
        assert waves[0] == ["p1"]
        assert sorted(waves[1]) == ["p2", "p3"]
        store.close()

    def test_compute_waves_empty(self, tmp_path: Path) -> None:
        store = DAGStore(tmp_path / "test.db")
        store.open()
        assert store.compute_waves() == []
        store.close()


class TestAggregateQueries:
    def test_total_cost(self, tmp_path: Path) -> None:
        store = DAGStore(tmp_path / "test.db")
        store.open()
        store.add_phase(Phase(id="p1", label="P1", cost_usd=5.0))
        store.add_phase(Phase(id="p2", label="P2", cost_usd=10.0))
        assert store.total_cost() == 15.0
        store.close()

    def test_all_completed(self, tmp_path: Path) -> None:
        store = DAGStore(tmp_path / "test.db")
        store.open()
        store.add_phase(Phase(id="p1", label="P1", status="completed"))
        store.add_phase(Phase(id="p2", label="P2", status="completed"))
        assert store.all_completed() is True
        store.close()

    def test_all_completed_false(self, tmp_path: Path) -> None:
        store = DAGStore(tmp_path / "test.db")
        store.open()
        store.add_phase(Phase(id="p1", label="P1", status="completed"))
        store.add_phase(Phase(id="p2", label="P2", status="pending"))
        assert store.all_completed() is False
        store.close()

    def test_has_failed(self, tmp_path: Path) -> None:
        store = DAGStore(tmp_path / "test.db")
        store.open()
        store.add_phase(Phase(id="p1", label="P1", status="completed"))
        store.add_phase(Phase(id="p2", label="P2", status="failed"))
        assert store.has_failed() is True
        store.close()

    def test_consecutive_failures(self, tmp_path: Path) -> None:
        store = DAGStore(tmp_path / "test.db")
        store.open()
        store.add_phase(Phase(id="p1", label="P1", status="completed", completed_at="2024-01-01T00:00:00"))
        store.add_phase(Phase(id="p2", label="P2", status="failed", completed_at="2024-01-01T01:00:00"))
        store.add_phase(Phase(id="p3", label="P3", status="failed", completed_at="2024-01-01T02:00:00"))
        assert store.consecutive_failures() == 2
        store.close()

    def test_consecutive_failures_reset_by_success(self, tmp_path: Path) -> None:
        store = DAGStore(tmp_path / "test.db")
        store.open()
        store.add_phase(Phase(id="p1", label="P1", status="failed", completed_at="2024-01-01T00:00:00"))
        store.add_phase(Phase(id="p2", label="P2", status="completed", completed_at="2024-01-01T01:00:00"))
        store.add_phase(Phase(id="p3", label="P3", status="failed", completed_at="2024-01-01T02:00:00"))
        assert store.consecutive_failures() == 1
        store.close()


class TestCheckpoints:
    def test_add_and_get(self, tmp_path: Path) -> None:
        store = DAGStore(tmp_path / "test.db")
        store.open()
        store.add_phase(Phase(id="p1", label="P1"))
        cp_id = store.add_checkpoint(
            gate_type="budget", decision="fail",
            reason="Over budget", phase_id="p1",
        )
        assert cp_id > 0
        cps = store.get_checkpoints(phase_id="p1")
        assert len(cps) == 1
        assert cps[0].gate_type == "budget"
        assert cps[0].decision == "fail"
        store.close()

    def test_get_all_checkpoints(self, tmp_path: Path) -> None:
        store = DAGStore(tmp_path / "test.db")
        store.open()
        store.add_phase(Phase(id="p1", label="P1"))
        store.add_phase(Phase(id="p2", label="P2"))
        store.add_checkpoint(gate_type="structural", decision="continue", phase_id="p1")
        store.add_checkpoint(gate_type="failure", decision="pause", phase_id="p2")
        all_cps = store.get_checkpoints()
        assert len(all_cps) == 2
        store.close()


class TestJournal:
    def test_log_and_get(self, tmp_path: Path) -> None:
        store = DAGStore(tmp_path / "test.db")
        store.open()
        store.add_phase(Phase(id="p1", label="P1"))
        store.log_event("plan", "Started planning")
        store.log_event("execute", "Running phase", phase_id="p1")
        journal = store.get_journal()
        assert len(journal) == 2
        # Most recent first
        assert journal[0]["event_type"] == "execute"
        assert journal[1]["event_type"] == "plan"
        store.close()


class TestMetadata:
    def test_set_and_get(self, tmp_path: Path) -> None:
        store = DAGStore(tmp_path / "test.db")
        store.open()
        store.set_metadata("config", {"directive": "test"})
        result = store.get_metadata("config")
        assert result == {"directive": "test"}
        store.close()

    def test_get_nonexistent(self, tmp_path: Path) -> None:
        store = DAGStore(tmp_path / "test.db")
        store.open()
        assert store.get_metadata("missing") is None
        store.close()

    def test_upsert(self, tmp_path: Path) -> None:
        store = DAGStore(tmp_path / "test.db")
        store.open()
        store.set_metadata("key", "value1")
        store.set_metadata("key", "value2")
        assert store.get_metadata("key") == "value2"
        store.close()


class TestSnapshot:
    def test_snapshot_structure(self, tmp_path: Path) -> None:
        store = DAGStore(tmp_path / "test.db")
        store.open()
        store.add_phase(Phase(id="p1", label="P1", cost_usd=5.0))
        store.add_phase(Phase(id="p2", label="P2", cost_usd=10.0))
        store.add_dependency("p2", "p1")
        store.add_checkpoint(gate_type="structural", decision="continue", phase_id="p1")
        store.log_event("plan", "test")

        snap = store.snapshot()
        assert len(snap["phases"]) == 2
        assert snap["phases"][1]["dependencies"] == ["p1"]
        assert len(snap["checkpoints"]) == 1
        assert len(snap["journal"]) == 1
        assert snap["total_cost_usd"] == 15.0
        store.close()


class TestPersistence:
    def test_data_persists_across_open_close(self, tmp_path: Path) -> None:
        db_path = tmp_path / "persist.db"
        with DAGStore(db_path) as store:
            store.add_phase(Phase(id="p1", label="Phase 1"))
            store.set_metadata("test_key", 42)

        with DAGStore(db_path) as store:
            p = store.get_phase("p1")
            assert p is not None
            assert p.label == "Phase 1"
            assert store.get_metadata("test_key") == 42
