"""Step definitions for git worktree sandbox coverage boost.

All steps use the ``gwtcb`` prefix to avoid collisions with existing steps.
These scenarios target uncovered lines and branches in git_worktree.py.
"""

from __future__ import annotations

import os
import subprocess
import tempfile
from unittest.mock import MagicMock, patch

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

from cleveragents.infrastructure.sandbox.git_worktree import (
    GitWorktreeSandbox,
    _sanitise_branch_name,
)
from cleveragents.infrastructure.sandbox.protocol import (
    SandboxCommitError,
    SandboxCreationError,
    SandboxRollbackError,
    SandboxStateError,
    SandboxStatus,
)

_MODULE = "cleveragents.infrastructure.sandbox.git_worktree"


def _make_sandbox(original_path: str) -> GitWorktreeSandbox:
    """Helper: create a GitWorktreeSandbox with sensible defaults."""
    return GitWorktreeSandbox(
        resource_id="res-cov-001",
        original_path=original_path,
        git_timeout=10,
    )


def _init_test_repo() -> str:
    """Create a temporary git repo with an initial commit."""
    repo_dir = tempfile.mkdtemp(prefix="gwtcb-test-repo-")
    subprocess.run(["git", "init"], cwd=repo_dir, capture_output=True, check=True)
    subprocess.run(
        ["git", "config", "user.email", "test@test.com"],
        cwd=repo_dir,
        capture_output=True,
        check=True,
    )
    subprocess.run(
        ["git", "config", "user.name", "Test"],
        cwd=repo_dir,
        capture_output=True,
        check=True,
    )
    subprocess.run(
        ["git", "config", "commit.gpgSign", "false"],
        cwd=repo_dir,
        capture_output=True,
        check=True,
    )
    readme = os.path.join(repo_dir, "README.md")
    with open(readme, "w") as f:
        f.write("# Test Repo\n")
    subprocess.run(["git", "add", "."], cwd=repo_dir, capture_output=True, check=True)
    subprocess.run(
        ["git", "commit", "-m", "Initial commit"],
        cwd=repo_dir,
        capture_output=True,
        check=True,
    )
    return repo_dir


# ---------------------------------------------------------------------------
# _sanitise_branch_name
# ---------------------------------------------------------------------------


@when('gwtcb _sanitise_branch_name is called with "{raw}"')
def step_gwtcb_sanitise(ctx: Context, raw: str) -> None:
    ctx.gwtcb_result = _sanitise_branch_name(raw)
    ctx.gwtcb_error = None


@then('gwtcb the result should be "{expected}"')
def step_gwtcb_sanitise_result(ctx: Context, expected: str) -> None:
    assert ctx.gwtcb_result == expected, (
        f"Expected '{expected}', got '{ctx.gwtcb_result}'"
    )


# ---------------------------------------------------------------------------
# create: path is not repo root
# ---------------------------------------------------------------------------


@given("a gwtcb sandbox pointed at a non-root subdirectory")
def step_gwtcb_non_root_subdir(ctx: Context) -> None:
    repo_dir = _init_test_repo()
    # Create a subdirectory inside the repo
    subdir = os.path.join(repo_dir, "subdir")
    os.makedirs(subdir, exist_ok=True)
    ctx.gwtcb_sandbox = _make_sandbox(subdir)
    ctx.gwtcb_error = None


@when('gwtcb create is called for plan "{plan_id}"')
def step_gwtcb_create(ctx: Context, plan_id: str) -> None:
    try:
        ctx.gwtcb_sandbox.create(plan_id)
    except (SandboxCreationError, SandboxStateError, ValueError) as exc:
        ctx.gwtcb_error = exc


# ---------------------------------------------------------------------------
# create: timeout
# ---------------------------------------------------------------------------


@given("a gwtcb sandbox with mocked _run_git that times out")
def step_gwtcb_create_timeout_setup(ctx: Context) -> None:
    repo_dir = _init_test_repo()
    ctx.gwtcb_sandbox = _make_sandbox(repo_dir)
    ctx.gwtcb_error = None

    # Patch _run_git to raise TimeoutExpired on first call
    patcher = patch(
        f"{_MODULE}._run_git",
        side_effect=subprocess.TimeoutExpired(cmd="git", timeout=10),
    )
    ctx.gwtcb_patcher = patcher
    patcher.start()


