"""Tests for schedule management API endpoints."""

from datetime import UTC, datetime
from typing import Literal
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from fastapi.testclient import TestClient

from mindroom.api.schedules import UpdateScheduleRequest, update_schedule
from mindroom.scheduling import CronSchedule, ScheduledTaskRecord, ScheduledWorkflow


def _task(
    task_id: str,
    *,
    room_id: str = "test_room",
    schedule_type: Literal["once", "cron"] = "once",
    execute_at: datetime | None = None,
    cron_fields: dict[str, str] | None = None,
    message: str = "@mindroom_test_agent ping",
    description: str = "Ping task",
    thread_id: str | None = "$thread123",
    new_thread: bool = False,
    history_limit: int | None = None,
) -> ScheduledTaskRecord:
    cron_schedule = None
    if cron_fields:
        cron_schedule = CronSchedule(**cron_fields)

    workflow = ScheduledWorkflow(
        schedule_type=schedule_type,
        execute_at=execute_at,
        cron_schedule=cron_schedule,
        message=message,
        description=description,
        history_limit=history_limit,
        thread_id=thread_id,
        room_id=room_id,
        created_by="@user:localhost",
        new_thread=new_thread,
    )
    return ScheduledTaskRecord(
        task_id=task_id,
        room_id=room_id,
        status="pending",
        created_at=datetime(2026, 1, 1, 12, 0, tzinfo=UTC),
        workflow=workflow,
    )


def _mock_agent_user() -> MagicMock:
    user = MagicMock()
    user.agent_name = "router"
    user.user_id = "@mindroom_router:localhost"
    user.display_name = "RouterAgent"
    user.password = "test_password"  # noqa: S105
    user.access_token = "test_token"  # noqa: S105
    return user


def _mock_matrix_client() -> AsyncMock:
    client = AsyncMock()
    client.close = AsyncMock()
    return client


def test_list_schedules_success(test_client: TestClient) -> None:
    """List schedules should return serialized pending tasks."""
    mock_client = _mock_matrix_client()
    tasks = [
        _task(
            "once1234",
            execute_at=datetime(2026, 2, 10, 15, 30, tzinfo=UTC),
            description="One-time task",
        ),
        _task(
            "cron1234",
            schedule_type="cron",
            cron_fields={"minute": "0", "hour": "9", "day": "*", "month": "*", "weekday": "*"},
            execute_at=None,
            description="Daily task",
            thread_id=None,
            new_thread=True,
            history_limit=5,
        ),
    ]

    with (
        patch("mindroom.api.schedules.create_agent_user", return_value=_mock_agent_user()),
        patch("mindroom.api.schedules.login_agent_user", return_value=mock_client),
        patch("mindroom.api.schedules.get_scheduled_tasks_for_room", return_value=tasks),
    ):
        response = test_client.get("/api/schedules")

    assert response.status_code == 200
    data = response.json()
    assert data["timezone"] == "UTC"
    assert len(data["tasks"]) == 2
    tasks_by_id = {task["task_id"]: task for task in data["tasks"]}
    assert tasks_by_id["once1234"]["schedule_type"] == "once"
    assert tasks_by_id["once1234"]["new_thread"] is False
    assert tasks_by_id["once1234"]["history_limit"] is None
    assert tasks_by_id["cron1234"]["cron_expression"] == "0 9 * * *"
    assert tasks_by_id["cron1234"]["new_thread"] is True
    assert tasks_by_id["cron1234"]["thread_id"] is None
    assert tasks_by_id["cron1234"]["history_limit"] == 5


def test_list_schedules_invalid_cron_does_not_fail(test_client: TestClient) -> None:
    """Invalid stored cron values should not crash schedule listing."""
    mock_client = _mock_matrix_client()
    tasks = [
        _task(
            "badcron1",
            schedule_type="cron",
            cron_fields={"minute": "70", "hour": "*", "day": "*", "month": "*", "weekday": "*"},
            execute_at=None,
            description="Invalid cron task",
        ),
    ]

    with (
        patch("mindroom.api.schedules.create_agent_user", return_value=_mock_agent_user()),
        patch("mindroom.api.schedules.login_agent_user", return_value=mock_client),
        patch("mindroom.api.schedules.get_scheduled_tasks_for_room", return_value=tasks),
    ):
        response = test_client.get("/api/schedules")

    assert response.status_code == 200
    data = response.json()
    assert len(data["tasks"]) == 1
    assert data["tasks"][0]["task_id"] == "badcron1"
    assert data["tasks"][0]["next_run_at"] is None


