"""Tests for todo platform of local_todo."""

from collections.abc import Awaitable, Callable
import textwrap
from typing import Any

import pytest

from homeassistant.components.todo import DOMAIN as TODO_DOMAIN
from homeassistant.core import HomeAssistant

from .conftest import TEST_ENTITY

from tests.typing import WebSocketGenerator


@pytest.fixture
async def ws_get_items(
    hass_ws_client: WebSocketGenerator,
) -> Callable[[], Awaitable[dict[str, str]]]:
    """Fixture to fetch items from the todo websocket."""

    async def get() -> list[dict[str, str]]:
        # Fetch items using To-do platform
        client = await hass_ws_client()
        await client.send_json_auto_id(
            {
                "type": "todo/item/list",
                "entity_id": TEST_ENTITY,
            }
        )
        resp = await client.receive_json()
        assert resp.get("success")
        return resp.get("result", {}).get("items", [])

    return get


@pytest.fixture
async def ws_move_item(
    hass_ws_client: WebSocketGenerator,
) -> Callable[[str, str | None], Awaitable[None]]:
    """Fixture to move an item in the todo list."""

    async def move(uid: str, previous_uid: str | None) -> None:
        # Fetch items using To-do platform
        client = await hass_ws_client()
        data = {
            "type": "todo/item/move",
            "entity_id": TEST_ENTITY,
            "uid": uid,
        }
        if previous_uid is not None:
            data["previous_uid"] = previous_uid
        await client.send_json_auto_id(data)
        resp = await client.receive_json()
        assert resp.get("success")

    return move


@pytest.fixture(autouse=True)
def set_time_zone(hass: HomeAssistant) -> None:
    """Set the time zone for the tests that keesp UTC-6 all year round."""
    hass.config.set_time_zone("America/Regina")


EXPECTED_ADD_ITEM = {
    "status": "needs_action",
    "summary": "replace batteries",
}


@pytest.mark.parametrize(
    ("item_data", "expected_item_data"),
    [
        ({}, EXPECTED_ADD_ITEM),
        ({"due_date": "2023-11-17"}, {**EXPECTED_ADD_ITEM, "due": "2023-11-17"}),
        (
            {"due_datetime": "2023-11-17T11:30:00+00:00"},
            {**EXPECTED_ADD_ITEM, "due": "2023-11-17T05:30:00-06:00"},
        ),
        (
            {"description": "Additional detail"},
            {**EXPECTED_ADD_ITEM, "description": "Additional detail"},
        ),
        ({"description": ""}, {**EXPECTED_ADD_ITEM, "description": ""}),
        ({"description": None}, EXPECTED_ADD_ITEM),
    ],
)
async def test_add_item(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
    setup_integration: None,
    ws_get_items: Callable[[], Awaitable[dict[str, str]]],
    item_data: dict[str, Any],
    expected_item_data: dict[str, Any],
) -> None:
    """Test adding a todo item."""

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.state == "0"

    await hass.services.async_call(
        TODO_DOMAIN,
        "add_item",
        {"item": "replace batteries", **item_data},
        target={"entity_id": TEST_ENTITY},
        blocking=True,
    )

    items = await ws_get_items()
    assert len(items) == 1
    item_data = items[0]
    assert "uid" in item_data
    del item_data["uid"]
    assert item_data == expected_item_data

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.state == "1"


@pytest.mark.parametrize(
    ("item_data", "expected_item_data"),
    [
        ({}, {}),
        ({"due_date": "2023-11-17"}, {"due": "2023-11-17"}),
        (
            {"due_datetime": "2023-11-17T11:30:00+00:00"},
            {"due": "2023-11-17T05:30:00-06:00"},
        ),
        ({"description": "Additional detail"}, {"description": "Additional detail"}),
    ],
)
async def test_remove_item(
    hass: HomeAssistant,
    setup_integration: None,
    ws_get_items: Callable[[], Awaitable[dict[str, str]]],
    item_data: dict[str, Any],
    expected_item_data: dict[str, Any],
) -> None:
    """Test removing a todo item."""
    await hass.services.async_call(
        TODO_DOMAIN,
        "add_item",
        {"item": "replace batteries", **item_data},
        target={"entity_id": TEST_ENTITY},
        blocking=True,
    )

    items = await ws_get_items()
    assert len(items) == 1
    assert items[0]["summary"] == "replace batteries"
    assert items[0]["status"] == "needs_action"
    for k, v in expected_item_data.items():
        assert items[0][k] == v
    assert "uid" in items[0]

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.state == "1"

    await hass.services.async_call(
        TODO_DOMAIN,
        "remove_item",
        {"item": [items[0]["uid"]]},
        target={"entity_id": TEST_ENTITY},
        blocking=True,
    )

    items = await ws_get_items()
    assert len(items) == 0

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.state == "0"


