"""Step definitions for transaction_sandbox_coverage.feature.

These steps target specific uncovered lines in transaction_sandbox.py:
- Line 102: sandbox_id property getter
- Line 112: context property getter
- Lines 147-151: sqlite3.Error handler in create()
- Lines 237-241: sqlite3.Error handler in commit()
- Lines 282-286: sqlite3.Error handler in rollback()
- Line 366: SandboxStateError when connection is None in execute()
"""

import sqlite3
from unittest.mock import MagicMock, patch

from behave import given, then, when

from cleveragents.infrastructure.sandbox.protocol import (
    SandboxCommitError,
    SandboxCreationError,
    SandboxRollbackError,
    SandboxStateError,
    SandboxStatus,
)
from cleveragents.infrastructure.sandbox.transaction_sandbox import TransactionSandbox


# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("the transaction sandbox module is imported")
def step_module_imported(context):
    """Ensure the module is importable."""
    assert TransactionSandbox is not None


# ---------------------------------------------------------------------------
# Scenario: sandbox_id property  (line 102)
# ---------------------------------------------------------------------------
@given('I have a fresh TransactionSandbox for resource "{res}" and path "{path}"')
def step_create_fresh_sandbox(context, res, path):
    """Instantiate a TransactionSandbox without calling create()."""
    context.sandbox = TransactionSandbox(resource_id=res, original_path=path)


@when("I access the sandbox_id property")
def step_access_sandbox_id(context):
    """Read the sandbox_id property to hit line 102."""
    context.sandbox_id_value = context.sandbox.sandbox_id


@then("the sandbox_id should be a non-empty string")
def step_verify_sandbox_id(context):
    """Verify sandbox_id is a non-empty ULID string."""
    assert isinstance(context.sandbox_id_value, str)
    assert len(context.sandbox_id_value) > 0


# ---------------------------------------------------------------------------
# Scenario: context property  (line 112)
# ---------------------------------------------------------------------------
@when("I access the context property before calling create")
def step_access_context_before_create(context):
    """Read the context property before create() to hit line 112."""
    context.context_before = context.sandbox.context


@then("the context should be None")
def step_verify_context_none(context):
    """Verify context is None before create."""
    assert context.context_before is None


@when('tscov I create the sandbox with plan "{plan_id}"')
def step_create_sandbox(context, plan_id):
    """Call create() on the sandbox."""
    context.sandbox_context = context.sandbox.create(plan_id)


@when("I access the context property after calling create")
def step_access_context_after_create(context):
    """Read the context property after create() to hit line 112 again."""
    context.context_after = context.sandbox.context


@then('the context should contain plan id "{plan_id}"')
def step_verify_context_plan_id(context, plan_id):
    """Verify context has the expected plan_id."""
    assert context.context_after is not None
    assert context.context_after.plan_id == plan_id


# ---------------------------------------------------------------------------
# Scenario: SandboxCreationError  (lines 147-151)
# ---------------------------------------------------------------------------
@given("I have a TransactionSandbox whose connection will fail during create")
def step_sandbox_with_failing_connect(context):
    """Create a sandbox and arrange for sqlite3.connect to fail."""
    context.sandbox = TransactionSandbox(
        resource_id="res-fail", original_path=":memory:"
    )


@when('I attempt to create the failing sandbox with plan "{plan_id}"')
def step_attempt_create_failing(context, plan_id):
    """Call create() with a mocked sqlite3.connect that raises sqlite3.Error."""
    context.caught_error = None
    with patch(
        "cleveragents.infrastructure.sandbox.transaction_sandbox.sqlite3.connect",
        side_effect=sqlite3.OperationalError("disk I/O error"),
    ):
        try:
            context.sandbox.create(plan_id)
        except SandboxCreationError as exc:
            context.caught_error = exc


@then("a SandboxCreationError should be raised")
def step_verify_creation_error(context):
    """Verify a SandboxCreationError was caught."""
    assert context.caught_error is not None
    assert isinstance(context.caught_error, SandboxCreationError)


@then("the sandbox status should be errored")
def step_verify_errored(context):
    """Verify the sandbox transitioned to ERRORED status."""
    assert context.sandbox.status == SandboxStatus.ERRORED


# ---------------------------------------------------------------------------
# Scenario: SandboxCommitError  (lines 237-241)
# ---------------------------------------------------------------------------
@given("I have an active TransactionSandbox with a connection that fails on COMMIT")
def step_sandbox_commit_fail(context):
    """Create, activate a sandbox, then replace connection to fail on COMMIT."""
    sb = TransactionSandbox(resource_id="res-commit-fail", original_path=":memory:")
    sb.create("plan-commit-fail")
    # Execute something to move to ACTIVE status
    sb.execute("SELECT 1")

    # Replace the connection with a mock that raises on COMMIT
    mock_conn = MagicMock()
    mock_conn.execute = MagicMock(side_effect=sqlite3.OperationalError("journal error"))
    sb._connection = mock_conn
    context.sandbox = sb


