"""Step definitions for resource_handler_sandbox.feature.

Tests sandbox lifecycle, checkpoint/rollback, and project_access for
GitCheckoutHandler and FsDirectoryHandler.

All step text uses the ``rh836-`` prefix to avoid collisions.

Issue #836: ResourceHandler sandbox and checkpoint methods.
"""

from __future__ import annotations

import subprocess
import tempfile
from pathlib import Path
from unittest.mock import MagicMock

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

from cleveragents.domain.models.core.resource import (
    PhysVirt,
    Resource,
    ResourceCapabilities,
)
from cleveragents.infrastructure.sandbox.factory import SandboxFactory
from cleveragents.infrastructure.sandbox.manager import SandboxManager
from cleveragents.resource.handlers.database import DatabaseResourceHandler
from cleveragents.resource.handlers.fs_directory import FsDirectoryHandler
from cleveragents.resource.handlers.git_checkout import GitCheckoutHandler

__all__: list[str] = []

_COUNTER = 0
_CB32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"


def _next_id() -> str:
    """Generate a valid 26-char Crockford Base32 ID for tests."""
    global _COUNTER
    _COUNTER += 1
    n = _COUNTER + 0x836000
    chars: list[str] = []
    for _ in range(26):
        chars.append(_CB32[n % 32])
        n //= 32
    return "".join(reversed(chars))


def _make_resource(rtype: str, location: str) -> Resource:
    return Resource(
        resource_id=_next_id(),
        resource_type_name=rtype,
        classification=PhysVirt.PHYSICAL,
        location=location,
        capabilities=ResourceCapabilities(
            readable=True, writable=True, sandboxable=True
        ),
    )


# ---------------------------------------------------------------------------
# Given: temp directories and git repos
# ---------------------------------------------------------------------------


@given('rh836- a temp directory with file "{fname}" containing "{content}"')
def step_rh836_temp_dir(context: Context, fname: str, content: str) -> None:
    context.rh836_tmpdir = tempfile.mkdtemp(prefix="rh836_fs_")
    Path(context.rh836_tmpdir, fname).write_text(content, encoding="utf-8")


@given("rh836- an fs-directory resource and sandbox manager")
def step_rh836_fs_resource_and_manager(context: Context) -> None:
    context.rh836_handler = FsDirectoryHandler()
    context.rh836_resource = _make_resource("fs-directory", context.rh836_tmpdir)
    context.rh836_plan_id = _next_id()
    factory = SandboxFactory()
    context.rh836_sandbox_mgr = SandboxManager(factory=factory, cleanup_on_exit=False)


@given('rh836- a temp git repo with file "{fname}" containing "{content}"')
def step_rh836_temp_git_repo(context: Context, fname: str, content: str) -> None:
    context.rh836_tmpdir = tempfile.mkdtemp(prefix="rh836_git_")
    subprocess.run(
        ["git", "init"], cwd=context.rh836_tmpdir, capture_output=True, check=True
    )
    subprocess.run(
        ["git", "config", "user.email", "test@test.com"],
        cwd=context.rh836_tmpdir,
        capture_output=True,
        check=True,
    )
    subprocess.run(
        ["git", "config", "user.name", "Test"],
        cwd=context.rh836_tmpdir,
        capture_output=True,
        check=True,
    )
    Path(context.rh836_tmpdir, fname).write_text(content, encoding="utf-8")
    subprocess.run(
        ["git", "add", "."], cwd=context.rh836_tmpdir, capture_output=True, check=True
    )
    subprocess.run(
        ["git", "commit", "-m", "init"],
        cwd=context.rh836_tmpdir,
        capture_output=True,
        check=True,
    )


@given("rh836- a git-checkout resource and sandbox manager")
def step_rh836_git_resource_and_manager(context: Context) -> None:
    context.rh836_handler = GitCheckoutHandler()
    context.rh836_resource = _make_resource("git-checkout", context.rh836_tmpdir)
    context.rh836_plan_id = _next_id()
    factory = SandboxFactory()
    context.rh836_sandbox_mgr = SandboxManager(factory=factory, cleanup_on_exit=False)


@given("rh836- a database handler and dummy resource")
def step_rh836_db_handler(context: Context) -> None:
    context.rh836_handler = DatabaseResourceHandler()
    context.rh836_resource = _make_resource("postgres", "/tmp/fake-db")
    context.rh836_plan_id = _next_id()
    context.rh836_sandbox_mgr = MagicMock(spec=SandboxManager)


# ---------------------------------------------------------------------------
# When: sandbox operations
# ---------------------------------------------------------------------------


@when("rh836- I call create_sandbox on the fs-directory handler")
@when("rh836- I call create_sandbox on the git-checkout handler")
def step_rh836_create_sandbox(context: Context) -> None:
    context.rh836_sandbox_result = context.rh836_handler.create_sandbox(
        resource=context.rh836_resource,
        plan_id=context.rh836_plan_id,
        sandbox_manager=context.rh836_sandbox_mgr,
    )


@when("rh836- I call create_sandbox on the fs-directory handler again")
@when("rh836- I call create_sandbox on the git-checkout handler again")
def step_rh836_create_sandbox_again(context: Context) -> None:
    context.rh836_sandbox_result_2 = context.rh836_handler.create_sandbox(
        resource=context.rh836_resource,
        plan_id=context.rh836_plan_id,
        sandbox_manager=context.rh836_sandbox_mgr,
    )