@when("gwtcb create is called expecting a timeout error")
def step_gwtcb_create_timeout(ctx: Context) -> None:
    try:
        ctx.gwtcb_sandbox.create("plan-timeout")
    except SandboxCreationError as exc:
        ctx.gwtcb_error = exc
    finally:
        if hasattr(ctx, "gwtcb_patcher"):
            ctx.gwtcb_patcher.stop()


# ---------------------------------------------------------------------------
# get_path: worktree_path is None
# ---------------------------------------------------------------------------


@given("a gwtcb sandbox in CREATED state with None worktree_path")
def step_gwtcb_created_none_path(ctx: Context) -> None:
    repo_dir = _init_test_repo()
    ctx.gwtcb_sandbox = _make_sandbox(repo_dir)
    ctx.gwtcb_error = None
    # Force internal state: CREATED with worktree_path = None
    ctx.gwtcb_sandbox._status = SandboxStatus.CREATED
    ctx.gwtcb_sandbox._worktree_path = None


@when('gwtcb get_path is called with "{path}"')
def step_gwtcb_get_path(ctx: Context, path: str) -> None:
    try:
        ctx.gwtcb_sandbox.get_path(path)
    except SandboxStateError as exc:
        ctx.gwtcb_error = exc


# ---------------------------------------------------------------------------
# commit: worktree not initialised
# ---------------------------------------------------------------------------


@given("a gwtcb sandbox in ACTIVE state with no worktree or branch")
def step_gwtcb_active_no_worktree(ctx: Context) -> None:
    repo_dir = _init_test_repo()
    ctx.gwtcb_sandbox = _make_sandbox(repo_dir)
    ctx.gwtcb_error = None
    # Force ACTIVE with None worktree and branch to hit line 344-345
    ctx.gwtcb_sandbox._status = SandboxStatus.ACTIVE
    ctx.gwtcb_sandbox._worktree_path = None
    ctx.gwtcb_sandbox._branch_name = None


@when('gwtcb commit is called with message "{message}"')
def step_gwtcb_commit(ctx: Context, message: str) -> None:
    try:
        ctx.gwtcb_commit_result = ctx.gwtcb_sandbox.commit(message)
    except (SandboxStateError, SandboxCommitError) as exc:
        ctx.gwtcb_error = exc


# ---------------------------------------------------------------------------
# commit: modified files in diff output
# ---------------------------------------------------------------------------


@given("a gwtcb sandbox ready to commit with modified-file diff output")
def step_gwtcb_commit_modified_diff(ctx: Context) -> None:
    repo_dir = _init_test_repo()
    ctx.gwtcb_sandbox = _make_sandbox(repo_dir)
    ctx.gwtcb_error = None
    ctx.gwtcb_commit_result = None

    # Set sandbox to ACTIVE with valid worktree/branch
    ctx.gwtcb_sandbox._status = SandboxStatus.ACTIVE
    ctx.gwtcb_sandbox._worktree_path = repo_dir
    ctx.gwtcb_sandbox._branch_name = "cleveragents/plan-test"
    ctx.gwtcb_sandbox._base_commit = "abc123"
    ctx.gwtcb_sandbox._original_branch = "main"

    # Prepare the diff output including M (modified), A (added), D (deleted)
    diff_output = "M\tsrc/modified.py\nA\tsrc/new.py\nD\tsrc/old.py\n"

    # Mock _run_git for add, commit, rev-parse, merge
    def mock_run_git(args, cwd, timeout=10):
        result = MagicMock()
        result.stdout = "abc456\n"
        result.stderr = ""
        return result

    ctx.gwtcb_run_git_patcher = patch(f"{_MODULE}._run_git", side_effect=mock_run_git)
    ctx.gwtcb_run_git_patcher.start()

    # Mock subprocess.run for diff --cached --name-status
    real_subprocess_run = subprocess.run

    def mock_subprocess_run(cmd, **kwargs):
        if isinstance(cmd, list) and "diff" in cmd and "--name-status" in cmd:
            result = MagicMock()
            result.stdout = diff_output
            result.stderr = ""
            result.returncode = 0
            return result
        return real_subprocess_run(cmd, **kwargs)

    ctx.gwtcb_subprocess_patcher = patch(
        "subprocess.run", side_effect=mock_subprocess_run
    )
    ctx.gwtcb_subprocess_patcher.start()


@then('the gwtcb commit result should include changed file "{filename}"')
def step_gwtcb_commit_changed_file(ctx: Context, filename: str) -> None:
    if hasattr(ctx, "gwtcb_run_git_patcher"):
        ctx.gwtcb_run_git_patcher.stop()
    if hasattr(ctx, "gwtcb_subprocess_patcher"):
        ctx.gwtcb_subprocess_patcher.stop()
    assert ctx.gwtcb_commit_result is not None, "Expected a commit result"
    assert filename in ctx.gwtcb_commit_result.changed_files, (
        f"Expected '{filename}' in changed_files, got {ctx.gwtcb_commit_result.changed_files}"
    )


