"""Step definitions for CopyOnWriteSandbox uncovered lines and branches.

Targets every gap identified in the coverage report for
``copy_on_write.py`` (line-rate=0.9124, branch-rate=0.8519):

- Lines 143-145: ``create()`` OSError handler wrapping into SandboxCreationError
- Line 196 branch False→199: ``get_path()`` when already ACTIVE (skip CREATED transition)
- Line 200: ``get_path()`` raises when ``_sandbox_path`` is ``None``
- Line 230: ``commit()`` raises when ``_sandbox_path`` is ``None``
- Line 242 branch False→244: ``commit()`` skips ``os.makedirs`` when ``dst_dir`` is empty
- Line 249 branch False→247: ``commit()`` deleted file already missing from original
- Lines 252-254: ``commit()`` OSError handler wrapping into SandboxCommitError
- Line 297: ``rollback()`` raises when ``_sandbox_path`` is ``None``
- Lines 311-313: ``rollback()`` OSError handler wrapping into SandboxRollbackError
- Line 341 branch False→347: ``cleanup()`` when ``_sandbox_path`` is ``None``
- Line 344 branch False→347: ``cleanup()`` when parent dir already removed

All steps use the ``cowcov`` prefix to avoid collisions with existing
``cow`` and ``cowcb`` step prefixes.
"""

from __future__ import annotations

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

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

from cleveragents.infrastructure.sandbox.copy_on_write import CopyOnWriteSandbox
from cleveragents.infrastructure.sandbox.protocol import (
    SandboxCommitError,
    SandboxCreationError,
    SandboxRollbackError,
    SandboxStateError,
    SandboxStatus,
)

# ----------------------------------------------------------------------
# Helpers
# ----------------------------------------------------------------------


def _cowcov_make_test_dir() -> str:
    """Create a temporary directory with seed files for testing."""
    d = tempfile.mkdtemp(prefix="cowcov-test-dir-")
    with open(os.path.join(d, "existing.txt"), "w") as f:
        f.write("original content")
    with open(os.path.join(d, "to_delete.txt"), "w") as f:
        f.write("will be deleted")
    os.makedirs(os.path.join(d, "sub"), exist_ok=True)
    with open(os.path.join(d, "sub", "nested.txt"), "w") as f:
        f.write("nested")
    return d


def _cowcov_cleanup_dir(path: str) -> None:
    """Remove a directory tree if it still exists."""
    if path and os.path.exists(path):
        shutil.rmtree(path, ignore_errors=True)


# ----------------------------------------------------------------------
# Given
# ----------------------------------------------------------------------


@given("a cowcov test directory is initialised")
def step_cowcov_init_dir(context: Context) -> None:
    """Create a fresh temporary directory with seed files."""
    context.cowcov_test_dir = _cowcov_make_test_dir()
    context.cowcov_sandbox = None
    context.cowcov_error = None
    context.cowcov_commit_result = None
    context.cowcov_resolved_paths = []
    context.cowcov_makedirs_mock = None
    context._cleanup_handlers.append(
        lambda: _cowcov_cleanup_dir(context.cowcov_test_dir)
    )


@given("shutil.copytree is patched to raise OSError in cowcov")
def step_cowcov_patch_copytree_oserror(context: Context) -> None:
    """Patch shutil.copytree in the copy_on_write module to raise OSError."""
    patcher = patch(
        "cleveragents.infrastructure.sandbox.copy_on_write.shutil.copytree",
        side_effect=OSError("mocked copytree failure"),
    )
    patcher.start()
    context._cleanup_handlers.append(patcher.stop)


@given('a cowcov sandbox is created for plan "{plan_id}"')
def step_cowcov_create_sandbox(context: Context, plan_id: str) -> None:
    """Create a CopyOnWriteSandbox and call create()."""
    context.cowcov_sandbox = CopyOnWriteSandbox(
        resource_id="res-cowcov",
        original_path=context.cowcov_test_dir,
    )
    context.cowcov_sandbox.create(plan_id)
    sandbox_path = context.cowcov_sandbox._sandbox_path
    if sandbox_path:
        parent = os.path.dirname(sandbox_path)
        context._cleanup_handlers.append(lambda: _cowcov_cleanup_dir(parent))


