"""Tests for the todo integration."""

import datetime
from typing import Any
import zoneinfo

import pytest
import voluptuous as vol

from homeassistant.components import conversation
from homeassistant.components.homeassistant.exposed_entities import async_expose_entity
from homeassistant.components.todo import (
    ATTR_DESCRIPTION,
    ATTR_DUE_DATE,
    ATTR_DUE_DATETIME,
    ATTR_ITEM,
    ATTR_RENAME,
    ATTR_STATUS,
    DOMAIN,
    TodoItem,
    TodoItemStatus,
    TodoListEntity,
    TodoListEntityFeature,
    TodoServices,
    intent as todo_intent,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
    HomeAssistantError,
    ServiceNotSupported,
    ServiceValidationError,
)
from homeassistant.helpers import intent
from homeassistant.setup import async_setup_component

from . import MockTodoListEntity, create_mock_platform

from tests.typing import WebSocketGenerator

ITEM_1 = {
    "uid": "1",
    "summary": "Item #1",
    "status": "needs_action",
}
ITEM_2 = {
    "uid": "2",
    "summary": "Item #2",
    "status": "completed",
}
TEST_TIMEZONE = zoneinfo.ZoneInfo("America/Regina")
TEST_OFFSET = "-06:00"


async def test_unload_entry(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
) -> None:
    """Test unloading a config entry with a todo entity."""

    config_entry = await create_mock_platform(hass, [test_entity])
    assert config_entry.state is ConfigEntryState.LOADED

    state = hass.states.get("todo.entity1")
    assert state

    assert await hass.config_entries.async_unload(config_entry.entry_id)
    await hass.async_block_till_done()
    assert config_entry.state is ConfigEntryState.NOT_LOADED

    state = hass.states.get("todo.entity1")
    assert not state


async def test_list_todo_items(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
    test_entity: TodoListEntity,
) -> None:
    """Test listing items in a To-do list."""

    await create_mock_platform(hass, [test_entity])

    state = hass.states.get("todo.entity1")
    assert state
    assert state.state == "1"
    assert state.attributes == {"supported_features": 15}

    client = await hass_ws_client(hass)
    await client.send_json(
        {"id": 1, "type": "todo/item/list", "entity_id": "todo.entity1"}
    )
    resp = await client.receive_json()
    assert resp.get("id") == 1
    assert resp.get("success")
    assert resp.get("result") == {
        "items": [
            ITEM_1,
            ITEM_2,
        ]
    }


@pytest.mark.parametrize(
    ("service_data", "expected_items"),
    [
        ({}, [ITEM_1, ITEM_2]),
        (
            {ATTR_STATUS: [TodoItemStatus.COMPLETED, TodoItemStatus.NEEDS_ACTION]},
            [ITEM_1, ITEM_2],
        ),
        ({ATTR_STATUS: [TodoItemStatus.NEEDS_ACTION]}, [ITEM_1]),
        ({ATTR_STATUS: [TodoItemStatus.COMPLETED]}, [ITEM_2]),
    ],
)
async def test_get_items_service(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
    test_entity: TodoListEntity,
    service_data: dict[str, Any],
    expected_items: list[dict[str, Any]],
) -> None:
    """Test listing items in a To-do list from a service call."""

    await create_mock_platform(hass, [test_entity])

    state = hass.states.get("todo.entity1")
    assert state
    assert state.state == "1"
    assert state.attributes == {ATTR_SUPPORTED_FEATURES: 15}

    result = await hass.services.async_call(
        DOMAIN,
        TodoServices.GET_ITEMS,
        service_data,
        target={ATTR_ENTITY_ID: "todo.entity1"},
        blocking=True,
        return_response=True,
    )
    assert result == {"todo.entity1": {"items": expected_items}}


async def test_unsupported_websocket(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
) -> None:
    """Test a To-do list for an entity that does not exist."""

    entity1 = TodoListEntity()
    entity1.entity_id = "todo.entity1"
    await create_mock_platform(hass, [entity1])

    client = await hass_ws_client(hass)
    await client.send_json(
        {
            "id": 1,
            "type": "todo/item/list",
            "entity_id": "todo.unknown",
        }
    )
    resp = await client.receive_json()
    assert resp.get("id") == 1
    assert resp.get("error", {}).get("code") == "not_found"


async def test_add_item_service(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
) -> None:
    """Test adding an item in a To-do list."""

    await create_mock_platform(hass, [test_entity])

    await hass.services.async_call(
        DOMAIN,
        TodoServices.ADD_ITEM,
        {ATTR_ITEM: "New item"},
        target={ATTR_ENTITY_ID: "todo.entity1"},
        blocking=True,
    )

    args = test_entity.async_create_todo_item.call_args
    assert args
    item = args.kwargs.get("item")
    assert item
    assert item.uid is None
    assert item.summary == "New item"
    assert item.status == TodoItemStatus.NEEDS_ACTION


