"""Step definitions for TDD Issue #680 — session list with missing database.

These steps exercise the ``session list`` CLI command when **no database file
exists**.  Unlike the #554 TDD tests (which create a temp database), these
tests deliberately point ``CLEVERAGENTS_DATABASE_URL`` at a non-existent
path so that both the DI wiring **and** auto-creation of the database are
exercised.

On ``master`` (before the fix), the Container has no ``db`` provider, so
``_get_session_service()`` raises ``AttributeError``.  After the fix, the
Container provides a ``db`` Singleton that auto-creates the database via
``Base.metadata.create_all()``, and ``session list`` returns an empty list.
"""

from __future__ import annotations

import json
import logging
import os
import shutil
import tempfile
from pathlib import Path

from behave import given, then, when
from behave.runner import Context
from typer.testing import CliRunner

from cleveragents.application.container import reset_container
from cleveragents.cli.commands import session as session_mod
from cleveragents.cli.commands.session import app as session_app
from cleveragents.config.settings import Settings


def _suppress_structlog_stdout() -> tuple[int, bool]:
    """Prevent structlog debug lines from contaminating CLI stdout.

    See ``tdd_session_shared_steps._suppress_structlog_stdout`` for the
    full rationale.
    """
    import structlog

    root = logging.getLogger()
    prev_level = root.level
    root.setLevel(logging.WARNING)

    prev_config = structlog.get_config()
    prev_cache: bool = prev_config.get("cache_logger_on_first_use", True)  # type: ignore[assignment]
    structlog.configure(
        logger_factory=structlog.stdlib.LoggerFactory(),
        wrapper_class=structlog.stdlib.BoundLogger,
        cache_logger_on_first_use=False,
    )

    return prev_level, prev_cache


def _restore_structlog(prev_level: int, prev_cache: bool) -> None:
    """Undo the suppression applied by :func:`_suppress_structlog_stdout`."""
    import structlog

    logging.getLogger().setLevel(prev_level)
    structlog.configure(cache_logger_on_first_use=prev_cache)


@given("a CLI runner with no database file present")
def step_runner_no_db(context: Context) -> None:
    """Set up a CLI runner pointing at a database path that does not exist.

    The database URL is set to a file inside a temporary directory, but
    the file is **not** created.  This forces the application to either
    auto-create the database or fail with an appropriate error.
    """
    context.runner = CliRunner()

    # Reset singletons before setup so stale state does not leak in.
    reset_container()
    Settings._instance = None  # type: ignore[attr-defined]

    # Suppress structlog debug output so CliRunner captures clean output.
    prev_level, prev_cache = _suppress_structlog_stdout()

    context.missing_db_tmpdir = tempfile.mkdtemp(prefix="tdd_missing_db_680_")
    # Point at a file that does NOT exist — this is the key difference from
    # the shared step ``a CLI runner using the real session DI path`` which
    # creates the file via ``tempfile.mkstemp``.
    db_path = os.path.join(context.missing_db_tmpdir, "nonexistent.db")
    context.missing_db_path = db_path

    os.environ["CLEVERAGENTS_DATABASE_URL"] = f"sqlite:///{db_path}"
    reset_container()
    session_mod._service = None

    def _cleanup() -> None:
        session_mod._service = None
        os.environ.pop("CLEVERAGENTS_DATABASE_URL", None)
        reset_container()
        Settings._instance = None  # type: ignore[attr-defined]
        # The fix may auto-create the database file, so clean it up.
        Path(db_path).unlink(missing_ok=True)
        # Remove the temp directory.
        shutil.rmtree(context.missing_db_tmpdir, ignore_errors=True)
        _restore_structlog(prev_level, prev_cache)

    context.add_cleanup(_cleanup)


@when("I invoke session list with missing db")
def step_invoke_list(context: Context) -> None:
    """Invoke ``session list`` through the real CLI app."""
    context.result = context.runner.invoke(session_app, ["list"])


@when("I invoke session list with missing db and format json")
def step_invoke_list_json(context: Context) -> None:
    """Invoke ``session list --format json`` through the real CLI app."""
    context.result = context.runner.invoke(session_app, ["list", "--format", "json"])


@then("the session list missing db command should exit successfully")
def step_exit_ok(context: Context) -> None:
    """Assert the command exits with code 0."""
    assert context.result.exit_code == 0, (
        f"Expected exit code 0, got {context.result.exit_code}.\n"
        f"Output:\n{context.result.output}\n"
        f"Exception: {getattr(context.result, 'exception', None)!r}"
    )


@then('the session list missing db output should not contain "{text}"')
def step_output_not_contains(context: Context, text: str) -> None:
    """Assert the output does NOT contain a specific string."""
    assert text not in context.result.output, (
        f"Output unexpectedly contained '{text}':\n{context.result.output}"
    )


@then("the session list missing db output should be valid JSON")
def step_output_valid_json(context: Context) -> None:
    """Assert the output is parseable JSON."""
    try:
        json.loads(context.result.output)
    except json.JSONDecodeError as exc:
        raise AssertionError(
            f"Output is not valid JSON:\n{context.result.output}"
        ) from exc


@then("the session list missing db output should indicate no sessions")
def step_output_no_sessions(context: Context) -> None:
    """Assert the output indicates an empty session list.

    The CLI shows 'No sessions found' for human-readable formats
    (rich, plain, color).
    """
    output = context.result.output
    assert "No sessions found" in output or "sessions" in output.lower(), (
        f"Expected indication of no sessions in output:\n{output}"
    )