@then('the gwtcb commit result should include added file "{filename}"')
def step_gwtcb_commit_added_file(ctx: Context, filename: str) -> None:
    assert ctx.gwtcb_commit_result is not None, "Expected a commit result"
    assert filename in ctx.gwtcb_commit_result.added_files, (
        f"Expected '{filename}' in added_files, got {ctx.gwtcb_commit_result.added_files}"
    )


@then('the gwtcb commit result should include deleted file "{filename}"')
def step_gwtcb_commit_deleted_file(ctx: Context, filename: str) -> None:
    assert ctx.gwtcb_commit_result is not None, "Expected a commit result"
    assert filename in ctx.gwtcb_commit_result.deleted_files, (
        f"Expected '{filename}' in deleted_files, got {ctx.gwtcb_commit_result.deleted_files}"
    )


# ---------------------------------------------------------------------------
# commit: timeout and CalledProcessError
# ---------------------------------------------------------------------------


@given("a gwtcb sandbox in ACTIVE state ready to commit")
def step_gwtcb_active_ready_commit(ctx: Context) -> None:
    repo_dir = _init_test_repo()
    ctx.gwtcb_sandbox = _make_sandbox(repo_dir)
    ctx.gwtcb_error = None
    ctx.gwtcb_commit_result = None

    ctx.gwtcb_sandbox._status = SandboxStatus.ACTIVE
    ctx.gwtcb_sandbox._worktree_path = repo_dir
    ctx.gwtcb_sandbox._branch_name = "cleveragents/plan-test"
    ctx.gwtcb_sandbox._base_commit = "abc123"
    ctx.gwtcb_sandbox._original_branch = "main"


@given("gwtcb _run_git is mocked to raise TimeoutExpired on commit")
def step_gwtcb_mock_commit_timeout(ctx: Context) -> None:
    patcher = patch(
        f"{_MODULE}._run_git",
        side_effect=subprocess.TimeoutExpired(cmd="git", timeout=10),
    )
    ctx.gwtcb_patcher = patcher
    patcher.start()


@when("gwtcb commit is called expecting a timeout error")
def step_gwtcb_commit_timeout(ctx: Context) -> None:
    try:
        ctx.gwtcb_commit_result = ctx.gwtcb_sandbox.commit("timeout commit")
    except SandboxCommitError as exc:
        ctx.gwtcb_error = exc
    finally:
        if hasattr(ctx, "gwtcb_patcher"):
            ctx.gwtcb_patcher.stop()


@given("gwtcb _run_git is mocked to raise CalledProcessError on commit")
def step_gwtcb_mock_commit_process_error(ctx: Context) -> None:
    patcher = patch(
        f"{_MODULE}._run_git",
        side_effect=subprocess.CalledProcessError(
            returncode=1,
            cmd="git commit",
            stderr="commit failed",
        ),
    )
    ctx.gwtcb_patcher = patcher
    patcher.start()


@when("gwtcb commit is called expecting a process error")
def step_gwtcb_commit_process_error(ctx: Context) -> None:
    try:
        ctx.gwtcb_commit_result = ctx.gwtcb_sandbox.commit("error commit")
    except SandboxCommitError as exc:
        ctx.gwtcb_error = exc
    finally:
        if hasattr(ctx, "gwtcb_patcher"):
            ctx.gwtcb_patcher.stop()


# ---------------------------------------------------------------------------
# rollback: worktree not initialised
# ---------------------------------------------------------------------------


@given("a gwtcb sandbox in ACTIVE state with no worktree or base commit")
def step_gwtcb_active_no_worktree_rollback(ctx: Context) -> None:
    repo_dir = _init_test_repo()
    ctx.gwtcb_sandbox = _make_sandbox(repo_dir)
    ctx.gwtcb_error = None
    # Force ACTIVE with None worktree and base_commit to hit line 472-473
    ctx.gwtcb_sandbox._status = SandboxStatus.ACTIVE
    ctx.gwtcb_sandbox._worktree_path = None
    ctx.gwtcb_sandbox._base_commit = None


@when("gwtcb rollback is called expecting a state error")
def step_gwtcb_rollback_state_error(ctx: Context) -> None:
    try:
        ctx.gwtcb_sandbox.rollback()
    except SandboxStateError as exc:
        ctx.gwtcb_error = exc


