"""Step definitions for CLI init --yes flag tests (bug #522).

These tests are TDD-style: they assert the CORRECT expected behaviour of
``agents init --yes`` and are expected to FAIL until the bug fix is applied.
"""

from __future__ import annotations

import os
import re
import shutil
import tempfile
from pathlib import Path
from unittest.mock import create_autospec, patch

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

from cleveragents.application.services.project_service import ProjectService
from cleveragents.cli.main import app


def _restore_cwd(context):
    """Restore the original working directory, env var, and clean up."""
    os.chdir(context._init_original_cwd)
    if context._init_original_home is None:
        os.environ.pop("CLEVERAGENTS_HOME", None)
    else:
        os.environ["CLEVERAGENTS_HOME"] = context._init_original_home
    shutil.rmtree(context.temp_dir, ignore_errors=True)


def _create_init_mocks(context):
    """Create and configure mocked container, service, and project.

    Returns ``(patcher, mock_service)`` where *patcher* is the started
    ``patch`` context manager.

    The service mock uses ``create_autospec(ProjectService)`` so that
    attribute access and method calls are validated against the real
    ``ProjectService`` interface.  The project mock uses a typed
    ``_MockProject`` class instead of ``MagicMock`` because the
    spec-required output fields (``data_dir``, ``config_path``,
    ``database_status``, ``directories``) do not yet exist on the legacy
    ``Project`` model — they will be added when the #522 fix aligns the
    model with ``docs/specification.md:1381-1386``.  Using
    ``create_autospec(Project)`` would reject those attribute assignments.
    """
    mock_service = create_autospec(ProjectService, instance=True)

    class _MockProject:
        """Lightweight stand-in for the Project model with spec fields."""

        name: str
        path: Path
        data_dir: Path
        config_path: Path
        database_status: str
        directories: list[str]

    mock_project = _MockProject()
    mock_project.name = Path(context.temp_dir).name
    mock_project.path = Path(context.temp_dir)
    # Pre-populate spec-required fields (specification.md:1381-1386) so
    # that when the #522 fix lands, output assertions fail for the right
    # reason (real bugs) rather than MagicMock stringification artefacts.
    mock_project.data_dir = Path(context.temp_dir)
    mock_project.config_path = Path(context.temp_dir) / "config.toml"
    mock_project.database_status = "initialized (schema v3)"
    mock_project.directories = ["logs", "cache", "sessions", "contexts"]
    mock_service.initialize_project.return_value = mock_project

    patcher = patch("cleveragents.application.container.get_container")
    mock_container = patcher.start()
    mock_container.return_value.project_service.return_value = mock_service

    return patcher, mock_service


@given("I have a temporary project directory for init")
def step_temp_project_directory(context):
    """Create a temporary directory and store it on *context*."""
    context.temp_dir = tempfile.mkdtemp()
    # Use step-private attribute names (prefixed with _init_) to avoid
    # colliding with environment.py's context.original_cwd (str).
    # Match the framework's type (str via os.getcwd()) for consistency.
    context._init_original_cwd = os.getcwd()
    context._init_original_home = os.environ.get("CLEVERAGENTS_HOME")
    os.environ["CLEVERAGENTS_HOME"] = context.temp_dir
    os.chdir(context.temp_dir)
    context.add_cleanup(_restore_cwd, context)


def _run_init_with_flag(context, flag: str) -> None:
    """Invoke ``agents init`` with the given flag via the Typer test runner."""
    runner = CliRunner()

    patcher, mock_service = _create_init_mocks(context)
    try:
        result = runner.invoke(app, ["init", flag])
    finally:
        patcher.stop()

    context.init_yes_result = {
        "exit_code": result.exit_code,
        "output": result.output,
    }
    context.init_yes_raw_result = result
    context.init_yes_mock_service = mock_service


@when("I run agents init with the --yes flag")
def step_run_init_yes(context):
    """Invoke ``agents init --yes`` via the Typer test runner."""
    _run_init_with_flag(context, "--yes")


@when("I run agents init with the -y flag")
def step_run_init_short_yes(context):
    """Invoke ``agents init -y`` via the Typer test runner."""
    _run_init_with_flag(context, "-y")


@then("the init command should exit with code {code:d}")
def step_init_exit_code(context, code):
    """Assert the init command exited with the expected code."""
    actual = context.init_yes_result["exit_code"]
    assert actual == code, (
        f"Expected exit code {code}, got {actual}. "
        f"Output: {context.init_yes_result['output']}"
    )


@then('the init output should contain "{text}"')
def step_init_output_contains(context, text):
    """Assert that the init command output contains *text*."""
    output = context.init_yes_result["output"]
    assert text in output, f"Expected '{text}' in output:\n{output}"


@then("the project service initialize_project should have been called")
def step_initialize_project_called(context):
    """Assert the mock project service's initialize_project was invoked."""
    context.init_yes_mock_service.initialize_project.assert_called_once()


@when("I run agents init without the --yes flag")
def step_run_init_no_yes(context):
    """Invoke ``agents init`` without --yes (interactive mode)."""
    runner = CliRunner()

    patcher, _mock_service = _create_init_mocks(context)
    try:
        result = runner.invoke(app, ["init"])
    finally:
        patcher.stop()

    context.init_yes_result = {
        "exit_code": result.exit_code,
        "output": result.output,
    }
    context.init_yes_raw_result = result


@then("the init output should indicate interactive mode")
def step_output_indicates_interactive(context):
    """Assert that without --yes the output does NOT contain the
    non-interactive marker, indicating the command ran in interactive mode.

    This is the negative complement to the --yes scenarios: when the fix
    lands, ``agents init`` (without --yes) should either present a prompt
    or omit the ``Initialized (non-interactive)`` marker.
    """
    output = context.init_yes_result["output"]
    assert "Initialized (non-interactive)" not in output, (
        "Expected interactive mode but output contains "
        f"'Initialized (non-interactive)':\n{output}"
    )


@then("no interactive prompt should have been presented")
def step_no_interactive_prompt(context):
    """Assert that no interactive prompt was presented.

    Verifies that the command output does not contain common prompt-like
    tokens, which would indicate the ``--yes`` flag failed to suppress
    interactive prompts.
    """
    output = context.init_yes_result["output"]
    prompt_tokens = (
        "[Y/n]",
        "[y/N]",
        "Continue? ",
        "Proceed? ",
        "Confirm ",
        "(yes/no)",
    )
    for token in prompt_tokens:
        assert token not in output, (
            f"Unexpected prompt token '{token}' found in output:\n{output}"
        )
    # Regex catches "Enter project name:", "Enter path:", "Enter value?"
    # without false-positiving on "Entered configuration" or "Enterprise".
    # Non-greedy .*? stops at the first : or ? to avoid over-matching
    # when the line contains colons in non-prompt contexts (e.g. "Config: /path").
    enter_prompt = re.search(r"Enter\s+\S+.*?[:?]", output)
    assert enter_prompt is None, (
        f"Unexpected Enter-style prompt found in output: "
        f"'{enter_prompt.group()}'\n{output}"
    )
