"""Tests for command parsing."""

from __future__ import annotations

from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock

import nio
import pytest

from mindroom.commands.handler import CommandHandlerContext, generate_welcome_message_for_room, handle_command
from mindroom.commands.parsing import (
    _COMMAND_DOCS,
    Command,
    CommandType,
    command_parser,
    get_command_help,
    get_compact_command_entries,
)
from mindroom.config.agent import AgentConfig, TeamConfig
from mindroom.config.main import Config
from mindroom.constants import RuntimePaths
from mindroom.matrix.identity import MatrixID
from mindroom.message_target import MessageTarget
from tests.conftest import make_event_cache_mock
from tests.identity_helpers import persist_entity_accounts

WELCOME_QUICK_COMMAND_LINES = [
    "\u2022 `!hi` - Show this welcome message again",
    "\u2022 `!schedule <time> <message>` - Schedule tasks and reminders",
    "\u2022 `!help [topic]` - Get detailed help",
]


def _test_runtime_paths(tmp_path: Path) -> RuntimePaths:
    return RuntimePaths(
        config_path=tmp_path / "config.yaml",
        config_dir=tmp_path,
        env_path=tmp_path / ".env",
        storage_root=tmp_path / "mindroom_data",
    )


def test_help_command() -> None:
    """Test help command parsing."""
    # Basic help
    command = command_parser.parse("!help")
    assert command is not None
    assert command.type == CommandType.HELP
    assert command.args["topic"] is None

    # Help with topic
    command = command_parser.parse("!help invite")
    assert command is not None
    assert command.type == CommandType.HELP
    assert command.args["topic"] == "invite"


def test_hi_command() -> None:
    """Test hi command parsing."""
    # Basic hi command
    command = command_parser.parse("!hi")
    assert command is not None
    assert command.type == CommandType.HI
    assert command.args == {}

    # Case insensitive
    command = command_parser.parse("!HI")
    assert command is not None
    assert command.type == CommandType.HI

    # With trailing space (should still work)
    command = command_parser.parse("!hi ")
    assert command is not None
    assert command.type == CommandType.HI


def test_invalid_commands() -> None:
    """Test that invalid commands are handled correctly."""
    # Commands that should return UNKNOWN
    unknown_commands = [
        "!invalid",
        "!unknowncmd",
        "!test123",
        "!notacommand",
    ]

    for cmd_text in unknown_commands:
        command = command_parser.parse(cmd_text)
        assert command is not None
        assert command.type == CommandType.UNKNOWN

    # Non-commands that should return None
    non_commands = [
        "invite calculator",  # Missing exclamation
        "just a regular message",
        "",
    ]

    for cmd_text in non_commands:
        command = command_parser.parse(cmd_text)
        assert command is None


def test_reload_plugins_command() -> None:
    """Test reload-plugins command parsing."""
    for cmd_text in ("!reload-plugins", "!reload_plugins", "!RELOAD-PLUGINS"):
        command = command_parser.parse(cmd_text)
        assert command is not None
        assert command.type == CommandType.RELOAD_PLUGINS
        assert command.args == {}


def test_schedule_command() -> None:
    """Test schedule command parsing."""
    # Basic schedule with time and message
    command = command_parser.parse("!schedule in 5 minutes Check the deployment")
    assert command is not None
    assert command.type == CommandType.SCHEDULE
    assert command.args["full_text"] == "in 5 minutes Check the deployment"

    # Schedule with just time expression
    command = command_parser.parse("!schedule tomorrow")
    assert command is not None
    assert command.type == CommandType.SCHEDULE
    assert command.args["full_text"] == "tomorrow"

    # Schedule with complex expression
    command = command_parser.parse("!schedule tomorrow at 3pm Send the weekly report")
    assert command is not None
    assert command.type == CommandType.SCHEDULE
    assert command.args["full_text"] == "tomorrow at 3pm Send the weekly report"


def test_list_schedules_command() -> None:
    """Test list schedules command parsing."""
    variations = [
        "!list_schedules",
        "!listschedules",
        "!list-schedules",
        "!list_schedule",  # singular with underscore
        "!listschedule",  # singular without separator
        "!list-schedule",  # singular with dash
        "!inspect_schedules",
        "!inspectschedules",
        "!inspect-schedules",
        "!inspect_schedule",
        "!inspectschedule",
        "!inspect-schedule",
        "!LIST_SCHEDULES",  # case insensitive
    ]

    for cmd_text in variations:
        command = command_parser.parse(cmd_text)
        assert command is not None
        assert command.type == CommandType.LIST_SCHEDULES
        assert command.args == {}


