"""Step definitions for PostgreSQL storage backend BDD scenarios.

Steps specific to the ``postgresql_backend.feature`` file.  The shared
``I have imported the database infrastructure modules`` step lives in
``database_infrastructure_steps.py`` and is reused here via Behave's
step-sharing mechanism.
"""

from __future__ import annotations

import inspect
import re

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

# ---------------------------------------------------------------------------
# Settings helpers
# ---------------------------------------------------------------------------


@given("I create settings with server_mode disabled")
def step_settings_local_mode(context: Context) -> None:
    """Create settings with local (SQLite) mode.

    Clears ``CLEVERAGENTS_SERVER_MODE`` and restores the default
    ``database_url`` so Settings resolves to SQLite.
    """
    import os

    from cleveragents.config.settings import Settings

    saved_db_url = os.environ.pop("CLEVERAGENTS_DATABASE_URL", None)
    os.environ.pop("CLEVERAGENTS_SERVER_MODE", None)
    try:
        context.settings = Settings()
    finally:
        if saved_db_url is not None:
            os.environ["CLEVERAGENTS_DATABASE_URL"] = saved_db_url


@given("I create settings with server_mode enabled")
def step_settings_server_mode(context: Context) -> None:
    """Create settings with server mode enabled.

    The ``before_scenario`` hook injects a per-scenario SQLite URL into
    ``CLEVERAGENTS_DATABASE_URL``.  We must clear it so that Settings
    falls back to its declared default (``sqlite:///cleveragents.db``),
    allowing ``resolve_database_url`` to detect the default and switch to
    PostgreSQL.
    """
    import os

    from cleveragents.config.settings import Settings

    saved_db_url = os.environ.pop("CLEVERAGENTS_DATABASE_URL", None)
    os.environ["CLEVERAGENTS_SERVER_MODE"] = "true"
    try:
        context.settings = Settings()
    finally:
        os.environ.pop("CLEVERAGENTS_SERVER_MODE", None)
        if saved_db_url is not None:
            os.environ["CLEVERAGENTS_DATABASE_URL"] = saved_db_url


@given("the database_url is the default SQLite value")
def step_database_url_is_default(context: Context) -> None:
    """Ensure database_url is the SQLite default.

    The default is already ``sqlite:///cleveragents.db`` so this is a
    no-op guard ensuring the previous step didn't override it.
    """
    assert context.settings.database_url.startswith("sqlite")


@given('the database_url is "{url}"')
def step_database_url_is(context: Context, url: str) -> None:
    """Recreate settings with an explicit database_url."""
    import os

    from cleveragents.config.settings import Settings

    os.environ["CLEVERAGENTS_DATABASE_URL"] = url
    server = "true" if context.settings.server_mode else "false"
    os.environ["CLEVERAGENTS_SERVER_MODE"] = server
    try:
        context.settings = Settings()
    finally:
        os.environ.pop("CLEVERAGENTS_DATABASE_URL", None)
        os.environ.pop("CLEVERAGENTS_SERVER_MODE", None)


@given("I create settings with custom pool configuration")
def step_settings_custom_pool(context: Context) -> None:
    """Create settings with custom pool values from the step table.

    Pydantic-settings reads from environment variables, so we set the
    ``CLEVERAGENTS_*`` env vars before constructing the Settings instance
    and clean them up afterward.  The ``before_scenario`` per-scenario
    database URL is temporarily cleared so the default applies.
    """
    import os

    from cleveragents.config.settings import Settings

    env_map: dict[str, str] = {
        "db_pool_size": "CLEVERAGENTS_DB_POOL_SIZE",
        "db_max_overflow": "CLEVERAGENTS_DB_MAX_OVERFLOW",
        "db_pool_recycle": "CLEVERAGENTS_DB_POOL_RECYCLE",
    }
    set_vars: list[str] = []

    saved_db_url = os.environ.pop("CLEVERAGENTS_DATABASE_URL", None)
    os.environ["CLEVERAGENTS_SERVER_MODE"] = "true"
    set_vars.append("CLEVERAGENTS_SERVER_MODE")

    if context.table is not None:
        for row in context.table:
            key = row["setting"]
            value = row["value"]
            env_name = env_map.get(key, f"CLEVERAGENTS_{key.upper()}")
            os.environ[env_name] = str(value)
            set_vars.append(env_name)

    try:
        context.settings = Settings()
    finally:
        for var in set_vars:
            os.environ.pop(var, None)
        if saved_db_url is not None:
            os.environ["CLEVERAGENTS_DATABASE_URL"] = saved_db_url


