"""Step definitions for repl_cli_coverage_boost.feature.

These steps target specific uncovered lines in
cleveragents/cli/commands/repl.py:
- Line 222: _build_reference_catalog file-count break
- Lines 331-332: _expand_references category match, no fuzzy match
- Line 360: _reference_suggestions bare @-sign token skipped
- Lines 423-424: _print_persona_list empty list
- Lines 467, 469: _parse_persona_create unknown flag / missing --actor
- Lines 506-507: /persona set missing name
- Lines 529-530: /persona pick no personas
- Lines 558-559: /persona delete missing name
- Lines 566-567: /persona delete not found
- Lines 579-580: /persona export missing name
- Lines 584-585: /persona export not found
- Lines 386-389, 404-408: _run_shell_command edge cases
"""

import os
import shlex
import subprocess
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch

from behave import given, then, when

from cleveragents.cli.commands.repl import (
    _MAX_REFERENCE_FILES,
    _best_fuzzy_match,
    _build_reference_catalog,
    _compose_prompt,
    _expand_references,
    _find_reference_candidates,
    _get_prompt_context,
    _handle_slash_command,
    _parse_persona_create,
    _print_persona_list,
    _reference_cache,
    _reference_suggestions,
    _ReplSessionState,
    _run_shell_command,
)


# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("the repl module is imported")
def step_repl_module_imported(context):
    """Ensure the REPL module is importable."""
    assert _build_reference_catalog is not None
    assert _expand_references is not None
    assert _handle_slash_command is not None


# ---------------------------------------------------------------------------
# _build_reference_catalog: file limit break (line 222)
# ---------------------------------------------------------------------------
@given("a temporary directory with more files than the reference limit")
def step_create_many_files(context):
    """Create a temp dir with more than _MAX_REFERENCE_FILES files."""
    context.tmpdir = tempfile.mkdtemp()
    count = _MAX_REFERENCE_FILES + 50
    for i in range(count):
        Path(context.tmpdir, f"file_{i:04d}.txt").touch()
    context.created_file_count = count


@when("I build the reference catalog from that directory")
def step_build_catalog_many_files(context):
    """Build the reference catalog with cwd set to the temp dir."""
    # Clear cache to force rebuild
    _reference_cache["cwd"] = None
    _reference_cache["created_at"] = 0.0
    _reference_cache["catalog"] = None
    with patch(
        "cleveragents.cli.commands.repl.Path.cwd", return_value=Path(context.tmpdir)
    ):
        context.catalog_result = _build_reference_catalog()


@then("the catalog file list should be capped at the maximum")
def step_verify_catalog_capped(context):
    """Verify that the catalog doesn't exceed the max."""
    file_count = len(context.catalog_result.get("file", []))
    assert file_count <= _MAX_REFERENCE_FILES, (
        f"Expected at most {_MAX_REFERENCE_FILES} files, got {file_count}"
    )


# ---------------------------------------------------------------------------
# _expand_references: category match but no fuzzy match (lines 331-332)
# ---------------------------------------------------------------------------
@given("a reference catalog with an empty actor list")
def step_catalog_empty_actor(context):
    """Create a catalog with an empty actor category."""
    context.test_catalog = {
        "file": ["readme.md"],
        "actor": [],
        "tool": [],
        "skill": [],
        "project": [],
        "plan": [],
    }


@when('I expand the reference "{token}" using that catalog')
def step_expand_reference_token(context, token):
    """Expand references in a line using the test catalog."""
    context.expanded_line = _expand_references(token, catalog=context.test_catalog)


@then('the token should remain unchanged as "{expected}"')
def step_verify_token_unchanged(context, expected):
    """Verify the token was not changed."""
    assert context.expanded_line == expected, (
        f"Expected '{expected}', got '{context.expanded_line}'"
    )


# ---------------------------------------------------------------------------
# _reference_suggestions: bare @-sign (line 360)
# ---------------------------------------------------------------------------
@given("a reference catalog with some files")
def step_catalog_with_files(context):
    """Create a catalog with some file entries."""
    context.test_catalog = {
        "file": ["readme.md", "src/main.py"],
        "actor": ["ns/coder"],
        "tool": [],
        "skill": [],
        "project": [],
        "plan": [],
    }


