"""Steps for tdd_cleanup_stale_destroys_execute_output.feature.

TDD issue-capture test for bug #11121:
_create_sandbox_for_plan() calls GitWorktreeSandbox.cleanup_stale() unconditionally,
destroying the cleveragents/plan-<id> branch when the plan is already in
execute/complete or execute/processing state (awaiting apply or still in progress).

The scenarios are tagged @tdd_issue_11121 as permanent regression guards.
"""

from __future__ import annotations

import contextlib
import shutil
import subprocess
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch

from behave import given, then, when
from behave.runner import Context

_PLAN_ID = "01TDDSANDBOX000000000000A"
_BRANCH_NAME = f"cleveragents/plan-{_PLAN_ID}"


def _git(args: list[str], cwd: str) -> subprocess.CompletedProcess[str]:
    return subprocess.run(
        ["git", *args],
        cwd=cwd,
        capture_output=True,
        text=True,
        check=True,
        timeout=15,
    )


def _branch_exists(repo_path: str, branch: str) -> bool:
    """Return True if the given branch exists in the repo."""
    result = subprocess.run(
        ["git", "rev-parse", "--verify", f"refs/heads/{branch}"],
        cwd=repo_path,
        capture_output=True,
        text=True,
        check=False,
        timeout=10,
    )
    return result.returncode == 0


def _init_git_repo(path: str) -> None:
    _git(["init", "-q", "-b", "main"], path)
    _git(["config", "user.name", "TDD Test"], path)
    _git(["config", "user.email", "tdd@test.local"], path)
    _git(["config", "commit.gpgsign", "false"], path)


def _build_execute_complete_mocks(
    context: Context,
    repo_path: str,
) -> None:
    """Build mock service + container for a plan in execute/complete state."""
    from cleveragents.domain.models.core.plan import PlanPhase, ProcessingState

    mock_resource = MagicMock()
    mock_resource.resource_type_name = "git-checkout"
    mock_resource.location = repo_path
    mock_resource.resource_id = "res-tdd-11121-test"

    mock_lr = MagicMock()
    mock_lr.resource_id = "res-tdd-11121-test"

    mock_project = MagicMock()
    mock_project.linked_resources = [mock_lr]

    # Plan is in execute/complete state — this is the critical state for the bug
    mock_plan = MagicMock()
    mock_plan.project_links = [MagicMock(project_name="local/tdd-11121-project")]
    mock_plan.phase = PlanPhase.EXECUTE
    mock_plan.processing_state = ProcessingState.COMPLETE
    mock_plan.state = ProcessingState.COMPLETE

    mock_service = MagicMock()
    mock_service.get_plan.return_value = mock_plan

    mock_project_repo = MagicMock()
    mock_project_repo.get.return_value = mock_project

    mock_resource_registry = MagicMock()
    mock_resource_registry.show_resource.return_value = mock_resource

    mock_container = MagicMock()
    mock_container.namespaced_project_repo.return_value = mock_project_repo
    mock_container.resource_registry_service.return_value = mock_resource_registry

    context.tdd11121_service = mock_service
    context.tdd11121_container = mock_container
    context.tdd11121_repo_path = repo_path
    context.tdd11121_plan_id = _PLAN_ID
    context.tdd11121_branch = _BRANCH_NAME