@given('a cowcov sandbox is created and activated for plan "{plan_id}"')
def step_cowcov_create_and_activate(context: Context, plan_id: str) -> None:
    """Create a sandbox and transition to ACTIVE via get_path."""
    context.cowcov_sandbox = CopyOnWriteSandbox(
        resource_id="res-cowcov",
        original_path=context.cowcov_test_dir,
    )
    context.cowcov_sandbox.create(plan_id)
    context.cowcov_sandbox.get_path("existing.txt")
    assert context.cowcov_sandbox.status == SandboxStatus.ACTIVE, (
        f"Expected ACTIVE, got {context.cowcov_sandbox.status}"
    )
    sandbox_path = context.cowcov_sandbox._sandbox_path
    if sandbox_path:
        parent = os.path.dirname(sandbox_path)
        context._cleanup_handlers.append(lambda: _cowcov_cleanup_dir(parent))


@given("a cowcov sandbox forced to ACTIVE with sandbox_path None")
def step_cowcov_force_active_none(context: Context) -> None:
    """Create a sandbox stub with status=ACTIVE and _sandbox_path=None."""
    d = _cowcov_make_test_dir()
    context.cowcov_test_dir = d
    sandbox = CopyOnWriteSandbox(resource_id="res-cowcov", original_path=d)
    sandbox._status = SandboxStatus.ACTIVE
    sandbox._sandbox_path = None
    context.cowcov_sandbox = sandbox
    context.cowcov_error = None
    context.cowcov_commit_result = None
    context.cowcov_resolved_paths = []
    context._cleanup_handlers.append(lambda: _cowcov_cleanup_dir(d))


@given("a cowcov sandbox forced to CREATED with sandbox_path None")
def step_cowcov_force_created_none(context: Context) -> None:
    """Create a sandbox stub with status=CREATED and _sandbox_path=None."""
    d = _cowcov_make_test_dir()
    context.cowcov_test_dir = d
    sandbox = CopyOnWriteSandbox(resource_id="res-cowcov", original_path=d)
    sandbox._status = SandboxStatus.CREATED
    sandbox._sandbox_path = None
    context.cowcov_sandbox = sandbox
    context.cowcov_error = None
    context.cowcov_commit_result = None
    context.cowcov_resolved_paths = []
    context._cleanup_handlers.append(lambda: _cowcov_cleanup_dir(d))


@given("a cowcov sandbox in PENDING state with sandbox_path None")
def step_cowcov_pending_none(context: Context) -> None:
    """Create a sandbox that is still PENDING (never created)."""
    d = _cowcov_make_test_dir()
    context.cowcov_test_dir = d
    sandbox = CopyOnWriteSandbox(resource_id="res-cowcov", original_path=d)
    assert sandbox._sandbox_path is None, "Expected _sandbox_path to be None"
    assert sandbox.status == SandboxStatus.PENDING, "Expected PENDING status"
    context.cowcov_sandbox = sandbox
    context.cowcov_error = None
    context.cowcov_commit_result = None
    context._cleanup_handlers.append(lambda: _cowcov_cleanup_dir(d))


@given("cowcov _compute_diff is patched to return a changed root file")
def step_cowcov_patch_compute_diff(context: Context) -> None:
    """Patch compute_diff to return one changed file at root level."""
    patcher = patch(
        "cleveragents.infrastructure.sandbox.copy_on_write.compute_diff",
        return_value=(["rootfile.txt"], [], []),
    )
    patcher.start()
    context._cleanup_handlers.append(patcher.stop)


@given("cowcov os.path.dirname is patched to return empty string")
def step_cowcov_patch_dirname(context: Context) -> None:
    """Patch os.path.dirname in the copy_on_write module to return ''."""
    patcher = patch(
        "cleveragents.infrastructure.sandbox.copy_on_write.os.path.dirname",
        return_value="",
    )
    patcher.start()
    context._cleanup_handlers.append(patcher.stop)


@given("cowcov shutil.copy2 is patched to no-op")
def step_cowcov_patch_copy2_noop(context: Context) -> None:
    """Patch shutil.copy2 in the copy_on_write module to do nothing."""
    makedirs_mock = MagicMock()
    patcher_copy2 = patch(
        "cleveragents.infrastructure.sandbox.copy_on_write.shutil.copy2",
    )
    patcher_makedirs = patch(
        "cleveragents.infrastructure.sandbox.copy_on_write.os.makedirs",
        makedirs_mock,
    )
    patcher_copy2.start()
    patcher_makedirs.start()
    context.cowcov_makedirs_mock = makedirs_mock
    context._cleanup_handlers.append(patcher_copy2.stop)
    context._cleanup_handlers.append(patcher_makedirs.stop)


