"""Tests for the Q10 B01 status trait."""

import asyncio
import json
import pathlib
from collections.abc import AsyncGenerator
from typing import Any
from unittest.mock import AsyncMock, Mock

import pytest

from roborock.data.b01_q10.b01_q10_code_mappings import (
    B01_Q10_DP,
    YXAreaUnit,
    YXCarpetCleanType,
    YXCleanLine,
    YXCleanType,
    YXDeviceCleanTask,
    YXDeviceDustCollectionFrequency,
    YXDeviceState,
    YXFanLevel,
    YXWaterLevel,
)
from roborock.data.b01_q10.b01_q10_containers import dpNetInfo, dpNotDisturbExpand, dpTimeZone
from roborock.devices.traits.b01.q10 import Q10PropertiesApi, create
from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol

TEST_DATA_DIR = pathlib.Path("tests/protocols/testdata/b01_q10_protocol")

TESTDATA_DP_STATUS_DP_CLEAN_TASK_TYPE = (TEST_DATA_DIR / "dpStatus-dpCleanTaskType.json").read_bytes()
TESTDATA_DP_REQUEST_DPS = (TEST_DATA_DIR / "dpRequetdps.json").read_bytes()


@pytest.fixture
def mock_channel():
    """Fixture for a mocked MQTT channel."""
    mock = AsyncMock()
    return mock


@pytest.fixture
def message_queue() -> asyncio.Queue[RoborockMessage]:
    """Fixture for a message queue used by the mock stream."""
    return asyncio.Queue()


@pytest.fixture
def mock_subscribe_stream(mock_channel: AsyncMock, message_queue: asyncio.Queue[RoborockMessage]) -> Mock:
    """Fixture to mock the subscribe_stream method to yield from a queue."""

    async def mock_stream() -> AsyncGenerator[RoborockMessage, None]:
        while True:
            yield await message_queue.get()

    mock = Mock(return_value=mock_stream())
    mock_channel.subscribe_stream = mock
    return mock


@pytest.fixture
async def q10_api(mock_channel: AsyncMock, mock_subscribe_stream: Mock) -> AsyncGenerator[Q10PropertiesApi, None]:
    """Fixture to create and manage the Q10PropertiesApi."""
    api = create(mock_channel)
    await api.start()
    yield api
    await api.close()


def build_message(payload: bytes) -> RoborockMessage:
    """Helper to build a RoborockMessage for testing."""
    return RoborockMessage(
        protocol=RoborockMessageProtocol.RPC_RESPONSE,
        payload=payload,
        version=b"B01",
    )


async def wait_for_attribute_value(obj: Any, attribute: str, value: Any, timeout: float = 2.0) -> None:
    """Wait for an attribute on an object to reach a specific value.

    This is a temporary polling solution until listeners are implemented.
    """
    for _ in range(int(timeout / 0.1)):
        if getattr(obj, attribute) == value:
            return
        await asyncio.sleep(0.1)
    pytest.fail(f"Timeout waiting for {attribute} to become {value} on {obj}")


async def test_status_trait_streaming(
    q10_api: Q10PropertiesApi,
    message_queue: asyncio.Queue[RoborockMessage],
) -> None:
    """Test that the StatusTrait updates its state from streaming messages."""
    # status (121) = 8 (CHARGING_STATE)
    # clean_task_type (138) = 0 (IDLE)
    message = build_message(TESTDATA_DP_STATUS_DP_CLEAN_TASK_TYPE)

    assert q10_api.status.status is None
    assert q10_api.status.clean_task_type is None

    # Push the message into the queue
    message_queue.put_nowait(message)

    # Wait for the update
    await wait_for_attribute_value(q10_api.status, "status", YXDeviceState.CHARGING)

    # Verify trait attributes are updated
    assert q10_api.status.status == YXDeviceState.CHARGING
    assert q10_api.status.clean_task_type == YXDeviceCleanTask.IDLE