@when("I attempt to commit the sandbox")
def step_attempt_commit(context):
    """Call commit() and expect an error."""
    context.caught_error = None
    try:
        context.sandbox.commit("test commit")
    except SandboxCommitError as exc:
        context.caught_error = exc


@then("a SandboxCommitError should be raised")
def step_verify_commit_error(context):
    """Verify a SandboxCommitError was caught."""
    assert context.caught_error is not None
    assert isinstance(context.caught_error, SandboxCommitError)


# ---------------------------------------------------------------------------
# Scenario: SandboxRollbackError  (lines 282-286)
# ---------------------------------------------------------------------------
@given("I have an active TransactionSandbox with a connection that fails on ROLLBACK")
def step_sandbox_rollback_fail(context):
    """Create, activate a sandbox, then replace connection to fail on ROLLBACK."""
    sb = TransactionSandbox(resource_id="res-rollback-fail", original_path=":memory:")
    sb.create("plan-rollback-fail")
    # Execute something to move to ACTIVE status
    sb.execute("SELECT 1")

    # Replace the connection with a mock that raises on ROLLBACK
    mock_conn = MagicMock()
    mock_conn.execute = MagicMock(
        side_effect=sqlite3.OperationalError("database is locked")
    )
    sb._connection = mock_conn
    context.sandbox = sb


@when("I attempt to rollback the sandbox")
def step_attempt_rollback(context):
    """Call rollback() and expect an error."""
    context.caught_error = None
    try:
        context.sandbox.rollback()
    except SandboxRollbackError as exc:
        context.caught_error = exc


@then("a SandboxRollbackError should be raised")
def step_verify_rollback_error(context):
    """Verify a SandboxRollbackError was caught."""
    assert context.caught_error is not None
    assert isinstance(context.caught_error, SandboxRollbackError)


# ---------------------------------------------------------------------------
# Scenario: Rollback from COMMITTED raises SandboxRollbackError
# ---------------------------------------------------------------------------
@when('tscov I execute SQL "SELECT 1" to activate the sandbox')
def step_tscov_execute_to_activate(context):
    """Execute a SQL statement to transition the sandbox to ACTIVE."""
    context.sandbox.execute("SELECT 1")


@when("tscov I commit the sandbox")
def step_tscov_commit(context):
    """Commit the sandbox."""
    context.sandbox.commit("test commit")


@then('tscov the sandbox status should be "{status}"')
def step_tscov_verify_status(context, status):
    """Verify the sandbox status."""
    expected = SandboxStatus(status)
    assert context.sandbox.status == expected, (
        f"Expected status {expected}, got {context.sandbox.status}"
    )


@when("tscov I attempt to rollback the committed sandbox")
def step_tscov_attempt_rollback_committed(context):
    """Call rollback() on a COMMITTED sandbox and expect SandboxRollbackError."""
    context.caught_error = None
    try:
        context.sandbox.rollback()
    except SandboxRollbackError as exc:
        context.caught_error = exc


@then("a SandboxRollbackError should be raised about irreversible commit")
def step_tscov_verify_irreversible_rollback(context):
    """Verify a SandboxRollbackError about irreversible COMMIT was raised."""
    assert context.caught_error is not None
    assert isinstance(context.caught_error, SandboxRollbackError)
    assert "cannot undo database commit" in str(context.caught_error).lower(), (
        f"Expected message about irreversible commit, got: {context.caught_error}"
    )


# ---------------------------------------------------------------------------
# Scenario: execute with None connection  (line 366)
# ---------------------------------------------------------------------------
@given("I have a TransactionSandbox in active state with no database connection")
def step_sandbox_no_connection(context):
    """Create and activate a sandbox, then remove its connection."""
    sb = TransactionSandbox(resource_id="res-no-conn", original_path=":memory:")
    sb.create("plan-no-conn")
    # Force connection to None while keeping CREATED status (valid for execute)
    sb._connection = None
    context.sandbox = sb


@when('I attempt to execute SQL "{sql}" on the sandbox')
def step_attempt_execute(context, sql):
    """Call execute() and expect a SandboxStateError."""
    context.caught_error = None
    try:
        context.sandbox.execute(sql)
    except SandboxStateError as exc:
        context.caught_error = exc


@then("a SandboxStateError about missing connection should be raised")
def step_verify_no_connection_error(context):
    """Verify a SandboxStateError about missing connection was raised."""
    assert context.caught_error is not None
    assert isinstance(context.caught_error, SandboxStateError)
    assert "connection not available" in str(context.caught_error).lower()
