"""Step definitions for alembic fileConfig error handling (issue #7874).

Each scenario uses its own isolated temp directory, preventing cross-test
contamination.  The malformed / valid INI files are written into those dirs
and the ``alembic`` CLI invocation is executed from within them so that
``fileConfig()`` reads directly from the target file under test.
"""

from __future__ import annotations

import os
import subprocess
import tempfile
from pathlib import Path
from typing import Any

from behave import given, then, when

# Absolute path to the project's alembic migrations package so that the
# subprocess invocations execute the real env.py (not a stub), ensuring the
# production fileConfig() error-handling path is covered by these scenarios.
_MIGRATIONS_DIR = str(
    Path(__file__).resolve().parent.parent.parent
    / "src"
    / "cleveragents"
    / "infrastructure"
    / "database"
    / "migrations"
)


def _write_malformed_ini(directory: Path) -> Path:
    """Write a deliberately malformed alembic.ini to *directory*."""
    ini = directory / "alembic.ini"
    ini.write_text(
        "[alembic]\n"
        f"script_location = {_MIGRATIONS_DIR}\n"
        "\n"
        # Reference a logger that does NOT exist anywhere.  fileConfig()
        # raises ``KeyError`` at startup when the referenced key is absent
        # from the INI file, producing an unhandled exception before any
        # migration runs.
        "[loggers]\n"
        "keys = root,invalid_nonexistent_logger\n"
        "\n",
    )
    return ini


def _write_valid_ini(directory: Path) -> Path:
    """Write a fully-valid alembic.ini to *directory*."""
    ini = directory / "alembic.ini"
    ini.write_text(
        "[alembic]\n"
        f"script_location = {_MIGRATIONS_DIR}\n"
        "\n"
        "[loggers]\n"
        "keys = root\n"
        "\n"
        "[handlers]\n"
        "keys = stream_handler\n"
        "\n"
        "[formatters]\n"
        "keys = simple\n"
        "\n"
        "[logger_root]\n"
        "level = WARN\n"
        "handlers = stream_handler\n"
        "\n"
        "[handler_stream_handler]\n"
        "class = StreamHandler\n"
        "args = (sys.stderr,)\n"
        "\n"
        "[formatter_simple]\n"
        "format = %(levelname)s:%(name)s:%(message)s\n",
    )
    return ini


@given("a fresh temporary directory")
def step_given_fresh_temp_dir(context: Any) -> None:
    context.test_dir = Path(tempfile.mkdtemp(prefix="fileconfig_test_"))


@when(
    "the alembic.ini contains an invalid logger section"
    " that references nonexistent loggers"
)
def step_when_invalid_log_section(context: Any) -> None:
    """Write a malformed alembic.ini to the test directory."""
    _write_malformed_ini(context.test_dir)


@when("the alembic.ini contains a malformed logging configuration")
def step_when_malformed_config(context: Any) -> None:
    """Alias for when steps that write a malformed config."""
    _write_malformed_ini(context.test_dir)


@when("migrations are invoked normally")
def step_when_invocations_normal(context: Any) -> None:
    """Invoke ``alembic current`` to trigger env.py startup (fileConfig)."""
    env = {
        **os.environ,
        "CLEVERAGENTS_DATABASE_URL": f"sqlite:///{context.test_dir}/test.db",
    }
    result = subprocess.run(
        ["alembic", "current"],
        cwd=context.test_dir,
        capture_output=True,
        text=True,
        timeout=10,
        env=env,
    )
    context.migration_result = result


@when("the alembic.ini contains a valid logger configuration")
def step_when_valid_config(context: Any) -> None:
    """Write a fully-valid alembic.ini to the test directory."""
    _write_valid_ini(context.test_dir)


@then(
    'the error output on stderr includes actionable guidance about "[loggers]" section'
)
def step_then_stderr_includes_guidance(context: Any) -> None:
    """Assert that the error message contains ``[loggers]`` guidance."""
    result = _assert_invocation(context)
    assert result.returncode != 0, (
        f"Expected non-zero exit code on configuration error. Got: {result.returncode}"
    )
    stderr = result.stderr
    assert "[loggers]" in stderr or "loggers" in stderr.lower(), (
        f"Error must be user-actionable and mention [loggers]. Got:\n{stderr}"
    )


@then('the error output on stderr includes the config file path "alembic.ini"')
def step_then_stderr_includes_alembic_ini(context: Any) -> None:
    """Assert that the error message contains the config file path."""
    result = _assert_invocation(context)
    stderr = result.stderr
    assert "alembic.ini" in stderr, f"Error must include 'alembic.ini'. Got:\n{stderr}"


@then("no error message appears on stderr")
def step_then_no_stderr_error(context: Any) -> None:
    """Assert that the alembic invocation produced no stderr output."""
    result = _assert_invocation(context)
    assert not result.stderr.strip(), (
        f"Unexpected stderr when config is valid:\n{result.stderr}"
    )


def _assert_invocation(context: Any) -> subprocess.CompletedProcess[str]:
    """Utility: assert that a migration invocation has been recorded."""
    if not hasattr(context, "migration_result"):
        msg = (
            "No migration result recorded. Did all required *When* steps execute? "
            "The test flow must be: Given … → When … → When … → Then …"
        )
        raise AssertionError(msg)
    return context.migration_result