async def test_status_trait_refresh(
    q10_api: Q10PropertiesApi,
    mock_channel: AsyncMock,
    message_queue: asyncio.Queue[RoborockMessage],
) -> None:
    """Test that the StatusTrait sends a refresh command and updates state."""
    assert q10_api.status.battery is None
    assert q10_api.status.status is None
    assert q10_api.status.fan_level is None
    assert q10_api.status.total_clean_count is None
    assert q10_api.consumable.main_brush_life is None
    assert q10_api.status.cleaning_progress is None
    assert q10_api.status.fault is None

    # Mock the response to refresh
    # battery (122) = 100
    # status (121) = 8 (CHARGING_STATE)
    # fan_level (123) = 2 (BALANCED)
    message = build_message(TESTDATA_DP_REQUEST_DPS)

    # Send a refresh command
    await q10_api.refresh()
    mock_channel.publish.assert_called_once()
    sent_message = mock_channel.publish.call_args[0][0]
    assert sent_message.protocol == RoborockMessageProtocol.RPC_REQUEST
    # Verify refresh payload
    data = json.loads(sent_message.payload)
    assert data
    assert data.get("dps")
    assert data.get("dps").get("102") == {}  # REQUEST_DPS code is 102

    # Push the response message into the queue
    message_queue.put_nowait(message)

    # Wait for the update
    await wait_for_attribute_value(q10_api.status, "battery", 100)

    # Verify trait attributes are updated
    assert q10_api.status.battery == 100
    assert q10_api.status.status == YXDeviceState.CHARGING
    assert q10_api.status.fan_level == YXFanLevel.BALANCED
    assert q10_api.status.total_clean_area == 0
    assert q10_api.status.total_clean_count == 0
    assert q10_api.status.total_clean_time == 0
    assert q10_api.consumable.main_brush_life == 0
    assert q10_api.consumable.side_brush_life == 0
    assert q10_api.consumable.filter_life == 0
    assert q10_api.consumable.sensor_life == 0
    assert q10_api.status.cleaning_progress == 100
    assert q10_api.status.fault == 0
    assert q10_api.status.clean_mode == YXCleanType.VAC_AND_MOP
    assert q10_api.status.water_level == YXWaterLevel.LOW

    # Settings with dedicated traits are read from those traits.
    assert q10_api.volume.volume == 74
    assert q10_api.do_not_disturb.not_disturb is True
    assert q10_api.do_not_disturb.is_on is True
    assert q10_api.child_lock.child_lock is False
    assert q10_api.child_lock.is_on is False
    assert q10_api.dust_collection.dust_switch is True
    assert q10_api.dust_collection.is_on is True
    assert q10_api.dust_collection.dust_setting == YXDeviceDustCollectionFrequency.REGULAR

    # Additional state captured on the Status trait.
    assert q10_api.status.mop_state is True
    assert q10_api.status.ground_clean is False
    assert q10_api.status.carpet_clean_type == YXCarpetCleanType.RISE
    assert q10_api.status.area_unit == YXAreaUnit.SQUARE_METER
    assert q10_api.status.auto_boost is False
    assert q10_api.status.map_save_switch is True
    assert q10_api.status.recent_clean_record is False
    assert q10_api.status.valley_point_charging is False
    assert q10_api.status.clean_line == YXCleanLine.FAST
    assert q10_api.status.line_laser_obstacle_avoidance is True
    assert q10_api.status.robot_country_code == "us"
    assert q10_api.status.robot_type == 1
    assert q10_api.status.time_zone == dpTimeZone(time_zone_city="America/Los_Angeles", time_zone_sec=-28800)

    # Nested containers are parsed into their dataclasses on their own traits.
    assert q10_api.do_not_disturb.not_disturb_expand == dpNotDisturbExpand(
        disturb_dust_enable=1, disturb_light=1, disturb_resume_clean=1, disturb_voice=1
    )
    assert q10_api.network_info.net_info == dpNetInfo(
        wifi_name="wifi-network-name", ip_adress="1.1.1.2", mac="99:AA:88:BB:77:CC", signal=-50
    )


async def test_status_trait_vacuum_only_refresh(
    q10_api: Q10PropertiesApi,
    message_queue: asyncio.Queue[RoborockMessage],
) -> None:
    """Test decoding a full status dump from a vacuum-only (no mop) Q10."""
    payload = (TEST_DATA_DIR / "dpRequestDps_vacuum_only.json").read_bytes()
    message_queue.put_nowait(build_message(payload))

    await wait_for_attribute_value(q10_api.status, "battery", 75)

    assert q10_api.status.fan_level == YXFanLevel.MAX_PLUS
    assert q10_api.status.water_level == YXWaterLevel.MEDIUM
    assert q10_api.status.clean_mode == YXCleanType.VACUUM
    assert q10_api.status.recent_clean_record is True
    assert q10_api.status.total_clean_count == 7


def test_status_trait_update_listener(q10_api: Q10PropertiesApi) -> None:
    """Test that status listeners receive updates and can unsubscribe."""
    event = asyncio.Event()

    unsubscribe = q10_api.status.add_update_listener(event.set)

    first_update = {B01_Q10_DP.BATTERY: 88}
    q10_api.status.update_from_dps(first_update)

    assert event.is_set()
    event.clear()

    unsubscribe()

    second_update = {B01_Q10_DP.BATTERY: 87}
    q10_api.status.update_from_dps(second_update)

    assert not event.is_set()


def test_status_trait_update_listener_ignores_value(q10_api: Q10PropertiesApi) -> None:
    """Test that status listeners are not notified for unrelated updates."""
    event = asyncio.Event()

    unsubscribe = q10_api.status.add_update_listener(event.set)

    first_update = {B01_Q10_DP.HEARTBEAT: 1}  # Not a value in `Status` dataclass
    q10_api.status.update_from_dps(first_update)

    assert not event.is_set()

    unsubscribe()
