"""Step definitions for checkpoint_service_coverage_r3.feature.

Targets uncovered lines in checkpoint_service.py:
- Lines 425-426: selective_rollback except when rev-parse HEAD fails
- Lines 453-458, 460: selective_rollback recovery failure logging
- Line 487: archive_artifacts with explicit archive_dir
- Line 541: _compute_diff_snapshot returns [] when refs match
- Lines 546, 548: _compute_diff_snapshot returns [] when sandbox missing
- Lines 556-562, 564, 566: _compute_diff_snapshot returns [] when git diff fails
"""

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

from behave import given, then, when
from ulid import ULID

from cleveragents.application.services.checkpoint_service import CheckpointService
from cleveragents.core.exceptions import BusinessRuleViolation

# Pre-generate valid ULID strings for use as plan IDs
_SANDBOX_PLAN_ID = str(ULID())
_DIFF_PLAN_ID = str(ULID())
_NO_SANDBOX_PLAN_ID = str(ULID())


# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("chkcov3 the checkpoint service module is available")
def step_chkcov3_module_available(context):
    """Verify the checkpoint service module can be imported."""
    assert CheckpointService is not None


# ---------------------------------------------------------------------------
# Shared: create a checkpoint service with in-memory storage
# ---------------------------------------------------------------------------
@given("chkcov3 a checkpoint service with in-memory storage")
def step_chkcov3_create_service(context):
    """Create a CheckpointService with no repository (in-memory)."""
    context.chkcov3_service = CheckpointService()


# ---------------------------------------------------------------------------
# Shared: create a temporary sandbox directory with .git
# ---------------------------------------------------------------------------
@given("chkcov3 a sandbox directory with .git exists")
def step_chkcov3_create_sandbox(context):
    """Create a temp dir with a .git subdir to act as a sandbox."""
    tmpdir = tempfile.mkdtemp(prefix="chkcov3_sandbox_")
    os.makedirs(os.path.join(tmpdir, ".git"), exist_ok=True)
    context.chkcov3_sandbox_path = tmpdir
    context.chkcov3_plan_id = _SANDBOX_PLAN_ID
    context.chkcov3_service.register_sandbox(context.chkcov3_plan_id, tmpdir)

    def _cleanup():
        shutil.rmtree(tmpdir, ignore_errors=True)

    context.add_cleanup(_cleanup)


# ---------------------------------------------------------------------------
# Shared: create a checkpoint for the sandbox plan
# ---------------------------------------------------------------------------
@given("chkcov3 a checkpoint exists for the sandbox plan")
def step_chkcov3_create_checkpoint(context):
    """Create a checkpoint under the sandbox plan."""
    cp = context.chkcov3_service.create_checkpoint(
        plan_id=context.chkcov3_plan_id,
        sandbox_ref="deadbeef1234",
        reason="chkcov3 test checkpoint",
    )
    context.chkcov3_checkpoint_id = cp.checkpoint_id


# ===================================================================
# Scenario: selective_rollback when rev-parse HEAD fails (425-426)
# ===================================================================
@given("chkcov3 _run_git is mocked to always fail")
def step_chkcov3_mock_run_git_always_fail(context):
    """Patch _run_git on the service to always raise."""
    patcher = patch.object(
        context.chkcov3_service,
        "_run_git",
        side_effect=BusinessRuleViolation("Git operation failed: mocked failure"),
    )
    patcher.start()
    context.add_cleanup(patcher.stop)


@when("chkcov3 I call selective_rollback for the plan and checkpoint")
def step_chkcov3_call_selective_rollback(context):
    """Call selective_rollback and capture any exception."""
    context.chkcov3_error = None
    try:
        context.chkcov3_service.selective_rollback(
            plan_id=context.chkcov3_plan_id,
            checkpoint_id=context.chkcov3_checkpoint_id,
        )
    except Exception as exc:
        context.chkcov3_error = exc


@then("chkcov3 an exception should be stored on context")
def step_chkcov3_exception_stored(context):
    """Verify an exception was captured."""
    assert context.chkcov3_error is not None, (
        "Expected an exception but none was raised"
    )