async def test_add_item_service_raises(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
) -> None:
    """Test adding an item in a To-do list that raises an error."""

    await create_mock_platform(hass, [test_entity])

    test_entity.async_create_todo_item.side_effect = HomeAssistantError("Ooops")
    with pytest.raises(HomeAssistantError, match="Ooops"):
        await hass.services.async_call(
            DOMAIN,
            TodoServices.ADD_ITEM,
            {ATTR_ITEM: "New item"},
            target={ATTR_ENTITY_ID: "todo.entity1"},
            blocking=True,
        )


@pytest.mark.parametrize(
    ("item_data", "expected_exception", "expected_error"),
    [
        ({}, vol.Invalid, "required key not provided"),
        ({ATTR_ITEM: ""}, vol.Invalid, "length of value must be at least 1"),
        (
            {ATTR_ITEM: "Submit forms", ATTR_DESCRIPTION: "Submit tax forms"},
            ServiceValidationError,
            "does not support setting field: description",
        ),
        (
            {ATTR_ITEM: "Submit forms", ATTR_DUE_DATE: "2023-11-17"},
            ServiceValidationError,
            "does not support setting field: due_date",
        ),
        (
            {
                ATTR_ITEM: "Submit forms",
                ATTR_DUE_DATETIME: f"2023-11-17T17:00:00{TEST_OFFSET}",
            },
            ServiceValidationError,
            "does not support setting field: due_datetime",
        ),
    ],
)
async def test_add_item_service_invalid_input(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
    item_data: dict[str, Any],
    expected_exception: str,
    expected_error: str,
) -> None:
    """Test invalid input to the add item service."""

    await create_mock_platform(hass, [test_entity])

    with pytest.raises(expected_exception) as exc:
        await hass.services.async_call(
            DOMAIN,
            TodoServices.ADD_ITEM,
            item_data,
            target={ATTR_ENTITY_ID: "todo.entity1"},
            blocking=True,
        )

    assert expected_error in str(exc.value)


@pytest.mark.parametrize(
    ("supported_entity_feature", "item_data", "expected_item"),
    [
        (
            TodoListEntityFeature.SET_DUE_DATE_ON_ITEM,
            {ATTR_ITEM: "New item", ATTR_DUE_DATE: "2023-11-13"},
            TodoItem(
                summary="New item",
                status=TodoItemStatus.NEEDS_ACTION,
                due=datetime.date(2023, 11, 13),
            ),
        ),
        (
            TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM,
            {
                ATTR_ITEM: "New item",
                ATTR_DUE_DATETIME: f"2023-11-13T17:00:00{TEST_OFFSET}",
            },
            TodoItem(
                summary="New item",
                status=TodoItemStatus.NEEDS_ACTION,
                due=datetime.datetime(2023, 11, 13, 17, 00, 00, tzinfo=TEST_TIMEZONE),
            ),
        ),
        (
            TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM,
            {ATTR_ITEM: "New item", ATTR_DUE_DATETIME: "2023-11-13T17:00:00+00:00"},
            TodoItem(
                summary="New item",
                status=TodoItemStatus.NEEDS_ACTION,
                due=datetime.datetime(2023, 11, 13, 11, 00, 00, tzinfo=TEST_TIMEZONE),
            ),
        ),
        (
            TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM,
            {ATTR_ITEM: "New item", ATTR_DUE_DATETIME: "2023-11-13"},
            TodoItem(
                summary="New item",
                status=TodoItemStatus.NEEDS_ACTION,
                due=datetime.datetime(2023, 11, 13, 0, 00, 00, tzinfo=TEST_TIMEZONE),
            ),
        ),
        (
            TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM,
            {ATTR_ITEM: "New item", ATTR_DESCRIPTION: "Submit revised draft"},
            TodoItem(
                summary="New item",
                status=TodoItemStatus.NEEDS_ACTION,
                description="Submit revised draft",
            ),
        ),
    ],
)
async def test_add_item_service_extended_fields(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
    supported_entity_feature: int,
    item_data: dict[str, Any],
    expected_item: TodoItem,
) -> None:
    """Test adding an item in a To-do list."""

    test_entity._attr_supported_features |= supported_entity_feature
    await create_mock_platform(hass, [test_entity])

    await hass.services.async_call(
        DOMAIN,
        TodoServices.ADD_ITEM,
        {ATTR_ITEM: "New item", **item_data},
        target={ATTR_ENTITY_ID: "todo.entity1"},
        blocking=True,
    )

    args = test_entity.async_create_todo_item.call_args
    assert args
    item = args.kwargs.get("item")
    assert item == expected_item