def test_update_schedule_once_success(test_client: TestClient) -> None:
    """Update endpoint should persist prompt and once schedule changes."""
    mock_client = _mock_matrix_client()
    existing_task = _task(
        "abc12345",
        execute_at=datetime(2026, 2, 10, 9, 0, tzinfo=UTC),
        description="Original description",
        message="@mindroom_test_agent original",
        thread_id=None,
        new_thread=True,
    )
    updated_workflow = ScheduledWorkflow(
        schedule_type="once",
        execute_at=datetime(2026, 3, 1, 10, 0, tzinfo=UTC),
        message="@mindroom_test_agent updated",
        description="Updated description",
        thread_id=existing_task.workflow.thread_id,
        room_id="test_room",
        created_by=existing_task.workflow.created_by,
        new_thread=existing_task.workflow.new_thread,
    )
    updated_task = ScheduledTaskRecord(
        task_id="abc12345",
        room_id="test_room",
        status="pending",
        created_at=existing_task.created_at,
        workflow=updated_workflow,
    )
    save_mock = AsyncMock(return_value=updated_task)

    with (
        patch("mindroom.api.schedules.create_agent_user", return_value=_mock_agent_user()),
        patch("mindroom.api.schedules.login_agent_user", return_value=mock_client),
        patch("mindroom.api.schedules.get_scheduled_task", return_value=existing_task),
        patch("mindroom.api.schedules.save_edited_scheduled_task", save_mock),
    ):
        response = test_client.put(
            "/api/schedules/abc12345",
            json={
                "room_id": "test_room",
                "schedule_type": "once",
                "execute_at": "2026-03-01T10:00:00Z",
                "message": "@mindroom_test_agent updated",
                "description": "Updated description",
            },
        )

    assert response.status_code == 200
    data = response.json()
    assert data["task_id"] == "abc12345"
    assert data["schedule_type"] == "once"
    assert data["message"] == "@mindroom_test_agent updated"
    assert data["description"] == "Updated description"
    assert data["execute_at"] == "2026-03-01T10:00:00Z"
    assert data["new_thread"] is True
    save_mock.assert_awaited_once()
    assert save_mock.await_args.kwargs["task_id"] == "abc12345"
    assert save_mock.await_args.kwargs["room_id"] == "test_room"
    assert save_mock.await_args.kwargs["workflow"].new_thread is True


@pytest.mark.asyncio
async def test_update_schedule_does_not_resolve_cache_path_when_not_restarting() -> None:
    """Pure API schedule edits should not construct or resolve an event cache."""
    mock_client = _mock_matrix_client()
    existing_task = _task(
        "abc12345",
        execute_at=datetime(2026, 2, 10, 9, 0, tzinfo=UTC),
        description="Original description",
        message="@mindroom_test_agent original",
    )
    updated_workflow = ScheduledWorkflow(
        schedule_type="once",
        execute_at=datetime(2026, 3, 1, 10, 0, tzinfo=UTC),
        message="@mindroom_test_agent updated",
        description="Updated description",
        thread_id=existing_task.workflow.thread_id,
        room_id="test_room",
        created_by=existing_task.workflow.created_by,
        new_thread=existing_task.workflow.new_thread,
    )
    updated_task = ScheduledTaskRecord(
        task_id="abc12345",
        room_id="test_room",
        status="pending",
        created_at=existing_task.created_at,
        workflow=updated_workflow,
    )
    runtime_config = MagicMock()
    runtime_config.cache.resolve_db_path.side_effect = AssertionError(
        "update_schedule should not resolve cache paths for state-only edits",
    )
    save_mock = AsyncMock(return_value=updated_task)
    api_request = MagicMock()

    with (
        patch(
            "mindroom.api.schedules.config_lifecycle.read_committed_runtime_config",
            return_value=(runtime_config, MagicMock()),
        ),
        patch("mindroom.api.schedules.resolve_room_id", return_value="test_room"),
        patch("mindroom.api.schedules.get_room_alias_from_id", return_value=None),
        patch("mindroom.api.schedules.create_agent_user", return_value=_mock_agent_user()),
        patch("mindroom.api.schedules.login_agent_user", return_value=mock_client),
        patch("mindroom.api.schedules.get_scheduled_task", return_value=existing_task),
        patch("mindroom.api.schedules.save_edited_scheduled_task", save_mock),
    ):
        response = await update_schedule(
            task_id="abc12345",
            request=UpdateScheduleRequest(
                room_id="test_room",
                schedule_type="once",
                execute_at=datetime(2026, 3, 1, 10, 0, tzinfo=UTC),
                message="@mindroom_test_agent updated",
                description="Updated description",
            ),
            api_request=api_request,
        )

    runtime_config.cache.resolve_db_path.assert_not_called()
    assert response.task_id == "abc12345"