@when('I request suggestions for a line containing only "@"')
def step_suggestions_bare_at(context):
    """Request suggestions for a line with just '@'."""
    context.suggestions = _reference_suggestions("@", catalog=context.test_catalog)


@then("no suggestions should be returned")
def step_verify_no_suggestions(context):
    """Verify empty suggestions."""
    assert context.suggestions == [], f"Expected empty list, got {context.suggestions}"


# ---------------------------------------------------------------------------
# _print_persona_list: empty list (lines 423-424)
# ---------------------------------------------------------------------------
@given("a mock persona registry that returns no personas")
def step_mock_registry_no_personas(context):
    """Create a mock PersonaRegistry that returns an empty list."""
    context.mock_registry = MagicMock()
    context.mock_registry.list_personas.return_value = []


@when("I call print persona list")
def step_call_print_persona_list(context):
    """Call _print_persona_list with the mock registry."""
    sessions = {"default": _ReplSessionState(name="default", active_persona="default")}
    _print_persona_list(context.mock_registry, sessions, "default")


@then("the output should contain a no-personas warning")
def step_verify_no_personas_warning(context):
    """Verify _print_persona_list was called without errors on empty list."""
    # The function prints to console; if it reached here it means
    # lines 423-424 were executed (early return on empty list).
    context.mock_registry.list_personas.assert_called_once()


# ---------------------------------------------------------------------------
# _parse_persona_create: unknown flags (line 467)
# ---------------------------------------------------------------------------
@when('I parse persona create with tokens "{token_str}"')
def step_parse_persona_create_tokens(context, token_str):
    """Parse persona create from a token string.

    _parse_persona_create expects the full token list including 'persona'
    at index 0, so we prepend it.
    """
    tokens = ["persona", *shlex.split(token_str)]
    try:
        context.parsed_persona = _parse_persona_create(tokens)
        context.parse_error = None
    except ValueError as exc:
        context.parsed_persona = None
        context.parse_error = exc


@then('the resulting persona should have name "{name}" and actor "{actor}"')
def step_verify_persona_name_actor(context, name, actor):
    """Verify the persona was parsed correctly."""
    assert context.parse_error is None, f"Unexpected error: {context.parse_error}"
    assert context.parsed_persona.name == name
    assert context.parsed_persona.actor == actor


# ---------------------------------------------------------------------------
# _parse_persona_create: missing --actor (line 469)
# ---------------------------------------------------------------------------
@then("a ValueError about missing actor should be raised")
def step_verify_missing_actor_error(context):
    """Verify the ValueError about missing --actor."""
    assert context.parse_error is not None
    assert "actor" in str(context.parse_error).lower()


# ---------------------------------------------------------------------------
# _parse_persona_create: too few tokens (line 434)
# ---------------------------------------------------------------------------
@when("I parse persona create with only two tokens")
def step_parse_persona_create_few_tokens(context):
    """Try to parse persona create with insufficient tokens (< 4 total)."""
    try:
        context.parsed_persona = _parse_persona_create(["persona", "create", "name"])
        context.parse_error = None
    except ValueError as exc:
        context.parsed_persona = None
        context.parse_error = exc


@then("a ValueError about usage should be raised")
def step_verify_usage_error(context):
    """Verify the ValueError about usage."""
    assert context.parse_error is not None
    assert "usage" in str(context.parse_error).lower()


# ---------------------------------------------------------------------------
# _parse_persona_create: --cycle-order invalid (lines 463-464)
# ---------------------------------------------------------------------------
@when("I parse persona create with a non-integer cycle-order")
def step_parse_persona_create_bad_cycle_order(context):
    """Try to parse with a non-integer --cycle-order."""
    tokens = [
        "persona",
        "create",
        "mybot",
        "--actor",
        "ns/bot",
        "--cycle-order",
        "abc",
    ]
    try:
        context.parsed_persona = _parse_persona_create(tokens)
        context.parse_error = None
    except ValueError as exc:
        context.parsed_persona = None
        context.parse_error = exc


@then("a ValueError about cycle-order should be raised")
def step_verify_cycle_order_error(context):
    """Verify the ValueError about cycle-order."""
    assert context.parse_error is not None
    assert (
        "cycle-order" in str(context.parse_error).lower()
        or "cycle_order" in str(context.parse_error).lower()
    )


