"""Steps for merge_conflict_abort.feature."""

from __future__ import annotations

import os
import shutil
import subprocess
import tempfile
from io import StringIO
from pathlib import Path
from unittest.mock import MagicMock, patch

from behave import given, then, when


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=10,
    )


# ── Shared setup steps ─────────────────────────────────


@given('a temp git project with a file "{filename}" for mca')
def step_create_project(context: object, filename: str) -> None:
    d = tempfile.mkdtemp(prefix="mca-")
    context.add_cleanup(shutil.rmtree, d, True)
    _git(["init", "-q", "-b", "main"], d)
    _git(["config", "user.name", "T"], d)
    _git(["config", "user.email", "t@t"], d)
    _git(["config", "commit.gpgsign", "false"], d)
    Path(d, filename).write_text("original content\n")
    _git(["add", "."], d)
    _git(["commit", "-q", "-m", "init"], d)
    context.mca_project = d
    context.mca_plan_id = "01TEST00000000000000CONFLICT"
    context.mca_branch = f"cleveragents/plan-{context.mca_plan_id}"


@given('a worktree branch with a conflicting change to "{filename}" for mca')
def step_create_worktree_branch(context: object, filename: str) -> None:
    repo = context.mca_project
    branch = context.mca_branch
    _git(["checkout", "-b", branch], repo)
    Path(repo, filename).write_text("branch change\n")
    _git(["add", "."], repo)
    _git(["commit", "-q", "-m", "branch edit"], repo)
    _git(["checkout", "main"], repo)


@given('the user commits a different change to "{filename}" on main for mca')
def step_user_edits_main(context: object, filename: str) -> None:
    repo = context.mca_project
    Path(repo, filename).write_text("user change\n")
    _git(["add", "."], repo)
    _git(["commit", "-q", "-m", "user edit"], repo)


@given("a worktree branch with a non-conflicting change for mca")
def step_create_non_conflicting_branch(context: object) -> None:
    repo = context.mca_project
    branch = context.mca_branch
    _git(["checkout", "-b", branch], repo)
    Path(repo, "new_file.py").write_text("# new file\n")
    _git(["add", "."], repo)
    _git(["commit", "-q", "-m", "add new file"], repo)
    _git(["checkout", "main"], repo)


# ── Helper: build mocks for _apply_sandbox_changes ─────


def _build_apply_mocks(
    context: object,
    repo_path: str,
    plan_id: str,
    branch_name: str,
) -> tuple[MagicMock, MagicMock]:
    """Build mock service + container for _apply_sandbox_changes."""
    mock_resource = MagicMock()
    mock_resource.resource_type_name = "git-checkout"
    mock_resource.location = repo_path
    mock_resource.resource_id = "res-mca-test"

    mock_lr = MagicMock()
    mock_lr.resource_id = "res-mca-test"

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

    mock_plan = MagicMock()
    mock_plan.project_links = [MagicMock(project_name="local/mca-test")]

    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

    return mock_service, mock_container


def _call_apply_sandbox(
    context: object,
    mock_service: MagicMock,
    mock_container: MagicMock,
) -> bool:
    """Call _apply_sandbox_changes with mocked dependencies."""
    from rich.console import Console

    from cleveragents.cli.commands.plan import _apply_sandbox_changes

    output = StringIO()
    console = Console(file=output, width=200)

    with patch(
        "cleveragents.application.container.get_container",
        return_value=mock_container,
    ):
        result = _apply_sandbox_changes(
            context.mca_plan_id,
            mock_service,
            console,
        )

    context.mca_apply_result = result
    context.mca_console_output = output.getvalue()
    return result


# ── Raw git merge steps (scenarios 1-2) ────────────────


@when("I attempt to merge the worktree branch for mca")
def step_attempt_merge(context: object) -> None:
    repo = context.mca_project
    branch = context.mca_branch
    result = subprocess.run(
        [
            "git",
            "-c",
            "commit.gpgsign=false",
            "merge",
            branch,
            "--no-edit",
            "-m",
            "test merge",
        ],
        cwd=repo,
        capture_output=True,
        text=True,
        check=False,
        timeout=10,
    )
    context.mca_merge_rc = result.returncode

    if result.returncode != 0:
        abort_result = subprocess.run(
            ["git", "merge", "--abort"],
            cwd=repo,
            capture_output=True,
            check=False,
            timeout=10,
        )
        context.mca_abort_rc = abort_result.returncode
    else:
        context.mca_abort_rc = None