@then("chkcov3 the stored exception should be a BusinessRuleViolation")
def step_chkcov3_exception_is_brv(context):
    """Verify the captured exception is a BusinessRuleViolation."""
    assert isinstance(context.chkcov3_error, BusinessRuleViolation), (
        f"Expected BusinessRuleViolation, got {type(context.chkcov3_error).__name__}: "
        f"{context.chkcov3_error}"
    )


# ===================================================================
# Scenario: selective_rollback recovery failure (453-458, 460)
# ===================================================================
@given("chkcov3 _run_git is mocked to succeed once then fail thereafter")
def step_chkcov3_mock_run_git_succeed_then_fail(context):
    """Patch _run_git to succeed on the first call (rev-parse HEAD),
    then raise on all subsequent calls (rollback + recovery)."""
    call_count = {"n": 0}
    original_error = BusinessRuleViolation("Git operation failed: mocked failure")

    def _side_effect(args, cwd):
        call_count["n"] += 1
        if call_count["n"] == 1:
            # First call is rev-parse HEAD — return a fake result
            mock_result = MagicMock()
            mock_result.stdout = "abc123fakeheadref\n"
            mock_result.stderr = ""
            mock_result.returncode = 0
            return mock_result
        # All subsequent calls fail
        raise original_error

    patcher = patch.object(
        context.chkcov3_service,
        "_run_git",
        side_effect=_side_effect,
    )
    patcher.start()
    context.add_cleanup(patcher.stop)


# ===================================================================
# Scenario: archive_artifacts with explicit archive_dir (line 487)
# ===================================================================
@given("chkcov3 a temporary sandbox with artifact files")
def step_chkcov3_sandbox_with_artifacts(context):
    """Create a temp sandbox dir with some artifact files."""
    tmpdir = tempfile.mkdtemp(prefix="chkcov3_archive_sandbox_")
    context.chkcov3_archive_sandbox = tmpdir

    # Create artifact files
    artifact_dir = os.path.join(tmpdir, "output")
    os.makedirs(artifact_dir, exist_ok=True)
    context.chkcov3_artifact_paths = ["output/result.txt", "output/data.json"]
    for rel_path in context.chkcov3_artifact_paths:
        full_path = os.path.join(tmpdir, rel_path)
        os.makedirs(os.path.dirname(full_path), exist_ok=True)
        with open(full_path, "w") as f:
            f.write(f"content of {rel_path}")

    def _cleanup():
        shutil.rmtree(tmpdir, ignore_errors=True)

    context.add_cleanup(_cleanup)


@given("chkcov3 an explicit archive directory")
def step_chkcov3_explicit_archive_dir(context):
    """Create a separate temp directory for the archive."""
    tmpdir = tempfile.mkdtemp(prefix="chkcov3_archive_dest_")
    context.chkcov3_archive_dir = tmpdir

    def _cleanup():
        shutil.rmtree(tmpdir, ignore_errors=True)

    context.add_cleanup(_cleanup)


@when("chkcov3 I call archive_artifacts with the explicit archive_dir")
def step_chkcov3_call_archive_artifacts(context):
    """Call archive_artifacts with the explicit archive_dir parameter."""
    context.chkcov3_archived = context.chkcov3_service.archive_artifacts(
        sandbox_path=context.chkcov3_archive_sandbox,
        artifact_paths=context.chkcov3_artifact_paths,
        archive_dir=context.chkcov3_archive_dir,
    )


@then("chkcov3 the archived list should contain the artifact paths")
def step_chkcov3_archived_list_check(context):
    """Verify the returned list contains the artifact relative paths."""
    for rel_path in context.chkcov3_artifact_paths:
        assert rel_path in context.chkcov3_archived, (
            f"Expected '{rel_path}' in archived list, got {context.chkcov3_archived}"
        )


@then("chkcov3 the artifacts should exist in the explicit archive directory")
def step_chkcov3_artifacts_in_archive(context):
    """Verify the artifact files were moved to the explicit archive dir."""
    for rel_path in context.chkcov3_artifact_paths:
        dest = os.path.join(context.chkcov3_archive_dir, rel_path)
        assert os.path.exists(dest), (
            f"Expected archived file at {dest} but it does not exist"
        )


