"""Step definitions for lock_service_coverage.feature - uncovered branches."""

from __future__ import annotations

from datetime import UTC, datetime, timedelta
from typing import Any

from behave import given, then, when
from behave.runner import Context
from sqlalchemy import create_engine, event
from sqlalchemy.orm import Session, sessionmaker

from cleveragents.application.services.lock_service import LockService
from cleveragents.core.exceptions import (
    LockConflictError,
    LockExpiredError,
    ValidationError,
)
from cleveragents.infrastructure.database.models import Base, LockModel

# ------------------------------------------------------------------
# Helper: build a real in-memory LockService
# ------------------------------------------------------------------


def _make_lock_service(context: Context) -> LockService:
    """Create a LockService backed by an in-memory SQLite database."""
    engine = create_engine(
        "sqlite:///:memory:",
        echo=False,
        future=True,
        connect_args={"check_same_thread": False},
    )

    @event.listens_for(engine, "connect")
    def _fk(dbapi_conn: Any, _rec: Any) -> None:
        cursor = dbapi_conn.cursor()
        cursor.execute("PRAGMA foreign_keys=ON")
        cursor.close()

    Base.metadata.create_all(engine)
    factory: sessionmaker[Session] = sessionmaker(
        bind=engine,
        expire_on_commit=False,
        autoflush=False,
        autocommit=False,
        class_=Session,
    )
    context.cov_engine = engine
    context.cov_session_factory = factory
    return LockService(session_factory=factory)


# ------------------------------------------------------------------
# Stub session that blows up on execute (for rollback tests)
# ------------------------------------------------------------------


class _StubResult:
    """Minimal stand-in for a SQLAlchemy result object."""

    rowcount: int = 0

    def scalar_one_or_none(self) -> None:
        """Return None."""
        return None

    def scalar_one(self) -> int:
        """Return 0."""
        return 0


class _ExplodingSession:
    """Session stub that raises RuntimeError on execute."""

    def __init__(self) -> None:
        """Initialise tracking flags."""
        self.rolled_back: bool = False
        self.committed: bool = False

    def execute(self, stmt: Any) -> Any:
        """Raise RuntimeError unconditionally."""
        raise RuntimeError("boom")

    def add(self, obj: Any) -> None:
        """No-op."""

    def commit(self) -> None:
        """Record commit."""
        self.committed = True

    def rollback(self) -> None:
        """Record rollback."""
        self.rolled_back = True


# ------------------------------------------------------------------
# Background
# ------------------------------------------------------------------


@given("I have a fresh lock service with in-memory database")
def step_fresh_lock_service(context: Context) -> None:
    """Create a fresh in-memory LockService for coverage scenarios."""
    context.cov_svc = _make_lock_service(context)
    context.cov_error = None
    context.cov_result = None
    context.cov_session = None


# ------------------------------------------------------------------
# Validation: release
# ------------------------------------------------------------------


@when(
    'I try to release with resource_type "{rtype}" owner "{owner}" resource_id "{rid}"'
)
def step_try_release_validation(
    context: Context, rtype: str, owner: str, rid: str
) -> None:
    """Attempt release with potentially invalid params."""
    try:
        context.cov_result = context.cov_svc.release(
            owner_id=owner,
            resource_type=rtype,
            resource_id=rid,
        )
    except ValidationError as exc:
        context.cov_error = exc


@when("I try to release with an empty resource_type")
def step_try_release_empty_rtype(context: Context) -> None:
    """Attempt release with empty resource_type."""
    try:
        context.cov_svc.release(owner_id="o", resource_type="", resource_id="r1")
    except ValidationError as exc:
        context.cov_error = exc


@when("I try to release with an empty resource_id")
def step_try_release_empty_rid(context: Context) -> None:
    """Attempt release with empty resource_id."""
    try:
        context.cov_svc.release(owner_id="o", resource_type="plan", resource_id="")
    except ValidationError as exc:
        context.cov_error = exc


# ------------------------------------------------------------------
# Validation: renew
# ------------------------------------------------------------------


@when('I try to renew with resource_type "{rtype}" owner "{owner}" resource_id "{rid}"')
def step_try_renew_validation(
    context: Context, rtype: str, owner: str, rid: str
) -> None:
    """Attempt renew with potentially invalid params."""
    try:
        context.cov_svc.renew(
            owner_id=owner,
            resource_type=rtype,
            resource_id=rid,
        )
    except ValidationError as exc:
        context.cov_error = exc


