"""Step definitions for correction_checkpoint_rollback.feature.

Exercises the wiring of checkpoint rollback into the correction service
revert flow, including workspace snapshots, physical artifact archival,
selective rollback, diff-based storage, and atomic rollback semantics.
"""

from __future__ import annotations

import os
import subprocess
import tempfile
from pathlib import Path

from behave import given, then, when

from cleveragents.application.services.checkpoint_service import CheckpointService
from cleveragents.application.services.correction_service import CorrectionService
from cleveragents.core.exceptions import BusinessRuleViolation, ResourceNotFoundError

# ─── Helpers ───────────────────────────────────────────────────────────


def _init_sandbox(tmpdir: str) -> str:
    """Create a bare git repo sandbox in tmpdir and return its path."""
    sandbox = os.path.join(tmpdir, "sandbox")
    os.makedirs(sandbox)
    subprocess.run(["git", "init"], cwd=sandbox, capture_output=True, check=True)
    subprocess.run(
        ["git", "config", "user.email", "test@test.com"],
        cwd=sandbox,
        capture_output=True,
        check=True,
    )
    subprocess.run(
        ["git", "config", "user.name", "Test"],
        cwd=sandbox,
        capture_output=True,
        check=True,
    )
    # Initial commit
    init_file = os.path.join(sandbox, "README.md")
    with open(init_file, "w") as f:
        f.write("initial\n")
    subprocess.run(["git", "add", "."], cwd=sandbox, capture_output=True, check=True)
    subprocess.run(
        ["git", "commit", "-m", "init"],
        cwd=sandbox,
        capture_output=True,
        check=True,
    )
    return sandbox


def _get_head(sandbox: str) -> str:
    """Get current HEAD commit hash."""
    result = subprocess.run(
        ["git", "rev-parse", "HEAD"],
        cwd=sandbox,
        capture_output=True,
        text=True,
        check=True,
    )
    return result.stdout.strip()


PLAN_ID = "01ARZ3NDEKTSV4RRFFQ69G5FAV"


# ─── Workspace snapshot creation ──────────────────────────────────────


@given("ccr a checkpoint service")
def step_ccr_checkpoint_service(context):
    context.ccr_svc = CheckpointService()


@when(
    'ccr I create a workspace snapshot for plan "{plan_id}" '
    'decision "{decision_id}" with sandbox_ref "{ref}"'
)
def step_ccr_create_workspace_snapshot(context, plan_id, decision_id, ref):
    context.ccr_snapshot = context.ccr_svc.create_workspace_snapshot(
        plan_id=plan_id,
        sandbox_ref=ref,
        decision_id=decision_id,
    )


@then('ccr the snapshot should be created with type "{cp_type}"')
def step_ccr_snapshot_type(context, cp_type):
    assert context.ccr_snapshot.checkpoint_type == cp_type


@then('ccr the snapshot should be aligned to decision "{decision_id}"')
def step_ccr_snapshot_decision(context, decision_id):
    assert context.ccr_snapshot.decision_id == decision_id


@then("ccr the snapshot metadata should indicate diff-based storage")
def step_ccr_snapshot_diff_based(context):
    assert context.ccr_snapshot.metadata.extra.get("diff_based") is True


@then('ccr the snapshot metadata extra should contain "diff_based" as true')
def step_ccr_snapshot_diff_based_true(context):
    assert context.ccr_snapshot.metadata.extra.get("diff_based") is True


@then('ccr the snapshot metadata extra should contain a "diff_hash"')
def step_ccr_snapshot_diff_hash(context):
    assert "diff_hash" in context.ccr_snapshot.metadata.extra
    assert isinstance(context.ccr_snapshot.metadata.extra["diff_hash"], str)
    assert len(context.ccr_snapshot.metadata.extra["diff_hash"]) > 0


# ─── CorrectionService.revert_decisions() wiring ─────────────────────


@given("ccr a correction service with checkpoint support")
def step_ccr_correction_with_checkpoint(context):
    context.ccr_cp_svc = CheckpointService()
    context.ccr_corr_svc = CorrectionService(
        checkpoint_service=context.ccr_cp_svc,
    )


@given("ccr a correction service without checkpoint support")
def step_ccr_correction_without_checkpoint(context):
    context.ccr_corr_svc = CorrectionService()


@when(
    'ccr I call revert_decisions for plan "{plan_id}" '
    'targeting decision "{decision_id}"'
)
def step_ccr_revert_decisions(context, plan_id, decision_id):
    context.ccr_result = context.ccr_corr_svc.revert_decisions(
        plan_id=plan_id,
        target_decision_id=decision_id,
    )


@then('ccr the correction result should have status "{status}"')
def step_ccr_result_status(context, status):
    assert context.ccr_result.status.value == status


@then("ccr the correction result should have reverted decisions")
def step_ccr_result_has_reverted(context):
    assert len(context.ccr_result.reverted_decisions) > 0