# ---------------------------------------------------------------------------
# rollback: timeout and CalledProcessError
# ---------------------------------------------------------------------------


@given("a gwtcb sandbox in ACTIVE state ready to rollback")
def step_gwtcb_active_ready_rollback(ctx: Context) -> None:
    repo_dir = _init_test_repo()
    ctx.gwtcb_sandbox = _make_sandbox(repo_dir)
    ctx.gwtcb_error = None

    ctx.gwtcb_sandbox._status = SandboxStatus.ACTIVE
    ctx.gwtcb_sandbox._worktree_path = repo_dir
    ctx.gwtcb_sandbox._base_commit = "abc123"


@given("gwtcb _run_git is mocked to raise TimeoutExpired on rollback")
def step_gwtcb_mock_rollback_timeout(ctx: Context) -> None:
    patcher = patch(
        f"{_MODULE}._run_git",
        side_effect=subprocess.TimeoutExpired(cmd="git", timeout=10),
    )
    ctx.gwtcb_patcher = patcher
    patcher.start()


@when("gwtcb rollback is called expecting a timeout error")
def step_gwtcb_rollback_timeout(ctx: Context) -> None:
    try:
        ctx.gwtcb_sandbox.rollback()
    except SandboxRollbackError as exc:
        ctx.gwtcb_error = exc
    finally:
        if hasattr(ctx, "gwtcb_patcher"):
            ctx.gwtcb_patcher.stop()


@given("gwtcb _run_git is mocked to raise CalledProcessError on rollback")
def step_gwtcb_mock_rollback_process_error(ctx: Context) -> None:
    patcher = patch(
        f"{_MODULE}._run_git",
        side_effect=subprocess.CalledProcessError(
            returncode=1,
            cmd="git reset",
            stderr="rollback failed",
        ),
    )
    ctx.gwtcb_patcher = patcher
    patcher.start()


@when("gwtcb rollback is called expecting a process error")
def step_gwtcb_rollback_process_error(ctx: Context) -> None:
    try:
        ctx.gwtcb_sandbox.rollback()
    except SandboxRollbackError as exc:
        ctx.gwtcb_error = exc
    finally:
        if hasattr(ctx, "gwtcb_patcher"):
            ctx.gwtcb_patcher.stop()


# ---------------------------------------------------------------------------
# cleanup: worktree remove failure (fallback to shutil)
# ---------------------------------------------------------------------------


@given("a gwtcb sandbox with a worktree directory that exists")
def step_gwtcb_cleanup_worktree_exists(ctx: Context) -> None:
    repo_dir = _init_test_repo()
    ctx.gwtcb_sandbox = _make_sandbox(repo_dir)
    ctx.gwtcb_error = None

    # Create a real temp dir to act as the worktree path
    worktree_dir = tempfile.mkdtemp(prefix="gwtcb-worktree-")
    ctx.gwtcb_sandbox._status = SandboxStatus.ACTIVE
    ctx.gwtcb_sandbox._worktree_path = worktree_dir
    ctx.gwtcb_sandbox._branch_name = "cleveragents/plan-cleanup-test"


@given("gwtcb _run_git is mocked to fail on worktree remove")
def step_gwtcb_mock_worktree_remove_fail(ctx: Context) -> None:
    call_count = {"n": 0}

    def mock_run_git(args, cwd, timeout=10):
        call_count["n"] += 1
        if args and args[0] == "worktree" and "remove" in args:
            raise subprocess.CalledProcessError(
                returncode=1,
                cmd="git worktree remove",
                stderr="worktree remove failed",
            )
        # Let branch -D and worktree prune succeed
        result = MagicMock()
        result.stdout = ""
        result.stderr = ""
        return result

    patcher = patch(f"{_MODULE}._run_git", side_effect=mock_run_git)
    ctx.gwtcb_patcher = patcher
    patcher.start()


@when("gwtcb cleanup is called")
def step_gwtcb_cleanup(ctx: Context) -> None:
    try:
        ctx.gwtcb_sandbox.cleanup()
    except Exception as exc:
        ctx.gwtcb_error = exc
    finally:
        if hasattr(ctx, "gwtcb_patcher"):
            ctx.gwtcb_patcher.stop()


# ---------------------------------------------------------------------------
# cleanup: branch delete failure
# ---------------------------------------------------------------------------