async def test_update_todo_item_service_by_id(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
) -> None:
    """Test updating an item in a To-do list."""

    await create_mock_platform(hass, [test_entity])

    await hass.services.async_call(
        DOMAIN,
        TodoServices.UPDATE_ITEM,
        {ATTR_ITEM: "1", ATTR_RENAME: "Updated item", ATTR_STATUS: "completed"},
        target={ATTR_ENTITY_ID: "todo.entity1"},
        blocking=True,
    )

    args = test_entity.async_update_todo_item.call_args
    assert args
    item = args.kwargs.get("item")
    assert item
    assert item.uid == "1"
    assert item.summary == "Updated item"
    assert item.status == TodoItemStatus.COMPLETED


async def test_update_todo_item_service_by_id_status_only(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
) -> None:
    """Test updating an item in a To-do list."""

    await create_mock_platform(hass, [test_entity])

    await hass.services.async_call(
        DOMAIN,
        TodoServices.UPDATE_ITEM,
        {ATTR_ITEM: "1", ATTR_STATUS: "completed"},
        target={ATTR_ENTITY_ID: "todo.entity1"},
        blocking=True,
    )

    args = test_entity.async_update_todo_item.call_args
    assert args
    item = args.kwargs.get("item")
    assert item
    assert item.uid == "1"
    assert item.summary == "Item #1"
    assert item.status == TodoItemStatus.COMPLETED


async def test_update_todo_item_service_by_id_rename(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
) -> None:
    """Test updating an item in a To-do list."""

    await create_mock_platform(hass, [test_entity])

    await hass.services.async_call(
        DOMAIN,
        TodoServices.UPDATE_ITEM,
        {ATTR_ITEM: "1", "rename": "Updated item"},
        target={ATTR_ENTITY_ID: "todo.entity1"},
        blocking=True,
    )

    args = test_entity.async_update_todo_item.call_args
    assert args
    item = args.kwargs.get("item")
    assert item
    assert item.uid == "1"
    assert item.summary == "Updated item"
    assert item.status == TodoItemStatus.NEEDS_ACTION


async def test_update_todo_item_service_raises(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
) -> None:
    """Test updating an item in a To-do list that raises an error."""

    await create_mock_platform(hass, [test_entity])

    await hass.services.async_call(
        DOMAIN,
        TodoServices.UPDATE_ITEM,
        {ATTR_ITEM: "1", "rename": "Updated item", "status": "completed"},
        target={ATTR_ENTITY_ID: "todo.entity1"},
        blocking=True,
    )

    test_entity.async_update_todo_item.side_effect = HomeAssistantError("Ooops")
    with pytest.raises(HomeAssistantError, match="Ooops"):
        await hass.services.async_call(
            DOMAIN,
            TodoServices.UPDATE_ITEM,
            {ATTR_ITEM: "1", "rename": "Updated item", "status": "completed"},
            target={ATTR_ENTITY_ID: "todo.entity1"},
            blocking=True,
        )


async def test_update_todo_item_service_by_summary(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
) -> None:
    """Test updating an item in a To-do list by summary."""

    await create_mock_platform(hass, [test_entity])

    await hass.services.async_call(
        DOMAIN,
        TodoServices.UPDATE_ITEM,
        {ATTR_ITEM: "Item #1", "rename": "Something else", "status": "completed"},
        target={ATTR_ENTITY_ID: "todo.entity1"},
        blocking=True,
    )

    args = test_entity.async_update_todo_item.call_args
    assert args
    item = args.kwargs.get("item")
    assert item
    assert item.uid == "1"
    assert item.summary == "Something else"
    assert item.status == TodoItemStatus.COMPLETED


async def test_update_todo_item_service_by_summary_only_status(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
) -> None:
    """Test updating an item in a To-do list by summary."""

    await create_mock_platform(hass, [test_entity])

    await hass.services.async_call(
        DOMAIN,
        TodoServices.UPDATE_ITEM,
        {ATTR_ITEM: "Item #1", "rename": "Something else"},
        target={ATTR_ENTITY_ID: "todo.entity1"},
        blocking=True,
    )

    args = test_entity.async_update_todo_item.call_args
    assert args
    item = args.kwargs.get("item")
    assert item
    assert item.uid == "1"
    assert item.summary == "Something else"
    assert item.status == TodoItemStatus.NEEDS_ACTION