@given("a temp git repo with an execute-output branch for tdd 11121")
def step_create_git_repo_with_execute_output(context: Context) -> None:
    """Create a real git repo with a cleveragents/plan-<id> branch holding execute output.

    This simulates the state after a successful plan execute: the worktree branch
    exists and contains at least one committed file representing execution output.
    """
    d = tempfile.mkdtemp(prefix="tdd-11121-")
    context.add_cleanup(shutil.rmtree, d, True)

    # Initialise the repo with a base commit on main
    _init_git_repo(d)
    Path(d, "README.md").write_text("# project\n")
    _git(["add", "."], d)
    _git(["commit", "-q", "-m", "init: base commit"], d)

    # Create the cleveragents/plan-<id> branch and commit an output file to it.
    # This simulates what _commit_worktree_changes() does after execute completes.
    _git(["checkout", "-q", "-b", _BRANCH_NAME], d)
    output_file = Path(d, "generated_output.py")
    output_file.write_text("# Generated by plan execute\nresult = 42\n")
    _git(["add", "."], d)
    _git(["commit", "-q", "-m", f"cleveragents: execute output for plan {_PLAN_ID}"], d)

    # Return to main so the repo is in a normal state
    _git(["checkout", "-q", "main"], d)

    context.tdd11121_repo_path = d
    # Verify the branch exists before the test
    assert _branch_exists(d, _BRANCH_NAME), (
        f"Pre-condition failed: branch {_BRANCH_NAME} should exist before test"
    )


def _build_execute_processing_mocks(
    context: Context,
    repo_path: str,
) -> None:
    """Build mock service + container for a plan in execute/processing state."""
    from cleveragents.domain.models.core.plan import PlanPhase, ProcessingState

    mock_resource = MagicMock()
    mock_resource.resource_type_name = "git-checkout"
    mock_resource.location = repo_path
    mock_resource.resource_id = "res-tdd-11121-test"

    mock_lr = MagicMock()
    mock_lr.resource_id = "res-tdd-11121-test"

    mock_project = MagicMock()
    mock_project.linked_resources = [mock_lr]

    mock_plan = MagicMock()
    mock_plan.project_links = [MagicMock(project_name="local/tdd-11121-project")]
    mock_plan.phase = PlanPhase.EXECUTE
    mock_plan.processing_state = ProcessingState.PROCESSING
    mock_plan.state = ProcessingState.PROCESSING

    mock_service = MagicMock()
    mock_service.get_plan.return_value = mock_plan

    mock_project_repo = MagicMock()
    mock_project_repo.get.return_value = mock_project

    mock_resource_registry = MagicMock()
    mock_resource_registry.show_resource.return_value = mock_resource

    mock_container = MagicMock()
    mock_container.namespaced_project_repo.return_value = mock_project_repo
    mock_container.resource_registry_service.return_value = mock_resource_registry

    context.tdd11121_service = mock_service
    context.tdd11121_container = mock_container
    context.tdd11121_repo_path = repo_path
    context.tdd11121_plan_id = _PLAN_ID
    context.tdd11121_branch = _BRANCH_NAME


@given("a mocked plan service with the plan in execute/complete state for tdd 11121")
def step_mock_service_execute_complete(context: Context) -> None:
    """Set up mock service returning a plan in execute/complete state."""
    _build_execute_complete_mocks(context, context.tdd11121_repo_path)


@given("a mocked plan service with the plan in execute/processing state for tdd 11121")
def step_mock_service_execute_processing(context: Context) -> None:
    """Set up mock service returning a plan in execute/processing state."""
    _build_execute_processing_mocks(context, context.tdd11121_repo_path)


@when(
    "I call _create_sandbox_for_plan a second time on the execute/complete plan for tdd 11121"
)
def step_call_create_sandbox_second_time(context: Context) -> None:
    """Call _create_sandbox_for_plan on a plan already in execute/complete state.

    This simulates the user re-running 'agents plan execute <PLAN_ID>' after
    execution has already completed. The bug causes cleanup_stale to run and
    destroy the cleveragents/plan-<id> branch that holds the execute output.
    """
    from cleveragents.cli.commands.plan import _create_sandbox_for_plan

    context.tdd11121_second_call_exception: Exception | None = None
    try:
        with (
            patch(
                "cleveragents.cli.commands.plan.get_container",
                return_value=context.tdd11121_container,
            ),
            patch(
                "cleveragents.application.container.get_container",
                return_value=context.tdd11121_container,
            ),
        ):
            _sandbox_root, sandbox_infos = _create_sandbox_for_plan(
                context.tdd11121_plan_id,
                context.tdd11121_service,
            )
            context.tdd11121_sandbox_infos = sandbox_infos
            # Clean up any newly created sandbox to avoid resource leaks
            for sinfo in sandbox_infos:
                with contextlib.suppress(Exception):
                    sinfo.sandbox_obj.cleanup()
    except Exception as exc:
        # Record but don't re-raise — we want to check branch state regardless
        context.tdd11121_second_call_exception = exc


