"""Behave step implementations for repl_uncovered_paths.feature.

Covers uncovered lines in ``src/cleveragents/cli/commands/repl.py``:
  610-611  persona import file-not-found
  619-621  persona import validation failure
  625-626  unknown persona sub-command
  651-652  session switch missing name
  792-794  @ reference suggestions in run_repl
  805      empty argv after shlex.split in run_repl
"""

from __future__ import annotations

import os
import shutil
import tempfile
from io import StringIO
from pathlib import Path
from unittest.mock import MagicMock, patch

from behave import given, then, when
from behave.runner import Context

# ===================================================================
# Helpers
# ===================================================================


class _FakeInputRplcov3:
    """Feed predefined lines then raise EOFError to exit the REPL."""

    def __init__(self, lines: list[str]) -> None:
        self._lines = list(lines)
        self._idx = 0

    def __call__(self, prompt: str = "") -> str:
        if self._idx >= len(self._lines):
            raise EOFError
        line = self._lines[self._idx]
        self._idx += 1
        return line


def _make_console_and_buf():
    """Return a (Console, StringIO) pair for capturing Rich output."""
    from rich.console import Console

    buf = StringIO()
    console = Console(file=buf, no_color=True, width=200)
    return console, buf


# ===================================================================
# Background
# ===================================================================


@given("rplcov3 the repl module is imported")
def step_rplcov3_import(context: Context) -> None:
    import cleveragents.cli.commands.repl as mod

    context.rplcov3_mod = mod


# ===================================================================
# Shared — temporary persona registry
# ===================================================================


@given("rplcov3 a temporary persona registry")
def step_rplcov3_temp_registry(context: Context) -> None:
    from cleveragents.cli.persona import PersonaRegistry

    config_dir = Path(tempfile.mkdtemp())
    context.rplcov3_config_dir = config_dir
    context.add_cleanup(lambda: shutil.rmtree(str(config_dir), ignore_errors=True))

    patcher = patch.dict(os.environ, {"CLEVERAGENTS_CONFIG_DIR": str(config_dir)})
    patcher.start()
    context.add_cleanup(patcher.stop)

    registry = PersonaRegistry()
    registry.ensure_default_persona()
    context.rplcov3_registry = registry

    # Prepare default sessions dict for slash-command tests
    from cleveragents.cli.commands.repl import _ReplSessionState

    active = registry.get_active_persona_name() or "default"
    context.rplcov3_sessions = {
        "default": _ReplSessionState(name="default", active_persona=active),
    }
    context.rplcov3_current_session = "default"


# ===================================================================
# Slash command runner helper
# ===================================================================


def _run_slash_command(context: Context, slash_text: str) -> None:
    """Execute _handle_slash_command and capture output."""
    console, buf = _make_console_and_buf()
    patcher = patch("cleveragents.cli.commands.repl._console", console)
    patcher.start()
    try:
        exit_code, session = context.rplcov3_mod._handle_slash_command(
            slash_text,
            registry=context.rplcov3_registry,
            sessions=context.rplcov3_sessions,
            current_session=context.rplcov3_current_session,
        )
    finally:
        patcher.stop()
    context.rplcov3_exit_code = exit_code
    context.rplcov3_session = session
    context.rplcov3_output = buf.getvalue()


# ===================================================================
# Scenario: persona import file-not-found (lines 610-611)
# ===================================================================


@when('rplcov3 I handle slash command "{cmd}"')
def step_rplcov3_handle_slash(context: Context, cmd: str) -> None:
    _run_slash_command(context, cmd)


@then("rplcov3 the exit code is {code:d}")
def step_rplcov3_exit_code(context: Context, code: int) -> None:
    assert context.rplcov3_exit_code == code, (
        f"Expected exit code {code}, got {context.rplcov3_exit_code}. "
        f"Output: {context.rplcov3_output}"
    )


@then('rplcov3 the output contains "{fragment}"')
def step_rplcov3_output_contains(context: Context, fragment: str) -> None:
    assert fragment in context.rplcov3_output, (
        f"Expected '{fragment}' in output. Got: {context.rplcov3_output}"
    )


# ===================================================================
# Scenario: persona import validation failure (lines 619-621)
# ===================================================================


@given("rplcov3 an importable YAML file with invalid persona data")
def step_rplcov3_invalid_yaml_file(context: Context) -> None:
    """Create a valid YAML file whose contents fail PersonaConfig validation."""
    tmp = tempfile.NamedTemporaryFile(  # noqa: SIM115
        suffix=".yaml",
        delete=False,
        mode="w",
        encoding="utf-8",
    )
    # Missing required 'actor' field and name with path separator -> validation error
    tmp.write("name: test-bad\nactor: noslash\n")
    tmp.flush()
    tmp.close()
    context.rplcov3_invalid_yaml_path = tmp.name
    context.add_cleanup(lambda: os.unlink(tmp.name))


@when("rplcov3 I handle slash command for import with invalid persona file")
def step_rplcov3_import_invalid_persona(context: Context) -> None:
    """Run persona import pointing at the invalid YAML file via a relative path."""
    abs_path = Path(context.rplcov3_invalid_yaml_path)
    # resolve_import_path requires a relative path; mock it to return the absolute path
    console, buf = _make_console_and_buf()
    patcher_console = patch("cleveragents.cli.commands.repl._console", console)
    patcher_resolve = patch.object(
        context.rplcov3_registry,
        "resolve_import_path",
        return_value=abs_path,
    )
    patcher_console.start()
    patcher_resolve.start()
    try:
        exit_code, session = context.rplcov3_mod._handle_slash_command(
            f"persona import {abs_path.name}",
            registry=context.rplcov3_registry,
            sessions=context.rplcov3_sessions,
            current_session=context.rplcov3_current_session,
        )
    finally:
        patcher_console.stop()
        patcher_resolve.stop()
    context.rplcov3_exit_code = exit_code
    context.rplcov3_session = session
    context.rplcov3_output = buf.getvalue()


