"""Unit tests for the execution engine with mocked backend."""

from pathlib import Path

import pytest

from juvenal.checkers import parse_verdict
from juvenal.engine import Engine, _extract_yaml
from juvenal.workflow import Phase, Workflow
from tests.conftest import MockBackend


class TestVerdictParsing:
    def test_pass(self):
        passed, reason, target = parse_verdict("some output\nVERDICT: PASS")
        assert passed
        assert reason == ""
        assert target is None

    def test_fail_with_reason(self):
        passed, reason, target = parse_verdict("output\nVERDICT: FAIL: tests broken")
        assert not passed
        assert reason == "tests broken"
        assert target is None

    def test_fail_without_reason(self):
        passed, reason, target = parse_verdict("VERDICT: FAIL")
        assert not passed
        assert reason == "unspecified"
        assert target is None

    def test_no_verdict(self):
        passed, reason, target = parse_verdict("no verdict here")
        assert not passed
        assert "did not emit a VERDICT" in reason
        assert target is None

    def test_verdict_scan_backwards(self):
        """Should find the last VERDICT line."""
        output = "VERDICT: FAIL: old\nmore stuff\nVERDICT: PASS"
        passed, reason, target = parse_verdict(output)
        assert passed
        assert reason == ""
        assert target is None

    def test_fail_with_bounce_target(self):
        """VERDICT: FAIL(target-id): reason extracts both target and reason."""
        passed, reason, target = parse_verdict("review\nVERDICT: FAIL(design-experiments): needs more data")
        assert not passed
        assert reason == "needs more data"
        assert target == "design-experiments"

    def test_fail_with_bounce_target_no_reason(self):
        """VERDICT: FAIL(target-id): with empty reason."""
        passed, reason, target = parse_verdict("VERDICT: FAIL(write-paper):")
        assert not passed
        assert reason == "unspecified"
        assert target == "write-paper"

    def test_fail_with_bounce_target_scan_backwards(self):
        """Should find the last VERDICT line even with targeted fail."""
        output = "VERDICT: FAIL: old reason\nmore output\nVERDICT: FAIL(phase-b): new reason"
        passed, reason, target = parse_verdict(output)
        assert not passed
        assert reason == "new reason"
        assert target == "phase-b"