async def test_update_todo_item_service_by_summary_not_found(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
) -> None:
    """Test updating an item in a To-do list by summary which is not found."""

    await create_mock_platform(hass, [test_entity])

    with pytest.raises(ServiceValidationError, match="Unable to find"):
        await hass.services.async_call(
            DOMAIN,
            TodoServices.UPDATE_ITEM,
            {ATTR_ITEM: "Item #7", "status": "completed"},
            target={ATTR_ENTITY_ID: "todo.entity1"},
            blocking=True,
        )


@pytest.mark.parametrize(
    ("item_data", "expected_error"),
    [
        ({}, r"required key not provided @ data\['item'\]"),
        ({"status": "needs_action"}, r"required key not provided @ data\['item'\]"),
        ({"item": "Item #1"}, "must contain at least one of"),
        (
            {"item": "", "status": "needs_action"},
            "length of value must be at least 1",
        ),
    ],
)
async def test_update_item_service_invalid_input(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
    item_data: dict[str, Any],
    expected_error: str,
) -> None:
    """Test invalid input to the update item service."""

    await create_mock_platform(hass, [test_entity])

    with pytest.raises(vol.Invalid, match=expected_error):
        await hass.services.async_call(
            DOMAIN,
            "update_item",
            item_data,
            target={ATTR_ENTITY_ID: "todo.entity1"},
            blocking=True,
        )


@pytest.mark.parametrize(
    ("update_data"),
    [
        ({"due_datetime": f"2023-11-13T17:00:00{TEST_OFFSET}"}),
        ({"due_date": "2023-11-13"}),
        ({"description": "Submit revised draft"}),
    ],
)
async def test_update_todo_item_field_unsupported(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
    update_data: dict[str, Any],
) -> None:
    """Test updating an item in a To-do list."""

    await create_mock_platform(hass, [test_entity])

    with pytest.raises(ServiceValidationError, match="does not support"):
        await hass.services.async_call(
            DOMAIN,
            TodoServices.UPDATE_ITEM,
            {ATTR_ITEM: "1", **update_data},
            target={ATTR_ENTITY_ID: "todo.entity1"},
            blocking=True,
        )


@pytest.mark.parametrize(
    ("supported_entity_feature", "update_data", "expected_update"),
    [
        (
            TodoListEntityFeature.SET_DUE_DATE_ON_ITEM,
            {"due_date": "2023-11-13"},
            TodoItem(
                uid="1",
                summary="Item #1",
                status=TodoItemStatus.NEEDS_ACTION,
                due=datetime.date(2023, 11, 13),
            ),
        ),
        (
            TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM,
            {"due_datetime": f"2023-11-13T17:00:00{TEST_OFFSET}"},
            TodoItem(
                uid="1",
                summary="Item #1",
                status=TodoItemStatus.NEEDS_ACTION,
                due=datetime.datetime(2023, 11, 13, 17, 0, 0, tzinfo=TEST_TIMEZONE),
            ),
        ),
        (
            TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM,
            {"description": "Submit revised draft"},
            TodoItem(
                uid="1",
                summary="Item #1",
                status=TodoItemStatus.NEEDS_ACTION,
                description="Submit revised draft",
            ),
        ),
    ],
)
async def test_update_todo_item_extended_fields(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
    supported_entity_feature: int,
    update_data: dict[str, Any],
    expected_update: TodoItem,
) -> None:
    """Test updating an item in a To-do list."""

    test_entity._attr_supported_features |= supported_entity_feature
    await create_mock_platform(hass, [test_entity])

    await hass.services.async_call(
        DOMAIN,
        TodoServices.UPDATE_ITEM,
        {ATTR_ITEM: "1", **update_data},
        target={ATTR_ENTITY_ID: "todo.entity1"},
        blocking=True,
    )

    args = test_entity.async_update_todo_item.call_args
    assert args
    item = args.kwargs.get("item")
    assert item == expected_update