def test_removed_skill_command_is_unknown() -> None:
    """Test that the removed skill command is parsed as unknown."""
    command = command_parser.parse("!skill repo-quick-audit")
    assert command is not None
    assert command.type == CommandType.UNKNOWN
    assert command.args["raw_command"] == "!skill repo-quick-audit"

    command = command_parser.parse("!skill summarize Release notes")
    assert command is not None
    assert command.type == CommandType.UNKNOWN
    assert command.args["raw_command"] == "!skill summarize Release notes"

    command = command_parser.parse("!skill   ")
    assert command is not None
    assert command.type == CommandType.UNKNOWN
    assert command.args["raw_command"] == "!skill"


def test_all_commands_have_documentation() -> None:
    """Test that all CommandType values have documentation."""
    # Check that all commands have documentation (except UNKNOWN which is special)
    commands_needing_docs = set(CommandType) - {CommandType.UNKNOWN}
    missing_docs = commands_needing_docs - set(_COMMAND_DOCS.keys())
    assert not missing_docs, f"Missing documentation for commands: {missing_docs}"

    # Check that there are no extra documentation entries
    extra_docs = set(_COMMAND_DOCS.keys()) - set(CommandType)
    assert not extra_docs, f"Documentation for non-existent commands: {extra_docs}"

    # Check that all documentation entries are properly formatted
    for cmd_type, (syntax, description) in _COMMAND_DOCS.items():
        assert syntax.startswith("!"), f"{cmd_type} syntax should start with '!'"
        assert len(description) > 0, f"{cmd_type} should have a description"


def test_cancel_schedule_command() -> None:
    """Test cancel schedule command parsing."""
    # Basic cancel
    command = command_parser.parse("!cancel_schedule abc123")
    assert command is not None
    assert command.type == CommandType.CANCEL_SCHEDULE
    assert command.args["task_id"] == "abc123"
    assert command.args["cancel_all"] is False

    # With hyphen
    command = command_parser.parse("!cancel-schedule xyz789")
    assert command is not None
    assert command.type == CommandType.CANCEL_SCHEDULE
    assert command.args["task_id"] == "xyz789"
    assert command.args["cancel_all"] is False

    # Case insensitive
    command = command_parser.parse("!CANCEL_SCHEDULE task456")
    assert command is not None
    assert command.type == CommandType.CANCEL_SCHEDULE
    assert command.args["cancel_all"] is False

    # Cancel all tasks
    command = command_parser.parse("!cancel_schedule all")
    assert command is not None
    assert command.type == CommandType.CANCEL_SCHEDULE
    assert command.args["task_id"] == "all"
    assert command.args["cancel_all"] is True

    # Cancel all with different case
    command = command_parser.parse("!cancel_schedule ALL")
    assert command is not None
    assert command.type == CommandType.CANCEL_SCHEDULE
    assert command.args["task_id"] == "ALL"
    assert command.args["cancel_all"] is True


def test_edit_schedule_command() -> None:
    """Test edit schedule command parsing."""
    command = command_parser.parse("!edit_schedule abc123 in 10 minutes check deployment")
    assert command is not None
    assert command.type == CommandType.EDIT_SCHEDULE
    assert command.args["task_id"] == "abc123"
    assert command.args["full_text"] == "in 10 minutes check deployment"

    command = command_parser.parse("!edit-schedule task42 tomorrow at 9am @finance market update")
    assert command is not None
    assert command.type == CommandType.EDIT_SCHEDULE
    assert command.args["task_id"] == "task42"
    assert command.args["full_text"] == "tomorrow at 9am @finance market update"

    command = command_parser.parse("!EDIT_SCHEDULE id999 every weekday at 8am check alerts")
    assert command is not None
    assert command.type == CommandType.EDIT_SCHEDULE
    assert command.args["task_id"] == "id999"


