"""Step definitions for session_create_error.feature (bug #570).

TDD regression tests for ``agents session create`` after ``agents init``.
These scenarios assert the correct expected behaviour and will fail until
the DI container fix is applied.

Design rationale
~~~~~~~~~~~~~~~~
``_get_session_service()`` calls ``container.db()`` but the DI ``Container``
class has no ``db`` provider, raising ``AttributeError``.  Same root cause
as bug #554.

We reset ``_service`` to ``None`` so the real ``_get_session_service()`` is
exercised.  A file-based SQLite database and ``CLEVERAGENTS_DATABASE_URL``
override ensure the commands can reach the database once the fix lands.

All CLI invocations use ``--format plain`` so output goes through
``typer.echo`` (captured by ``CliRunner``) rather than ``rich.console``.

Private API access (``session_mod._service``, ``session_mod._reset_session_service``)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
These step definitions access private attributes of the ``session`` CLI
module because the DI integration tests *must* force the module to re-run
its service resolution logic.  The module caches a singleton
``_service`` instance; resetting it to ``None`` is the only way to make
the CLI re-exercise ``_get_session_service()`` (the buggy code path).
If the module's internal caching mechanism changes, these tests will
need to be updated accordingly.
"""

from __future__ import annotations

import os
import shutil
import tempfile
from typing import Any

from behave import given, then, when
from sqlalchemy import create_engine
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.infrastructure.database.models import Base

runner = CliRunner()


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


def _setup_real_di_path(context: Any) -> None:
    """Prepare a temp dir with a fresh SQLite DB and override container.

    Registers cleanup immediately after capturing original state so that
    env vars and module state are restored even if later setup lines raise.
    """
    # Store original _service so cleanup can restore it.
    context.sce_original_service = session_mod._service
    context.sce_tmpdir = tempfile.mkdtemp(prefix="session_create_err_570_")

    # Register cleanup early so env/state is always restored (SEC-3).
    context.add_cleanup(_cleanup_sce, context)

    context.sce_db_path = os.path.join(context.sce_tmpdir, "test.db")
    db_url = f"sqlite:///{context.sce_db_path}"

    # Create schema so the DB file exists with all tables.
    engine = create_engine(db_url, echo=False)
    Base.metadata.create_all(engine)
    engine.dispose()

    # Override the container's database_url so real DI can find the DB.
    os.environ["CLEVERAGENTS_DATABASE_URL"] = db_url

    # Reset global DI container so post-fix DI reads the fresh env var.
    reset_container()

    # Reset the module-level _service so _get_session_service() is used.
    # This direct attribute mutation is fragile — if the module's internal
    # caching mechanism changes (e.g. lazy singleton via descriptor), this
    # line will need to be updated.  See module docstring for rationale.
    session_mod._service = None

    context.sce_result = None
    context.sce_list_result = None


def _cleanup_sce(context: Any) -> None:
    """Remove temp dir, restore env and original _service.

    Also resets the module-level ``_service`` cache via the public API so
    that any DI-created engine is released before the temp dir is removed.
    """
    # Ensure module-level cache is cleared; GC will release any engine when
    # the container is reset (done separately via reset_container()).
    session_mod._reset_session_service()
    # Then restore the original _service value.
    session_mod._service = context.sce_original_service
    # Release any DI-created connections before removing temp dir.
    reset_container()
    os.environ.pop("CLEVERAGENTS_DATABASE_URL", None)
    shutil.rmtree(context.sce_tmpdir, ignore_errors=True)


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


@given("a session-create-error CLI runner using the real DI path")
def step_session_create_error_runner(context: Any) -> None:
    _setup_real_di_path(context)


# ---------------------------------------------------------------------------
# When - create
# ---------------------------------------------------------------------------


@when("I invoke session-create-error create with no arguments")
def step_invoke_create_no_args(context: Any) -> None:
    context.sce_result = runner.invoke(session_app, ["create", "--format", "plain"])


@when('I invoke session-create-error create with actor "{actor}"')
def step_invoke_create_with_actor(context: Any, actor: str) -> None:
    context.sce_result = runner.invoke(
        session_app, ["create", "--actor", actor, "--format", "plain"]
    )


# ---------------------------------------------------------------------------
# When - list (for persistence verification)
# ---------------------------------------------------------------------------


@when("I invoke session-create-error list to verify persistence")
def step_invoke_list_after_create(context: Any) -> None:
    context.sce_list_result = runner.invoke(session_app, ["list", "--format", "plain"])


# ---------------------------------------------------------------------------
# Then - assertions
# ---------------------------------------------------------------------------


@then("the session-create-error command should exit successfully")
def step_exit_success(context: Any) -> None:
    result = context.sce_result
    assert result is not None, "No command was invoked"
    assert result.exit_code == 0, (
        f"Expected exit code 0, got {result.exit_code}.\n"
        f"Output: {result.output}\n"
        f"Exception: {result.exception!r}"
    )


@then('the session-create-error output should contain "{text}"')
def step_output_contains(context: Any, text: str) -> None:
    result = context.sce_result
    assert result is not None, "No command was invoked"
    assert text in result.output, (
        f"Expected '{text}' in output but got:\n{result.output}"
    )


@then("the session-create-error list should show at least one session")
def step_list_shows_sessions(context: Any) -> None:
    result = context.sce_list_result
    assert result is not None, "List was not invoked"
    assert result.exit_code == 0, (
        f"Expected list exit code 0, got {result.exit_code}.\n"
        f"Output: {result.output}\n"
        f"Exception: {result.exception!r}"
    )
    # In plain format the output should contain "total:" with a count > 0.
    assert "total:" in result.output, (
        f"Expected 'total:' in plain list output but got:\n{result.output}"
    )
    assert "total: 0" not in result.output, (
        f"Expected at least one session but got:\n{result.output}"
    )