@given('a cowcov file "existing.txt" is modified in the sandbox')
def step_cowcov_modify_existing(context: Context) -> None:
    """Modify a file inside the sandbox copy."""
    sandbox_path = context.cowcov_sandbox._sandbox_path
    assert sandbox_path is not None, "Sandbox path must be set"
    fpath = os.path.join(sandbox_path, "existing.txt")
    with open(fpath, "w") as f:
        f.write("modified for OSError test")


@given("cowcov shutil.copy2 is patched to raise OSError")
def step_cowcov_patch_copy2_oserror(context: Context) -> None:
    """Patch shutil.copy2 in copy_on_write module to raise OSError."""
    patcher = patch(
        "cleveragents.infrastructure.sandbox.copy_on_write.shutil.copy2",
        side_effect=OSError("mocked copy2 failure"),
    )
    patcher.start()
    context._cleanup_handlers.append(patcher.stop)


@given("cowcov shutil.copytree is patched to raise OSError for rollback")
def step_cowcov_patch_copytree_rollback(context: Context) -> None:
    """Patch shutil.copytree to raise OSError (for rollback re-copy)."""
    patcher = patch(
        "cleveragents.infrastructure.sandbox.copy_on_write.shutil.copytree",
        side_effect=OSError("mocked copytree failure during rollback"),
    )
    patcher.start()
    context._cleanup_handlers.append(patcher.stop)


@given("the cowcov sandbox parent directory is manually removed")
def step_cowcov_remove_parent(context: Context) -> None:
    """Remove the sandbox parent temp dir before cleanup."""
    sandbox_path = context.cowcov_sandbox._sandbox_path
    assert sandbox_path is not None, "Sandbox path must be set"
    parent = os.path.dirname(sandbox_path)
    if os.path.exists(parent):
        shutil.rmtree(parent, ignore_errors=True)


# ----------------------------------------------------------------------
# When
# ----------------------------------------------------------------------


@when('a cowcov sandbox create is attempted for plan "{plan_id}"')
def step_cowcov_attempt_create(context: Context, plan_id: str) -> None:
    """Attempt to create a sandbox, capturing any error."""
    context.cowcov_sandbox = CopyOnWriteSandbox(
        resource_id="res-cowcov",
        original_path=context.cowcov_test_dir,
    )
    try:
        context.cowcov_sandbox.create(plan_id)
    except (SandboxCreationError, SandboxStateError) as exc:
        context.cowcov_error = exc


@when('cowcov get_path is called with "{path}"')
def step_cowcov_get_path(context: Context, path: str) -> None:
    """Call get_path and record the resolved path."""
    resolved = context.cowcov_sandbox.get_path(path)
    context.cowcov_resolved_paths.append(resolved)


@when('cowcov get_path is attempted with "{path}"')
def step_cowcov_attempt_get_path(context: Context, path: str) -> None:
    """Attempt get_path, capturing any error."""
    try:
        context.cowcov_sandbox.get_path(path)
    except (SandboxStateError, ValueError) as exc:
        context.cowcov_error = exc


@when("cowcov commit is attempted")
def step_cowcov_attempt_commit(context: Context) -> None:
    """Attempt commit, capturing any error."""
    try:
        context.cowcov_commit_result = context.cowcov_sandbox.commit()
    except (SandboxStateError, SandboxCommitError) as exc:
        context.cowcov_error = exc


@when('the cowcov file "existing.txt" is deleted from both sandbox and original')
def step_cowcov_delete_from_both(context: Context) -> None:
    """Delete a file from both sandbox copy and original directory."""
    sandbox_path = context.cowcov_sandbox._sandbox_path
    assert sandbox_path is not None, "Sandbox path must be set"

    for base in (sandbox_path, context.cowcov_test_dir):
        fpath = os.path.join(base, "existing.txt")
        if os.path.exists(fpath):
            os.remove(fpath)


@when("cowcov rollback is attempted")
def step_cowcov_attempt_rollback(context: Context) -> None:
    """Attempt rollback, capturing any error."""
    try:
        context.cowcov_sandbox.rollback()
    except (SandboxStateError, SandboxRollbackError) as exc:
        context.cowcov_error = exc


