"""Tests for shared schedule entrypoint and scheduler tool."""

from __future__ import annotations

import tempfile
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

import mindroom.tools  # noqa: F401
from mindroom.config.agent import AgentConfig
from mindroom.config.main import Config
from mindroom.custom_tools.scheduler import SchedulerTools
from mindroom.message_target import MessageTarget
from mindroom.scheduling import SchedulingRuntime, _extract_mentioned_agents_from_text
from mindroom.tool_system.runtime_context import ToolRuntimeContext, tool_runtime_context
from tests.conftest import bind_runtime_paths, make_event_cache_mock, runtime_paths_for, test_runtime_paths
from tests.identity_helpers import entity_ids, persist_entity_accounts


def _bind_runtime_paths(config: Config) -> Config:
    runtime_paths = test_runtime_paths(Path(tempfile.mkdtemp()))
    bound = bind_runtime_paths(config, runtime_paths)
    persist_entity_accounts(bound, runtime_paths)
    return bound


def _make_context(config: Config, *, matrix_admin: object | None = None) -> ToolRuntimeContext:
    return ToolRuntimeContext(
        agent_name="general",
        target=MessageTarget.resolve(
            room_id="!room:localhost",
            thread_id="$thread",
            reply_to_event_id=None,
        ),
        requester_id="@user:localhost",
        client=AsyncMock(),
        config=config,
        runtime_paths=runtime_paths_for(config),
        conversation_cache=MagicMock(),
        event_cache=make_event_cache_mock(),
        room=MagicMock(),
        storage_path=None,
        matrix_admin=matrix_admin,
    )


def test_extract_mentioned_agents_from_text() -> None:
    """Agent mentions should be extracted from scheduling text."""
    config = _bind_runtime_paths(Config(agents={"general": AgentConfig(display_name="General Agent")}))
    result = _extract_mentioned_agents_from_text(
        "in 5 minutes @general check deployment",
        config,
        runtime_paths_for(config),
    )
    expected_agent = entity_ids(config, runtime_paths_for(config))["general"]
    assert result == [expected_agent]


@pytest.mark.asyncio
async def test_scheduler_tool_requires_context() -> None:
    """Tool should fail clearly when called outside Matrix response context."""
    tools = SchedulerTools()

    result = await tools.schedule("in 10 minutes remind me to check logs", new_thread=False)

    assert "unavailable" in result


def test_scheduler_tool_requires_explicit_delivery_mode() -> None:
    """The model-facing schema must not silently choose a delivery scope."""
    function = SchedulerTools().async_functions["schedule"].model_copy(deep=True)
    function.process_entrypoint()

    assert set(function.parameters["required"]) == {"request", "new_thread"}


@pytest.mark.asyncio
async def test_scheduler_tool_uses_shared_backend() -> None:
    """Tool should call the same scheduling backend path as !schedule."""
    tools = SchedulerTools()
    config = _bind_runtime_paths(Config(agents={"general": AgentConfig(display_name="General Agent")}))
    matrix_admin = object()
    context = _make_context(config, matrix_admin=matrix_admin)

    with (
        patch(
            "mindroom.custom_tools.scheduler.schedule_task",
            new=AsyncMock(return_value=("task123", "✅ Scheduled")),
        ) as mock_schedule,
        tool_runtime_context(context),
    ):
        result = await tools.schedule("tomorrow at 3pm check deployment", new_thread=False)
        new_thread_result = await tools.schedule("tomorrow at 4pm check deployment", new_thread=True)
        limited_result = await tools.schedule("every 25 minutes poll the queue", new_thread=False, history_limit=0)

    assert result == "✅ Scheduled"
    assert new_thread_result == "✅ Scheduled"
    assert limited_result == "✅ Scheduled"
    assert mock_schedule.await_count == 3
    first_call = mock_schedule.await_args_list[0].kwargs
    second_call = mock_schedule.await_args_list[1].kwargs
    third_call = mock_schedule.await_args_list[2].kwargs
    expected_runtime = SchedulingRuntime(
        client=context.client,
        config=context.config,
        runtime_paths=context.runtime_paths,
        room=context.room,
        conversation_cache=context.conversation_cache,
        event_cache=context.event_cache,
        matrix_admin=matrix_admin,
    )
    assert first_call == {
        "runtime": expected_runtime,
        "room_id": context.room_id,
        "thread_id": context.resolved_thread_id,
        "scheduled_by": context.requester_id,
        "full_text": "tomorrow at 3pm check deployment",
        "new_thread": False,
        "history_limit": None,
    }
    assert second_call == {
        "runtime": expected_runtime,
        "room_id": context.room_id,
        "thread_id": context.resolved_thread_id,
        "scheduled_by": context.requester_id,
        "full_text": "tomorrow at 4pm check deployment",
        "new_thread": True,
        "history_limit": None,
    }
    assert third_call == {
        "runtime": expected_runtime,
        "room_id": context.room_id,
        "thread_id": context.resolved_thread_id,
        "scheduled_by": context.requester_id,
        "full_text": "every 25 minutes poll the queue",
        "new_thread": False,
        "history_limit": 0,
    }


