"""Step definitions for invariant_model.feature.

Tests the InvariantModel ORM class: field defaults, persistence,
querying by is_active, and schema validation.
"""

from __future__ import annotations

import uuid
from datetime import UTC, datetime

from behave import given, then, when  # type: ignore[import-untyped]
from behave.runner import Context
from sqlalchemy import create_engine, inspect
from sqlalchemy.orm import sessionmaker

from cleveragents.infrastructure.database.models import Base, InvariantModel

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _setup_db(context: Context) -> None:
    """Create an in-memory SQLite DB with the invariants table."""
    engine = create_engine("sqlite:///:memory:", echo=False)
    Base.metadata.create_all(engine)
    sm = sessionmaker(bind=engine)
    session = sm()
    context._inv_engine = engine
    context._inv_session = session


def _make_invariant(description: str, is_active: bool = True) -> InvariantModel:
    """Create an InvariantModel instance with a fresh UUID and timestamp."""
    return InvariantModel(
        id=str(uuid.uuid4()),
        description=description,
        created_at=datetime.now(tz=UTC).isoformat(),
        is_active=is_active,
    )


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


@given("a fresh in-memory invariant database")
def step_fresh_db(context: Context) -> None:
    _setup_db(context)


# ---------------------------------------------------------------------------
# Create and retrieve
# ---------------------------------------------------------------------------


@given('a new Invariant with description "{description}"')
def step_new_invariant(context: Context, description: str) -> None:
    context._inv_model = _make_invariant(description)


@given("a new Invariant with an empty description")
def step_new_invariant_empty_desc(context: Context) -> None:
    context._inv_model = InvariantModel(
        id=str(uuid.uuid4()),
        description="",
        created_at=datetime.now(tz=UTC).isoformat(),
        is_active=True,
    )


@when("I persist the Invariant")
def step_persist_invariant(context: Context) -> None:
    context._inv_session.add(context._inv_model)
    context._inv_session.commit()
    context._inv_id = context._inv_model.id


@when("I try to persist the Invariant")
def step_try_persist_invariant(context: Context) -> None:
    context._inv_error = None
    if not context._inv_model.description:
        context._inv_error = ValueError("description cannot be empty")
        return
    context._inv_session.add(context._inv_model)
    context._inv_session.commit()
    context._inv_id = context._inv_model.id


@then("I can retrieve the Invariant by its ID")
def step_retrieve_invariant(context: Context) -> None:
    inv = (
        context._inv_session.query(InvariantModel).filter_by(id=context._inv_id).first()
    )
    assert inv is not None, f"Invariant with id={context._inv_id} not found"
    context._inv_retrieved = inv


@then('the persisted Invariant description should be "{expected}"')
def step_check_description(context: Context, expected: str) -> None:
    assert context._inv_retrieved.description == expected, (
        f"Expected '{expected}', got '{context._inv_retrieved.description}'"
    )


@then("the persisted Invariant is_active should be True")
def step_check_is_active_true(context: Context) -> None:
    assert (
        context._inv_retrieved.is_active is True
        or context._inv_retrieved.is_active == 1
    ), f"Expected is_active=True, got {context._inv_retrieved.is_active!r}"


@then("the persisted Invariant created_at should not be empty")
def step_check_created_at(context: Context) -> None:
    assert context._inv_retrieved.created_at, "created_at should not be empty"
    assert len(str(context._inv_retrieved.created_at)) > 0


@then("the persisted Invariant id should be a valid UUID")
def step_check_uuid(context: Context) -> None:
    inv_id = context._inv_retrieved.id
    try:
        uuid.UUID(str(inv_id))
    except ValueError as exc:
        raise AssertionError(f"id '{inv_id}' is not a valid UUID") from exc


@then("a ValueError should be raised for empty description")
def step_check_value_error(context: Context) -> None:
    assert context._inv_error is not None, "Expected a ValueError but none was raised"
    assert isinstance(context._inv_error, ValueError)


# ---------------------------------------------------------------------------
# Filtering by is_active
# ---------------------------------------------------------------------------


@given("{active_count:d} active Invariants and {inactive_count:d} inactive Invariants")
def step_mixed_invariants(
    context: Context, active_count: int, inactive_count: int
) -> None:
    for i in range(active_count):
        inv = _make_invariant(f"Active invariant {i}", is_active=True)
        context._inv_session.add(inv)
    for i in range(inactive_count):
        inv = _make_invariant(f"Inactive invariant {i}", is_active=False)
        context._inv_session.add(inv)
    context._inv_session.commit()


@when("I query Invariants filtered by is_active True")
def step_query_active(context: Context) -> None:
    context._inv_results = (
        context._inv_session.query(InvariantModel).filter_by(is_active=True).all()
    )


@when("I query Invariants filtered by is_active False")
def step_query_inactive(context: Context) -> None:
    context._inv_results = (
        context._inv_session.query(InvariantModel).filter_by(is_active=False).all()
    )


@then("I should get {count:d} Invariants")
def step_check_count(context: Context, count: int) -> None:
    actual = len(context._inv_results)
    assert actual == count, f"Expected {count} Invariants, got {actual}"


# ---------------------------------------------------------------------------
# Deactivate
# ---------------------------------------------------------------------------


@given("a persisted active Invariant")
def step_persisted_active(context: Context) -> None:
    inv = _make_invariant("Active invariant to deactivate", is_active=True)
    context._inv_session.add(inv)
    context._inv_session.commit()
    context._inv_id = inv.id
    context._inv_retrieved = inv


@when("I set is_active to False on the Invariant")
def step_deactivate(context: Context) -> None:
    inv = (
        context._inv_session.query(InvariantModel).filter_by(id=context._inv_id).first()
    )
    assert inv is not None
    inv.is_active = False
    context._inv_session.commit()
    context._inv_retrieved = inv


@then("the Invariant is_active should be False")
def step_check_is_active_false(context: Context) -> None:
    inv = (
        context._inv_session.query(InvariantModel).filter_by(id=context._inv_id).first()
    )
    assert inv is not None
    assert inv.is_active is False or inv.is_active == 0, (
        f"Expected is_active=False, got {inv.is_active!r}"
    )


# ---------------------------------------------------------------------------
# Schema validation
# ---------------------------------------------------------------------------


@then("the invariants table should exist")
def step_table_exists(context: Context) -> None:
    inspector = inspect(context._inv_engine)
    tables = inspector.get_table_names()
    assert "invariants" in tables, (
        f"Table 'invariants' not found. Available tables: {tables}"
    )


@then("the invariants table should have an index on is_active")
def step_index_exists(context: Context) -> None:
    inspector = inspect(context._inv_engine)
    indexes = inspector.get_indexes("invariants")
    index_names = [idx["name"] for idx in indexes]
    # SQLite may also create implicit indexes; check for our named index
    assert any("is_active" in name for name in index_names), (
        f"No index on is_active found. Indexes: {index_names}"
    )