def test_update_schedule_invalid_cron_expression(test_client: TestClient) -> None:
    """Invalid cron expressions should return 400."""
    mock_client = _mock_matrix_client()
    existing_task = _task(
        "cronbad1",
        schedule_type="cron",
        cron_fields={"minute": "0", "hour": "9", "day": "*", "month": "*", "weekday": "*"},
        execute_at=None,
        description="Cron task",
    )

    with (
        patch("mindroom.api.schedules.create_agent_user", return_value=_mock_agent_user()),
        patch("mindroom.api.schedules.login_agent_user", return_value=mock_client),
        patch("mindroom.api.schedules.get_scheduled_task", return_value=existing_task),
    ):
        response = test_client.put(
            "/api/schedules/cronbad1",
            json={
                "room_id": "test_room",
                "schedule_type": "cron",
                "cron_expression": "bad cron",
            },
        )

    assert response.status_code == 400
    assert "Invalid cron expression" in response.json()["detail"]


def test_cancel_schedule_success(test_client: TestClient) -> None:
    """Cancel endpoint should return success wrapper."""
    mock_client = _mock_matrix_client()
    existing_task = _task("abc12345", execute_at=datetime(2026, 2, 10, 9, 0, tzinfo=UTC))
    cancel_mock = AsyncMock(return_value="✅ Cancelled task `abc12345`")
    with (
        patch("mindroom.api.schedules.create_agent_user", return_value=_mock_agent_user()),
        patch("mindroom.api.schedules.login_agent_user", return_value=mock_client),
        patch("mindroom.api.schedules.get_scheduled_task", return_value=existing_task),
        patch("mindroom.api.schedules.cancel_scheduled_task", cancel_mock),
    ):
        response = test_client.delete("/api/schedules/abc12345?room_id=test_room")

    assert response.status_code == 200
    assert response.json()["success"] is True
    assert cancel_mock.await_args.kwargs["task_id"] == "abc12345"
    assert cancel_mock.await_args.kwargs["room_id"] == "test_room"


def test_cancel_schedule_returns_server_error_when_backend_cancel_fails(test_client: TestClient) -> None:
    """Cancel endpoint should report persistence failures as server errors."""
    mock_client = _mock_matrix_client()
    existing_task = _task("abc12345", execute_at=datetime(2026, 2, 10, 9, 0, tzinfo=UTC))
    cancel_mock = AsyncMock(return_value="❌ Failed to cancel task `abc12345`: Matrix rejected state write")
    with (
        patch("mindroom.api.schedules.create_agent_user", return_value=_mock_agent_user()),
        patch("mindroom.api.schedules.login_agent_user", return_value=mock_client),
        patch("mindroom.api.schedules.get_scheduled_task", return_value=existing_task),
        patch("mindroom.api.schedules.cancel_scheduled_task", cancel_mock),
    ):
        response = test_client.delete("/api/schedules/abc12345?room_id=test_room")

    assert response.status_code == 500
    assert response.json()["detail"] == "Failed to cancel task `abc12345`: Matrix rejected state write"


def test_cancel_schedule_returns_not_found_when_backend_cancel_reports_missing(test_client: TestClient) -> None:
    """Cancel endpoint should preserve 404 when the task disappears after the guard read."""
    mock_client = _mock_matrix_client()
    existing_task = _task("abc12345", execute_at=datetime(2026, 2, 10, 9, 0, tzinfo=UTC))
    cancel_mock = AsyncMock(return_value="❌ Task `abc12345` not found.")
    with (
        patch("mindroom.api.schedules.create_agent_user", return_value=_mock_agent_user()),
        patch("mindroom.api.schedules.login_agent_user", return_value=mock_client),
        patch("mindroom.api.schedules.get_scheduled_task", return_value=existing_task),
        patch("mindroom.api.schedules.cancel_scheduled_task", cancel_mock),
    ):
        response = test_client.delete("/api/schedules/abc12345?room_id=test_room")

    assert response.status_code == 404
    assert response.json()["detail"] == "Task `abc12345` not found."