# ---------------------------------------------------------------------------
# Settings assertions
# ---------------------------------------------------------------------------


@then("resolving the database URL should raise a ValueError")
def step_resolve_url_raises_value_error(context: Context) -> None:
    """Assert that resolve_database_url raises ValueError in server mode."""
    try:
        context.settings.resolve_database_url()
    except ValueError:
        pass  # expected
    else:
        raise AssertionError("Expected ValueError from resolve_database_url()")


@then('the resolved database URL should start with "{prefix}"')
def step_resolved_url_starts_with(context: Context, prefix: str) -> None:
    """Assert the resolved database URL prefix."""
    url = context.settings.resolve_database_url()
    assert url.startswith(prefix), f"Expected URL to start with '{prefix}', got '{url}'"


@then('the resolved database URL should be "{expected}"')
def step_resolved_url_equals(context: Context, expected: str) -> None:
    """Assert the resolved database URL matches exactly."""
    url = context.settings.resolve_database_url()
    assert url == expected, f"Expected '{expected}', got '{url}'"


@then("is_postgresql should return True")
def step_is_postgresql_true(context: Context) -> None:
    """Assert is_postgresql returns True."""
    assert context.settings.is_postgresql(), "Expected is_postgresql() to be True"


@then("is_postgresql should return False")
def step_is_postgresql_false(context: Context) -> None:
    """Assert is_postgresql returns False."""
    assert not context.settings.is_postgresql(), "Expected is_postgresql() to be False"


@then("the db_pool_size should be {value:d}")
def step_pool_size_equals(context: Context, value: int) -> None:
    """Assert db_pool_size setting."""
    assert context.settings.db_pool_size == value, (
        f"Expected {value}, got {context.settings.db_pool_size}"
    )


@then("the db_max_overflow should be {value:d}")
def step_max_overflow_equals(context: Context, value: int) -> None:
    """Assert db_max_overflow setting."""
    assert context.settings.db_max_overflow == value, (
        f"Expected {value}, got {context.settings.db_max_overflow}"
    )


@then("the db_pool_recycle should be {value:d}")
def step_pool_recycle_equals(context: Context, value: int) -> None:
    """Assert db_pool_recycle setting."""
    assert context.settings.db_pool_recycle == value, (
        f"Expected {value}, got {context.settings.db_pool_recycle}"
    )


# ---------------------------------------------------------------------------
# UnitOfWork engine
# ---------------------------------------------------------------------------

_VALID_SQL_IDENTIFIER = re.compile(r"^[a-z_][a-z0-9_]*$")


@given('I create a UnitOfWork with URL "{url}"')
def step_create_uow(context: Context, url: str) -> None:
    """Create a UnitOfWork instance with the given URL."""
    from cleveragents.infrastructure.database.unit_of_work import UnitOfWork

    context.uow = UnitOfWork(
        url,
        prompt_for_migration=lambda _msg: True,
    )


@given('I create a UnitOfWork with URL "{url}" and pool_size {size:d}')
def step_create_uow_with_pool(context: Context, url: str, size: int) -> None:
    """Create a UnitOfWork with explicit pool_size."""
    from cleveragents.infrastructure.database.unit_of_work import UnitOfWork

    context.uow = UnitOfWork(
        url,
        prompt_for_migration=lambda _msg: True,
        pool_size=size,
    )


@when("I access the engine")
def step_access_engine(context: Context) -> None:
    """Trigger engine creation."""
    context.engine = context.uow.engine


@then('the engine dialect should be "{dialect}"')
def step_engine_dialect(context: Context, dialect: str) -> None:
    """Assert the engine dialect name."""
    actual = context.engine.dialect.name
    assert actual == dialect, f"Expected dialect '{dialect}', got '{actual}'"