@pytest.mark.parametrize(
    ("test_entity_items", "update_data", "expected_update"),
    [
        (
            [TodoItem(uid="1", summary="Summary", description="description")],
            {"description": "Submit revised draft"},
            TodoItem(uid="1", summary="Summary", description="Submit revised draft"),
        ),
        (
            [TodoItem(uid="1", summary="Summary", description="description")],
            {"description": ""},
            TodoItem(uid="1", summary="Summary", description=""),
        ),
        (
            [TodoItem(uid="1", summary="Summary", description="description")],
            {"description": None},
            TodoItem(uid="1", summary="Summary"),
        ),
        (
            [TodoItem(uid="1", summary="Summary", due=datetime.date(2024, 1, 1))],
            {"due_date": datetime.date(2024, 1, 2)},
            TodoItem(uid="1", summary="Summary", due=datetime.date(2024, 1, 2)),
        ),
        (
            [TodoItem(uid="1", summary="Summary", due=datetime.date(2024, 1, 1))],
            {"due_date": None},
            TodoItem(uid="1", summary="Summary"),
        ),
        (
            [TodoItem(uid="1", summary="Summary", due=datetime.date(2024, 1, 1))],
            {"due_datetime": datetime.datetime(2024, 1, 1, 10, 0, 0)},
            TodoItem(
                uid="1",
                summary="Summary",
                due=datetime.datetime(
                    2024, 1, 1, 10, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="America/Regina")
                ),
            ),
        ),
        (
            [
                TodoItem(
                    uid="1",
                    summary="Summary",
                    due=datetime.datetime(2024, 1, 1, 10, 0, 0),
                )
            ],
            {"due_datetime": None},
            TodoItem(uid="1", summary="Summary"),
        ),
    ],
    ids=[
        "overwrite_description",
        "overwrite_empty_description",
        "clear_description",
        "overwrite_due_date",
        "clear_due_date",
        "overwrite_due_date_with_time",
        "clear_due_date_time",
    ],
)
async def test_update_todo_item_extended_fields_overwrite_existing_values(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
    update_data: dict[str, Any],
    expected_update: TodoItem,
) -> None:
    """Test updating an item in a To-do list."""

    test_entity._attr_supported_features |= (
        TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
        | TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
        | TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM
    )
    await create_mock_platform(hass, [test_entity])

    await hass.services.async_call(
        DOMAIN,
        TodoServices.UPDATE_ITEM,
        {ATTR_ITEM: "1", **update_data},
        target={ATTR_ENTITY_ID: "todo.entity1"},
        blocking=True,
    )

    args = test_entity.async_update_todo_item.call_args
    assert args
    item = args.kwargs.get("item")
    assert item == expected_update


async def test_remove_todo_item_service_by_id(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
) -> None:
    """Test removing an item in a To-do list."""

    await create_mock_platform(hass, [test_entity])

    await hass.services.async_call(
        DOMAIN,
        TodoServices.REMOVE_ITEM,
        {ATTR_ITEM: ["1", "2"]},
        target={ATTR_ENTITY_ID: "todo.entity1"},
        blocking=True,
    )

    args = test_entity.async_delete_todo_items.call_args
    assert args
    assert args.kwargs.get("uids") == ["1", "2"]


async def test_remove_todo_item_service_raises(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
) -> None:
    """Test removing an item in a To-do list that raises an error."""

    await create_mock_platform(hass, [test_entity])

    test_entity.async_delete_todo_items.side_effect = HomeAssistantError("Ooops")
    with pytest.raises(HomeAssistantError, match="Ooops"):
        await hass.services.async_call(
            DOMAIN,
            TodoServices.REMOVE_ITEM,
            {ATTR_ITEM: ["1", "2"]},
            target={ATTR_ENTITY_ID: "todo.entity1"},
            blocking=True,
        )


async def test_remove_todo_item_service_invalid_input(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
) -> None:
    """Test invalid input to the remove item service."""

    await create_mock_platform(hass, [test_entity])

    with pytest.raises(
        vol.Invalid, match=r"required key not provided @ data\['item'\]"
    ):
        await hass.services.async_call(
            DOMAIN,
            TodoServices.REMOVE_ITEM,
            {},
            target={ATTR_ENTITY_ID: "todo.entity1"},
            blocking=True,
        )


async def test_remove_todo_item_service_by_summary(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
) -> None:
    """Test removing an item in a To-do list by summary."""

    await create_mock_platform(hass, [test_entity])

    await hass.services.async_call(
        DOMAIN,
        TodoServices.REMOVE_ITEM,
        {ATTR_ITEM: ["Item #1"]},
        target={ATTR_ENTITY_ID: "todo.entity1"},
        blocking=True,
    )

    args = test_entity.async_delete_todo_items.call_args
    assert args
    assert args.kwargs.get("uids") == ["1"]


async def test_remove_todo_item_service_by_summary_not_found(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
) -> None:
    """Test removing an item in a To-do list by summary which is not found."""

    await create_mock_platform(hass, [test_entity])

    with pytest.raises(ServiceValidationError, match="Unable to find"):
        await hass.services.async_call(
            DOMAIN,
            TodoServices.REMOVE_ITEM,
            {ATTR_ITEM: ["Item #7"]},
            target={ATTR_ENTITY_ID: "todo.entity1"},
            blocking=True,
        )


