"""Step definitions for project_show_after_create.feature (bug #590).

Regression tests verifying that ``agents project show`` finds a project
immediately after ``agents project create`` commits it.

Root cause (fixed)
~~~~~~~~~~~~~~~~~~
Identical to bug #589.  ``NamespacedProjectRepository.create()`` called
``session.flush()`` but never ``session.commit()``.  The ``show`` command
obtains a fresh session via ``_get_namespaced_project_repo()`` and calls
``repo.get(name)``, which could not see the flushed-but-uncommitted row.

Test design mirrors bug #589 tests: file-based SQLite with a fresh
``NamespacedProjectRepository`` per CLI invocation.
"""

from __future__ import annotations

import os
import shutil
import tempfile
from typing import Any
from unittest.mock import patch

from behave import given, then, when
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import NullPool
from typer.testing import CliRunner

from cleveragents.cli.commands.project import app as project_app
from cleveragents.infrastructure.database.models import Base
from cleveragents.infrastructure.database.repositories import (
    NamespacedProjectRepository,
)

# Patch targets - module-level DI helpers in the project CLI.
_PATCH_PROJECT_REPO = "cleveragents.cli.commands.project._get_namespaced_project_repo"
_PATCH_STORE_EXTRAS = "cleveragents.cli.commands.project._store_project_extras"

# NOTE: typer.testing.CliRunner always mixes stderr into stdout
# (no mix_stderr parameter exposed).  Scenario 3 relies on this
# behaviour to assert "not found" appears in result.output even
# though the CLI writes it to stderr via err_console.
runner = CliRunner()


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


def _make_fresh_repo(db_path: str) -> NamespacedProjectRepository:
    """Build a *new* repo with its own engine/session for *db_path*.

    ``NullPool`` ensures SQLite file handles are released immediately
    when the engine is no longer referenced.  Schema is pre-created
    once during the Background step, so no DDL is needed here.
    """
    engine = create_engine(f"sqlite:///{db_path}", echo=False, poolclass=NullPool)
    factory = sessionmaker(bind=engine, expire_on_commit=False)
    return NamespacedProjectRepository(session_factory=factory)


def _cleanup_show_db(context: Any) -> None:
    """Remove the temporary directory."""
    shutil.rmtree(context.show_tmpdir, ignore_errors=True)


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


@given("a fresh project-show database is initialised")
def step_init_show_db(context: Any) -> None:
    """Create a temp dir with a fresh SQLite file for show tests."""
    context.show_tmpdir = tempfile.mkdtemp(prefix="show_590_")
    context.show_db_path = os.path.join(context.show_tmpdir, "test.db")

    # Pre-create schema.
    engine = create_engine(
        f"sqlite:///{context.show_db_path}",
        echo=False,
        poolclass=NullPool,
    )
    Base.metadata.create_all(engine)
    engine.dispose()

    context.show_result = None
    context.add_cleanup(_cleanup_show_db, context)


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


@when('I create a project named "{name}" via the project-show CLI')
def step_create_project_show(context: Any, name: str) -> None:
    """Invoke ``project create <name>`` with a fresh repo."""
    repo = _make_fresh_repo(context.show_db_path)
    with (
        patch(_PATCH_PROJECT_REPO, return_value=repo),
        patch(_PATCH_STORE_EXTRAS),
    ):
        result = runner.invoke(project_app, ["create", name])
    assert result.exit_code == 0, (
        f"project create '{name}' should exit 0 but got "
        f"{result.exit_code}. output:\n{result.output}"
    )


@when(
    'I create a described project named "{name}" with description "{desc}" '
    "via the project-show CLI"
)
def step_create_project_show_desc(context: Any, name: str, desc: str) -> None:
    """Invoke ``project create <name> -d <desc>`` with a fresh repo."""
    repo = _make_fresh_repo(context.show_db_path)
    with (
        patch(_PATCH_PROJECT_REPO, return_value=repo),
        patch(_PATCH_STORE_EXTRAS),
    ):
        result = runner.invoke(project_app, ["create", name, "-d", desc])
    assert result.exit_code == 0, (
        f"project create '{name}' with description should exit 0 but got "
        f"{result.exit_code}. output:\n{result.output}"
    )


# ---------------------------------------------------------------------------
# When steps - show
# ---------------------------------------------------------------------------


@when('I show the project "{name}" via the project-show CLI')
def step_show_project(context: Any, name: str) -> None:
    """Invoke ``project show <name>`` with a fresh repo."""
    repo = _make_fresh_repo(context.show_db_path)
    with patch(_PATCH_PROJECT_REPO, return_value=repo):
        context.show_result = runner.invoke(project_app, ["show", name])


# ---------------------------------------------------------------------------
# Then steps
# ---------------------------------------------------------------------------


@then('the project-show output should contain "{text}"')
def step_show_output_contains(context: Any, text: str) -> None:
    """Assert that the show output contains *text* (case-insensitive)."""
    result = context.show_result
    assert result is not None, "project show was not invoked"
    output = result.output
    assert text.lower() in output.lower(), (
        f"Expected '{text}' in show output but got:\n{output}"
    )


@then("the project-show exit code should be {code:d}")
def step_show_exit_code(context: Any, code: int) -> None:
    """Assert the show command exited with the expected code."""
    result = context.show_result
    assert result is not None, "project show was not invoked"
    actual = result.exit_code
    assert actual == code, (
        f"Expected exit code {code}, got {actual}. Output:\n{result.output}"
    )


@then("the project-show exit code should not be {code:d}")
def step_show_exit_code_not(context: Any, code: int) -> None:
    """Assert the show command did NOT exit with the given code."""
    result = context.show_result
    assert result is not None, "project show was not invoked"
    actual = result.exit_code
    assert actual != code, (
        f"Expected exit code != {code}, got {actual}. Output:\n{result.output}"
    )