async def test_bulk_remove(
    hass: HomeAssistant,
    setup_integration: None,
    ws_get_items: Callable[[], Awaitable[dict[str, str]]],
) -> None:
    """Test removing multiple todo items."""
    for i in range(0, 5):
        await hass.services.async_call(
            TODO_DOMAIN,
            "add_item",
            {"item": f"soda #{i}"},
            target={"entity_id": TEST_ENTITY},
            blocking=True,
        )

    items = await ws_get_items()
    assert len(items) == 5
    uids = [item["uid"] for item in items]

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.state == "5"

    await hass.services.async_call(
        TODO_DOMAIN,
        "remove_item",
        {"item": uids},
        target={"entity_id": TEST_ENTITY},
        blocking=True,
    )

    items = await ws_get_items()
    assert len(items) == 0

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.state == "0"


EXPECTED_UPDATE_ITEM = {
    "status": "needs_action",
    "summary": "soda",
}


@pytest.mark.parametrize(
    ("item_data", "expected_item_data", "expected_state"),
    [
        ({"status": "completed"}, {**EXPECTED_UPDATE_ITEM, "status": "completed"}, "0"),
        (
            {"due_date": "2023-11-17"},
            {**EXPECTED_UPDATE_ITEM, "due": "2023-11-17"},
            "1",
        ),
        (
            {"due_datetime": "2023-11-17T11:30:00+00:00"},
            {**EXPECTED_UPDATE_ITEM, "due": "2023-11-17T05:30:00-06:00"},
            "1",
        ),
        (
            {"description": "Additional detail"},
            {**EXPECTED_UPDATE_ITEM, "description": "Additional detail"},
            "1",
        ),
    ],
)
async def test_update_item(
    hass: HomeAssistant,
    setup_integration: None,
    ws_get_items: Callable[[], Awaitable[dict[str, str]]],
    item_data: dict[str, Any],
    expected_item_data: dict[str, Any],
    expected_state: str,
) -> None:
    """Test updating a todo item."""

    # Create new item
    await hass.services.async_call(
        TODO_DOMAIN,
        "add_item",
        {"item": "soda"},
        target={"entity_id": TEST_ENTITY},
        blocking=True,
    )

    # Fetch item
    items = await ws_get_items()
    assert len(items) == 1

    item = items[0]
    assert item["summary"] == "soda"
    assert item["status"] == "needs_action"

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.state == "1"

    # Update item
    await hass.services.async_call(
        TODO_DOMAIN,
        "update_item",
        {"item": item["uid"], **item_data},
        target={"entity_id": TEST_ENTITY},
        blocking=True,
    )

    # Verify item is updated
    items = await ws_get_items()
    assert len(items) == 1
    item = items[0]
    assert item["summary"] == "soda"
    assert "uid" in item
    del item["uid"]
    assert item == expected_item_data

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.state == expected_state


@pytest.mark.parametrize(
    ("item_data", "expected_item_data"),
    [
        (
            {"status": "completed"},
            {
                "summary": "soda",
                "status": "completed",
                "description": "Additional detail",
                "due": "2024-01-01",
            },
        ),
        (
            {"due_date": "2024-01-02"},
            {
                "summary": "soda",
                "status": "needs_action",
                "description": "Additional detail",
                "due": "2024-01-02",
            },
        ),
        (
            {"due_date": None},
            {
                "summary": "soda",
                "status": "needs_action",
                "description": "Additional detail",
            },
        ),
        (
            {"due_datetime": "2024-01-01 10:30:00"},
            {
                "summary": "soda",
                "status": "needs_action",
                "description": "Additional detail",
                "due": "2024-01-01T10:30:00-06:00",
            },
        ),
        (
            {"due_datetime": None},
            {
                "summary": "soda",
                "status": "needs_action",
                "description": "Additional detail",
            },
        ),
        (
            {"description": "updated description"},
            {
                "summary": "soda",
                "status": "needs_action",
                "due": "2024-01-01",
                "description": "updated description",
            },
        ),
        (
            {"description": None},
            {"summary": "soda", "status": "needs_action", "due": "2024-01-01"},
        ),
    ],
    ids=[
        "status",
        "due_date",
        "clear_due_date",
        "due_datetime",
        "clear_due_datetime",
        "description",
        "clear_description",
    ],
)
async def test_update_existing_field(
    hass: HomeAssistant,
    setup_integration: None,
    ws_get_items: Callable[[], Awaitable[dict[str, str]]],
    item_data: dict[str, Any],
    expected_item_data: dict[str, Any],
) -> None:
    """Test updating a todo item."""

    # Create new item
    await hass.services.async_call(
        TODO_DOMAIN,
        "add_item",
        {"item": "soda", "description": "Additional detail", "due_date": "2024-01-01"},
        target={"entity_id": TEST_ENTITY},
        blocking=True,
    )

    # Fetch item
    items = await ws_get_items()
    assert len(items) == 1

    item = items[0]
    assert item["summary"] == "soda"
    assert item["status"] == "needs_action"

    # Perform update
    await hass.services.async_call(
        TODO_DOMAIN,
        "update_item",
        {"item": item["uid"], **item_data},
        target={"entity_id": TEST_ENTITY},
        blocking=True,
    )

    # Verify item is updated
    items = await ws_get_items()
    assert len(items) == 1
    item = items[0]
    assert item["summary"] == "soda"
    assert "uid" in item
    del item["uid"]
    assert item == expected_item_data