@pytest.mark.asyncio
async def test_scheduler_tool_raises_when_backend_rejects_request() -> None:
    """Invalid schedule requests should fail the tool call instead of returning error text."""
    tools = SchedulerTools()
    config = _bind_runtime_paths(Config(agents={"general": AgentConfig(display_name="General Agent")}))
    context = _make_context(config)

    with (
        patch(
            "mindroom.custom_tools.scheduler.schedule_task",
            new=AsyncMock(return_value=(None, "❌ Failed to schedule: schedule is not valid")),
        ),
        tool_runtime_context(context),
        pytest.raises(RuntimeError, match="schedule is not valid"),
    ):
        await tools.schedule("next message", new_thread=False)


@pytest.mark.asyncio
async def test_edit_schedule_tool_requires_context() -> None:
    """Edit tool should fail clearly when called outside Matrix response context."""
    tools = SchedulerTools()
    result = await tools.edit_schedule("task123", "tomorrow at 9am check logs")
    assert "unavailable" in result


@pytest.mark.asyncio
async def test_edit_schedule_tool_calls_backend() -> None:
    """Edit tool should call edit_scheduled_task with correct arguments."""
    tools = SchedulerTools()
    config = _bind_runtime_paths(Config(agents={"general": AgentConfig(display_name="General Agent")}))
    context = _make_context(config)

    with (
        patch(
            "mindroom.custom_tools.scheduler.edit_scheduled_task",
            new=AsyncMock(return_value="✅ Updated task `task123`."),
        ) as mock_edit,
        tool_runtime_context(context),
    ):
        result = await tools.edit_schedule("task123", "tomorrow at 9am check deployment")
        limited_result = await tools.edit_schedule("task123", "keep the same schedule", history_limit=5)

    assert "Updated" in result
    assert "Updated" in limited_result
    expected_runtime = SchedulingRuntime(
        client=context.client,
        config=context.config,
        runtime_paths=context.runtime_paths,
        room=context.room,
        conversation_cache=context.conversation_cache,
        event_cache=context.event_cache,
    )
    assert mock_edit.await_count == 2
    assert mock_edit.await_args_list[0].kwargs == {
        "runtime": expected_runtime,
        "room_id": context.room_id,
        "task_id": "task123",
        "full_text": "tomorrow at 9am check deployment",
        "scheduled_by": context.requester_id,
        "thread_id": context.resolved_thread_id,
        "history_limit": None,
    }
    assert mock_edit.await_args_list[1].kwargs == {
        "runtime": expected_runtime,
        "room_id": context.room_id,
        "task_id": "task123",
        "full_text": "keep the same schedule",
        "scheduled_by": context.requester_id,
        "thread_id": context.resolved_thread_id,
        "history_limit": 5,
    }


