"""Tests for Habitica todo platform."""

from collections.abc import Generator
from datetime import date
from typing import Any
from unittest.mock import AsyncMock, patch
from uuid import UUID

from habiticalib import (
    Direction,
    HabiticaTaskOrderResponse,
    HabiticaTasksResponse,
    Task,
    TaskType,
)
import pytest
from syrupy.assertion import SnapshotAssertion

from homeassistant.components.habitica.const import DOMAIN
from homeassistant.components.todo import (
    ATTR_DESCRIPTION,
    ATTR_DUE_DATE,
    ATTR_ITEM,
    ATTR_RENAME,
    ATTR_STATUS,
    DOMAIN as TODO_DOMAIN,
    TodoServices,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import entity_registry as er

from .conftest import ERROR_NOT_FOUND, ERROR_TOO_MANY_REQUESTS

from tests.common import (
    MockConfigEntry,
    async_get_persistent_notifications,
    async_load_fixture,
    snapshot_platform,
)
from tests.typing import WebSocketGenerator


@pytest.fixture(autouse=True)
def todo_only() -> Generator[None]:
    """Enable only the todo platform."""
    with patch(
        "homeassistant.components.habitica.PLATFORMS",
        [Platform.TODO],
    ):
        yield


@pytest.mark.usefixtures("habitica")
async def test_todos(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    snapshot: SnapshotAssertion,
    entity_registry: er.EntityRegistry,
) -> None:
    """Test todo platform."""

    config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.LOADED

    await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)


@pytest.mark.parametrize(
    ("entity_id"),
    [
        "todo.test_user_to_do_s",
        "todo.test_user_dailies",
    ],
)
@pytest.mark.usefixtures("habitica")
async def test_todo_items(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    snapshot: SnapshotAssertion,
    entity_id: str,
) -> None:
    """Test items on todo lists."""

    config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.LOADED

    result = await hass.services.async_call(
        TODO_DOMAIN,
        TodoServices.GET_ITEMS,
        {},
        target={ATTR_ENTITY_ID: entity_id},
        blocking=True,
        return_response=True,
    )

    assert result == snapshot


@pytest.mark.freeze_time("2024-09-21 00:00:00")
@pytest.mark.parametrize(
    ("entity_id", "uid"),
    [
        ("todo.test_user_to_do_s", "88de7cd9-af2b-49ce-9afd-bf941d87336b"),
        ("todo.test_user_dailies", "f2c85972-1a19-4426-bc6d-ce3337b9d99f"),
    ],
    ids=["todo", "daily"],
)
async def test_complete_todo_item(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    habitica: AsyncMock,
    snapshot: SnapshotAssertion,
    entity_id: str,
    uid: str,
) -> None:
    """Test completing an item on the todo list."""

    config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.LOADED

    await hass.services.async_call(
        TODO_DOMAIN,
        TodoServices.UPDATE_ITEM,
        {ATTR_ITEM: uid, ATTR_STATUS: "completed"},
        target={ATTR_ENTITY_ID: entity_id},
        blocking=True,
    )

    habitica.update_score.assert_awaited_once_with(UUID(uid), Direction.UP)

    # Test notification for item drop
    notifications = async_get_persistent_notifications(hass)
    assert len(notifications) == 1
    _id, *_ = notifications
    assert snapshot == (notifications[_id]["title"], notifications[_id]["message"])


@pytest.mark.parametrize(
    ("entity_id", "uid"),
    [
        ("todo.test_user_to_do_s", "162f0bbe-a097-4a06-b4f4-8fbeed85d2ba"),
        ("todo.test_user_dailies", "564b9ac9-c53d-4638-9e7f-1cd96fe19baa"),
    ],
    ids=["todo", "daily"],
)
async def test_uncomplete_todo_item(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    habitica: AsyncMock,
    entity_id: str,
    uid: str,
) -> None:
    """Test uncompleting an item on the todo list."""

    config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.LOADED

    await hass.services.async_call(
        TODO_DOMAIN,
        TodoServices.UPDATE_ITEM,
        {ATTR_ITEM: uid, ATTR_STATUS: "needs_action"},
        target={ATTR_ENTITY_ID: entity_id},
        blocking=True,
    )

    habitica.update_score.assert_called_once_with(UUID(uid), Direction.DOWN)