def test_get_command_help() -> None:
    """Test help text generation."""
    # General help
    help_text = get_command_help()
    assert "Available Commands" in help_text
    assert "!schedule" in help_text
    assert "!help" in help_text
    assert "!schedule" in help_text
    assert "!list_schedules" in help_text
    assert "!cancel_schedule" in help_text
    assert "!edit_schedule" in help_text
    assert "!skill" not in help_text

    # Specific command help
    schedule_help = get_command_help("schedule")
    assert "Schedule Command" in schedule_help
    assert "Usage:" in schedule_help
    assert "Reminders" in schedule_help or "Workflows" in schedule_help

    # Schedule command help
    schedule_help = get_command_help("schedule")
    assert "Schedule Command" in schedule_help
    assert "Simple Reminders:" in schedule_help
    assert "Agent and Team Workflows:" in schedule_help
    assert "in 5 minutes" in schedule_help
    assert "Requires an explicit recurring polling cadence" in schedule_help
    assert "Automatically converts to smart polling" not in schedule_help

    list_schedules_help = get_command_help("list_schedules")
    assert "List Schedules Command" in list_schedules_help

    cancel_help = get_command_help("cancel_schedule")
    assert "Cancel Schedule Command" in cancel_help
    assert "cancel_schedule" in cancel_help

    edit_help = get_command_help("edit_schedule")
    assert "Edit Schedule Command" in edit_help
    assert "edit_schedule" in edit_help

    reload_help = get_command_help("reload-plugins")
    assert "Reload Plugins Command" in reload_help
    assert "!reload-plugins" in reload_help
    assert "Admin only" in reload_help

    reload_help_alias = get_command_help("reload_plugins")
    assert reload_help_alias == reload_help

    skill_help = get_command_help("skill")
    assert "Available Commands" in skill_help
    assert "!skill" not in skill_help


def test_compact_command_entries_characterize_welcome_subset() -> None:
    """Compact command docs preserve the existing welcome quick-command wording."""
    assert (
        get_compact_command_entries(
            (CommandType.HI, CommandType.SCHEDULE, CommandType.HELP),
            format_code=True,
        )
        == WELCOME_QUICK_COMMAND_LINES
    )


@pytest.mark.asyncio
async def test_welcome_message_uses_compact_command_docs(tmp_path: Path) -> None:
    """The welcome quick commands should match the parser-owned compact docs."""
    room = nio.MatrixRoom(room_id="!room:localhost", own_user_id="@mindroom_router:localhost")
    runtime_paths = _test_runtime_paths(tmp_path)
    config = Config()
    persist_entity_accounts(config, runtime_paths, usernames={"router": "mindroom_router_oldns"})
    welcome_message = await generate_welcome_message_for_room(
        None,
        room,
        "@alice:localhost",
        config,
        runtime_paths,
    )

    quick_command_block = "\u26a1 **Quick commands:**\n" + "\n".join(WELCOME_QUICK_COMMAND_LINES)
    assert quick_command_block in welcome_message
    assert "using its configured alias" in welcome_message
    assert "@mindroom_assistant" not in welcome_message


@pytest.mark.asyncio
async def test_welcome_message_lists_configured_teams(tmp_path: Path) -> None:
    """Rooms configured through teams should advertise those team mention targets."""
    room = nio.MatrixRoom(room_id="!room:localhost", own_user_id="@mindroom_router:localhost")
    config = Config(
        agents={"calculator": AgentConfig(display_name="Calculator")},
        teams={
            "ops": TeamConfig(
                display_name="Ops Team",
                role="Operations escalation team",
                agents=["calculator"],
                rooms=["!room:localhost"],
            ),
        },
    )
    runtime_paths = _test_runtime_paths(tmp_path)
    persist_entity_accounts(
        config,
        runtime_paths,
        usernames={
            "router": "mindroom_router_oldns",
            "calculator": "mindroom_calculator_oldns",
            "ops": "mindroom_ops_oldns",
        },
    )
    welcome_message = await generate_welcome_message_for_room(
        None,
        room,
        "@alice:localhost",
        config,
        runtime_paths,
    )

    assert "\U0001f9e0 **Available agents and teams in this room:**" in welcome_message
    assert "\u2022 **@ops**: Operations escalation team (Team of 1 agent)" in welcome_message


