"""Tests for the main orchestration loop (runner)."""

from __future__ import annotations

import asyncio
import json
import sys
from pathlib import Path
from unittest.mock import AsyncMock, patch, MagicMock

import pytest

from cleave.orchestrator.config import OrchestratorConfig
from cleave.orchestrator.errors import (
    CircuitBreakerTrippedError,
    OrchestratorError,
)
from cleave.orchestrator.runner import OrchestratorResult, run, run_sync
from cleave.orchestrator.state import ChildState, OrchestratorState


def _make_config(tmp_path: Path, **kwargs) -> OrchestratorConfig:
    (tmp_path / ".git").mkdir(exist_ok=True)
    defaults = {
        "directive": "Test directive",
        "repo_path": tmp_path,
        "success_criteria": ["tests pass"],
    }
    defaults.update(kwargs)
    return OrchestratorConfig(**defaults)


def _mock_subprocess(returncode: int = 0, stdout: str = "", stderr: str = ""):
    proc = AsyncMock()
    proc.stdin = AsyncMock()
    proc.stdin.write = lambda x: None
    proc.stdin.drain = AsyncMock()
    proc.stdin.close = lambda: None
    proc.pid = 12345
    proc.communicate = AsyncMock(return_value=(stdout.encode(), stderr.encode()))
    proc.returncode = returncode
    proc.terminate = AsyncMock()
    proc.kill = AsyncMock()
    return proc


@pytest.mark.skipif(sys.platform == "win32", reason="Orchestrator uses POSIX signal handlers")
class TestDryRun:
    def test_dry_run_stops_after_planning(self, tmp_path: Path) -> None:
        config = _make_config(tmp_path, dry_run=True)
        plan_response = json.dumps({
            "children": [
                {"label": "child-a", "description": "Do A"},
                {"label": "child-b", "description": "Do B"},
            ],
            "rationale": "Split A and B",
        })

        async def mock_exec(*args, **kwargs):
            cmd = args[0] if args else ""
            if cmd == "git":
                # git commands
                sub = args[1] if len(args) > 1 else ""
                if sub == "rev-parse":
                    return _mock_subprocess(stdout="main")
                if sub == "status":
                    return _mock_subprocess(stdout="")
                return _mock_subprocess()
            else:
                # claude CLI call (planner)
                return _mock_subprocess(stdout=plan_response)

        with patch("shutil.which", return_value="/usr/bin/claude"):
            with patch("cleave.orchestrator.worktree.asyncio.create_subprocess_exec", side_effect=mock_exec):
                with patch("cleave.orchestrator.planner.asyncio.create_subprocess_exec", side_effect=mock_exec):
                    result = asyncio.run(run(config))

        assert result.success is True
        assert result.state.phase == "complete"
        assert result.state.plan is not None
        assert len(result.state.plan["children"]) == 2
        # Should NOT have dispatched children
        assert len(result.state.children) == 0


class TestBudgetEnforcement:
    def test_rejects_negative_budget(self, tmp_path: Path) -> None:
        config = _make_config(tmp_path, max_budget_usd=-1)
        with pytest.raises(OrchestratorError, match="positive"):
            config.validate()


@pytest.mark.skipif(sys.platform == "win32", reason="Orchestrator uses POSIX signal handlers")
class TestConfirmGate:
    def test_confirm_stops_at_planned_phase(self, tmp_path: Path) -> None:
        config = _make_config(tmp_path, confirm=True)
        plan_response = json.dumps({
            "children": [
                {"label": "child-a", "description": "Do A"},
                {"label": "child-b", "description": "Do B"},
            ],
            "rationale": "Split A and B",
        })

        async def mock_exec(*args, **kwargs):
            cmd = args[0] if args else ""
            if cmd == "git":
                sub = args[1] if len(args) > 1 else ""
                if sub == "rev-parse":
                    return _mock_subprocess(stdout="main")
                if sub == "status":
                    return _mock_subprocess(stdout="")
                return _mock_subprocess()
            else:
                return _mock_subprocess(stdout=plan_response)

        with patch("shutil.which", return_value="/usr/bin/claude"):
            with patch("cleave.orchestrator.worktree.asyncio.create_subprocess_exec", side_effect=mock_exec):
                with patch("cleave.orchestrator.planner.asyncio.create_subprocess_exec", side_effect=mock_exec):
                    result = asyncio.run(run(config))

        assert result.success is True
        assert result.state.phase == "planned"
        assert result.state.plan is not None
        # plan-review.md should have been written
        if result.workspace_path:
            review_path = result.workspace_path / "plan-review.md"
            assert review_path.exists()
            content = review_path.read_text()
            assert "child-a" in content
            assert "child-b" in content