# ---------------------------------------------------------------------------
# Helpers for /persona slash commands
# ---------------------------------------------------------------------------
def _make_sessions():
    """Create a default sessions dict."""
    return {"default": _ReplSessionState(name="default", active_persona="default")}


@given("a mock persona registry for slash commands")
def step_mock_registry_for_slash(context):
    """Create a standard mock registry for slash command tests."""
    context.mock_registry = MagicMock()
    context.mock_registry.list_personas.return_value = []
    context.mock_registry.get_persona.return_value = None
    context.mock_registry.delete_persona.return_value = False
    context.sessions = _make_sessions()
    context.current_session = "default"


@given("a mock persona registry that returns no personas for slash commands")
def step_mock_registry_no_personas_slash(context):
    """Create a mock registry with no personas for pick."""
    context.mock_registry = MagicMock()
    context.mock_registry.list_personas.return_value = []
    context.sessions = _make_sessions()
    context.current_session = "default"


@given("a mock persona registry that cannot delete for slash commands")
def step_mock_registry_cannot_delete(context):
    """Create a mock registry where delete returns False."""
    context.mock_registry = MagicMock()
    context.mock_registry.delete_persona.return_value = False
    context.sessions = _make_sessions()
    context.current_session = "default"


@given("a mock persona registry that cannot find persona for slash commands")
def step_mock_registry_cannot_find(context):
    """Create a mock registry where get_persona returns None."""
    context.mock_registry = MagicMock()
    context.mock_registry.get_persona.return_value = None
    context.sessions = _make_sessions()
    context.current_session = "default"


@when('I handle slash command "{cmd}"')
def step_handle_slash_command(context, cmd):
    """Handle a slash command."""
    exit_code, session = _handle_slash_command(
        cmd,
        registry=context.mock_registry,
        sessions=context.sessions,
        current_session=context.current_session,
    )
    context.slash_exit_code = exit_code
    context.current_session = session


@then("the repl slash exit code should be {code:d}")
def step_verify_exit_code(context, code):
    """Verify the exit code from slash command."""
    assert context.slash_exit_code == code, (
        f"Expected exit code {code}, got {context.slash_exit_code}"
    )


# ---------------------------------------------------------------------------
# _run_shell_command: empty text (lines 386-387)
# ---------------------------------------------------------------------------
@when("I run a shell command with empty text")
def step_run_shell_empty(context):
    """Run a shell command with empty text."""
    context.shell_exit_code = _run_shell_command("")


@then("the shell exit code should be {code:d}")
def step_verify_shell_exit_code(context, code):
    """Verify the shell command exit code."""
    assert context.shell_exit_code == code, (
        f"Expected shell exit code {code}, got {context.shell_exit_code}"
    )


# ---------------------------------------------------------------------------
# _run_shell_command: disabled (lines 388-389)
# ---------------------------------------------------------------------------
@given("shell mode is disabled via environment")
def step_disable_shell_mode(context):
    """Set CLEVERAGENTS_DISABLE_SHELL_MODE=1."""
    context.old_disable_shell = os.environ.get("CLEVERAGENTS_DISABLE_SHELL_MODE")
    os.environ["CLEVERAGENTS_DISABLE_SHELL_MODE"] = "1"

    def restore():
        if context.old_disable_shell is None:
            os.environ.pop("CLEVERAGENTS_DISABLE_SHELL_MODE", None)
        else:
            os.environ["CLEVERAGENTS_DISABLE_SHELL_MODE"] = context.old_disable_shell

    context.add_cleanup(restore)


@when('I run a shell command with text "{cmd}"')
def step_run_shell_with_text(context, cmd):
    """Run a shell command with given text."""
    context.shell_exit_code = _run_shell_command(cmd)


@then("the shell mode disabled message is shown")
def step_verify_shell_disabled_msg(context):
    """Verify that the disabled message was shown (exit code 2 suffices)."""
    assert context.shell_exit_code == 2