@pytest.mark.asyncio
async def test_senderless_welcome_lists_configured_room_responders(tmp_path: Path) -> None:
    """Senderless configured-room welcomes should advertise static room responders."""
    room = nio.MatrixRoom(room_id="!room:localhost", own_user_id="@mindroom_router:localhost")
    room.add_member("@mindroom_research_oldns:localhost", "Research", None)
    room.members_synced = True
    config = Config(
        agents={
            "code": AgentConfig(
                display_name="Code",
                role="Writes code",
                rooms=["!room:localhost"],
            ),
            "research": AgentConfig(
                display_name="Research",
                role="Finds sources",
            ),
        },
        teams={
            "ops": TeamConfig(
                display_name="Ops Team",
                role="Operations escalation team",
                agents=["code"],
                rooms=["!room:localhost"],
            ),
        },
    )
    runtime_paths = _test_runtime_paths(tmp_path)
    persist_entity_accounts(
        config,
        runtime_paths,
        usernames={
            "router": "mindroom_router_oldns",
            "code": "mindroom_code_oldns",
            "research": "mindroom_research_oldns",
            "ops": "mindroom_ops_oldns",
        },
    )

    welcome_message = await generate_welcome_message_for_room(
        None,
        room,
        None,
        config,
        runtime_paths,
    )

    assert "\U0001f9e0 **Available agents and teams in this room:**" in welcome_message
    assert "\u2022 **@code**: Writes code" in welcome_message
    assert "\u2022 **@ops**: Operations escalation team (Team of 1 agent)" in welcome_message
    assert "@research" not in welcome_message


@pytest.mark.asyncio
async def test_hi_command_lists_ad_hoc_present_responder(tmp_path: Path) -> None:
    """Ad-hoc room welcomes should advertise the same live responders routing can target."""
    config = Config(
        agents={
            "code": AgentConfig(
                display_name="Code",
                role="Writes code",
            ),
        },
    )
    runtime_paths = _test_runtime_paths(tmp_path)
    persist_entity_accounts(
        config,
        runtime_paths,
        usernames={"router": "mindroom_router_oldns", "code": "mindroom_code_oldns"},
    )
    room = nio.MatrixRoom(room_id="!adhoc:localhost", own_user_id="@mindroom_router:localhost")
    room.add_member("@mindroom_code_oldns:localhost", "Code", None)
    room.members_synced = True
    send_response = AsyncMock(return_value="$welcome")
    command = Command(type=CommandType.HI, args={}, raw_text="!hi")
    event = SimpleNamespace(
        sender="@alice:localhost",
        event_id="$event",
        body="!hi",
        source={"content": {"body": "!hi"}},
    )
    context = CommandHandlerContext(
        client=AsyncMock(),
        config=config,
        runtime_paths=runtime_paths,
        logger=MagicMock(),
        conversation_cache=MagicMock(),
        event_cache=make_event_cache_mock(),
        stable_target=MessageTarget.resolve("!adhoc:localhost", None, "$event"),
        record_handled_turn=MagicMock(),
        record_command_result=AsyncMock(),
        send_response=send_response,
    )

    await handle_command(
        context=context,
        room=room,
        event=event,
        command=command,
        requester_user_id="@alice:localhost",
    )

    response_text = send_response.await_args.args[0]
    assert "\U0001f9e0 **Available agents and teams in this room:**" in response_text
    assert "\u2022 **@code**: Writes code" in response_text
    context.client.joined_members.assert_not_awaited()


@pytest.mark.asyncio
async def test_hi_command_uses_live_responder_candidates_when_available(tmp_path: Path) -> None:
    """The live command path should mirror routing's responder candidate boundary."""
    config = Config(
        agents={
            "code": AgentConfig(
                display_name="Code",
                role="Writes code",
            ),
            "research": AgentConfig(
                display_name="Research",
                role="Researches topics",
            ),
        },
    )
    runtime_paths = _test_runtime_paths(tmp_path)
    persist_entity_accounts(
        config,
        runtime_paths,
        usernames={
            "router": "mindroom_router",
            "code": "mindroom_code",
            "research": "mindroom_research",
        },
    )
    room = nio.MatrixRoom(room_id="!room:localhost", own_user_id="@mindroom_router:localhost")
    send_response = AsyncMock(return_value="$welcome")
    candidate_resolver = AsyncMock(return_value=[MatrixID.parse("@mindroom_code:localhost")])
    context = CommandHandlerContext(
        client=AsyncMock(),
        config=config,
        runtime_paths=runtime_paths,
        logger=MagicMock(),
        conversation_cache=MagicMock(),
        event_cache=make_event_cache_mock(),
        stable_target=MessageTarget.resolve("!room:localhost", None, "$event"),
        record_handled_turn=MagicMock(),
        record_command_result=AsyncMock(),
        send_response=send_response,
        responder_candidates_for_room=candidate_resolver,
    )

    await handle_command(
        context=context,
        room=room,
        event=SimpleNamespace(
            sender="@alice:localhost",
            event_id="$event",
            body="!hi",
            source={"content": {"body": "!hi"}},
        ),
        command=Command(type=CommandType.HI, args={}, raw_text="!hi"),
        requester_user_id="@alice:localhost",
    )

    candidate_resolver.assert_awaited_once_with(room, "@alice:localhost")
    response_text = send_response.await_args.args[0]
    assert "\u2022 **@code**: Writes code" in response_text
    assert "@research" not in response_text