@when("rh836- I create a checkpoint on the fs-directory handler")
@when("rh836- I create a checkpoint on the git-checkout handler")
def step_rh836_create_checkpoint(context: Context) -> None:
    context.rh836_checkpoint_result = context.rh836_handler.create_checkpoint(
        resource=context.rh836_resource,
        plan_id=context.rh836_plan_id,
        sandbox_manager=context.rh836_sandbox_mgr,
    )


@when('rh836- I modify the sandbox file "{fname}" to "{content}"')
def step_rh836_modify_sandbox_file(context: Context, fname: str, content: str) -> None:
    sandbox_path = context.rh836_sandbox_result.sandbox_path
    Path(sandbox_path, fname).write_text(content, encoding="utf-8")


@when("rh836- I rollback the fs-directory handler to the checkpoint")
@when("rh836- I rollback the git-checkout handler to the checkpoint")
def step_rh836_rollback(context: Context) -> None:
    context.rh836_rollback_result = context.rh836_handler.rollback_to(
        resource=context.rh836_resource,
        plan_id=context.rh836_plan_id,
        checkpoint_id=context.rh836_checkpoint_result.checkpoint_id,
        sandbox_manager=context.rh836_sandbox_mgr,
    )


@when('rh836- I check project_access for principal "{principal}" action "{action}"')
def step_rh836_check_access(context: Context, principal: str, action: str) -> None:
    context.rh836_access_result = context.rh836_handler.project_access(
        resource=context.rh836_resource,
        principal=principal,
        action=action,
    )


@when("rh836- I call create_checkpoint on the database handler")
def step_rh836_db_checkpoint(context: Context) -> None:
    try:
        context.rh836_result = context.rh836_handler.create_checkpoint(
            resource=context.rh836_resource,
            plan_id=context.rh836_plan_id,
            sandbox_manager=context.rh836_sandbox_mgr,
        )
        context.rh836_error = None
    except Exception as exc:
        context.rh836_error = exc
        context.rh836_result = None


@when("rh836- I call rollback_to on the database handler")
def step_rh836_db_rollback(context: Context) -> None:
    try:
        context.rh836_result = context.rh836_handler.rollback_to(
            resource=context.rh836_resource,
            plan_id=context.rh836_plan_id,
            checkpoint_id="fake-checkpoint",
            sandbox_manager=context.rh836_sandbox_mgr,
        )
        context.rh836_error = None
    except Exception as exc:
        context.rh836_error = exc
        context.rh836_result = None


# ---------------------------------------------------------------------------
# Then: assertions
# ---------------------------------------------------------------------------


@then("rh836- the sandbox result should be created")
def step_rh836_sandbox_created(context: Context) -> None:
    assert context.rh836_sandbox_result.created is True


@then("rh836- the sandbox result path should exist")
def step_rh836_sandbox_path_exists(context: Context) -> None:
    assert Path(context.rh836_sandbox_result.sandbox_path).exists()


@then("rh836- the second sandbox result should not be created")
def step_rh836_second_not_created(context: Context) -> None:
    assert context.rh836_sandbox_result_2.created is False


@then("rh836- both sandbox results should have the same sandbox_id")
def step_rh836_same_sandbox_id(context: Context) -> None:
    assert (
        context.rh836_sandbox_result.sandbox_id
        == context.rh836_sandbox_result_2.sandbox_id
    )


@then("rh836- the rollback result should be successful")
def step_rh836_rollback_success(context: Context) -> None:
    assert context.rh836_rollback_result.success is True, (
        f"Rollback failed: {context.rh836_rollback_result.message}"
    )


@then('rh836- the sandbox file "{fname}" should contain "{expected}"')
def step_rh836_sandbox_file_contains(
    context: Context, fname: str, expected: str
) -> None:
    sandbox_path = context.rh836_sandbox_result.sandbox_path
    actual = Path(sandbox_path, fname).read_text(encoding="utf-8").strip()
    assert actual == expected, f"Expected '{expected}', got '{actual}'"


@then("rh836- the access result should be permitted")
def step_rh836_access_permitted(context: Context) -> None:
    assert context.rh836_access_result.permitted is True


@then('rh836- the access result reason should contain "{fragment}"')
def step_rh836_access_reason(context: Context, fragment: str) -> None:
    assert fragment in context.rh836_access_result.reason, (
        f"'{fragment}' not in '{context.rh836_access_result.reason}'"
    )


@then('rh836- a NotImplementedError should be raised containing "{fragment}"')
def step_rh836_not_implemented(context: Context, fragment: str) -> None:
    assert isinstance(context.rh836_error, NotImplementedError), (
        f"Expected NotImplementedError, got {type(context.rh836_error)}"
    )
    assert fragment in str(context.rh836_error), (
        f"'{fragment}' not in '{context.rh836_error}'"
    )


@then("rh836- a checkpoint result should be returned without error")
def step_rh836_checkpoint_result_no_error(context: Context) -> None:
    assert context.rh836_error is None, (
        f"Expected no error, got {type(context.rh836_error)}: {context.rh836_error}"
    )
    assert context.rh836_result is not None, "Expected a checkpoint result, got None"


@then("rh836- a rollback result should be returned without error")
def step_rh836_rollback_result_no_error(context: Context) -> None:
    assert context.rh836_error is None, (
        f"Expected no error, got {type(context.rh836_error)}: {context.rh836_error}"
    )
    assert context.rh836_result is not None, "Expected a rollback result, got None"