@pytest.mark.asyncio
async def test_edit_schedule_tool_raises_when_backend_rejects_request() -> None:
    """Edit failures should fail the tool call instead of returning error text."""
    tools = SchedulerTools()
    config = _bind_runtime_paths(Config(agents={"general": AgentConfig(display_name="General Agent")}))
    context = _make_context(config)

    with (
        patch(
            "mindroom.custom_tools.scheduler.edit_scheduled_task",
            new=AsyncMock(return_value="❌ Failed to edit task `task123`."),
        ),
        tool_runtime_context(context),
        pytest.raises(RuntimeError, match="Failed to edit task"),
    ):
        await tools.edit_schedule("task123", "tomorrow at 9am check deployment")


@pytest.mark.asyncio
async def test_list_schedules_tool_requires_context() -> None:
    """List tool should fail clearly when called outside Matrix response context."""
    tools = SchedulerTools()
    result = await tools.list_schedules()
    assert "unavailable" in result


@pytest.mark.asyncio
async def test_list_schedules_tool_calls_backend() -> None:
    """List tool should call list_scheduled_tasks with correct arguments."""
    tools = SchedulerTools()
    config = _bind_runtime_paths(Config(agents={"general": AgentConfig(display_name="General Agent")}))
    context = _make_context(config)

    with (
        patch(
            "mindroom.custom_tools.scheduler.list_scheduled_tasks",
            new=AsyncMock(return_value="**Scheduled Tasks:**\n• `abc` - in 5 minutes"),
        ) as mock_list,
        tool_runtime_context(context),
    ):
        result = await tools.list_schedules()

    assert "Scheduled Tasks" in result
    mock_list.assert_awaited_once_with(
        client=context.client,
        room_id=context.room_id,
        thread_id=context.resolved_thread_id,
        config=context.config,
    )


@pytest.mark.asyncio
async def test_list_schedules_tool_raises_when_backend_rejects_request() -> None:
    """List failures should fail the tool call instead of returning error text."""
    tools = SchedulerTools()
    config = _bind_runtime_paths(Config(agents={"general": AgentConfig(display_name="General Agent")}))
    context = _make_context(config)

    with (
        patch(
            "mindroom.custom_tools.scheduler.list_scheduled_tasks",
            new=AsyncMock(return_value="Unable to retrieve scheduled tasks."),
        ),
        tool_runtime_context(context),
        pytest.raises(RuntimeError, match="Unable to retrieve scheduled tasks"),
    ):
        await tools.list_schedules()


@pytest.mark.asyncio
async def test_cancel_schedule_tool_requires_context() -> None:
    """Cancel tool should fail clearly when called outside Matrix response context."""
    tools = SchedulerTools()
    result = await tools.cancel_schedule("task123")
    assert "unavailable" in result


@pytest.mark.asyncio
async def test_cancel_schedule_tool_calls_backend() -> None:
    """Cancel tool should call cancel_scheduled_task with correct arguments."""
    tools = SchedulerTools()
    config = _bind_runtime_paths(Config(agents={"general": AgentConfig(display_name="General Agent")}))
    matrix_admin = object()
    context = _make_context(config, matrix_admin=matrix_admin)

    with (
        patch(
            "mindroom.custom_tools.scheduler.cancel_scheduled_task",
            new=AsyncMock(return_value="✅ Cancelled task `task123`"),
        ) as mock_cancel,
        tool_runtime_context(context),
    ):
        result = await tools.cancel_schedule("task123")

    assert "Cancelled" in result
    mock_cancel.assert_awaited_once_with(
        client=context.client,
        room_id=context.room_id,
        task_id="task123",
        matrix_admin=matrix_admin,
    )


@pytest.mark.asyncio
async def test_cancel_schedule_tool_raises_when_backend_rejects_request() -> None:
    """Cancel failures should fail the tool call instead of returning error text."""
    tools = SchedulerTools()
    config = _bind_runtime_paths(Config(agents={"general": AgentConfig(display_name="General Agent")}))
    context = _make_context(config)

    with (
        patch(
            "mindroom.custom_tools.scheduler.cancel_scheduled_task",
            new=AsyncMock(return_value="❌ Task `task123` not found."),
        ),
        tool_runtime_context(context),
        pytest.raises(RuntimeError, match="not found"),
    ):
        await tools.cancel_schedule("task123")