# ===================================================================
# Scenario: _compute_diff_snapshot returns [] when refs match (541)
# ===================================================================
@given('chkcov3 a diff-plan checkpoint with ref "{ref}"')
def step_chkcov3_create_diff_plan_checkpoint(context, ref):
    """Create a checkpoint with a specific sandbox_ref for a diff test plan."""
    context.chkcov3_diff_plan_id = _DIFF_PLAN_ID
    context.chkcov3_service.create_checkpoint(
        plan_id=context.chkcov3_diff_plan_id,
        sandbox_ref=ref,
        reason="chkcov3 diff test checkpoint",
    )


@when('chkcov3 I call _compute_diff_snapshot with current_ref "{ref}"')
def step_chkcov3_call_compute_diff_same_ref(context, ref):
    """Call _compute_diff_snapshot with a ref that matches the existing checkpoint."""
    context.chkcov3_diff_result = context.chkcov3_service._compute_diff_snapshot(
        plan_id=context.chkcov3_diff_plan_id,
        current_ref=ref,
    )


@then("chkcov3 the diff result should be an empty list")
def step_chkcov3_diff_result_empty(context):
    """Verify the diff result is an empty list."""
    assert context.chkcov3_diff_result == [], (
        f"Expected empty list, got {context.chkcov3_diff_result}"
    )


# ===================================================================
# Scenario: _compute_diff_snapshot returns [] when sandbox missing (546, 548)
# ===================================================================
@given('chkcov3 an unregistered plan "{plan_id}" has a checkpoint with ref "{ref}"')
def step_chkcov3_create_checkpoint_for_unregistered_plan(context, plan_id, ref):
    """Create a checkpoint for a plan that has no sandbox registered.
    The plan_id from the feature is ignored; we use a valid ULID instead."""
    context.chkcov3_no_sandbox_plan_id = _NO_SANDBOX_PLAN_ID
    context.chkcov3_service.create_checkpoint(
        plan_id=_NO_SANDBOX_PLAN_ID,
        sandbox_ref=ref,
        reason="chkcov3 no-sandbox test",
    )


@when(
    'chkcov3 I call _compute_diff_snapshot for plan "{plan_id}" with current_ref "{ref}"'
)
def step_chkcov3_call_compute_diff_for_plan(context, plan_id, ref):
    """Call _compute_diff_snapshot for a plan that has no sandbox registered.
    Uses the ULID plan stored on context rather than the feature string."""
    actual_plan_id = getattr(context, "chkcov3_no_sandbox_plan_id", _NO_SANDBOX_PLAN_ID)
    context.chkcov3_diff_result = context.chkcov3_service._compute_diff_snapshot(
        plan_id=actual_plan_id,
        current_ref=ref,
    )


# ===================================================================
# Scenario: _compute_diff_snapshot returns [] when git diff fails (556-566)
# ===================================================================
@given('chkcov3 the sandbox plan has a checkpoint with ref "{ref}"')
def step_chkcov3_create_checkpoint_for_sandbox_plan(context, ref):
    """Create a checkpoint for the sandbox plan with a specific ref."""
    context.chkcov3_service.create_checkpoint(
        plan_id=context.chkcov3_plan_id,
        sandbox_ref=ref,
        reason="chkcov3 diff-fail test",
    )


@given("chkcov3 _run_git is mocked to raise on diff command")
def step_chkcov3_mock_run_git_diff_fail(context):
    """Patch _run_git so it raises when called (for the diff in _compute_diff_snapshot)."""
    patcher = patch.object(
        context.chkcov3_service,
        "_run_git",
        side_effect=BusinessRuleViolation("Git operation failed: diff error"),
    )
    patcher.start()
    context.add_cleanup(patcher.stop)


@when(
    'chkcov3 I call _compute_diff_snapshot for the sandbox plan with current_ref "{ref}"'
)
def step_chkcov3_call_compute_diff_sandbox_plan(context, ref):
    """Call _compute_diff_snapshot for the sandbox plan with a different ref."""
    context.chkcov3_diff_result = context.chkcov3_service._compute_diff_snapshot(
        plan_id=context.chkcov3_plan_id,
        current_ref=ref,
    )