@when(
    'I try to renew with TTL {ttl:d} owner "{owner}" resource_type "{rtype}" resource_id "{rid}"'
)
def step_try_renew_bad_ttl(
    context: Context, ttl: int, owner: str, rtype: str, rid: str
) -> None:
    """Attempt renew with out-of-range TTL."""
    try:
        context.cov_svc.renew(
            owner_id=owner,
            resource_type=rtype,
            resource_id=rid,
            ttl_seconds=ttl,
        )
    except ValidationError as exc:
        context.cov_error = exc


@when("I try to renew with an empty resource_type")
def step_try_renew_empty_rtype(context: Context) -> None:
    """Attempt renew with empty resource_type."""
    try:
        context.cov_svc.renew(owner_id="o", resource_type="", resource_id="r1")
    except ValidationError as exc:
        context.cov_error = exc


@when("I try to renew with an empty resource_id")
def step_try_renew_empty_rid(context: Context) -> None:
    """Attempt renew with empty resource_id."""
    try:
        context.cov_svc.renew(owner_id="o", resource_type="plan", resource_id="")
    except ValidationError as exc:
        context.cov_error = exc


# ------------------------------------------------------------------
# Validation: is_locked
# ------------------------------------------------------------------


@when('I try to check is_locked with resource_type "{rtype}" resource_id "{rid}"')
def step_try_is_locked_validation(context: Context, rtype: str, rid: str) -> None:
    """Attempt is_locked with potentially invalid params."""
    try:
        context.cov_result = context.cov_svc.is_locked(
            resource_type=rtype,
            resource_id=rid,
        )
    except ValidationError as exc:
        context.cov_error = exc


@when("I try to check is_locked with an empty resource_type")
def step_try_is_locked_empty_rtype(context: Context) -> None:
    """Attempt is_locked with empty resource_type."""
    try:
        context.cov_svc.is_locked(resource_type="", resource_id="r1")
    except ValidationError as exc:
        context.cov_error = exc


@when("I try to check is_locked with an empty resource_id")
def step_try_is_locked_empty_rid(context: Context) -> None:
    """Attempt is_locked with empty resource_id."""
    try:
        context.cov_svc.is_locked(resource_type="plan", resource_id="")
    except ValidationError as exc:
        context.cov_error = exc


# ------------------------------------------------------------------
# Validation assertion
# ------------------------------------------------------------------


@then(
    'a lock_service validation error should be raised with message containing "{fragment}"'
)
def step_assert_validation_error_message(context: Context, fragment: str) -> None:
    """Assert a ValidationError was raised whose message contains fragment."""
    assert isinstance(context.cov_error, ValidationError), (
        f"Expected ValidationError, got {type(context.cov_error).__name__}: {context.cov_error}"
    )
    assert fragment in str(context.cov_error), (
        f"Expected '{fragment}' in '{context.cov_error}'"
    )


# ------------------------------------------------------------------
# TTL boundary / custom TTL acquire
# ------------------------------------------------------------------


@when(
    'I acquire a lock with TTL {ttl:d} owner "{owner}" resource_type "{rtype}" resource_id "{rid}"'
)
def step_acquire_with_ttl(
    context: Context, ttl: int, owner: str, rtype: str, rid: str
) -> None:
    """Acquire a lock with an explicit TTL."""
    context.cov_result = context.cov_svc.acquire(
        owner_id=owner,
        resource_type=rtype,
        resource_id=rid,
        ttl_seconds=ttl,
    )


@then("the lock_service result should be true")
def step_result_true(context: Context) -> None:
    """Assert the result is True."""
    assert context.cov_result is True, f"Expected True, got {context.cov_result!r}"


@then("the lock_service result should be false")
def step_result_false(context: Context) -> None:
    """Assert the result is False."""
    assert context.cov_result is False, f"Expected False, got {context.cov_result!r}"


# ------------------------------------------------------------------
# Re-entrant project lock
# ------------------------------------------------------------------


@given('owner "{owner}" already holds a project lock on "{rid}"')
def step_owner_holds_project_lock(context: Context, owner: str, rid: str) -> None:
    """Pre-acquire a project lock."""
    context.cov_svc.acquire(
        owner_id=owner,
        resource_type="project",
        resource_id=rid,
    )