async def test_rename(
    hass: HomeAssistant,
    setup_integration: None,
    ws_get_items: Callable[[], Awaitable[dict[str, str]]],
) -> None:
    """Test renaming a todo item."""

    # Create new item
    await hass.services.async_call(
        TODO_DOMAIN,
        "add_item",
        {"item": "soda"},
        target={"entity_id": TEST_ENTITY},
        blocking=True,
    )

    # Fetch item
    items = await ws_get_items()
    assert len(items) == 1
    item = items[0]
    assert item["summary"] == "soda"
    assert item["status"] == "needs_action"

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.state == "1"

    # Rename item
    await hass.services.async_call(
        TODO_DOMAIN,
        "update_item",
        {"item": item["uid"], "rename": "water"},
        target={"entity_id": TEST_ENTITY},
        blocking=True,
    )

    # Verify item has been renamed
    items = await ws_get_items()
    assert len(items) == 1
    item = items[0]
    assert item["summary"] == "water"
    assert item["status"] == "needs_action"

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.state == "1"


@pytest.mark.parametrize(
    ("src_idx", "dst_idx", "expected_items"),
    [
        # Move any item to the front of the list
        (0, None, ["item 1", "item 2", "item 3", "item 4"]),
        (1, None, ["item 2", "item 1", "item 3", "item 4"]),
        (2, None, ["item 3", "item 1", "item 2", "item 4"]),
        (3, None, ["item 4", "item 1", "item 2", "item 3"]),
        # Move items right
        (0, 1, ["item 2", "item 1", "item 3", "item 4"]),
        (0, 2, ["item 2", "item 3", "item 1", "item 4"]),
        (0, 3, ["item 2", "item 3", "item 4", "item 1"]),
        (1, 2, ["item 1", "item 3", "item 2", "item 4"]),
        (1, 3, ["item 1", "item 3", "item 4", "item 2"]),
        # Move items left
        (2, 0, ["item 1", "item 3", "item 2", "item 4"]),
        (3, 0, ["item 1", "item 4", "item 2", "item 3"]),
        (3, 1, ["item 1", "item 2", "item 4", "item 3"]),
        # No-ops
        (0, 0, ["item 1", "item 2", "item 3", "item 4"]),
        (2, 1, ["item 1", "item 2", "item 3", "item 4"]),
        (2, 2, ["item 1", "item 2", "item 3", "item 4"]),
        (3, 2, ["item 1", "item 2", "item 3", "item 4"]),
        (3, 3, ["item 1", "item 2", "item 3", "item 4"]),
    ],
)
async def test_move_item(
    hass: HomeAssistant,
    setup_integration: None,
    ws_get_items: Callable[[], Awaitable[dict[str, str]]],
    ws_move_item: Callable[[str, str | None], Awaitable[None]],
    src_idx: int,
    dst_idx: int | None,
    expected_items: list[str],
) -> None:
    """Test moving a todo item within the list."""
    for i in range(1, 5):
        await hass.services.async_call(
            TODO_DOMAIN,
            "add_item",
            {"item": f"item {i}"},
            target={"entity_id": TEST_ENTITY},
            blocking=True,
        )

    items = await ws_get_items()
    assert len(items) == 4
    uids = [item["uid"] for item in items]
    summaries = [item["summary"] for item in items]
    assert summaries == ["item 1", "item 2", "item 3", "item 4"]

    # Prepare items for moving
    previous_uid = None
    if dst_idx is not None:
        previous_uid = uids[dst_idx]
    await ws_move_item(uids[src_idx], previous_uid)

    items = await ws_get_items()
    assert len(items) == 4
    summaries = [item["summary"] for item in items]
    assert summaries == expected_items