async def test_move_todo_item_service_by_id(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
    hass_ws_client: WebSocketGenerator,
) -> None:
    """Test moving an item in a To-do list."""

    await create_mock_platform(hass, [test_entity])

    client = await hass_ws_client()
    await client.send_json(
        {
            "id": 1,
            "type": "todo/item/move",
            "entity_id": "todo.entity1",
            "uid": "item-1",
            "previous_uid": "item-2",
        }
    )
    resp = await client.receive_json()
    assert resp.get("id") == 1
    assert resp.get("success")

    args = test_entity.async_move_todo_item.call_args
    assert args
    assert args.kwargs.get("uid") == "item-1"
    assert args.kwargs.get("previous_uid") == "item-2"


async def test_move_todo_item_service_raises(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
    hass_ws_client: WebSocketGenerator,
) -> None:
    """Test moving an item in a To-do list that raises an error."""

    await create_mock_platform(hass, [test_entity])

    test_entity.async_move_todo_item.side_effect = HomeAssistantError("Ooops")
    client = await hass_ws_client()
    await client.send_json(
        {
            "id": 1,
            "type": "todo/item/move",
            "entity_id": "todo.entity1",
            "uid": "item-1",
            "previous_uid": "item-2",
        }
    )
    resp = await client.receive_json()
    assert resp.get("id") == 1
    assert resp.get("error", {}).get("code") == "failed"
    assert resp.get("error", {}).get("message") == "Ooops"


@pytest.mark.parametrize(
    ("item_data", "expected_status", "expected_error"),
    [
        (
            {"entity_id": "todo.unknown", "uid": "item-1"},
            "not_found",
            "Entity not found",
        ),
        ({"entity_id": "todo.entity1"}, "invalid_format", "required key not provided"),
        (
            {"entity_id": "todo.entity1", "previous_uid": "item-2"},
            "invalid_format",
            "required key not provided",
        ),
    ],
)
async def test_move_todo_item_service_invalid_input(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
    hass_ws_client: WebSocketGenerator,
    item_data: dict[str, Any],
    expected_status: str,
    expected_error: str,
) -> None:
    """Test invalid input for the move item service."""

    await create_mock_platform(hass, [test_entity])

    client = await hass_ws_client()
    await client.send_json(
        {
            "id": 1,
            "type": "todo/item/move",
            **item_data,
        }
    )
    resp = await client.receive_json()
    assert resp.get("id") == 1
    assert resp.get("error", {}).get("code") == expected_status
    assert expected_error in resp.get("error", {}).get("message")


@pytest.mark.parametrize(
    ("service_name", "payload"),
    [
        (
            TodoServices.ADD_ITEM,
            {
                ATTR_ITEM: "New item",
            },
        ),
        (
            TodoServices.REMOVE_ITEM,
            {
                ATTR_ITEM: ["1"],
            },
        ),
        (
            TodoServices.UPDATE_ITEM,
            {
                ATTR_ITEM: "1",
                ATTR_RENAME: "Updated item",
            },
        ),
        (
            TodoServices.REMOVE_COMPLETED_ITEMS,
            None,
        ),
    ],
)
async def test_unsupported_service(
    hass: HomeAssistant,
    service_name: str,
    payload: dict[str, Any] | None,
) -> None:
    """Test a To-do list that does not support features."""
    # Fetch translations
    await async_setup_component(hass, "homeassistant", "")
    entity1 = TodoListEntity()
    entity1.entity_id = "todo.entity1"
    await create_mock_platform(hass, [entity1])

    with pytest.raises(
        ServiceNotSupported,
        match=f"Entity todo.entity1 does not support action {DOMAIN}.{service_name}",
    ):
        await hass.services.async_call(
            DOMAIN,
            service_name,
            payload,
            target={ATTR_ENTITY_ID: "todo.entity1"},
            blocking=True,
        )


async def test_move_item_unsupported(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
) -> None:
    """Test invalid input for the move item service."""

    entity1 = TodoListEntity()
    entity1.entity_id = "todo.entity1"
    await create_mock_platform(hass, [entity1])

    client = await hass_ws_client()
    await client.send_json(
        {
            "id": 1,
            "type": "todo/item/move",
            "entity_id": "todo.entity1",
            "uid": "item-1",
            "previous_uid": "item-2",
        }
    )
    resp = await client.receive_json()
    assert resp.get("id") == 1
    assert resp.get("error", {}).get("code") == "not_supported"