@pytest.mark.parametrize(
    ("uid", "status"),
    [
        ("88de7cd9-af2b-49ce-9afd-bf941d87336b", "completed"),
        ("162f0bbe-a097-4a06-b4f4-8fbeed85d2ba", "needs_action"),
    ],
    ids=["completed", "needs_action"],
)
@pytest.mark.parametrize(
    ("exception", "exc_msg", "expected_exception"),
    [
        (
            ERROR_NOT_FOUND,
            r"Unable to update the score for your Habitica to-do `.+`, please try again",
            ServiceValidationError,
        ),
        (
            ERROR_TOO_MANY_REQUESTS,
            "Rate limit exceeded, try again in 5 seconds",
            HomeAssistantError,
        ),
    ],
)
async def test_complete_todo_item_exception(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    habitica: AsyncMock,
    uid: str,
    status: str,
    exception: Exception,
    exc_msg: str,
    expected_exception: Exception,
) -> None:
    """Test exception when completing/uncompleting an item on the todo list."""

    config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.LOADED

    habitica.update_score.side_effect = exception
    with pytest.raises(
        expected_exception=expected_exception,
        match=exc_msg,
    ):
        await hass.services.async_call(
            TODO_DOMAIN,
            TodoServices.UPDATE_ITEM,
            {ATTR_ITEM: uid, ATTR_STATUS: status},
            target={ATTR_ENTITY_ID: "todo.test_user_to_do_s"},
            blocking=True,
        )


@pytest.mark.parametrize(
    ("entity_id", "service_data", "call_args"),
    [
        (
            "todo.test_user_to_do_s",
            {
                ATTR_ITEM: "88de7cd9-af2b-49ce-9afd-bf941d87336b",
                ATTR_RENAME: "test-summary",
                ATTR_DESCRIPTION: "test-description",
                ATTR_DUE_DATE: date(2024, 7, 30),
            },
            (
                UUID("88de7cd9-af2b-49ce-9afd-bf941d87336b"),
                Task(
                    notes="test-description",
                    text="test-summary",
                    date=date(2024, 7, 30),
                ),
            ),
        ),
        (
            "todo.test_user_to_do_s",
            {
                ATTR_ITEM: "88de7cd9-af2b-49ce-9afd-bf941d87336b",
                ATTR_RENAME: "test-summary",
                ATTR_DESCRIPTION: "test-description",
                ATTR_DUE_DATE: None,
            },
            (
                UUID("88de7cd9-af2b-49ce-9afd-bf941d87336b"),
                Task(
                    notes="test-description",
                    text="test-summary",
                    date=None,
                ),
            ),
        ),
        (
            "todo.test_user_to_do_s",
            {
                ATTR_ITEM: "88de7cd9-af2b-49ce-9afd-bf941d87336b",
                ATTR_RENAME: "test-summary",
                ATTR_DESCRIPTION: None,
                ATTR_DUE_DATE: date(2024, 7, 30),
            },
            (
                UUID("88de7cd9-af2b-49ce-9afd-bf941d87336b"),
                Task(
                    notes="",
                    text="test-summary",
                    date=date(2024, 7, 30),
                ),
            ),
        ),
        (
            "todo.test_user_dailies",
            {
                ATTR_ITEM: "f2c85972-1a19-4426-bc6d-ce3337b9d99f",
                ATTR_RENAME: "test-summary",
                ATTR_DESCRIPTION: "test-description",
            },
            (
                UUID("f2c85972-1a19-4426-bc6d-ce3337b9d99f"),
                Task(
                    notes="test-description",
                    text="test-summary",
                ),
            ),
        ),
    ],
    ids=["todo", "todo remove date", "todo remove notes", "daily"],
)
async def test_update_todo_item(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    habitica: AsyncMock,
    entity_id: str,
    service_data: dict[str, Any],
    call_args: tuple[UUID, Task],
) -> None:
    """Test update details of an item on the todo list."""

    config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.LOADED

    await hass.services.async_call(
        TODO_DOMAIN,
        TodoServices.UPDATE_ITEM,
        service_data,
        target={ATTR_ENTITY_ID: entity_id},
        blocking=True,
    )

    habitica.update_task.assert_awaited_once_with(*call_args)