@when("I attempt to merge the worktree branch and the abort fails for mca")
def step_attempt_merge_abort_fails(context: object) -> None:
    repo = context.mca_project
    branch = context.mca_branch
    result = subprocess.run(
        [
            "git",
            "-c",
            "commit.gpgsign=false",
            "merge",
            branch,
            "--no-edit",
            "-m",
            "test merge",
        ],
        cwd=repo,
        capture_output=True,
        text=True,
        check=False,
        timeout=10,
    )
    context.mca_merge_rc = result.returncode

    if result.returncode != 0:
        subprocess.run(
            ["git", "merge", "--abort"],
            cwd=repo,
            capture_output=True,
            check=False,
            timeout=10,
        )
        abort_result = subprocess.run(
            ["git", "merge", "--abort"],
            cwd=repo,
            capture_output=True,
            check=False,
            timeout=10,
        )
        context.mca_abort_rc = abort_result.returncode
    else:
        context.mca_abort_rc = 0


# ── _apply_sandbox_changes integration steps (scenarios 3-4) ──


@when("I call _apply_sandbox_changes with the conflicting project for mca")
def step_call_apply_conflict(context: object) -> None:
    mock_service, mock_container = _build_apply_mocks(
        context,
        context.mca_project,
        context.mca_plan_id,
        context.mca_branch,
    )
    _call_apply_sandbox(context, mock_service, mock_container)


@when("I call _apply_sandbox_changes with the clean project for mca")
def step_call_apply_clean(context: object) -> None:
    mock_service, mock_container = _build_apply_mocks(
        context,
        context.mca_project,
        context.mca_plan_id,
        context.mca_branch,
    )
    _call_apply_sandbox(context, mock_service, mock_container)


# ── Timeout mock steps (scenarios 5-6) ─────────────────


@given("a mock subprocess that raises TimeoutExpired on merge for mca")
def step_mock_merge_timeout(context: object) -> None:
    d = tempfile.mkdtemp(prefix="mca-timeout-")
    context.add_cleanup(shutil.rmtree, d, True)
    _git(["init", "-q", "-b", "main"], d)
    _git(["config", "user.name", "T"], d)
    _git(["config", "user.email", "t@t"], d)
    _git(["config", "commit.gpgsign", "false"], d)
    Path(d, "f.py").write_text("x\n")
    _git(["add", "."], d)
    _git(["commit", "-q", "-m", "init"], d)
    # Create the branch so rev-parse finds it
    _git(["checkout", "-b", "cleveragents/plan-01TESTTIMEOUT0000000000000"], d)
    Path(d, "f.py").write_text("changed\n")
    _git(["add", "."], d)
    _git(["commit", "-q", "-m", "change"], d)
    _git(["checkout", "main"], d)
    context.mca_project = d
    context.mca_plan_id = "01TESTTIMEOUT0000000000000"
    context.mca_branch = "cleveragents/plan-01TESTTIMEOUT0000000000000"
    context.mca_timeout_target = "merge"


@given("a mock subprocess that raises TimeoutExpired on abort for mca")
def step_mock_abort_timeout(context: object) -> None:
    d = tempfile.mkdtemp(prefix="mca-timeout-")
    context.add_cleanup(shutil.rmtree, d, True)
    _git(["init", "-q", "-b", "main"], d)
    _git(["config", "user.name", "T"], d)
    _git(["config", "user.email", "t@t"], d)
    _git(["config", "commit.gpgsign", "false"], d)
    Path(d, "f.py").write_text("original\n")
    _git(["add", "."], d)
    _git(["commit", "-q", "-m", "init"], d)
    # Create conflicting branch
    _git(["checkout", "-b", "cleveragents/plan-01TESTABORTTIMEOUT000000000"], d)
    Path(d, "f.py").write_text("branch\n")
    _git(["add", "."], d)
    _git(["commit", "-q", "-m", "branch"], d)
    _git(["checkout", "main"], d)
    Path(d, "f.py").write_text("main\n")
    _git(["add", "."], d)
    _git(["commit", "-q", "-m", "main"], d)
    context.mca_project = d
    context.mca_plan_id = "01TESTABORTTIMEOUT000000000"
    context.mca_branch = "cleveragents/plan-01TESTABORTTIMEOUT000000000"
    context.mca_timeout_target = "abort"


@when("I call _apply_sandbox_changes with the mocked merge for mca")
def step_call_apply_merge_timeout(context: object) -> None:
    mock_service, mock_container = _build_apply_mocks(
        context,
        context.mca_project,
        context.mca_plan_id,
        context.mca_branch,
    )

    original_run = subprocess.run

    def _timeout_on_merge(*args: object, **kwargs: object) -> object:
        cmd = args[0] if args else kwargs.get("args", [])
        if isinstance(cmd, list) and "merge" in cmd and "--abort" not in cmd:
            raise subprocess.TimeoutExpired(cmd, 30)
        return original_run(*args, **kwargs)

    with patch("subprocess.run", side_effect=_timeout_on_merge):
        _call_apply_sandbox(context, mock_service, mock_container)