async def test_add_item_intent(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
) -> None:
    """Test adding items to lists using an intent."""
    assert await async_setup_component(hass, "homeassistant", {})
    await todo_intent.async_setup_intents(hass)

    entity1 = MockTodoListEntity()
    entity1._attr_name = "List 1"
    entity1.entity_id = "todo.list_1"

    entity2 = MockTodoListEntity()
    entity2._attr_name = "List 2"
    entity2.entity_id = "todo.list_2"

    await create_mock_platform(hass, [entity1, entity2])

    # Add to first list
    response = await intent.async_handle(
        hass,
        "test",
        todo_intent.INTENT_LIST_ADD_ITEM,
        {ATTR_ITEM: {"value": " beer "}, "name": {"value": "list 1"}},
        assistant=conversation.DOMAIN,
    )
    assert response.response_type == intent.IntentResponseType.ACTION_DONE
    assert response.success_results[0].name == "list 1"
    assert response.success_results[0].type == intent.IntentResponseTargetType.ENTITY
    assert response.success_results[0].id == entity1.entity_id

    assert len(entity1.items) == 1
    assert len(entity2.items) == 0
    assert entity1.items[0].summary == "beer"  # summary is trimmed
    assert entity1.items[0].status == TodoItemStatus.NEEDS_ACTION
    entity1.items.clear()

    # Add to second list
    response = await intent.async_handle(
        hass,
        "test",
        todo_intent.INTENT_LIST_ADD_ITEM,
        {ATTR_ITEM: {"value": "cheese"}, "name": {"value": "List 2"}},
        assistant=conversation.DOMAIN,
    )
    assert response.response_type == intent.IntentResponseType.ACTION_DONE

    assert len(entity1.items) == 0
    assert len(entity2.items) == 1
    assert entity2.items[0].summary == "cheese"
    assert entity2.items[0].status == TodoItemStatus.NEEDS_ACTION

    # List name is case insensitive
    response = await intent.async_handle(
        hass,
        "test",
        todo_intent.INTENT_LIST_ADD_ITEM,
        {ATTR_ITEM: {"value": "wine"}, "name": {"value": "lIST 2"}},
        assistant=conversation.DOMAIN,
    )
    assert response.response_type == intent.IntentResponseType.ACTION_DONE

    assert len(entity1.items) == 0
    assert len(entity2.items) == 2
    assert entity2.items[1].summary == "wine"
    assert entity2.items[1].status == TodoItemStatus.NEEDS_ACTION

    # Should fail if lists are not exposed
    async_expose_entity(hass, conversation.DOMAIN, entity1.entity_id, False)
    async_expose_entity(hass, conversation.DOMAIN, entity2.entity_id, False)
    with pytest.raises(intent.MatchFailedError) as err:
        await intent.async_handle(
            hass,
            "test",
            todo_intent.INTENT_LIST_ADD_ITEM,
            {"item": {"value": "cookies"}, "name": {"value": "list 1"}},
            assistant=conversation.DOMAIN,
        )
    assert err.value.result.no_match_reason == intent.MatchFailedReason.ASSISTANT

    # Missing list
    with pytest.raises(intent.MatchFailedError):
        await intent.async_handle(
            hass,
            "test",
            todo_intent.INTENT_LIST_ADD_ITEM,
            {"item": {"value": "wine"}, "name": {"value": "This list does not exist"}},
            assistant=conversation.DOMAIN,
        )

    # Fail with empty name/item
    with pytest.raises(intent.InvalidSlotInfo):
        await intent.async_handle(
            hass,
            "test",
            todo_intent.INTENT_LIST_ADD_ITEM,
            {"item": {"value": "wine"}, "name": {"value": ""}},
            assistant=conversation.DOMAIN,
        )

    with pytest.raises(intent.InvalidSlotInfo):
        await intent.async_handle(
            hass,
            "test",
            todo_intent.INTENT_LIST_ADD_ITEM,
            {"item": {"value": ""}, "name": {"value": "list 1"}},
            assistant=conversation.DOMAIN,
        )


async def test_remove_completed_items_service(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
) -> None:
    """Test remove completed todo items service."""
    await create_mock_platform(hass, [test_entity])

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

    args = test_entity.async_delete_todo_items.call_args
    assert args
    assert args.kwargs.get("uids") == ["2"]

    test_entity.async_delete_todo_items.reset_mock()

    # calling service multiple times will not call the entity method
    await hass.services.async_call(
        DOMAIN,
        TodoServices.REMOVE_COMPLETED_ITEMS,
        target={ATTR_ENTITY_ID: "todo.entity1"},
        blocking=True,
    )
    test_entity.async_delete_todo_items.assert_not_called()