class TestMergeFailureSurfacing:
    def test_merge_failure_causes_result_failure(self) -> None:
        """Merge conflicts should cause result.success == False."""
        state = OrchestratorState(directive="test")
        state.merge_report = {
            "all_succeeded": False,
            "results": [
                {"branch": "cleave/a", "success": True, "conflict_files": [], "error": None},
                {"branch": "cleave/b", "success": False, "conflict_files": ["src/foo.py"], "error": "conflict"},
            ],
        }
        # Even if all children completed, merge failure means overall failure
        state.children = [
            ChildState(child_id=0, label="a", status="completed"),
            ChildState(child_id=1, label="b", status="completed"),
        ]
        all_children_ok = all(c.status == "completed" for c in state.children)
        merge_ok = state.merge_report["all_succeeded"]
        all_ok = all_children_ok and merge_ok
        assert all_children_ok is True
        assert merge_ok is False
        assert all_ok is False


class TestOrchestratorResult:
    def test_result_fields(self) -> None:
        state = OrchestratorState(directive="test")
        result = OrchestratorResult(
            success=True,
            state=state,
            report_path=Path("/tmp/report.md"),
            workspace_path=Path("/tmp/workspace"),
        )
        assert result.success is True
        assert result.error is None
        assert result.report_path == Path("/tmp/report.md")

    def test_failure_result(self) -> None:
        state = OrchestratorState(directive="test")
        result = OrchestratorResult(
            success=False,
            state=state,
            error="Budget exceeded",
        )
        assert result.success is False
        assert result.error == "Budget exceeded"


@pytest.mark.skipif(sys.platform == "win32", reason="Orchestrator uses POSIX signal handlers")
class TestDirtyMode:
    def test_dirty_snapshots_and_restores(self, tmp_path: Path) -> None:
        """--dirty should call snapshot_dirty_state in preflight instead of ensure_clean_worktree."""
        config = _make_config(tmp_path, dirty=True, dry_run=True)
        plan_response = json.dumps({
            "children": [
                {"label": "child-a", "description": "Do A"},
                {"label": "child-b", "description": "Do B"},
            ],
            "rationale": "Split A and B",
        })

        async def mock_exec(*args, **kwargs):
            cmd = args[0] if args else ""
            if cmd == "git":
                sub = args[1] if len(args) > 1 else ""
                if sub == "rev-parse":
                    return _mock_subprocess(stdout="abc123def456")
                if sub == "status":
                    return _mock_subprocess(stdout="")
                if sub == "add":
                    return _mock_subprocess()
                if sub == "commit":
                    return _mock_subprocess()
                return _mock_subprocess()
            else:
                return _mock_subprocess(stdout=plan_response)

        with patch("shutil.which", return_value="/usr/bin/claude"):
            with patch("cleave.orchestrator.worktree.asyncio.create_subprocess_exec", side_effect=mock_exec):
                with patch("cleave.orchestrator.planner.asyncio.create_subprocess_exec", side_effect=mock_exec):
                    result = asyncio.run(run(config))

        assert result.success is True
        # pre_dirty_head should have been set
        assert result.state.pre_dirty_head == "abc123def456"

    def test_dirty_config_serializes(self, tmp_path: Path) -> None:
        """dirty flag should round-trip through to_dict/from_dict."""
        config = _make_config(tmp_path, dirty=True)
        d = config.to_dict()
        assert d["dirty"] is True
        config2 = OrchestratorConfig.from_dict(d)
        assert config2.dirty is True


class TestModeDetection:
    def test_robust_keyword_sets_mode(self) -> None:
        """Directive containing cleave-robust should trigger robust mode."""
        from cleave.core.assessment import detect_flags
        flags = detect_flags("Fix the bug. cleave-robust")
        assert flags["robust"] is True

    def test_no_keyword_defaults_lean(self) -> None:
        from cleave.core.assessment import detect_flags
        flags = detect_flags("Fix the bug")
        assert flags["robust"] is False