@given('owner "{owner}" already holds a plan lock on "{rid}"')
def step_owner_holds_plan_lock(context: Context, owner: str, rid: str) -> None:
    """Pre-acquire a plan lock."""
    context.cov_svc.acquire(
        owner_id=owner,
        resource_type="plan",
        resource_id=rid,
    )


# ------------------------------------------------------------------
# is_locked on expired lock
# ------------------------------------------------------------------


@given('an expired lock exists for owner "{owner}" on plan "{plan_id}"')
def step_insert_expired_lock(context: Context, owner: str, plan_id: str) -> None:
    """Insert an already-expired lock directly into the database."""
    now = datetime.now(tz=UTC)
    expired_at = (now - timedelta(seconds=60)).isoformat()
    acquired_at = (now - timedelta(seconds=120)).isoformat()

    session: Session = context.cov_session_factory()
    lock = LockModel(
        owner_id=owner,
        resource_type="plan",
        resource_id=plan_id,
        acquired_at=acquired_at,
        expires_at=expired_at,
    )
    session.add(lock)
    session.commit()
    session.close()


@when('I check is_locked for resource_type "{rtype}" resource_id "{rid}"')
def step_check_is_locked_cov(context: Context, rtype: str, rid: str) -> None:
    """Check is_locked on a resource."""
    context.cov_result = context.cov_svc.is_locked(
        resource_type=rtype,
        resource_id=rid,
    )


# ------------------------------------------------------------------
# Zero-count paths
# ------------------------------------------------------------------


@when('I release all locks for owner_id "{owner}"')
def step_release_all_cov(context: Context, owner: str) -> None:
    """Release all locks for an owner via the coverage service."""
    context.cov_result = context.cov_svc.release_all_for_owner(owner_id=owner)


@when("I run cleanup of expired locks")
def step_cleanup_expired_cov(context: Context) -> None:
    """Run cleanup_expired via the coverage service."""
    context.cov_result = context.cov_svc.cleanup_expired()


@when("I count stale locks via the service")
def step_count_stale_cov(context: Context) -> None:
    """Count stale locks via the coverage service."""
    context.cov_result = context.cov_svc.count_stale_locks()


@then("the lock_service integer result should be {expected:d}")
def step_assert_int_result(context: Context, expected: int) -> None:
    """Assert the result equals an expected integer."""
    assert context.cov_result == expected, (
        f"Expected {expected}, got {context.cov_result!r}"
    )


# ------------------------------------------------------------------
# Exception rollback paths (mocked session)
# ------------------------------------------------------------------


@given("a lock service whose session raises an unexpected error on execute")
def step_exploding_session_service(context: Context) -> None:
    """Build a LockService whose session factory returns an exploding session."""
    exploding = _ExplodingSession()
    context.cov_session = exploding
    context.cov_svc = LockService(session_factory=lambda: exploding)  # type: ignore[arg-type]
    context.cov_error = None
    context.cov_result = None


@when("I try to acquire and expect a RuntimeError")
def step_try_acquire_runtime_error(context: Context) -> None:
    """Attempt acquire expecting a RuntimeError from the stub session."""
    try:
        context.cov_svc.acquire(
            owner_id="owner-x",
            resource_type="plan",
            resource_id="res-x",
        )
    except RuntimeError as exc:
        context.cov_error = exc


@when("I try to release and expect a RuntimeError")
def step_try_release_runtime_error(context: Context) -> None:
    """Attempt release expecting a RuntimeError from the stub session."""
    try:
        context.cov_svc.release(
            owner_id="owner-x",
            resource_type="plan",
            resource_id="res-x",
        )
    except RuntimeError as exc:
        context.cov_error = exc


@when("I try to renew and expect a RuntimeError")
def step_try_renew_runtime_error(context: Context) -> None:
    """Attempt renew expecting a RuntimeError from the stub session."""
    try:
        context.cov_svc.renew(
            owner_id="owner-x",
            resource_type="plan",
            resource_id="res-x",
        )
    except RuntimeError as exc:
        context.cov_error = exc


@when("I try to release_all_for_owner and expect a RuntimeError")
def step_try_release_all_runtime_error(context: Context) -> None:
    """Attempt release_all_for_owner expecting RuntimeError."""
    try:
        context.cov_svc.release_all_for_owner(owner_id="owner-x")
    except RuntimeError as exc:
        context.cov_error = exc