async def test_remove_completed_items_service_raises(
    hass: HomeAssistant,
    test_entity: TodoListEntity,
) -> None:
    """Test removing all completed item from a To-do list that raises an error."""

    await create_mock_platform(hass, [test_entity])

    test_entity.async_delete_todo_items.side_effect = HomeAssistantError("Ooops")
    with pytest.raises(HomeAssistantError, match="Ooops"):
        await hass.services.async_call(
            DOMAIN,
            TodoServices.REMOVE_COMPLETED_ITEMS,
            target={ATTR_ENTITY_ID: "todo.entity1"},
            blocking=True,
        )


async def test_subscribe(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
    test_entity: TodoListEntity,
) -> None:
    """Test subscribing to todo updates."""

    await create_mock_platform(hass, [test_entity])

    client = await hass_ws_client(hass)

    await client.send_json_auto_id(
        {
            "type": "todo/item/subscribe",
            "entity_id": test_entity.entity_id,
        }
    )
    msg = await client.receive_json()
    assert msg["success"]
    assert msg["result"] is None
    subscription_id = msg["id"]

    msg = await client.receive_json()
    assert msg["id"] == subscription_id
    assert msg["type"] == "event"
    event_message = msg["event"]
    assert event_message == {
        "items": [
            {
                "summary": "Item #1",
                "uid": "1",
                "status": "needs_action",
                "due": None,
                "description": None,
            },
            {
                "summary": "Item #2",
                "uid": "2",
                "status": "completed",
                "due": None,
                "description": None,
            },
        ]
    }
    test_entity._attr_todo_items = [
        *test_entity._attr_todo_items,
        TodoItem(summary="Item #3", uid="3", status=TodoItemStatus.NEEDS_ACTION),
    ]

    test_entity.async_write_ha_state()
    msg = await client.receive_json()
    event_message = msg["event"]
    assert event_message == {
        "items": [
            {
                "summary": "Item #1",
                "uid": "1",
                "status": "needs_action",
                "due": None,
                "description": None,
            },
            {
                "summary": "Item #2",
                "uid": "2",
                "status": "completed",
                "due": None,
                "description": None,
            },
            {
                "summary": "Item #3",
                "uid": "3",
                "status": "needs_action",
                "due": None,
                "description": None,
            },
        ]
    }

    test_entity._attr_todo_items = None
    test_entity.async_write_ha_state()
    msg = await client.receive_json()
    event_message = msg["event"]
    assert event_message == {
        "items": [],
    }


async def test_subscribe_entity_does_not_exist(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
    test_entity: TodoListEntity,
) -> None:
    """Test failure to subscribe to an entity that does not exist."""

    await create_mock_platform(hass, [test_entity])

    client = await hass_ws_client(hass)

    await client.send_json_auto_id(
        {
            "type": "todo/item/subscribe",
            "entity_id": "todo.unknown",
        }
    )
    msg = await client.receive_json()
    assert not msg["success"]
    assert msg["error"] == {
        "code": "invalid_entity_id",
        "message": "To-do list entity not found: todo.unknown",
    }


@pytest.mark.parametrize(
    ("item_data", "expected_item_data"),
    [
        ({"due": datetime.date(2023, 11, 17)}, {"due": "2023-11-17"}),
        (
            {"due": datetime.datetime(2023, 11, 17, 17, 0, 0, tzinfo=TEST_TIMEZONE)},
            {"due": f"2023-11-17T17:00:00{TEST_OFFSET}"},
        ),
        ({"description": "Some description"}, {"description": "Some description"}),
    ],
)
async def test_list_todo_items_extended_fields(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
    test_entity: TodoListEntity,
    item_data: dict[str, Any],
    expected_item_data: dict[str, Any],
) -> None:
    """Test listing items in a To-do list with extended fields."""

    test_entity._attr_todo_items = [
        TodoItem(
            **ITEM_1,
            **item_data,
        ),
    ]
    await create_mock_platform(hass, [test_entity])

    client = await hass_ws_client(hass)
    await client.send_json(
        {"id": 1, "type": "todo/item/list", "entity_id": "todo.entity1"}
    )
    resp = await client.receive_json()
    assert resp.get("id") == 1
    assert resp.get("success")
    assert resp.get("result") == {
        "items": [
            {
                **ITEM_1,
                **expected_item_data,
            },
        ]
    }

    result = await hass.services.async_call(
        DOMAIN,
        "get_items",
        {},
        target={ATTR_ENTITY_ID: "todo.entity1"},
        blocking=True,
        return_response=True,
    )
    assert result == {
        "todo.entity1": {
            "items": [
                {
                    **ITEM_1,
                    **expected_item_data,
                },
            ]
        }
    }