@when("I call _apply_sandbox_changes with the mocked abort for mca")
def step_call_apply_abort_timeout(context: object) -> None:
    mock_service, mock_container = _build_apply_mocks(
        context,
        context.mca_project,
        context.mca_plan_id,
        context.mca_branch,
    )

    original_run = subprocess.run
    merge_done = {"value": False}

    def _timeout_on_abort(*args: object, **kwargs: object) -> object:
        cmd = args[0] if args else kwargs.get("args", [])
        if isinstance(cmd, list) and "merge" in cmd:
            if "--abort" in cmd:
                raise subprocess.TimeoutExpired(cmd, 10)
            # Let the merge fail with conflict (use original)
            merge_done["value"] = True
            return original_run(*args, **kwargs)
        return original_run(*args, **kwargs)

    with patch("subprocess.run", side_effect=_timeout_on_abort):
        _call_apply_sandbox(context, mock_service, mock_container)


# ── Flat file copy failure step (scenario 7) ───────────


@given("a temp sandbox with a file that cannot be copied for mca")
def step_create_failing_sandbox(context: object) -> None:
    d = tempfile.mkdtemp(prefix="mca-flat-")
    context.add_cleanup(shutil.rmtree, d, True)
    plan_id = "01TESTFLATFAIL00000000000000"
    sandbox = os.path.join(d, "plan-output", plan_id)
    os.makedirs(sandbox)
    Path(sandbox, "output.py").write_text("# generated\n")
    # Create a read-only destination directory to cause copy failure
    dst_dir = os.path.join(d, "readonly_dir")
    os.makedirs(dst_dir)
    Path(dst_dir, "output.py").write_text("# original\n")
    os.chmod(dst_dir, 0o444)
    context.add_cleanup(os.chmod, dst_dir, 0o755)
    context.mca_flat_project = d
    context.mca_plan_id = plan_id


@when("I call _apply_sandbox_changes with the failing flat copy for mca")
def step_call_apply_flat_fail(context: object) -> None:
    from rich.console import Console

    from cleveragents.cli.commands.plan import _apply_sandbox_changes

    # Mock service with no git resources (forces flat copy path)
    mock_plan = MagicMock()
    mock_plan.project_links = []
    mock_service = MagicMock()
    mock_service.get_plan.return_value = mock_plan

    mock_container = MagicMock()

    output = StringIO()
    console = Console(file=output, width=200)

    # Patch os.getcwd to return our test dir (flat copy uses cwd)
    with (
        patch(
            "cleveragents.application.container.get_container",
            return_value=mock_container,
        ),
        patch(
            "cleveragents.cli.commands.plan.os.getcwd",
            return_value=context.mca_flat_project,
        ),
        patch(
            "cleveragents.cli.commands.plan.shutil.copy2",
            side_effect=OSError("Permission denied"),
        ),
    ):
        result = _apply_sandbox_changes(
            context.mca_plan_id,
            mock_service,
            console,
        )

    context.mca_apply_result = result
    context.mca_console_output = output.getvalue()


# ── Then assertions ────────────────────────────────────


@then("the merge should fail for mca")
def step_merge_failed(context: object) -> None:
    assert context.mca_merge_rc != 0, (
        f"Expected merge to fail but got rc={context.mca_merge_rc}"
    )


@then("the merge should be aborted for mca")
def step_merge_aborted(context: object) -> None:
    assert context.mca_abort_rc == 0, (
        f"Expected merge abort to succeed but got rc={context.mca_abort_rc}"
    )


@then("the abort failure should be reported for mca")
def step_abort_failure_reported(context: object) -> None:
    assert context.mca_abort_rc != 0, (
        f"Expected abort to fail but got rc={context.mca_abort_rc}"
    )


@then('"{filename}" should not contain conflict markers for mca')
def step_no_conflict_markers(context: object, filename: str) -> None:
    content = Path(context.mca_project, filename).read_text()
    for marker in ("<<<<<<<", "=======", ">>>>>>>"):
        assert marker not in content, f"Found conflict marker '{marker}' in {filename}"


@then("git status should be clean for mca")
def step_git_clean(context: object) -> None:
    result = subprocess.run(
        ["git", "status", "--porcelain"],
        cwd=context.mca_project,
        capture_output=True,
        text=True,
        check=True,
        timeout=10,
    )
    assert result.stdout.strip() == "", (
        f"Expected clean git status but got:\n{result.stdout}"
    )


@then("_apply_sandbox_changes should return False for mca")
def step_apply_returns_false(context: object) -> None:
    assert context.mca_apply_result is False, (
        f"Expected False but got {context.mca_apply_result}"
    )


@then("_apply_sandbox_changes should return True for mca")
def step_apply_returns_true(context: object) -> None:
    assert context.mca_apply_result is True, (
        f"Expected True but got {context.mca_apply_result}"
    )


@then("the timeout error message should be displayed for mca")
def step_timeout_message(context: object) -> None:
    output = context.mca_console_output
    assert "timed out" in output.lower(), (
        f"Expected timeout message in output:\n{output}"
    )


@then("the abort timeout message should be displayed for mca")
def step_abort_timeout_message(context: object) -> None:
    output = context.mca_console_output
    assert "timed out" in output.lower(), (
        f"Expected abort timeout message in output:\n{output}"
    )
