"""Tests for orchestrator state persistence."""

from __future__ import annotations

from pathlib import Path

import pytest

from cleave.orchestrator.errors import ResumeError
from cleave.orchestrator.state import (
    ChildState,
    OrchestratorState,
    load_state,
    reset_interrupted_children,
    save_state,
)


class TestChildState:
    def test_defaults(self) -> None:
        cs = ChildState(child_id=1, label="auth-core")
        assert cs.status == "pending"
        assert cs.branch_name == ""
        assert cs.pid is None
        assert cs.cost_usd is None
        assert cs.retry_count == 0

    def test_to_dict_roundtrip(self) -> None:
        cs = ChildState(
            child_id=1,
            label="auth-core",
            status="completed",
            branch_name="cleave/auth-core",
            exit_code=0,
            cost_usd=1.23,
        )
        d = cs.to_dict()
        assert d["child_id"] == 1
        assert d["label"] == "auth-core"
        assert d["status"] == "completed"

        cs2 = ChildState.from_dict(d)
        assert cs2.child_id == cs.child_id
        assert cs2.status == cs.status
        assert cs2.cost_usd == cs.cost_usd


class TestOrchestratorState:
    def test_defaults(self) -> None:
        state = OrchestratorState()
        assert state.phase == "preflight"
        assert state.total_cost_usd == 0.0
        assert state.depth == 0
        assert state.children == []

    def test_set_phase_valid(self) -> None:
        state = OrchestratorState()
        state.set_phase("planning")
        assert state.phase == "planning"

    def test_set_phase_invalid(self) -> None:
        state = OrchestratorState()
        with pytest.raises(ValueError, match="Unknown phase"):
            state.set_phase("bogus")

    def test_to_dict_roundtrip(self) -> None:
        state = OrchestratorState(
            directive="Fix bug",
            repo_path="/tmp/repo",
            depth=1,
        )
        state.children = [
            ChildState(child_id=1, label="fix-core", status="completed"),
            ChildState(child_id=2, label="fix-tests", status="pending"),
        ]
        state.plan = {"children": [{"label": "fix-core"}]}

        d = state.to_dict()
        assert d["directive"] == "Fix bug"
        assert len(d["children"]) == 2
        assert d["children"][0]["label"] == "fix-core"
        assert d["plan"]["children"][0]["label"] == "fix-core"

        state2 = OrchestratorState.from_dict(d)
        assert state2.directive == "Fix bug"
        assert len(state2.children) == 2
        assert state2.children[0].label == "fix-core"
        assert state2.depth == 1

    def test_from_config(self, tmp_path: Path) -> None:
        from cleave.orchestrator.config import OrchestratorConfig
        (tmp_path / ".git").mkdir()
        cfg = OrchestratorConfig(directive="Test", repo_path=tmp_path)
        state = OrchestratorState.from_config(cfg, depth=2, parent_run_id="parent-123")
        assert state.directive == "Test"
        assert state.depth == 2
        assert state.parent_run_id == "parent-123"
        assert state.config["directive"] == "Test"


class TestStatePersistence:
    def test_save_load_roundtrip(self, tmp_path: Path) -> None:
        state = OrchestratorState(
            directive="Test save/load",
            repo_path=str(tmp_path),
            workspace_path=str(tmp_path),
            base_branch="main",
        )
        state.children = [
            ChildState(child_id=1, label="child-a", status="completed", cost_usd=2.5),
        ]
        state.total_cost_usd = 2.5

        save_state(state, tmp_path)

        loaded = load_state(tmp_path)
        assert loaded.run_id == state.run_id
        assert loaded.directive == "Test save/load"
        assert loaded.base_branch == "main"
        assert len(loaded.children) == 1
        assert loaded.children[0].label == "child-a"
        assert loaded.total_cost_usd == 2.5

    def test_load_missing_file(self, tmp_path: Path) -> None:
        with pytest.raises(ResumeError, match="No orchestrator.json"):
            load_state(tmp_path)

    def test_save_creates_json_file(self, tmp_path: Path) -> None:
        state = OrchestratorState(directive="JSON test", repo_path=str(tmp_path))
        save_state(state, tmp_path)
        assert (tmp_path / "orchestrator.json").exists()
        assert not (tmp_path / "orchestrator.yaml").exists()

    def test_json_preserves_types(self, tmp_path: Path) -> None:
        """Verify JSON round-trip preserves types that YAML would coerce."""
        state = OrchestratorState(directive="true", repo_path=str(tmp_path))
        state.children = [
            ChildState(child_id=0, label="123"),
            ChildState(child_id=1, label="null"),
            ChildState(child_id=2, label="true"),
        ]
        save_state(state, tmp_path)
        loaded = load_state(tmp_path)

        # These would be coerced by YAML: "true"→bool, "123"→int, "null"→None
        assert loaded.directive == "true"
        assert isinstance(loaded.directive, str)
        assert loaded.children[0].label == "123"
        assert isinstance(loaded.children[0].label, str)
        assert loaded.children[1].label == "null"
        assert isinstance(loaded.children[1].label, str)
        assert loaded.children[2].label == "true"
        assert isinstance(loaded.children[2].label, str)

    def test_legacy_yaml_migration(self, tmp_path: Path) -> None:
        """Verify load_state migrates legacy YAML to JSON."""
        from cleave.core.yaml_utils import to_yaml

        state = OrchestratorState(directive="legacy", repo_path=str(tmp_path))
        # Write as legacy YAML
        yaml_content = to_yaml(state.to_dict())
        (tmp_path / "orchestrator.yaml").write_text(yaml_content, encoding="utf-8")

        loaded = load_state(tmp_path)
        assert loaded.directive == "legacy"
        # Should have created JSON file
        assert (tmp_path / "orchestrator.json").exists()

    def test_from_dict_does_not_mutate_input(self) -> None:
        """Verify from_dict doesn't mutate the caller's dict."""
        data = {
            "run_id": "test",
            "directive": "test",
            "children": [{"child_id": 0, "label": "a"}],
        }
        original_keys = set(data.keys())
        OrchestratorState.from_dict(data)
        assert set(data.keys()) == original_keys
        assert "children" in data


class TestResetInterruptedChildren:
    def test_resets_running_to_pending(self) -> None:
        state = OrchestratorState()
        state.children = [
            ChildState(child_id=1, label="a", status="running", pid=1234),
            ChildState(child_id=2, label="b", status="completed"),
            ChildState(child_id=3, label="c", status="running", pid=5678),
        ]
        count = reset_interrupted_children(state)
        assert count == 2
        assert state.children[0].status == "pending"
        assert state.children[0].pid is None
        assert state.children[1].status == "completed"
        assert state.children[2].status == "pending"

    def test_no_running_children(self) -> None:
        state = OrchestratorState()
        state.children = [
            ChildState(child_id=1, label="a", status="completed"),
        ]
        count = reset_interrupted_children(state)
        assert count == 0