# ---------------------------------------------------------------------------
# _run_shell_command: timeout (lines 404-408)
# ---------------------------------------------------------------------------
@when("I run a shell command that will time out")
def step_run_shell_timeout(context):
    """Run a shell command that times out by mocking subprocess.run."""
    with patch(
        "cleveragents.cli.commands.repl.subprocess.run",
        side_effect=subprocess.TimeoutExpired(cmd="sleep 999", timeout=30),
    ):
        context.shell_exit_code = _run_shell_command("sleep 999")


# ---------------------------------------------------------------------------
# _compose_prompt
# ---------------------------------------------------------------------------
@when(
    'I compose a prompt with base "{base}" and session "{session}" and persona "{persona}"'
)
def step_compose_prompt(context, base, session, persona):
    """Call _compose_prompt."""
    state = _ReplSessionState(name=session, active_persona=persona)
    context.composed_prompt = _compose_prompt(base + "> ", state)


@then("the prompt should contain session and persona markers")
def step_verify_prompt_markers(context):
    """Verify the composed prompt contains session and persona info."""
    assert "[" in context.composed_prompt
    assert "]" in context.composed_prompt
    assert "s1" in context.composed_prompt
    assert "coder" in context.composed_prompt


# ---------------------------------------------------------------------------
# _get_prompt_context
# ---------------------------------------------------------------------------
@given('rclcb CLEVERAGENTS_PROJECT is set to "{value}"')
def step_set_project(context, value):
    """Set the CLEVERAGENTS_PROJECT env var."""
    context.old_project = os.environ.get("CLEVERAGENTS_PROJECT")
    os.environ["CLEVERAGENTS_PROJECT"] = value

    def restore():
        if context.old_project is None:
            os.environ.pop("CLEVERAGENTS_PROJECT", None)
        else:
            os.environ["CLEVERAGENTS_PROJECT"] = context.old_project

    context.add_cleanup(restore)


@given('CLEVERAGENTS_PLAN is set to "{value}"')
def step_set_plan(context, value):
    """Set the CLEVERAGENTS_PLAN env var."""
    context.old_plan = os.environ.get("CLEVERAGENTS_PLAN")
    os.environ["CLEVERAGENTS_PLAN"] = value

    def restore():
        if context.old_plan is None:
            os.environ.pop("CLEVERAGENTS_PLAN", None)
        else:
            os.environ["CLEVERAGENTS_PLAN"] = context.old_plan

    context.add_cleanup(restore)


@given("CLEVERAGENTS_PROJECT is not set")
def step_unset_project(context):
    """Ensure CLEVERAGENTS_PROJECT is unset."""
    context.old_project = os.environ.pop("CLEVERAGENTS_PROJECT", None)

    def restore():
        if context.old_project is not None:
            os.environ["CLEVERAGENTS_PROJECT"] = context.old_project

    context.add_cleanup(restore)


@given("CLEVERAGENTS_PLAN is not set")
def step_unset_plan(context):
    """Ensure CLEVERAGENTS_PLAN is unset."""
    context.old_plan = os.environ.pop("CLEVERAGENTS_PLAN", None)

    def restore():
        if context.old_plan is not None:
            os.environ["CLEVERAGENTS_PLAN"] = context.old_plan

    context.add_cleanup(restore)


@when("I get the prompt context")
def step_get_prompt_context(context):
    """Call _get_prompt_context."""
    context.prompt_context = _get_prompt_context()


@then('the prompt context should contain "{expected}"')
def step_verify_prompt_context_contains(context, expected):
    """Verify prompt context contains expected string."""
    assert expected in context.prompt_context, (
        f"Expected '{expected}' in '{context.prompt_context}'"
    )


@then('the prompt context should be the default "{expected}"')
def step_verify_prompt_context_default(context, expected):
    """Verify prompt context is the default."""
    assert context.prompt_context == expected, (
        f"Expected '{expected}', got '{context.prompt_context}'"
    )


# ---------------------------------------------------------------------------
# _find_reference_candidates: empty query
# ---------------------------------------------------------------------------
@given("a list of reference options")
def step_create_reference_options(context):
    """Create a list of reference options."""
    context.ref_options = ["alpha.py", "beta.py", "gamma.py", "delta.py"]


