"""Tests verifying that post-push is a blocking gate in iteration.py.

When the post-push hook returns a non-zero exit code, the iteration
must stop, write the failure details to backlog.md, and set status to
``deploy-failed``.
"""

import contextlib
import io
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "skills" / "build-feature"))

from hooks import HookResult


def _common_patches():
    """Patches shared by tests where iteration continues past push."""
    return [
        ("iteration.run_augment_hook", {"return_value": ""}),
        ("iteration.read_file_text", {"return_value": ""}),
        ("iteration.consume_backlog", {"return_value": ""}),
        ("iteration.find_all_unplanned", {"return_value": []}),
        ("iteration.find_planned_not_executed", {"return_value": []}),
        ("iteration.write_status", {}),
        ("iteration.sync_skills_to_project", {"return_value": 0}),
        ("iteration.setup_project_agents_md", {"return_value": True}),
        ("iteration.cleanup_stale_mcp", {}),
        ("iteration.ensure_gitignore_entries", {}),
        ("iteration.build_phase_prompt", {"return_value": "prompt"}),
        ("iteration.run_streaming", {"return_value": 0}),
        ("iteration.normalize_task_filenames", {}),
        ("iteration.load_tasks", {"return_value": {}}),
        ("iteration.get_task_ids", {"return_value": {}}),
        ("iteration.resolve_active_initiative", {"return_value": "0001-test-app"}),
        ("iteration.get_initiative_tasks_dir", {"return_value": Path("/tmp/tasks")}),
        ("iteration.get_initiative_spec", {"return_value": "spec"}),
        ("iteration.get_initiative_constraints", {"return_value": ""}),
        ("iteration.initiatives_base_dir", {"return_value": Path("/tmp/initiatives")}),
        ("iteration.initiative_has_tasks", {"return_value": True}),
        ("iteration.summarize_all_executed_tasks", {"return_value": ""}),
        ("iteration.next_task_number", {"return_value": 1}),
        ("iteration.generate_memory_index", {"return_value": "# Memory"}),
        ("iteration.list_initiatives", {"return_value": []}),
    ]


def _call_run_iteration(project, log_fh):
    from iteration import run_iteration
    run_iteration(
        iteration=1,
        project_dir=project,
        project_id="test-app",
        root_initiative="test-app",
        backlog_path=project / ".autobuilder" / "backlog.md",
        status_path=project / ".autobuilder" / "status.md",
        model="test-model",
        executor="cursor",
        env_file=None,
        port=3080,
        consecutive_idle=0,
        previous_improvements=[],
        no_push=False,
        log_fh=log_fh,
    )


class TestPostPushBlocking:
    @pytest.fixture
    def project(self, tmp_path):
        proj = tmp_path / "proj"
        proj.mkdir()
        ab = proj / ".autobuilder"
        ab.mkdir()
        (ab / "initiatives").mkdir()
        (ab / "scope.md").write_text("# Scope\n")
        (ab / "constraints.md").write_text("")
        (ab / "backlog.md").write_text("")
        (ab / "status.md").write_text("")
        return proj

    def test_post_push_failure_stops_iteration(self, project):
        log_fh = io.StringIO()
        backlog_path = project / ".autobuilder" / "backlog.md"
        failed_result = HookResult(
            name="post-push", exit_code=1,
            stdout="Deploy failed: build error", stderr="",
        )

        def hook_side_effect(proj_dir, name, fh, **kwargs):
            if name == "pre-push":
                return None
            if name == "post-push":
                return failed_result
            return None

        with contextlib.ExitStack() as stack:
            stack.enter_context(patch("iteration.has_unpushed_commits", return_value=True))
            stack.enter_context(patch("iteration.get_current_branch", return_value="main"))
            stack.enter_context(patch("iteration.push_commits", return_value=True))
            mock_hook = stack.enter_context(patch("iteration.run_hook"))
            mock_status = stack.enter_context(patch("iteration.write_status"))
            stack.enter_context(patch("iteration.build_hook_env", return_value={}))

            mock_hook.side_effect = hook_side_effect
            _call_run_iteration(project, log_fh)

        backlog_text = backlog_path.read_text()
        assert "Deploy failed: build error" in backlog_text
        assert "deploy-failed" in str(mock_status.call_args_list)

    def test_post_push_success_continues(self, project):
        from iteration import NothingToImprove

        log_fh = io.StringIO()
        ok_result = HookResult(
            name="post-push", exit_code=0,
            stdout="deployed ok", stderr="",
        )

        def hook_side_effect(proj_dir, name, fh, **kwargs):
            if name == "post-push":
                return ok_result
            return None

        with contextlib.ExitStack() as stack:
            stack.enter_context(patch("iteration.has_unpushed_commits", return_value=True))
            stack.enter_context(patch("iteration.get_current_branch", return_value="main"))
            stack.enter_context(patch("iteration.push_commits", return_value=True))
            mock_hook = stack.enter_context(patch("iteration.run_hook"))
            stack.enter_context(patch("iteration.build_hook_env", return_value={}))
            for target, kw in _common_patches():
                stack.enter_context(patch(target, **kw))

            mock_hook.side_effect = hook_side_effect

            with pytest.raises(NothingToImprove):
                _call_run_iteration(project, log_fh)

    def test_post_push_absent_continues(self, project):
        from iteration import NothingToImprove

        log_fh = io.StringIO()

        with contextlib.ExitStack() as stack:
            stack.enter_context(patch("iteration.has_unpushed_commits", return_value=True))
            stack.enter_context(patch("iteration.get_current_branch", return_value="main"))
            stack.enter_context(patch("iteration.push_commits", return_value=True))
            stack.enter_context(patch("iteration.run_hook", return_value=None))
            stack.enter_context(patch("iteration.build_hook_env", return_value={}))
            for target, kw in _common_patches():
                stack.enter_context(patch(target, **kw))

            with pytest.raises(NothingToImprove):
                _call_run_iteration(project, log_fh)