@when("cowcov cleanup is called")
def step_cowcov_cleanup(context: Context) -> None:
    """Call cleanup on the sandbox."""
    context.cowcov_sandbox.cleanup()


# ----------------------------------------------------------------------
# Then
# ----------------------------------------------------------------------


@then("a cowcov SandboxCreationError should be raised")
def step_cowcov_check_creation_error(context: Context) -> None:
    """Assert a SandboxCreationError was captured."""
    assert context.cowcov_error is not None, (
        "Expected SandboxCreationError but no error occurred"
    )
    assert isinstance(context.cowcov_error, SandboxCreationError), (
        f"Expected SandboxCreationError, got {type(context.cowcov_error).__name__}: "
        f"{context.cowcov_error}"
    )


@then('the cowcov error message should contain "{fragment}"')
def step_cowcov_error_message_contains(context: Context, fragment: str) -> None:
    """Assert the error message contains the given fragment."""
    assert context.cowcov_error is not None, "No error was captured"
    assert fragment in str(context.cowcov_error), (
        f"Expected '{fragment}' in error message, got: {context.cowcov_error}"
    )


@then('the cowcov sandbox status should be "{status}"')
def step_cowcov_check_status(context: Context, status: str) -> None:
    """Assert the sandbox status matches the expected value."""
    expected = SandboxStatus(status)
    assert context.cowcov_sandbox.status == expected, (
        f"Expected status {expected.value}, got {context.cowcov_sandbox.status.value}"
    )


@then("both cowcov resolved paths should be valid")
def step_cowcov_two_paths_valid(context: Context) -> None:
    """Assert two paths were resolved and both are inside the sandbox."""
    assert len(context.cowcov_resolved_paths) == 2, (
        f"Expected 2 resolved paths, got {len(context.cowcov_resolved_paths)}"
    )
    sandbox_path = context.cowcov_sandbox._sandbox_path
    assert sandbox_path is not None, "Sandbox path should be set"
    for p in context.cowcov_resolved_paths:
        assert p.startswith(sandbox_path), (
            f"Resolved path {p} is not inside sandbox {sandbox_path}"
        )


@then('a cowcov SandboxStateError should be raised with message "{msg}"')
def step_cowcov_check_state_error_msg(context: Context, msg: str) -> None:
    """Assert a SandboxStateError with the expected message was captured."""
    assert context.cowcov_error is not None, (
        "Expected SandboxStateError but no error occurred"
    )
    assert isinstance(context.cowcov_error, SandboxStateError), (
        f"Expected SandboxStateError, got {type(context.cowcov_error).__name__}: "
        f"{context.cowcov_error}"
    )
    assert msg in str(context.cowcov_error), (
        f"Expected '{msg}' in error message, got: {context.cowcov_error}"
    )


@then("the cowcov commit should succeed")
def step_cowcov_commit_success(context: Context) -> None:
    """Assert commit completed without error."""
    assert context.cowcov_error is None, (
        f"Expected no error but got: {context.cowcov_error}"
    )
    assert context.cowcov_commit_result is not None, "Expected a commit result"
    assert context.cowcov_commit_result.success is True, (
        "Expected commit result success=True"
    )


@then("cowcov os.makedirs should not have been called")
def step_cowcov_makedirs_not_called(context: Context) -> None:
    """Assert os.makedirs was not invoked (dst_dir was empty)."""
    assert context.cowcov_makedirs_mock is not None, "makedirs mock not set up"
    context.cowcov_makedirs_mock.assert_not_called()


@then("a cowcov SandboxCommitError should be raised")
def step_cowcov_check_commit_error(context: Context) -> None:
    """Assert a SandboxCommitError was captured."""
    assert context.cowcov_error is not None, (
        "Expected SandboxCommitError but no error occurred"
    )
    assert isinstance(context.cowcov_error, SandboxCommitError), (
        f"Expected SandboxCommitError, got {type(context.cowcov_error).__name__}: "
        f"{context.cowcov_error}"
    )


@then("a cowcov SandboxRollbackError should be raised")
def step_cowcov_check_rollback_error(context: Context) -> None:
    """Assert a SandboxRollbackError was captured."""
    assert context.cowcov_error is not None, (
        "Expected SandboxRollbackError but no error occurred"
    )
    assert isinstance(context.cowcov_error, SandboxRollbackError), (
        f"Expected SandboxRollbackError, got {type(context.cowcov_error).__name__}: "
        f"{context.cowcov_error}"
    )