@pytest.mark.asyncio
@pytest.mark.parametrize("extra_member", [None, "@bob:localhost"])
async def test_desktop_command_resolves_exact_agent_from_router_candidates(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
    extra_member: str | None,
) -> None:
    """Router-owned commands require the sole eligible agent and no unrelated room member."""
    runtime_paths = _test_runtime_paths(tmp_path)
    config = Config.validate_with_runtime(
        {
            "defaults": {"tools": []},
            "agents": {
                "code": {
                    "display_name": "Code",
                    "role": "Writes code",
                    "tools": ["desktop"],
                },
            },
        },
        runtime_paths,
    )
    persist_entity_accounts(
        config,
        runtime_paths,
        usernames={"router": "mindroom_router", "code": "mindroom_code"},
    )
    room = nio.MatrixRoom(room_id="!room:localhost", own_user_id="@mindroom_router:localhost")
    for user_id in ("@mindroom_router:localhost", "@mindroom_code:localhost", "@alice:localhost"):
        room.add_member(user_id, None, None)
    if extra_member is not None:
        room.add_member(extra_member, None, None)
    send_response = AsyncMock(return_value="$desktop")
    candidate_resolver = AsyncMock(return_value=[MatrixID.parse("@mindroom_code:localhost")])
    desktop_handler = MagicMock(return_value="desktop status")
    monkeypatch.setattr("mindroom.commands.handler.handle_desktop_command", desktop_handler)
    context = CommandHandlerContext(
        client=AsyncMock(),
        config=config,
        runtime_paths=runtime_paths,
        logger=MagicMock(),
        conversation_cache=MagicMock(),
        event_cache=make_event_cache_mock(),
        stable_target=MessageTarget.resolve("!room:localhost", None, "$event"),
        record_handled_turn=MagicMock(),
        record_command_result=AsyncMock(),
        send_response=send_response,
        responder_candidates_for_room=candidate_resolver,
    )

    await handle_command(
        context=context,
        room=room,
        event=SimpleNamespace(
            sender="@alice:localhost",
            event_id="$event",
            body="!desktop status",
            source={"content": {"body": "!desktop status"}},
        ),
        command=Command(
            type=CommandType.DESKTOP,
            args={"args_text": "status"},
            raw_text="!desktop status",
        ),
        requester_user_id="@alice:localhost",
    )

    if extra_member is None:
        assert desktop_handler.call_args.kwargs["scope"].agent_name == "code"
        assert desktop_handler.call_args.kwargs["scope"].requester_id == "@alice:localhost"
    else:
        desktop_handler.assert_not_called()
        assert "private room" in send_response.await_args.args[0]


def test_docs_index_chat_commands_summary_lists_all_supported_commands() -> None:
    """The docs index summary should stay in sync with the supported command set."""
    docs_index = Path(__file__).resolve().parents[1] / "docs" / "index.md"
    contents = docs_index.read_text(encoding="utf-8")
    table_row = next(line for line in contents.splitlines() if line.startswith("| **Chat Commands** |"))
    doc_link = next(line for line in contents.splitlines() if line.startswith("- [Chat Commands]("))

    for syntax, _description in _COMMAND_DOCS.values():
        # Table cells escape literal pipes in command syntax as "\|".
        assert syntax in table_row.replace("\\|", "|")
        assert syntax in doc_link