@pytest.mark.parametrize(
    ("exception", "exc_msg", "expected_exception"),
    [
        (
            ERROR_NOT_FOUND,
            "Unable to update the Habitica to-do `test-summary`, please try again",
            ServiceValidationError,
        ),
        (
            ERROR_TOO_MANY_REQUESTS,
            "Rate limit exceeded, try again in 5 seconds",
            HomeAssistantError,
        ),
    ],
)
async def test_update_todo_item_exception(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    habitica: AsyncMock,
    exception: Exception,
    exc_msg: str,
    expected_exception: Exception,
) -> None:
    """Test exception when update item on the todo list."""
    uid = "88de7cd9-af2b-49ce-9afd-bf941d87336b"
    config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.LOADED

    habitica.update_task.side_effect = exception
    with pytest.raises(expected_exception=expected_exception, match=exc_msg):
        await hass.services.async_call(
            TODO_DOMAIN,
            TodoServices.UPDATE_ITEM,
            {
                ATTR_ITEM: uid,
                ATTR_RENAME: "test-summary",
                ATTR_DESCRIPTION: "test-description",
                ATTR_DUE_DATE: "2024-07-30",
            },
            target={ATTR_ENTITY_ID: "todo.test_user_to_do_s"},
            blocking=True,
        )


async def test_add_todo_item(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    habitica: AsyncMock,
) -> None:
    """Test add a todo item to the todo list."""

    config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.LOADED

    await hass.services.async_call(
        TODO_DOMAIN,
        TodoServices.ADD_ITEM,
        {
            ATTR_ITEM: "test-summary",
            ATTR_DESCRIPTION: "test-description",
            ATTR_DUE_DATE: "2024-07-30",
        },
        target={ATTR_ENTITY_ID: "todo.test_user_to_do_s"},
        blocking=True,
    )

    habitica.create_task.assert_awaited_once_with(
        Task(
            date=date(2024, 7, 30),
            notes="test-description",
            text="test-summary",
            type=TaskType.TODO,
        )
    )


@pytest.mark.parametrize(
    ("exception", "exc_msg", "expected_exception"),
    [
        (
            ERROR_NOT_FOUND,
            "Unable to create new to-do `test-summary` for Habitica, please try again",
            ServiceValidationError,
        ),
        (
            ERROR_TOO_MANY_REQUESTS,
            "Rate limit exceeded, try again in 5 seconds",
            HomeAssistantError,
        ),
    ],
)
async def test_add_todo_item_exception(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    habitica: AsyncMock,
    exception: Exception,
    exc_msg: str,
    expected_exception: Exception,
) -> None:
    """Test exception when adding a todo item to the todo list."""

    config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.LOADED

    habitica.create_task.side_effect = exception
    with pytest.raises(
        expected_exception=expected_exception,
        # match="Unable to create new to-do `test-summary` for Habitica, please try again",
        match=exc_msg,
    ):
        await hass.services.async_call(
            TODO_DOMAIN,
            TodoServices.ADD_ITEM,
            {
                ATTR_ITEM: "test-summary",
                ATTR_DESCRIPTION: "test-description",
                ATTR_DUE_DATE: "2024-07-30",
            },
            target={ATTR_ENTITY_ID: "todo.test_user_to_do_s"},
            blocking=True,
        )


async def test_delete_todo_item(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    habitica: AsyncMock,
) -> None:
    """Test deleting a todo item from the todo list."""

    uid = "2f6fcabc-f670-4ec3-ba65-817e8deea490"
    config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.LOADED

    await hass.services.async_call(
        TODO_DOMAIN,
        TodoServices.REMOVE_ITEM,
        {ATTR_ITEM: uid},
        target={ATTR_ENTITY_ID: "todo.test_user_to_do_s"},
        blocking=True,
    )

    habitica.delete_task.assert_awaited_once_with(UUID(uid))


