"""Step definitions for TDD Issue #783 — init --yes should not require user input.

These steps exercise the *real* ``agents init --yes`` code path while
simulating a TTY environment by replacing the migration runner's
``_default_prompt_for_migration`` with a function that declines
migration (simulating a user on a real terminal providing no input).
The ``@tdd_expected_fail`` tag on the scenarios inverts the result:
these tests **pass** CI while the bug is present and will **fail** once
the bug is fixed (signalling that the tag should be removed).

Root cause
~~~~~~~~~~
``init_command()`` in ``project.py`` receives the ``--yes`` flag but
does not forward it to the migration runner.  The migration runner's
``_default_prompt_for_migration()`` calls ``typer.confirm()`` when a
TTY is detected, blocking the command for user input even though
``--yes`` was specified.  In a non-TTY environment (like CliRunner),
the prompt is silently skipped — the bug only surfaces in real terminal
sessions.  We replace the prompt function to reproduce the TTY code
path.

Mock strategy
~~~~~~~~~~~~~
CliRunner replaces ``sys.stdin`` during ``invoke()``, so patching
``sys.stdin.isatty()`` alone is ineffective.  Instead we:

1. Patch ``sys.stdin`` on the real ``sys`` module (documents intent
   and is the correct target per the project's established pattern in
   ``features/steps/migration_runner_steps.py``).
2. Patch ``MigrationRunner._default_prompt_for_migration`` with a
   replacement that returns ``False`` — simulating a TTY prompt where
   the user declines migration.  This ensures the prompt code path is
   exercised deterministically regardless of CliRunner's stdin
   replacement.
3. Set ``CLEVERAGENTS_DATABASE_URL`` to a path inside our tmp directory
   whose filename does **not** match the ``before_scenario`` template-DB
   prefixes (``cleveragents_*``, ``test_*``, ``db.*``).  This forces the
   migration runner's ``init_or_upgrade`` through the real Alembic path
   instead of the test fast-path that copies a pre-migrated template.

With bug #783 present, ``require_confirmation=True`` is hardcoded in
``UnitOfWork._ensure_database_initialized()``, so the prompt fires and
returns ``False``, causing ``MigrationNotApprovedError``.  After the
fix, ``--yes`` should bypass the prompt entirely.
"""

from __future__ import annotations

import os
import shutil
import sys
import tempfile
from unittest.mock import MagicMock, patch

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.project import app as project_app
from cleveragents.infrastructure.database.migration_runner import MigrationRunner

runner = CliRunner()


def _tty_prompt_declines(_message: str) -> bool:
    """Simulate a TTY migration prompt that declines (no user input).

    Replaces ``_default_prompt_for_migration`` to reproduce the TTY
    code path without depending on ``sys.stdin.isatty()``, which
    CliRunner overrides during ``invoke()``.
    """
    return False


# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------


@given("a fresh environment for tdd-init-yes-no-input")
def step_fresh_environment(context: Context) -> None:
    """Set up a fresh temporary environment with no existing database.

    Creates a temporary directory to serve as the working directory so
    that ``agents init`` creates a fresh ``.cleveragents`` directory.
    Disables auto-apply environment variables so the real prompt
    behaviour is exercised.
    """
    context.tdd_init_tmpdir = tempfile.mkdtemp(prefix="tdd_init_yes_")

    # Save env vars that would bypass the prompt or redirect the
    # database, and remove them.  ``CLEVERAGENTS_DATABASE_URL`` and
    # ``CLEVERAGENTS_TEST_DATABASE_URL`` are set by ``before_scenario``
    # to pre-migrated scenario databases — removing them forces the
    # container to derive a fresh DB path from ``CLEVERAGENTS_HOME``.
    context.tdd_init_saved_env = {
        k: os.environ.pop(k, None)
        for k in (
            "CLEVERAGENTS_AUTO_APPLY_MIGRATIONS",
            "CI",
            "BEHAVE_TESTING",
            "CLEVERAGENTS_DATABASE_URL",
            "CLEVERAGENTS_TEST_DATABASE_URL",
        )
    }

    # Register cleanup to restore env vars and remove tmpdir even if
    # subsequent steps are never reached (e.g. hook errors).
    saved_env = context.tdd_init_saved_env
    tmpdir = context.tdd_init_tmpdir

    def _cleanup() -> None:
        for key, val in saved_env.items():
            if val is not None:
                os.environ[key] = val
            else:
                os.environ.pop(key, None)
        shutil.rmtree(tmpdir, ignore_errors=True)
        reset_container()

    context.add_cleanup(_cleanup)


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