@then("ccr the correction result checkpoint_restored should be false")
def step_ccr_result_no_checkpoint(context):
    assert context.ccr_result.checkpoint_restored is False


# ─── Physical artifact archival ───────────────────────────────────────


@given("ccr a temporary sandbox directory with artifact files")
def step_ccr_sandbox_with_artifacts(context):
    context.ccr_tmpdir = tempfile.mkdtemp()
    sandbox = os.path.join(context.ccr_tmpdir, "sandbox")
    os.makedirs(sandbox)
    # Create artifact files
    context.ccr_artifact_paths = ["output/result.txt", "output/log.txt"]
    for rel in context.ccr_artifact_paths:
        full = os.path.join(sandbox, rel)
        os.makedirs(os.path.dirname(full), exist_ok=True)
        with open(full, "w") as f:
            f.write(f"artifact: {rel}\n")
    context.ccr_sandbox_path = sandbox


@given("ccr a temporary sandbox directory without artifact files")
def step_ccr_sandbox_without_artifacts(context):
    context.ccr_tmpdir = tempfile.mkdtemp()
    sandbox = os.path.join(context.ccr_tmpdir, "sandbox")
    os.makedirs(sandbox)
    context.ccr_artifact_paths = ["nonexistent/file.txt"]
    context.ccr_sandbox_path = sandbox


@when("ccr I archive artifacts from the sandbox")
def step_ccr_archive_artifacts(context):
    context.ccr_error = None
    try:
        context.ccr_archived = context.ccr_svc.archive_artifacts(
            context.ccr_sandbox_path,
            context.ccr_artifact_paths,
        )
    except Exception as exc:
        context.ccr_error = exc
        context.ccr_archived = []


@then("ccr the artifacts should exist in the archive directory")
def step_ccr_artifacts_in_archive(context):
    archive_root = (
        Path(context.ccr_sandbox_path) / ".cleveragents" / "archived_artifacts"
    )
    for rel in context.ccr_archived:
        assert (archive_root / rel).exists(), f"Archived file missing: {rel}"


@then("ccr the artifacts should not exist in their original locations")
def step_ccr_artifacts_not_in_original(context):
    for rel in context.ccr_archived:
        original = Path(context.ccr_sandbox_path) / rel
        assert not original.exists(), f"Original file still exists: {rel}"


@then("ccr no errors should be raised")
def step_ccr_no_errors(context):
    assert context.ccr_error is None


@then("ccr the archived count should be {count:d}")
def step_ccr_archived_count(context, count):
    assert len(context.ccr_archived) == count


# ─── Selective rollback ───────────────────────────────────────────────


@given('ccr a checkpoint service with sandbox for plan "{plan_id}"')
def step_ccr_svc_with_sandbox(context, plan_id):
    context.ccr_tmpdir = tempfile.mkdtemp()
    context.ccr_sandbox = _init_sandbox(context.ccr_tmpdir)
    context.ccr_svc = CheckpointService()
    context.ccr_svc.register_sandbox(plan_id, context.ccr_sandbox)
    context.ccr_plan_id = plan_id


@given('ccr checkpoints "cp1" and "cp2" exist for plan "{plan_id}"')
def step_ccr_two_checkpoints(context, plan_id):
    head1 = _get_head(context.ccr_sandbox)
    context.ccr_cp1 = context.ccr_svc.create_checkpoint(
        plan_id=plan_id,
        sandbox_ref=head1,
        reason="cp1",
    )
    # Make a change and commit for cp2
    fpath = os.path.join(context.ccr_sandbox, "file2.txt")
    with open(fpath, "w") as f:
        f.write("cp2 content\n")
    subprocess.run(
        ["git", "add", "."], cwd=context.ccr_sandbox, capture_output=True, check=True
    )
    subprocess.run(
        ["git", "commit", "-m", "cp2"],
        cwd=context.ccr_sandbox,
        capture_output=True,
        check=True,
    )
    head2 = _get_head(context.ccr_sandbox)
    context.ccr_cp2 = context.ccr_svc.create_checkpoint(
        plan_id=plan_id,
        sandbox_ref=head2,
        reason="cp2",
    )


@when('ccr I selectively rollback to checkpoint "cp1"')
def step_ccr_selective_rollback_cp1(context):
    context.ccr_rollback_result = context.ccr_svc.selective_rollback(
        context.ccr_plan_id,
        context.ccr_cp1.checkpoint_id,
    )


@then("ccr the rollback should succeed")
def step_ccr_rollback_success(context):
    assert context.ccr_rollback_result is not None
    assert context.ccr_rollback_result.restored_files_count >= 0


@when("ccr I attempt selective rollback to a nonexistent checkpoint")
def step_ccr_selective_rollback_nonexistent(context):
    context.ccr_original_head = _get_head(context.ccr_sandbox)
    context.ccr_rollback_error = None
    try:
        context.ccr_svc.selective_rollback(
            context.ccr_plan_id,
            "01NONEXISTENT0000000000000",
        )
    except (ResourceNotFoundError, BusinessRuleViolation) as exc:
        context.ccr_rollback_error = exc