def test_cancel_schedule_not_found(test_client: TestClient) -> None:
    """Cancel endpoint should return 404 when task does not exist."""
    mock_client = _mock_matrix_client()
    with (
        patch("mindroom.api.schedules.create_agent_user", return_value=_mock_agent_user()),
        patch("mindroom.api.schedules.login_agent_user", return_value=mock_client),
        patch("mindroom.api.schedules.get_scheduled_task", return_value=None),
    ):
        response = test_client.delete("/api/schedules/missing?room_id=test_room")

    assert response.status_code == 404


def test_update_schedule_once_to_cron(test_client: TestClient) -> None:
    """Switching from once to cron is rejected by the API."""
    mock_client = _mock_matrix_client()
    existing_task = _task(
        "switch01",
        execute_at=datetime(2026, 2, 10, 9, 0, tzinfo=UTC),
        thread_id=None,
        new_thread=True,
    )
    save_mock = AsyncMock()

    with (
        patch("mindroom.api.schedules.create_agent_user", return_value=_mock_agent_user()),
        patch("mindroom.api.schedules.login_agent_user", return_value=mock_client),
        patch("mindroom.api.schedules.get_scheduled_task", return_value=existing_task),
        patch("mindroom.api.schedules.save_edited_scheduled_task", save_mock),
    ):
        response = test_client.put(
            "/api/schedules/switch01",
            json={
                "room_id": "test_room",
                "schedule_type": "cron",
                "cron_expression": "30 8 * * 1-5",
            },
        )

    assert response.status_code == 400
    assert "Changing schedule_type is not supported" in response.json()["detail"]
    save_mock.assert_not_awaited()


def test_update_schedule_cron_to_once(test_client: TestClient) -> None:
    """Switching from cron to once is rejected by the API."""
    mock_client = _mock_matrix_client()
    existing_task = _task(
        "switch02",
        schedule_type="cron",
        cron_fields={"minute": "0", "hour": "9", "day": "*", "month": "*", "weekday": "*"},
        execute_at=None,
    )
    save_mock = AsyncMock()

    with (
        patch("mindroom.api.schedules.create_agent_user", return_value=_mock_agent_user()),
        patch("mindroom.api.schedules.login_agent_user", return_value=mock_client),
        patch("mindroom.api.schedules.get_scheduled_task", return_value=existing_task),
        patch("mindroom.api.schedules.save_edited_scheduled_task", save_mock),
    ):
        response = test_client.put(
            "/api/schedules/switch02",
            json={
                "room_id": "test_room",
                "schedule_type": "once",
                "execute_at": "2026-04-01T12:00:00Z",
            },
        )

    assert response.status_code == 400
    assert "Changing schedule_type is not supported" in response.json()["detail"]
    save_mock.assert_not_awaited()


def test_update_schedule_conflicting_fields(test_client: TestClient) -> None:
    """Sending execute_at with cron schedule_type should return 400."""
    mock_client = _mock_matrix_client()
    existing_task = _task(
        "conflict1",
        schedule_type="cron",
        cron_fields={"minute": "0", "hour": "9", "day": "*", "month": "*", "weekday": "*"},
        execute_at=None,
    )

    with (
        patch("mindroom.api.schedules.create_agent_user", return_value=_mock_agent_user()),
        patch("mindroom.api.schedules.login_agent_user", return_value=mock_client),
        patch("mindroom.api.schedules.get_scheduled_task", return_value=existing_task),
    ):
        response = test_client.put(
            "/api/schedules/conflict1",
            json={
                "room_id": "test_room",
                "schedule_type": "cron",
                "execute_at": "2026-04-01T12:00:00Z",
            },
        )

    assert response.status_code == 400
    assert "execute_at" in response.json()["detail"]


def test_update_schedule_empty_message(test_client: TestClient) -> None:
    """Updating with an empty message should return 400."""
    mock_client = _mock_matrix_client()
    existing_task = _task(
        "empty_msg",
        execute_at=datetime(2026, 2, 10, 9, 0, tzinfo=UTC),
    )

    with (
        patch("mindroom.api.schedules.create_agent_user", return_value=_mock_agent_user()),
        patch("mindroom.api.schedules.login_agent_user", return_value=mock_client),
        patch("mindroom.api.schedules.get_scheduled_task", return_value=existing_task),
    ):
        response = test_client.put(
            "/api/schedules/empty_msg",
            json={
                "room_id": "test_room",
                "message": "   ",
            },
        )

    assert response.status_code == 400
    assert "message" in response.json()["detail"]