@when(
    "I call _create_sandbox_for_plan a second time on the execute/processing plan for tdd 11121"
)
def step_call_create_sandbox_second_time_processing(context: Context) -> None:
    """Call _create_sandbox_for_plan on a plan already in execute/processing state.

    This simulates the user re-running 'agents plan execute <PLAN_ID>' while the
    plan is still in progress (execute/processing). The guard must protect the
    sandbox branch from cleanup_stale in this state too.
    """
    from cleveragents.cli.commands.plan import _create_sandbox_for_plan

    context.tdd11121_second_call_exception: Exception | None = None
    try:
        with (
            patch(
                "cleveragents.cli.commands.plan.get_container",
                return_value=context.tdd11121_container,
            ),
            patch(
                "cleveragents.application.container.get_container",
                return_value=context.tdd11121_container,
            ),
        ):
            _sandbox_root, sandbox_infos = _create_sandbox_for_plan(
                context.tdd11121_plan_id,
                context.tdd11121_service,
            )
            context.tdd11121_sandbox_infos = sandbox_infos
            for sinfo in sandbox_infos:
                with contextlib.suppress(Exception):
                    sinfo.sandbox_obj.cleanup()
    except Exception as exc:
        context.tdd11121_second_call_exception = exc


@then(
    "the cleveragents plan branch should still exist after the second call for tdd 11121"
)
def step_branch_still_exists(context: Context) -> None:
    """Assert the cleveragents/plan-<id> branch was NOT destroyed by cleanup_stale.

    The guard in _create_sandbox_for_plan skips cleanup_stale when the plan is
    already in execute/processing or execute/complete state, preserving the
    sandbox branch so plan apply can merge it.
    """
    repo_path = context.tdd11121_repo_path
    branch = context.tdd11121_branch
    assert _branch_exists(repo_path, branch), (
        f"Bug #11121: branch '{branch}' was destroyed by cleanup_stale during "
        f"a second _create_sandbox_for_plan call. "
        f"The branch must survive until plan apply merges it."
    )


@then("the apply sandbox changes should find at least one artifact for tdd 11121")
def step_apply_finds_artifacts(context: Context) -> None:
    """Assert that apply can find the execute output after a re-invoked execute.

    The guard in _create_sandbox_for_plan preserves the sandbox branch, so the
    git diff between HEAD and the plan branch finds at least one artifact.
    """
    repo_path = context.tdd11121_repo_path
    branch = context.tdd11121_branch

    if not _branch_exists(repo_path, branch):
        raise AssertionError(
            f"Bug #11121: branch '{branch}' was destroyed by cleanup_stale. "
            f"plan apply would find 0 artifacts (empty changeset). "
            f"The branch must persist until apply merges it."
        )

    result = subprocess.run(
        ["git", "diff", "--stat", f"HEAD...{branch}"],
        cwd=repo_path,
        capture_output=True,
        text=True,
        check=False,
        timeout=10,
    )
    stat_lines = (result.stdout or "").strip().splitlines()
    artifact_count = max(0, len(stat_lines) - 1) if stat_lines else 0

    assert artifact_count > 0, (
        f"Bug #11121: plan apply would find {artifact_count} artifacts after "
        f"re-invoked execute on execute/complete plan. Expected >= 1 artifact. "
        f"diff --stat output: {result.stdout!r}"
    )