@then("ccr the sandbox should remain at the original HEAD")
def step_ccr_sandbox_at_original_head(context):
    assert context.ccr_rollback_error is not None
    current_head = _get_head(context.ccr_sandbox)
    assert current_head == context.ccr_original_head


# ─── Diff-based storage ──────────────────────────────────────────────


@given('ccr an initial checkpoint exists for plan "{plan_id}"')
def step_ccr_initial_checkpoint(context, plan_id):
    head = _get_head(context.ccr_sandbox)
    context.ccr_initial_cp = context.ccr_svc.create_checkpoint(
        plan_id=plan_id,
        sandbox_ref=head,
        reason="initial",
    )


@given("ccr a file is modified and committed in the sandbox")
def step_ccr_modify_file(context):
    fpath = os.path.join(context.ccr_sandbox, "modified.txt")
    with open(fpath, "w") as f:
        f.write("modified content\n")
    subprocess.run(
        ["git", "add", "."], cwd=context.ccr_sandbox, capture_output=True, check=True
    )
    subprocess.run(
        ["git", "commit", "-m", "modify"],
        cwd=context.ccr_sandbox,
        capture_output=True,
        check=True,
    )
    context.ccr_modified_file = "modified.txt"


@when("ccr I create a workspace snapshot capturing the diff")
def step_ccr_workspace_snapshot_diff(context):
    head = _get_head(context.ccr_sandbox)
    context.ccr_diff_snapshot = context.ccr_svc.create_workspace_snapshot(
        plan_id=context.ccr_plan_id,
        sandbox_ref=head,
        decision_id="01DEC0000000000000000000",
    )


@then("ccr the snapshot diff_paths should include the modified file")
def step_ccr_diff_paths_contain_modified(context):
    diff_paths = context.ccr_diff_snapshot.metadata.extra.get("diff_paths", [])
    assert context.ccr_modified_file in diff_paths, (
        f"Expected {context.ccr_modified_file!r} in {diff_paths}"
    )


# ─── Atomic rollback ─────────────────────────────────────────────────


@given("ccr a checkpoint is created from sandbox HEAD")
def step_ccr_checkpoint_from_head(context):
    head = _get_head(context.ccr_sandbox)
    context.ccr_atomic_cp = context.ccr_svc.create_checkpoint(
        plan_id=context.ccr_plan_id,
        sandbox_ref=head,
        reason="atomic-test",
    )
    # Record original content
    readme = os.path.join(context.ccr_sandbox, "README.md")
    with open(readme) as f:
        context.ccr_original_readme = f.read()


@when("ccr I perform selective rollback to the checkpoint")
def step_ccr_selective_rollback_atomic(context):
    context.ccr_rollback_result = context.ccr_svc.selective_rollback(
        context.ccr_plan_id,
        context.ccr_atomic_cp.checkpoint_id,
    )


@then("ccr the modified file should be reverted to its original state")
def step_ccr_file_reverted(context):
    readme = os.path.join(context.ccr_sandbox, "README.md")
    with open(readme) as f:
        content = f.read()
    assert content == context.ccr_original_readme


# ─── DI container wiring ─────────────────────────────────────────────


@given("ccr a fresh DI container")
def step_ccr_fresh_container(context):
    # Container will be resolved in the when step
    pass


@when("ccr I resolve the correction_service from the container")
def step_ccr_resolve_correction_service(context):
    from cleveragents.application.container import get_container

    container = get_container()
    context.ccr_container_correction_svc = container.correction_service()


@then("ccr the correction_service should have a non-None checkpoint_service")
def step_ccr_correction_has_checkpoint(context):
    svc = context.ccr_container_correction_svc
    assert svc._checkpoint_service is not None, (
        "CorrectionService._checkpoint_service should be wired by the DI container"
    )


# ─── CLI rollback --to-checkpoint ─────────────────────────────────────


@given("ccr a CLI runner")
def step_ccr_cli_runner(context):
    from typer.testing import CliRunner

    context.ccr_cli_runner = CliRunner()


@when("ccr I invoke plan rollback with --to-checkpoint option")
def step_ccr_cli_rollback_to_checkpoint(context):
    from cleveragents.cli.commands.plan import app as plan_app

    result = context.ccr_cli_runner.invoke(
        plan_app,
        ["rollback", "PLAN-DUMMY", "--to-checkpoint", "CP-DUMMY", "--yes"],
    )
    context.ccr_cli_result = result


@then("ccr the command should recognize the --to-checkpoint flag")
def step_ccr_cli_to_checkpoint_flag(context):
    result = context.ccr_cli_result
    # Typer/Click returns exit code 2 with "No such option" for unrecognized flags.
    # Any other exit code means the flag was accepted by the CLI parser.
    assert "No such option" not in result.output, (
        f"--to-checkpoint was not recognized: {result.output}"
    )