@pytest.mark.parametrize(
    ("exception", "exc_msg", "expected_exception"),
    [
        (
            ERROR_NOT_FOUND,
            "Unable to delete item from Habitica to-do list, please try again",
            ServiceValidationError,
        ),
        (
            ERROR_TOO_MANY_REQUESTS,
            "Rate limit exceeded, try again in 5 seconds",
            HomeAssistantError,
        ),
    ],
)
async def test_delete_todo_item_exception(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    habitica: AsyncMock,
    exception: Exception,
    exc_msg: str,
    expected_exception: Exception,
) -> None:
    """Test exception when deleting a todo item from the todo list."""

    uid = "2f6fcabc-f670-4ec3-ba65-817e8deea490"
    config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.LOADED

    habitica.delete_task.side_effect = exception

    with pytest.raises(
        expected_exception=expected_exception,
        match=exc_msg,
    ):
        await hass.services.async_call(
            TODO_DOMAIN,
            TodoServices.REMOVE_ITEM,
            {ATTR_ITEM: uid},
            target={ATTR_ENTITY_ID: "todo.test_user_to_do_s"},
            blocking=True,
        )


async def test_delete_completed_todo_items(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    habitica: AsyncMock,
) -> None:
    """Test deleting completed todo items from the todo list."""
    config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.LOADED

    await hass.services.async_call(
        TODO_DOMAIN,
        TodoServices.REMOVE_COMPLETED_ITEMS,
        {},
        target={ATTR_ENTITY_ID: "todo.test_user_to_do_s"},
        blocking=True,
    )

    habitica.delete_completed_todos.assert_awaited_once()


@pytest.mark.parametrize(
    ("exception", "exc_msg", "expected_exception"),
    [
        (
            ERROR_NOT_FOUND,
            "Unable to delete completed to-do items from Habitica to-do list, please try again",
            ServiceValidationError,
        ),
        (
            ERROR_TOO_MANY_REQUESTS,
            "Rate limit exceeded, try again in 5 seconds",
            HomeAssistantError,
        ),
    ],
)
async def test_delete_completed_todo_items_exception(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    habitica: AsyncMock,
    exception: Exception,
    exc_msg: str,
    expected_exception: Exception,
) -> None:
    """Test exception when deleting completed todo items from the todo list."""
    config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.LOADED

    habitica.delete_completed_todos.side_effect = exception
    with pytest.raises(
        expected_exception=expected_exception,
        match=exc_msg,
    ):
        await hass.services.async_call(
            TODO_DOMAIN,
            TodoServices.REMOVE_COMPLETED_ITEMS,
            {},
            target={ATTR_ENTITY_ID: "todo.test_user_to_do_s"},
            blocking=True,
        )


@pytest.mark.parametrize(
    ("entity_id", "uid", "second_pos", "third_pos", "fixture", "task_type"),
    [
        (
            "todo.test_user_to_do_s",
            "1aa3137e-ef72-4d1f-91ee-41933602f438",
            "88de7cd9-af2b-49ce-9afd-bf941d87336b",
            "2f6fcabc-f670-4ec3-ba65-817e8deea490",
            "reorder_todos_response.json",
            "todos",
        ),
        (
            "todo.test_user_dailies",
            "2c6d136c-a1c3-4bef-b7c4-fa980784b1e1",
            "f21fa608-cfc6-4413-9fc7-0eb1b48ca43a",
            "bc1d1855-b2b8-4663-98ff-62e7b763dfc4",
            "reorder_dailies_response.json",
            "dailys",
        ),
    ],
    ids=["todo", "daily"],
)
async def test_move_todo_item(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    habitica: AsyncMock,
    hass_ws_client: WebSocketGenerator,
    entity_id: str,
    uid: str,
    second_pos: str,
    third_pos: str,
    fixture: str,
    task_type: str,
) -> None:
    """Test move todo items."""
    reorder_response = HabiticaTaskOrderResponse.from_json(
        await async_load_fixture(hass, fixture, DOMAIN)
    )
    habitica.reorder_task.return_value = reorder_response
    config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.LOADED

    client = await hass_ws_client()
    # move up to second position
    data = {
        "id": id,
        "type": "todo/item/move",
        "entity_id": entity_id,
        "uid": uid,
        "previous_uid": second_pos,
    }
    await client.send_json_auto_id(data)
    resp = await client.receive_json()
    assert resp.get("success")

    habitica.reorder_task.assert_awaited_once_with(UUID(uid), 1)

    habitica.reorder_task.reset_mock()

    # move down to third position
    data = {
        "id": id,
        "type": "todo/item/move",
        "entity_id": entity_id,
        "uid": uid,
        "previous_uid": third_pos,
    }
    await client.send_json_auto_id(data)
    resp = await client.receive_json()
    assert resp.get("success")

    habitica.reorder_task.assert_awaited_once_with(UUID(uid), 2)

    habitica.reorder_task.reset_mock()

    # move to top position
    data = {
        "id": id,
        "type": "todo/item/move",
        "entity_id": entity_id,
        "uid": uid,
    }
    await client.send_json_auto_id(data)
    resp = await client.receive_json()
    assert resp.get("success")

    habitica.reorder_task.assert_awaited_once_with(UUID(uid), 0)
    assert (
        getattr(config_entry.runtime_data.data.user.tasksOrder, task_type)
        == reorder_response.data
    )


