"""Step definitions for CopyOnWriteSandbox coverage round 3.

Targets the remaining uncovered lines in copy_on_write.py:
- Lines 263-265: backup_directory raises during commit → cleanup backup_dir and re-raise
- Lines 297-302: safe_restore fails during commit error recovery → log warning, preserve backup
- Lines 430-432: cleanup() removes _pre_commit_backup when it is not None

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

from __future__ import annotations

import logging
import os
import shutil
import tempfile
from unittest.mock import 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,
    SandboxStatus,
)

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


def _cowcov3_make_test_dir() -> str:
    """Create a temporary directory with a seed file for testing."""
    d = tempfile.mkdtemp(prefix="cowcov3-test-dir-")
    with open(os.path.join(d, "existing.txt"), "w") as f:
        f.write("original content")
    return d


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


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


@given("a cowcov3 test directory is set up")
def step_cowcov3_test_dir(ctx: Context) -> None:
    """Create a temporary test directory and initialise context attributes."""
    ctx.cowcov3_test_dir = _cowcov3_make_test_dir()
    ctx.cowcov3_sandbox = None
    ctx.cowcov3_error = None
    ctx.cowcov3_backup_dir_cleaned = None
    ctx.cowcov3_log_records: list[logging.LogRecord] = []
    ctx.add_cleanup(lambda: _cowcov3_cleanup_dir(ctx.cowcov3_test_dir))


@given("a cowcov3 sandbox is created and has changes")
def step_cowcov3_sandbox_with_changes(ctx: Context) -> None:
    """Create a sandbox, activate it, and make a change so commit detects diffs."""
    sandbox = CopyOnWriteSandbox("cowcov3-res", ctx.cowcov3_test_dir)
    sandbox.create("cowcov3-plan")
    # Activate by calling get_path, then modify a file so compute_diff sees changes
    path = sandbox.get_path("existing.txt")
    with open(path, "w") as f:
        f.write("modified content")
    ctx.cowcov3_sandbox = sandbox
    ctx.add_cleanup(lambda: sandbox.cleanup())


@given("cowcov3 backup_directory is patched to raise an OSError")
def step_cowcov3_patch_backup_directory(ctx: Context) -> None:
    """Patch backup_directory to raise so lines 263-265 execute."""
    patcher = patch(
        "cleveragents.infrastructure.sandbox.copy_on_write.backup_directory",
        side_effect=OSError("disk full"),
    )
    patcher.start()
    ctx.add_cleanup(patcher.stop)


@given("cowcov3 copy phase is patched to fail after backup succeeds")
def step_cowcov3_patch_copy_phase(ctx: Context) -> None:
    """Patch shutil.copy2 in the copy_on_write module to fail.

    This makes the file-copy loop (line 275) raise AFTER backup_directory
    has already succeeded, so _pre_commit_backup is set when we enter
    the except block at line 283.
    """
    patcher = patch(
        "cleveragents.infrastructure.sandbox.copy_on_write.shutil.copy2",
        side_effect=OSError("copy failed"),
    )
    patcher.start()
    ctx.add_cleanup(patcher.stop)


@given("cowcov3 safe_restore is patched to raise an OSError")
def step_cowcov3_patch_safe_restore(ctx: Context) -> None:
    """Patch safe_restore to fail so lines 297-302 execute."""
    patcher = patch(
        "cleveragents.infrastructure.sandbox.copy_on_write.safe_restore",
        side_effect=OSError("restore failed"),
    )
    patcher.start()
    ctx.add_cleanup(patcher.stop)


@given("cowcov3 the sandbox has a pre_commit_backup directory")
def step_cowcov3_set_pre_commit_backup(ctx: Context) -> None:
    """Manually set _pre_commit_backup to a real temp directory.

    This simulates the state after a failed commit where the backup
    was preserved.  cleanup() should remove it (lines 430-432).
    """
    sandbox = ctx.cowcov3_sandbox
    backup_dir = tempfile.mkdtemp(prefix="cowcov3-backup-")
    # Put a marker file so we can verify it existed
    with open(os.path.join(backup_dir, "marker.txt"), "w") as f:
        f.write("backup marker")
    sandbox._pre_commit_backup = backup_dir
    ctx.cowcov3_backup_path = backup_dir


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


@when("cowcov3 commit is attempted")
def step_cowcov3_commit(ctx: Context) -> None:
    """Attempt to commit and capture any exception."""
    # Attach a log handler to capture warnings from the cow module
    cow_logger = logging.getLogger("cleveragents.infrastructure.sandbox.copy_on_write")

    class _ListHandler(logging.Handler):
        def __init__(self, records_list: list[logging.LogRecord]) -> None:
            super().__init__()
            self._records = records_list

        def emit(self, record: logging.LogRecord) -> None:
            self._records.append(record)

    log_records: list[logging.LogRecord] = []
    list_handler = _ListHandler(log_records)
    list_handler.setLevel(logging.DEBUG)
    cow_logger.addHandler(list_handler)
    ctx.add_cleanup(lambda: cow_logger.removeHandler(list_handler))

    try:
        ctx.cowcov3_sandbox.commit("cowcov3 commit")
    except Exception as exc:
        ctx.cowcov3_error = exc

    ctx.cowcov3_log_records = log_records


@when("cowcov3 cleanup is called")
def step_cowcov3_cleanup(ctx: Context) -> None:
    """Call cleanup on the sandbox."""
    ctx.cowcov3_sandbox.cleanup()


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


@then("cowcov3 a SandboxCommitError should be raised")
def step_cowcov3_assert_commit_error(ctx: Context) -> None:
    """Verify that a SandboxCommitError was raised."""
    assert ctx.cowcov3_error is not None, "Expected an error but none was raised"
    assert isinstance(ctx.cowcov3_error, SandboxCommitError), (
        f"Expected SandboxCommitError, got {type(ctx.cowcov3_error).__name__}: "
        f"{ctx.cowcov3_error}"
    )


@then("cowcov3 the temporary backup dir should have been cleaned up")
def step_cowcov3_assert_backup_cleaned(ctx: Context) -> None:
    """Verify that backup_dir was cleaned up after backup_directory failed.

    When backup_directory raises (lines 263-265), the except block does
    shutil.rmtree(backup_dir) then re-raises.  The sandbox should have
    _pre_commit_backup = None because it was never assigned.
    """
    sandbox = ctx.cowcov3_sandbox
    assert sandbox._pre_commit_backup is None, (
        "Expected _pre_commit_backup to be None after backup_directory failure, "
        f"but got {sandbox._pre_commit_backup}"
    )


@then("cowcov3 the safe_restore failure should have been logged as a warning")
def step_cowcov3_assert_warning_logged(ctx: Context) -> None:
    """Verify that the safe_restore failure produced a warning log."""
    warning_records = [
        r for r in ctx.cowcov3_log_records if r.levelno == logging.WARNING
    ]
    assert len(warning_records) > 0, (
        "Expected a WARNING log record from safe_restore failure, but none found. "
        f"All records: {[(r.levelno, r.getMessage()) for r in ctx.cowcov3_log_records]}"
    )
    msg = warning_records[0].getMessage()
    assert "Failed to restore original from backup" in msg, (
        f"Expected warning about failed restore, got: {msg}"
    )


@then("cowcov3 the pre_commit_backup should be preserved for manual recovery")
def step_cowcov3_assert_backup_preserved(ctx: Context) -> None:
    """Verify _pre_commit_backup is NOT None (preserved for manual recovery).

    When safe_restore fails (lines 297-302), the code logs a warning
    and does NOT set _pre_commit_backup to None, preserving it for
    manual recovery.
    """
    sandbox = ctx.cowcov3_sandbox
    assert sandbox._pre_commit_backup is not None, (
        "Expected _pre_commit_backup to be preserved for manual recovery, "
        "but it was set to None"
    )


@then("cowcov3 the pre_commit_backup directory should be removed")
def step_cowcov3_assert_backup_removed(ctx: Context) -> None:
    """Verify the pre_commit_backup directory was removed by cleanup (lines 430-432)."""
    backup_path = ctx.cowcov3_backup_path
    assert not os.path.exists(backup_path), (
        f"Expected pre_commit_backup directory to be removed, "
        f"but it still exists at {backup_path}"
    )


@then("cowcov3 the sandbox status should be CLEANED_UP")
def step_cowcov3_assert_cleaned_up(ctx: Context) -> None:
    """Verify the sandbox transitioned to CLEANED_UP."""
    assert ctx.cowcov3_sandbox.status == SandboxStatus.CLEANED_UP, (
        f"Expected CLEANED_UP status, got {ctx.cowcov3_sandbox.status.value}"
    )