@then("the UnitOfWork pool_size should be {value:d}")
def step_uow_pool_size(context: Context, value: int) -> None:
    """Assert UnitOfWork.pool_size via read-only property."""
    assert context.uow.pool_size == value


@then("the UnitOfWork max_overflow should be {value:d}")
def step_uow_max_overflow(context: Context, value: int) -> None:
    """Assert UnitOfWork.max_overflow via read-only property."""
    assert context.uow.max_overflow == value


@then("the UnitOfWork pool_recycle should be {value:d}")
def step_uow_pool_recycle(context: Context, value: int) -> None:
    """Assert UnitOfWork.pool_recycle via read-only property."""
    assert context.uow.pool_recycle == value


# ---------------------------------------------------------------------------
# ORM Model dialect-agnostic checks
# ---------------------------------------------------------------------------

# Standard SQLAlchemy types that are portable across all dialects.
_PORTABLE_TYPE_NAMES = frozenset(
    {
        "VARCHAR",
        "STRING",
        "TEXT",
        "INTEGER",
        "BIGINTEGER",
        "SMALLINTEGER",
        "FLOAT",
        "NUMERIC",
        "BOOLEAN",
        "DATE",
        "DATETIME",
        "TIME",
        "TIMESTAMP",
        "JSON",
        "JSONB",
        "BLOB",
        "LARGEBINARY",
        "ENUM",
        "UUID",
        "INTERVAL",
        "ARRAY",
    }
)


@given("I inspect all ORM models from the Base metadata")
def step_inspect_orm_models(context: Context) -> None:
    """Collect all tables from the ORM metadata."""
    from cleveragents.infrastructure.database.models import Base

    context.metadata = Base.metadata
    context.tables = list(Base.metadata.tables.values())


@then("no column should use a SQLite-specific type")
def step_no_sqlite_specific_types(context: Context) -> None:
    """Assert no column uses a SQLite-only type."""
    for table in context.tables:
        for col in table.columns:
            type_name = type(col.type).__name__.upper()
            # Known SQLite-only types that are NOT portable
            sqlite_only = {"SQLITEINTEGER", "SQLITETEXT", "SQLITEBLOB"}
            assert type_name not in sqlite_only, (
                f"Column {table.name}.{col.name} uses SQLite-specific type {type_name}"
            )


@then("all columns should use standard SQLAlchemy types")
def step_all_standard_types(context: Context) -> None:
    """Assert all columns use portable SQLAlchemy types."""
    for table in context.tables:
        for col in table.columns:
            type_name = type(col.type).__name__.upper()
            # Allow common base types and their variants
            assert type_name in _PORTABLE_TYPE_NAMES or any(
                base in type_name for base in _PORTABLE_TYPE_NAMES
            ), f"Column {table.name}.{col.name} uses non-standard type {type_name}"


@then("all table names should be valid SQL identifiers")
def step_valid_table_names(context: Context) -> None:
    """Assert all table names are valid (lowercase) SQL identifiers."""
    for table in context.tables:
        assert _VALID_SQL_IDENTIFIER.match(table.name), (
            f"Table name '{table.name}' is not a valid SQL identifier"
        )


@given("I read the models module source code")
def step_read_models_source(context: Context) -> None:
    """Read the models.py source for textual analysis."""
    from cleveragents.infrastructure.database import models

    context.models_source = inspect.getsource(models)


@then('the source should not contain "{forbidden}"')
def step_source_not_contain(context: Context, forbidden: str) -> None:
    """Assert the source code does not contain a forbidden string."""
    assert forbidden not in context.models_source, (
        f"models.py should not contain '{forbidden}'"
    )


# ---------------------------------------------------------------------------
# Migration runner
# ---------------------------------------------------------------------------


@given('I create a MigrationRunner with URL "{url}"')
def step_create_migration_runner(context: Context, url: str) -> None:
    """Create a MigrationRunner instance."""
    from cleveragents.infrastructure.database.migration_runner import MigrationRunner

    context.migration_runner = MigrationRunner(url)


@then('the migration runner database_url should start with "{prefix}"')
def step_runner_url_starts_with(context: Context, prefix: str) -> None:
    """Assert migration runner database_url prefix."""
    url = context.migration_runner.database_url
    assert url.startswith(prefix), f"Expected '{prefix}', got '{url}'"