@when("I try to cleanup_expired and expect a RuntimeError")
def step_try_cleanup_runtime_error(context: Context) -> None:
    """Attempt cleanup_expired expecting RuntimeError."""
    try:
        context.cov_svc.cleanup_expired()
    except RuntimeError as exc:
        context.cov_error = exc


@then("a RuntimeError should have been raised")
def step_assert_runtime_error(context: Context) -> None:
    """Assert that a RuntimeError was captured."""
    assert isinstance(context.cov_error, RuntimeError), (
        f"Expected RuntimeError, got {type(context.cov_error).__name__}: {context.cov_error}"
    )


@then("the session should have been rolled back")
def step_assert_session_rolled_back(context: Context) -> None:
    """Assert the stub session's rollback method was called."""
    assert context.cov_session is not None, "No stub session on context"
    assert context.cov_session.rolled_back is True, (
        "Expected session.rollback() to have been called"
    )


# ------------------------------------------------------------------
# Acquire conflict rollback path (real DB)
# ------------------------------------------------------------------


@given('a lock service with a real database and owner "{owner}" holds plan "{plan_id}"')
def step_real_db_with_holder(context: Context, owner: str, plan_id: str) -> None:
    """Build a real-DB lock service and pre-acquire a lock."""
    context.cov_svc = _make_lock_service(context)
    context.cov_error = None
    context.cov_result = None
    context.cov_svc.acquire(
        owner_id=owner,
        resource_type="plan",
        resource_id=plan_id,
    )


@when('I try to acquire plan "{plan_id}" as "{owner}" and capture the conflict error')
def step_try_acquire_conflict(context: Context, plan_id: str, owner: str) -> None:
    """Attempt acquire expecting a LockConflictError."""
    try:
        context.cov_svc.acquire(
            owner_id=owner,
            resource_type="plan",
            resource_id=plan_id,
        )
    except LockConflictError as exc:
        context.cov_error = exc


@then("a LockConflictError should have been raised")
def step_assert_lock_conflict(context: Context) -> None:
    """Assert that a LockConflictError was captured."""
    assert isinstance(context.cov_error, LockConflictError), (
        f"Expected LockConflictError, got {type(context.cov_error).__name__}"
    )


@then('the conflict owner should be "{expected}"')
def step_assert_conflict_owner(context: Context, expected: str) -> None:
    """Assert the conflict error reports the expected owner."""
    err = context.cov_error
    assert isinstance(err, LockConflictError), "Not a LockConflictError"
    assert err.owner_id == expected, (
        f"Expected conflict owner '{expected}', got '{err.owner_id}'"
    )


# ------------------------------------------------------------------
# Renew LockExpiredError rollback path (real DB)
# ------------------------------------------------------------------


@given(
    'a lock service with a real database and an expired lock for "{owner}" on plan "{plan_id}"'
)
def step_real_db_with_expired_lock(context: Context, owner: str, plan_id: str) -> None:
    """Build a real-DB lock service and insert an expired lock."""
    context.cov_svc = _make_lock_service(context)
    context.cov_error = None
    context.cov_result = None

    now = datetime.now(tz=UTC)
    expired_at = (now - timedelta(seconds=60)).isoformat()
    acquired_at = (now - timedelta(seconds=120)).isoformat()

    session: Session = context.cov_session_factory()
    lock = LockModel(
        owner_id=owner,
        resource_type="plan",
        resource_id=plan_id,
        acquired_at=acquired_at,
        expires_at=expired_at,
    )
    session.add(lock)
    session.commit()
    session.close()


@when('I try to renew plan "{plan_id}" as "{owner}" and capture the expired error')
def step_try_renew_expired_cov(context: Context, plan_id: str, owner: str) -> None:
    """Attempt renew expecting a LockExpiredError."""
    try:
        context.cov_svc.renew(
            owner_id=owner,
            resource_type="plan",
            resource_id=plan_id,
        )
    except LockExpiredError as exc:
        context.cov_error = exc


@then("a LockExpiredError should have been raised")
def step_assert_lock_expired(context: Context) -> None:
    """Assert that a LockExpiredError was captured."""
    assert isinstance(context.cov_error, LockExpiredError), (
        f"Expected LockExpiredError, got {type(context.cov_error).__name__}"
    )