# ===================================================================
# Scenario: @ reference suggestions in run_repl (lines 792-794)
# ===================================================================


@given("rplcov3 a reference catalog with known entries")
def step_rplcov3_reference_catalog(context: Context) -> None:
    """Prepare a catalog that has entries so suggestions are produced."""
    context.rplcov3_catalog = {
        "file": ["README.md", "setup.py", "main.py"],
        "actor": ["local/helper"],
        "tool": [],
        "skill": [],
        "project": [],
        "plan": [],
    }


@when('rplcov3 I run the REPL with input "@zzz_nomatch_token" and mocked catalog')
def step_rplcov3_repl_at_suggestions(context: Context) -> None:
    """Feed the REPL a line with an @ token that won't expand but will trigger suggestions."""
    from cleveragents.cli.commands.repl import run_repl

    console, buf = _make_console_and_buf()
    # The line contains @ so _build_reference_catalog is called; mock it
    # _expand_references will NOT change line (no fuzzy match for zzz_nomatch_token
    # against our catalog entries), so we fall into the elif branch (line 791).
    # _reference_suggestions should then return hints.
    catalog = context.rplcov3_catalog

    # Make _expand_references return the line unchanged (it will, because no match)
    # Make _reference_suggestions return some hints
    fake_input = _FakeInputRplcov3(["@file:rea"])
    mock_dispatch = MagicMock(return_value=0)

    patches = [
        patch("cleveragents.cli.commands.repl._console", console),
        patch("builtins.input", fake_input),
        patch("cleveragents.cli.commands.repl.dispatch_command", mock_dispatch),
        patch(
            "cleveragents.cli.commands.repl._build_reference_catalog",
            return_value=catalog,
        ),
        # Mock _expand_references to return the input unchanged so we hit the elif branch
        patch(
            "cleveragents.cli.commands.repl._expand_references",
            side_effect=lambda line, **kw: line,
        ),
        # Mock _reference_suggestions to return hints
        patch(
            "cleveragents.cli.commands.repl._reference_suggestions",
            return_value=["@file:README.md", "@file:setup.py"],
        ),
    ]
    for p in patches:
        p.start()
    try:
        code = run_repl(no_history=True)
    finally:
        for p in patches:
            p.stop()

    context.rplcov3_repl_exit_code = code
    context.rplcov3_repl_output = buf.getvalue()


@then("rplcov3 the repl exit code is {code:d}")
def step_rplcov3_repl_exit_code(context: Context, code: int) -> None:
    assert context.rplcov3_repl_exit_code == code, (
        f"Expected REPL exit code {code}, got {context.rplcov3_repl_exit_code}. "
        f"Output: {context.rplcov3_repl_output}"
    )


@then('rplcov3 the repl output contains "{fragment}"')
def step_rplcov3_repl_output_contains(context: Context, fragment: str) -> None:
    assert fragment in context.rplcov3_repl_output, (
        f"Expected '{fragment}' in REPL output. Got: {context.rplcov3_repl_output}"
    )


# ===================================================================
# Scenario: empty argv after shlex.split in run_repl (line 805)
# ===================================================================


@when("rplcov3 I run the REPL with input that tokenises to empty")
def step_rplcov3_repl_empty_argv(context: Context) -> None:
    """Feed the REPL a line that shlex.split produces an empty list from.

    A line of whitespace is stripped to '' by line.strip() and caught
    by the `if not line: continue` guard. To reach shlex.split we need
    a non-empty string that tokenises to nothing.  We can achieve this
    by passing a line consisting of only a comment character, but shlex
    will not produce empty there either.

    The simplest way: mock shlex.split to return [] for a specific line,
    then verify dispatch_command was never called.
    """
    from cleveragents.cli.commands.repl import run_repl

    console, buf = _make_console_and_buf()
    # Feed a real non-empty line so it passes through all guards
    fake_input = _FakeInputRplcov3(["someline"])
    mock_dispatch = MagicMock(return_value=0)

    original_shlex_split = __import__("shlex").split

    def fake_shlex_split(text):
        # Return empty list for our test line to hit line 805
        if text == "someline":
            return []
        return original_shlex_split(text)

    patches = [
        patch("cleveragents.cli.commands.repl._console", console),
        patch("builtins.input", fake_input),
        patch("cleveragents.cli.commands.repl.dispatch_command", mock_dispatch),
        patch("shlex.split", side_effect=fake_shlex_split),
    ]
    for p in patches:
        p.start()
    try:
        code = run_repl(no_history=True)
    finally:
        for p in patches:
            p.stop()

    context.rplcov3_repl_exit_code = code
    context.rplcov3_repl_output = buf.getvalue()
    context.rplcov3_mock_dispatch = mock_dispatch


@then("rplcov3 dispatch_command was not called")
def step_rplcov3_dispatch_not_called(context: Context) -> None:
    assert not context.rplcov3_mock_dispatch.called, (
        "dispatch_command should not have been called for empty argv"
    )