@pytest.mark.parametrize(
    ("exception", "exc_msg"),
    [
        (
            ERROR_NOT_FOUND,
            "Unable to move the Habitica to-do to position 0, please try again",
        ),
        (
            ERROR_TOO_MANY_REQUESTS,
            "Rate limit exceeded, try again in 5 seconds",
        ),
    ],
)
async def test_move_todo_item_exception(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    habitica: AsyncMock,
    hass_ws_client: WebSocketGenerator,
    exception: Exception,
    exc_msg: str,
) -> None:
    """Test exception when moving todo item."""

    uid = "1aa3137e-ef72-4d1f-91ee-41933602f438"
    config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.LOADED

    habitica.reorder_task.side_effect = exception
    client = await hass_ws_client()

    data = {
        "id": id,
        "type": "todo/item/move",
        "entity_id": "todo.test_user_to_do_s",
        "uid": uid,
    }
    await client.send_json_auto_id(data)

    resp = await client.receive_json()
    habitica.reorder_task.assert_awaited_once_with(UUID(uid), 0)

    assert resp["success"] is False
    assert resp["error"]["message"] == exc_msg


@pytest.mark.parametrize(
    ("fixture", "calculated_due_date"),
    [
        ("duedate_fixture_1.json", "2024-09-22"),
        ("duedate_fixture_2.json", "2024-09-24"),
        ("duedate_fixture_3.json", "2024-10-23"),
        ("duedate_fixture_4.json", "2024-10-23"),
        ("duedate_fixture_5.json", "2024-09-28"),
        ("duedate_fixture_6.json", "2024-10-21"),
        ("duedate_fixture_7.json", None),
        ("duedate_fixture_8.json", None),
        ("duedate_fixture_9.json", None),
    ],
    ids=[
        "default",
        "daily starts on startdate",
        "monthly starts on startdate",
        "yearly starts on startdate",
        "weekly",
        "monthly starts on fixed day",
        "grey daily",
        "empty nextDue",
        "grey daily no weekdays",
    ],
)
@pytest.mark.usefixtures("set_tz")
async def test_next_due_date(
    hass: HomeAssistant,
    fixture: str,
    calculated_due_date: str | None,
    config_entry: MockConfigEntry,
    habitica: AsyncMock,
) -> None:
    """Test next_due_date calculation."""

    dailies_entity = "todo.test_user_dailies"

    habitica.get_tasks.side_effect = [
        HabiticaTasksResponse.from_json(
            await async_load_fixture(hass, fixture, DOMAIN)
        ),
        HabiticaTasksResponse.from_dict({"success": True, "data": []}),
    ]

    config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.LOADED

    result = await hass.services.async_call(
        TODO_DOMAIN,
        TodoServices.GET_ITEMS,
        {},
        target={ATTR_ENTITY_ID: dailies_entity},
        blocking=True,
        return_response=True,
    )

    assert result[dailies_entity]["items"][0].get("due") == calculated_due_date