@when("I invoke agents init --yes with no stdin")
def step_invoke_init_yes_no_stdin(context: Context) -> None:
    """Invoke ``agents init --yes`` simulating a TTY with no real input.

    Uses ``CliRunner`` with ``input=""`` to provide no input.  Patches
    ``sys.stdin`` on the real ``sys`` module for intent documentation
    and replaces ``MigrationRunner._default_prompt_for_migration`` with
    a function that returns ``False`` (simulating a TTY prompt where the
    user declines).  See module docstring for the full mock strategy.
    """
    # Create a mock stdin that reports as a TTY but has no data
    mock_stdin = MagicMock(spec=sys.stdin)
    mock_stdin.isatty.return_value = True

    # Reset the global container to ensure a fresh UoW is created
    # using our env var overrides (no stale CLEVERAGENTS_DATABASE_URL).
    reset_container()

    # Use a DB path whose filename (``init_yes_test.sqlite``) does
    # NOT start with the template-fast-path prefixes
    # (``cleveragents_*``, ``test_*``, ``db.*``) so the real Alembic
    # migration path runs instead of copying a pre-migrated template.
    # Do NOT pre-create directories — ``init_command`` expects a
    # fresh path without an existing ``.cleveragents`` directory.
    tdd_db_path = os.path.join(
        context.tdd_init_tmpdir, ".cleveragents", "init_yes_test.sqlite"
    )
    tdd_db_url = f"sqlite:///{tdd_db_path}"

    with patch.dict(
        os.environ,
        {
            "CLEVERAGENTS_HOME": context.tdd_init_tmpdir,
            "CLEVERAGENTS_DATABASE_URL": tdd_db_url,
        },
    ):
        # Remove auto-apply env vars inside the patched environment
        for key in (
            "CLEVERAGENTS_AUTO_APPLY_MIGRATIONS",
            "CI",
            "BEHAVE_TESTING",
        ):
            os.environ.pop(key, None)

        # Patch sys.stdin on the real sys module (correct target per
        # project convention) and replace the prompt function to
        # simulate a TTY decline.  See module docstring for why
        # both patches are needed.
        with (
            patch.object(sys, "stdin", mock_stdin),
            patch.object(
                MigrationRunner,
                "_default_prompt_for_migration",
                staticmethod(_tty_prompt_declines),
            ),
        ):
            context.tdd_init_result = runner.invoke(
                project_app,
                ["init", "--yes", "--path", context.tdd_init_tmpdir],
                input="",
            )


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


@then("the tdd-init-yes-no-input command should exit successfully")
def step_exit_success(context: Context) -> None:
    """Assert the init command exited with code 0."""
    result = context.tdd_init_result
    assert result is not None, "init --yes was not invoked"
    actual = result.exit_code
    assert actual == 0, f"Expected exit code 0, got {actual}.\nOutput:\n{result.output}"


@then('the tdd-init-yes-no-input output should not contain "{text}"')
def step_output_not_contains(context: Context, text: str) -> None:
    """Assert the given text does NOT appear in the command output."""
    result = context.tdd_init_result
    assert result is not None, "init --yes was not invoked"
    output = result.output
    assert text.lower() not in output.lower(), (
        f"Did NOT expect '{text}' in output but found it:\n{output}"
    )


@then('the tdd-init-yes-no-input output should contain "{text}"')
def step_output_contains(context: Context, text: str) -> None:
    """Assert the given text appears in the command output."""
    result = context.tdd_init_result
    assert result is not None, "init --yes was not invoked"
    output = result.output
    assert text.lower() in output.lower(), (
        f"Expected '{text}' in output but did not find it:\n{output}"
    )