@given("a gwtcb sandbox with a branch name set but no worktree directory")
def step_gwtcb_cleanup_no_worktree_dir(ctx: Context) -> None:
    repo_dir = _init_test_repo()
    ctx.gwtcb_sandbox = _make_sandbox(repo_dir)
    ctx.gwtcb_error = None

    # Set branch name but worktree_path to a non-existent path
    # so the worktree removal block is skipped
    ctx.gwtcb_sandbox._status = SandboxStatus.ACTIVE
    ctx.gwtcb_sandbox._worktree_path = "/tmp/nonexistent-gwtcb-worktree"
    ctx.gwtcb_sandbox._branch_name = "cleveragents/plan-branch-delete-fail"


@given("gwtcb _run_git is mocked to fail on branch delete")
def step_gwtcb_mock_branch_delete_fail(ctx: Context) -> None:

    def mock_run_git(args, cwd, timeout=10):
        if args and args[0] == "branch" and "-D" in args:
            raise subprocess.CalledProcessError(
                returncode=1,
                cmd="git branch -D",
                stderr="branch delete failed",
            )
        # Let worktree prune succeed
        result = MagicMock()
        result.stdout = ""
        result.stderr = ""
        return result

    patcher = patch(f"{_MODULE}._run_git", side_effect=mock_run_git)
    ctx.gwtcb_patcher = patcher
    patcher.start()


# ---------------------------------------------------------------------------
# Then - common assertions
# ---------------------------------------------------------------------------


@then('the gwtcb sandbox should be in the "{status}" state')
def step_gwtcb_check_status(ctx: Context, status: str) -> None:
    expected = SandboxStatus(status)
    assert ctx.gwtcb_sandbox.status == expected, (
        f"Expected status {expected}, got {ctx.gwtcb_sandbox.status}"
    )


@then("a gwtcb SandboxCreationError should be raised")
def step_gwtcb_creation_error(ctx: Context) -> None:
    assert ctx.gwtcb_error is not None, "Expected an error but none occurred"
    assert isinstance(ctx.gwtcb_error, SandboxCreationError), (
        f"Expected SandboxCreationError, got {type(ctx.gwtcb_error).__name__}: {ctx.gwtcb_error}"
    )


@then('a gwtcb SandboxCreationError should be raised with message "{msg}"')
def step_gwtcb_creation_error_msg(ctx: Context, msg: str) -> None:
    assert ctx.gwtcb_error is not None, "Expected an error but none occurred"
    assert isinstance(ctx.gwtcb_error, SandboxCreationError), (
        f"Expected SandboxCreationError, got {type(ctx.gwtcb_error).__name__}"
    )
    assert msg in str(ctx.gwtcb_error), (
        f"Expected '{msg}' in error message, got: {ctx.gwtcb_error}"
    )


@then('a gwtcb SandboxStateError should be raised with message "{msg}"')
def step_gwtcb_state_error_msg(ctx: Context, msg: str) -> None:
    assert ctx.gwtcb_error is not None, "Expected an error but none occurred"
    assert isinstance(ctx.gwtcb_error, SandboxStateError), (
        f"Expected SandboxStateError, got {type(ctx.gwtcb_error).__name__}: {ctx.gwtcb_error}"
    )
    assert msg in str(ctx.gwtcb_error), (
        f"Expected '{msg}' in error message, got: {ctx.gwtcb_error}"
    )


@then('a gwtcb SandboxCommitError should be raised with message "{msg}"')
def step_gwtcb_commit_error_msg(ctx: Context, msg: str) -> None:
    assert ctx.gwtcb_error is not None, "Expected an error but none occurred"
    assert isinstance(ctx.gwtcb_error, SandboxCommitError), (
        f"Expected SandboxCommitError, got {type(ctx.gwtcb_error).__name__}: {ctx.gwtcb_error}"
    )
    assert msg in str(ctx.gwtcb_error), (
        f"Expected '{msg}' in error message, got: {ctx.gwtcb_error}"
    )


@then('a gwtcb SandboxRollbackError should be raised with message "{msg}"')
def step_gwtcb_rollback_error_msg(ctx: Context, msg: str) -> None:
    assert ctx.gwtcb_error is not None, "Expected an error but none occurred"
    assert isinstance(ctx.gwtcb_error, SandboxRollbackError), (
        f"Expected SandboxRollbackError, got {type(ctx.gwtcb_error).__name__}: {ctx.gwtcb_error}"
    )
    assert msg in str(ctx.gwtcb_error), (
        f"Expected '{msg}' in error message, got: {ctx.gwtcb_error}"
    )