class TestEngineWithMockedBackend:
    def _make_engine(self, workflow, backend, tmp_path, **kwargs):
        """Create an engine with injected mock backend."""
        engine = Engine(workflow, state_file=str(tmp_path / "state.json"), **kwargs)
        engine.backend = backend
        return engine

    def test_single_phase_pass(self, tmp_path):
        """Implement phase followed by a script phase, both pass."""
        backend = MockBackend()
        backend.add_response(exit_code=0, output="done")  # implement
        workflow = Workflow(
            name="test",
            phases=[
                Phase(id="setup", type="implement", prompt="Do it."),
                Phase(id="setup-check", type="script", run="true"),
            ],
            max_bounces=3,
        )
        engine = self._make_engine(workflow, backend, tmp_path)
        assert engine.run() == 0

    def test_implementation_crash_bounces(self, tmp_path):
        """Implement crash bounces back to self, retried on next visit."""
        backend = MockBackend()
        backend.add_response(exit_code=1, output="crash")  # attempt 1 crashes -> bounce
        backend.add_response(exit_code=0, output="done")  # attempt 2 succeeds
        workflow = Workflow(
            name="test",
            phases=[
                Phase(id="setup", type="implement", prompt="Do it."),
                Phase(id="setup-check", type="script", run="true"),
            ],
            max_bounces=3,
        )
        engine = self._make_engine(workflow, backend, tmp_path)
        assert engine.run() == 0

    def test_script_checker_failure_bounces(self, tmp_path):
        """Script failure bounces back; global bounce counter exhausts."""
        backend = MockBackend()
        backend.add_response(exit_code=0, output="done")  # implement attempt 1
        # Script fails -> bounce 1 -> back to implement
        backend.add_response(exit_code=0, output="done")  # implement attempt 2
        # Script fails -> bounce 2 -> exhausted (max_bounces=2)
        workflow = Workflow(
            name="test",
            phases=[
                Phase(id="setup", type="implement", prompt="Do it."),
                Phase(id="setup-check", type="script", run="false"),  # always fails
            ],
            max_bounces=2,
        )
        engine = self._make_engine(workflow, backend, tmp_path)
        assert engine.run() == 1  # exhausted

    def test_agent_checker_pass(self, tmp_path):
        """Implement + check phase, check passes."""
        backend = MockBackend()
        backend.add_response(exit_code=0, output="implemented")  # implement
        backend.add_response(exit_code=0, output="looks good\nVERDICT: PASS")  # check
        workflow = Workflow(
            name="test",
            phases=[
                Phase(id="setup", type="implement", prompt="Do it."),
                Phase(id="setup-review", type="check", role="tester"),
            ],
            max_bounces=3,
        )
        engine = self._make_engine(workflow, backend, tmp_path)
        assert engine.run() == 0

    def test_agent_checker_fail_bounces(self, tmp_path):
        """Check failure bounces back to most recent implement phase."""
        backend = MockBackend()
        backend.add_response(exit_code=0, output="implemented")  # implement attempt 1
        backend.add_response(exit_code=0, output="VERDICT: FAIL: bad code")  # check fails -> bounce 1
        backend.add_response(exit_code=0, output="fixed")  # implement attempt 2
        backend.add_response(exit_code=0, output="VERDICT: PASS")  # check passes
        workflow = Workflow(
            name="test",
            phases=[
                Phase(id="setup", type="implement", prompt="Do it."),
                Phase(id="setup-review", type="check", role="tester"),
            ],
            max_bounces=3,
        )
        engine = self._make_engine(workflow, backend, tmp_path)
        assert engine.run() == 0

    def test_multi_phase(self, tmp_path):
        backend = MockBackend()
        # Phase 1: implement passes
        backend.add_response(exit_code=0, output="phase1 done")
        # Phase 2: implement passes
        backend.add_response(exit_code=0, output="phase2 done")
        workflow = Workflow(
            name="test",
            phases=[
                Phase(id="phase1", type="implement", prompt="Do phase 1."),
                Phase(id="phase1-check", type="script", run="true"),
                Phase(id="phase2", type="implement", prompt="Do phase 2."),
                Phase(id="phase2-check", type="script", run="true"),
            ],
            max_bounces=3,
        )
        engine = self._make_engine(workflow, backend, tmp_path)
        assert engine.run() == 0

    def test_global_bounce_exhaustion_across_phases(self, tmp_path):
        """Global bounce counter accumulates across different phases."""
        backend = MockBackend()
        # Phase 1 implement: pass
        backend.add_response(exit_code=0, output="phase1 done")
        # Phase 1 check: fail -> bounce 1
        backend.add_response(exit_code=0, output="VERDICT: FAIL: not good")
        # Phase 1 implement again: pass
        backend.add_response(exit_code=0, output="phase1 fixed")
        # Phase 1 check: pass
        backend.add_response(exit_code=0, output="VERDICT: PASS")
        # Phase 2 implement: crash -> bounce 2 -> exhausted (max_bounces=2)
        backend.add_response(exit_code=1, output="crash")
        workflow = Workflow(
            name="test",
            phases=[
                Phase(id="phase1", type="implement", prompt="Do phase 1."),
                Phase(id="phase1-review", type="check", role="tester"),
                Phase(id="phase2", type="implement", prompt="Do phase 2."),
            ],
            max_bounces=2,
        )
        engine = self._make_engine(workflow, backend, tmp_path)
        assert engine.run() == 1  # exhausted

    def test_per_phase_bounce_target(self, tmp_path):
        """Phase-level bounce_target directs bounce to a specific phase."""
        backend = MockBackend()
        backend.add_response(exit_code=0, output="setup done")  # setup implement
        backend.add_response(exit_code=0, output="feature done")  # feature implement
        backend.add_response(exit_code=0, output="VERDICT: FAIL: bad")  # feature check -> bounce to setup
        backend.add_response(exit_code=0, output="setup redone")  # setup implement again
        backend.add_response(exit_code=0, output="feature redone")  # feature implement again
        backend.add_response(exit_code=0, output="VERDICT: PASS")  # feature check passes
        workflow = Workflow(
            name="test",
            phases=[
                Phase(id="setup", type="implement", prompt="Set up."),
                Phase(id="feature", type="implement", prompt="Build feature."),
                Phase(id="feature-review", type="check", role="tester", bounce_target="setup"),
            ],
            max_bounces=3,
        )
        engine = self._make_engine(workflow, backend, tmp_path)
        assert engine.run() == 0

    def test_agent_guided_bounce_valid_target(self, tmp_path):
        """Check with bounce_targets: agent picks a valid target via VERDICT: FAIL(target)."""
        backend = MockBackend()
        backend.add_response(exit_code=0, output="setup done")  # setup implement
        backend.add_response(exit_code=0, output="experiments designed")  # design-experiments implement
        backend.add_response(exit_code=0, output="paper written")  # write-paper implement
        # Review picks design-experiments as bounce target
        backend.add_response(exit_code=0, output="VERDICT: FAIL(design-experiments): needs more data")
        # Re-run from design-experiments
        backend.add_response(exit_code=0, output="experiments redesigned")  # design-experiments again
        backend.add_response(exit_code=0, output="paper rewritten")  # write-paper again
        backend.add_response(exit_code=0, output="VERDICT: PASS")  # review passes
        workflow = Workflow(
            name="test",
            phases=[
                Phase(id="setup", type="implement", prompt="Set up."),
                Phase(id="design-experiments", type="implement", prompt="Design experiments."),
                Phase(id="write-paper", type="implement", prompt="Write paper."),
                Phase(
                    id="review",
                    type="check",
                    role="tester",
                    bounce_targets=["design-experiments", "write-paper"],
                ),
            ],
            max_bounces=5,
        )
        engine = self._make_engine(workflow, backend, tmp_path)
        assert engine.run() == 0

    def test_agent_guided_bounce_invalid_target_falls_back(self, tmp_path):
        """Agent picks a target not in bounce_targets — falls back to first in list."""
        backend = MockBackend()
        backend.add_response(exit_code=0, output="phase-a done")  # phase-a
        backend.add_response(exit_code=0, output="phase-b done")  # phase-b
        # Review specifies invalid target -> falls back to phase-a (first in list)
        backend.add_response(exit_code=0, output="VERDICT: FAIL(nonexistent): bad")
        backend.add_response(exit_code=0, output="phase-a redone")  # phase-a again
        backend.add_response(exit_code=0, output="phase-b redone")  # phase-b again
        backend.add_response(exit_code=0, output="VERDICT: PASS")  # review passes
        workflow = Workflow(
            name="test",
            phases=[
                Phase(id="phase-a", type="implement", prompt="Do A."),
                Phase(id="phase-b", type="implement", prompt="Do B."),
                Phase(
                    id="review",
                    type="check",
                    role="tester",
                    bounce_targets=["phase-a", "phase-b"],
                ),
            ],
            max_bounces=5,
        )
        engine = self._make_engine(workflow, backend, tmp_path)
        assert engine.run() == 0

    def test_agent_guided_bounce_no_target_in_verdict(self, tmp_path):
        """Agent emits VERDICT: FAIL without a target — falls back to first in bounce_targets."""
        backend = MockBackend()
        backend.add_response(exit_code=0, output="phase-a done")  # phase-a
        backend.add_response(exit_code=0, output="phase-b done")  # phase-b
        # Review fails without specifying target -> falls back to phase-a (first in list)
        backend.add_response(exit_code=0, output="VERDICT: FAIL: something wrong")
        backend.add_response(exit_code=0, output="phase-a redone")  # phase-a again
        backend.add_response(exit_code=0, output="phase-b redone")  # phase-b again
        backend.add_response(exit_code=0, output="VERDICT: PASS")
        workflow = Workflow(
            name="test",
            phases=[
                Phase(id="phase-a", type="implement", prompt="Do A."),
                Phase(id="phase-b", type="implement", prompt="Do B."),
                Phase(
                    id="review",
                    type="check",
                    role="tester",
                    bounce_targets=["phase-a", "phase-b"],
                ),
            ],
            max_bounces=5,
        )
        engine = self._make_engine(workflow, backend, tmp_path)
        assert engine.run() == 0

    def test_dry_run(self, tmp_path, capsys):
        workflow = Workflow(
            name="test",
            phases=[
                Phase(id="setup", type="implement", prompt="Do the thing."),
                Phase(id="setup-check", type="script", run="true"),
            ],
        )
        engine = self._make_engine(workflow, MockBackend(), tmp_path, dry_run=True)
        assert engine.run() == 0
        captured = capsys.readouterr()
        assert "test" in captured.out
        assert "setup" in captured.out
        assert "implement" in captured.out
        assert "script" in captured.out