async def test_move_item_unknown(
    hass: HomeAssistant,
    setup_integration: None,
    hass_ws_client: WebSocketGenerator,
) -> None:
    """Test moving a todo item that does not exist."""

    # Prepare items for moving
    client = await hass_ws_client()
    data = {
        "id": 1,
        "type": "todo/item/move",
        "entity_id": TEST_ENTITY,
        "uid": "unknown",
        "previous_uid": "item-2",
    }
    await client.send_json(data)
    resp = await client.receive_json()
    assert resp.get("id") == 1
    assert not resp.get("success")
    assert resp.get("error", {}).get("code") == "failed"
    assert "not found in todo list" in resp["error"]["message"]


async def test_move_item_previous_unknown(
    hass: HomeAssistant,
    setup_integration: None,
    hass_ws_client: WebSocketGenerator,
    ws_get_items: Callable[[], Awaitable[dict[str, str]]],
) -> None:
    """Test moving a todo item that does not exist."""

    await hass.services.async_call(
        TODO_DOMAIN,
        "add_item",
        {"item": "item 1"},
        target={"entity_id": TEST_ENTITY},
        blocking=True,
    )
    items = await ws_get_items()
    assert len(items) == 1

    # Prepare items for moving
    client = await hass_ws_client()
    data = {
        "id": 1,
        "type": "todo/item/move",
        "entity_id": TEST_ENTITY,
        "uid": items[0]["uid"],
        "previous_uid": "unknown",
    }
    await client.send_json(data)
    resp = await client.receive_json()
    assert resp.get("id") == 1
    assert not resp.get("success")
    assert resp.get("error", {}).get("code") == "failed"
    assert "not found in todo list" in resp["error"]["message"]


@pytest.mark.parametrize(
    ("ics_content", "expected_state"),
    [
        ("", "0"),
        (None, "0"),
        (
            textwrap.dedent(
                """\
                    BEGIN:VCALENDAR
                    PRODID:-//homeassistant.io//local_todo 1.0//EN
                    VERSION:2.0
                    BEGIN:VTODO
                    DTSTAMP:20231024T014011
                    UID:077cb7f2-6c89-11ee-b2a9-0242ac110002
                    CREATED:20231017T010348
                    LAST-MODIFIED:20231024T014011
                    SEQUENCE:1
                    STATUS:COMPLETED
                    SUMMARY:Complete Task
                    END:VTODO
                    END:VCALENDAR
                """
            ),
            "0",
        ),
        (
            textwrap.dedent(
                """\
                    BEGIN:VCALENDAR
                    PRODID:-//homeassistant.io//local_todo 1.0//EN
                    VERSION:2.0
                    BEGIN:VTODO
                    DTSTAMP:20231024T014011
                    UID:077cb7f2-6c89-11ee-b2a9-0242ac110002
                    CREATED:20231017T010348
                    LAST-MODIFIED:20231024T014011
                    SEQUENCE:1
                    STATUS:NEEDS-ACTION
                    SUMMARY:Incomplete Task
                    END:VTODO
                    END:VCALENDAR
                """
            ),
            "1",
        ),
    ],
    ids=("empty", "not_exists", "completed", "needs_action"),
)
async def test_parse_existing_ics(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
    setup_integration: None,
    expected_state: str,
) -> None:
    """Test parsing ics content."""

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.state == expected_state


async def test_susbcribe(
    hass: HomeAssistant,
    setup_integration: None,
    hass_ws_client: WebSocketGenerator,
) -> None:
    """Test subscribing to item updates."""

    # Create new item
    await hass.services.async_call(
        TODO_DOMAIN,
        "add_item",
        {"item": "soda"},
        target={"entity_id": TEST_ENTITY},
        blocking=True,
    )

    # Subscribe and get the initial list
    client = await hass_ws_client(hass)
    await client.send_json_auto_id(
        {
            "type": "todo/item/subscribe",
            "entity_id": TEST_ENTITY,
        }
    )
    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"
    items = msg["event"].get("items")
    assert items
    assert len(items) == 1
    assert items[0]["summary"] == "soda"
    assert items[0]["status"] == "needs_action"
    uid = items[0]["uid"]
    assert uid

    # Rename item
    await hass.services.async_call(
        TODO_DOMAIN,
        "update_item",
        {"item": uid, "rename": "milk"},
        target={"entity_id": TEST_ENTITY},
        blocking=True,
    )

    # Verify update is published
    msg = await client.receive_json()
    assert msg["id"] == subscription_id
    assert msg["type"] == "event"
    items = msg["event"].get("items")
    assert items
    assert len(items) == 1
    assert items[0]["summary"] == "milk"
    assert items[0]["status"] == "needs_action"
    assert "uid" in items[0]