@when("I find candidates with an empty query")
def step_find_candidates_empty(context):
    """Call _find_reference_candidates with empty query."""
    context.ref_candidates = _find_reference_candidates(
        "", context.ref_options, limit=3
    )


@then("the first items from the list should be returned")
def step_verify_first_items(context):
    """Verify the first N items are returned."""
    assert context.ref_candidates == ["alpha.py", "beta.py", "gamma.py"]


# ---------------------------------------------------------------------------
# _best_fuzzy_match: substring branch
# ---------------------------------------------------------------------------
@given('a list of options including "{option}"')
def step_create_options_list(context, option):
    """Create an options list containing the given option."""
    context.fuzzy_options = ["short", option, "other-thing"]


@when('I fuzzy match with query "{query}"')
def step_fuzzy_match(context, query):
    """Call _best_fuzzy_match."""
    context.fuzzy_result = _best_fuzzy_match(query, context.fuzzy_options)


@then('the match should be "{expected}"')
def step_verify_fuzzy_match(context, expected):
    """Verify fuzzy match result."""
    assert context.fuzzy_result == expected, (
        f"Expected '{expected}', got '{context.fuzzy_result}'"
    )


@then("the match should be None")
def step_verify_fuzzy_match_none(context):
    """Verify fuzzy match returned None."""
    assert context.fuzzy_result is None, f"Expected None, got '{context.fuzzy_result}'"


# ---------------------------------------------------------------------------
# _reference_suggestions: category-prefixed token
# ---------------------------------------------------------------------------
@given('a reference catalog with actors "{actor1}" and "{actor2}"')
def step_catalog_with_actors(context, actor1, actor2):
    """Create a catalog with specific actors."""
    context.test_catalog = {
        "file": [],
        "actor": [actor1, actor2],
        "tool": [],
        "skill": [],
        "project": [],
        "plan": [],
    }


@when('I request suggestions for a line containing "{token}"')
def step_suggestions_for_token(context, token):
    """Request suggestions for a line."""
    context.suggestions = _reference_suggestions(token, catalog=context.test_catalog)


@then("suggestions should include actor-prefixed candidates")
def step_verify_actor_suggestions(context):
    """Verify suggestions include @actor: prefixed values."""
    assert len(context.suggestions) > 0, "Expected at least one suggestion"
    assert any(s.startswith("@actor:") for s in context.suggestions), (
        f"Expected @actor: prefixed suggestions in {context.suggestions}"
    )


# ---------------------------------------------------------------------------
# _expand_references: escaped at-sign
# ---------------------------------------------------------------------------
@when("I expand a line containing a backslash-escaped at-sign token")
def step_expand_escaped_at(context):
    """Expand a line with \\@ to verify the backslash is stripped."""
    context.expanded_line = _expand_references(
        "\\@literal", catalog=context.test_catalog
    )


@then("the escaped at-sign should be stripped to a bare at-sign")
def step_verify_unescaped(context):
    """Verify the expanded line has the backslash removed."""
    assert context.expanded_line == "@literal", (
        f"Expected '@literal', got '{context.expanded_line!r}'"
    )


# ---------------------------------------------------------------------------
# _parse_persona_create: all optional flags
# ---------------------------------------------------------------------------
@when("I parse persona create with all flags")
def step_parse_persona_create_all_flags(context):
    """Parse persona create with all optional flags set."""
    tokens = [
        "persona",
        "create",
        "fullbot",
        "--actor",
        "ns/fullbot",
        "--description",
        "A full bot",
        "--icon",
        "robot",
        "--greeting",
        "Hello!",
        "--cycle-order",
        "5",
    ]
    try:
        context.parsed_persona = _parse_persona_create(tokens)
        context.parse_error = None
    except ValueError as exc:
        context.parsed_persona = None
        context.parse_error = exc


@then("the persona should have description icon greeting and cycle-order set")
def step_verify_all_flags_parsed(context):
    """Verify all optional fields were parsed."""
    assert context.parse_error is None, f"Unexpected error: {context.parse_error}"
    p = context.parsed_persona
    assert p.name == "fullbot"
    assert p.actor == "ns/fullbot"
    assert p.description == "A full bot"
    assert p.icon == "robot"
    assert p.greeting == "Hello!"
    assert p.cycle_order == 5