class TestExtractYaml:
    def test_yaml_code_fence(self):
        text = "Here's the workflow:\n```yaml\nname: test\nphases: []\n```\nDone."
        assert "name: test" in _extract_yaml(text)
        assert "```" not in _extract_yaml(text)

    def test_generic_code_fence(self):
        text = "Here:\n```\nname: test\nphases: []\n```\n"
        assert "name: test" in _extract_yaml(text)

    def test_no_fence_with_prose(self):
        text = "Sure, here is your workflow.\n\nname: test\nphases:\n  - id: a\n    prompt: do it\n"
        result = _extract_yaml(text)
        assert "name: test" in result
        assert "Sure, here" not in result

    def test_raw_yaml(self):
        text = "name: test\nphases:\n  - id: a\n    prompt: do it\n"
        assert _extract_yaml(text) == text


class TestPlanWorkflow:
    def test_plan_creates_temp_structure(self, tmp_path):
        """plan_workflow creates temp dir with .plan/goal.md and runs Engine."""
        from unittest.mock import MagicMock, patch

        from juvenal.engine import plan_workflow

        mock_engine_instance = MagicMock()
        mock_engine_instance.run.return_value = 0

        def fake_engine_init(self_engine, workflow, **kwargs):
            # Write a valid workflow.yaml in the working dir to simulate engine output
            wd = Path(workflow.working_dir)
            (wd / "workflow.yaml").write_text("name: test\nphases:\n  - id: a\n    prompt: do it\n")
            self_engine.workflow = workflow
            self_engine.backend = MagicMock()
            self_engine.display = MagicMock()
            self_engine.dry_run = False
            self_engine.state = MagicMock()
            self_engine._start_idx = 0
            # Verify .plan/goal.md was created
            assert (wd / ".plan" / "goal.md").exists()
            assert (wd / ".plan" / "goal.md").read_text() == "build something"

        with (
            patch.object(Engine, "__init__", fake_engine_init),
            patch.object(Engine, "run", return_value=0),
        ):
            out_path = str(tmp_path / "workflow.yaml")
            plan_workflow("build something", out_path)

        assert Path(out_path).exists()
        content = Path(out_path).read_text()
        assert "phases" in content

    def test_plan_copies_output_on_success(self, tmp_path):
        """plan_workflow copies workflow.yaml to output path and cleans up."""
        from unittest.mock import MagicMock, patch

        from juvenal.engine import plan_workflow

        yaml_content = "name: result\nphases:\n  - id: x\n    prompt: hi\n"

        def fake_engine_init(self_engine, workflow, **kwargs):
            wd = Path(workflow.working_dir)
            (wd / "workflow.yaml").write_text(yaml_content)
            self_engine.workflow = workflow
            self_engine.backend = MagicMock()
            self_engine.display = MagicMock()
            self_engine.dry_run = False
            self_engine.state = MagicMock()
            self_engine._start_idx = 0

        with (
            patch.object(Engine, "__init__", fake_engine_init),
            patch.object(Engine, "run", return_value=0),
        ):
            out_path = str(tmp_path / "out.yaml")
            plan_workflow("goal", out_path)

        import yaml

        parsed = yaml.safe_load(Path(out_path).read_text())
        assert parsed["name"] == "result"
        assert "phases" in parsed

    def test_plan_fails_on_engine_failure(self, tmp_path):
        """plan_workflow raises SystemExit if engine returns non-zero."""
        from unittest.mock import MagicMock, patch

        from juvenal.engine import plan_workflow

        def fake_engine_init(self_engine, workflow, **kwargs):
            self_engine.workflow = workflow
            self_engine.backend = MagicMock()
            self_engine.display = MagicMock()
            self_engine.dry_run = False
            self_engine.state = MagicMock()
            self_engine._start_idx = 0

        with (
            patch.object(Engine, "__init__", fake_engine_init),
            patch.object(Engine, "run", return_value=1),
        ):
            with pytest.raises(SystemExit):
                plan_workflow("goal", str(tmp_path / "out.yaml"))
