import datetime
from unittest.mock import Mock

import pytest
from voluptuous import (
    All,
    Any,
    Coerce,
    Range,
    Schema,
)

from kasa import Device, DeviceType, EmeterStatus, Module
from kasa.interfaces.energy import Energy
from kasa.iot import IotStrip
from kasa.iot.modules.emeter import Emeter
from tests.conftest import has_emeter_iot, no_emeter_iot

CURRENT_CONSUMPTION_SCHEMA = Schema(
    Any(
        {
            "voltage_mv": Any(All(float, Range(min=0, max=300000)), int, None),
            "power_mw": Any(Coerce(float), None),
            "current_ma": Any(All(float), int, None),
            "energy_wh": Any(Coerce(float), None),
            "total_wh": Any(Coerce(float), None),
            "voltage": Any(All(float, Range(min=0, max=300)), None),
            "power": Any(Coerce(float), None),
            "current": Any(All(float), None),
            "total": Any(Coerce(float), None),
            "energy": Any(Coerce(float), None),
            "slot_id": Any(Coerce(int), None),
        },
        None,
    )
)


@no_emeter_iot
async def test_no_emeter(dev):
    assert not dev.has_emeter

    with pytest.raises(AttributeError):
        await dev.get_emeter_realtime()

    with pytest.raises(AttributeError):
        await dev.get_emeter_daily()
    with pytest.raises(AttributeError):
        await dev.get_emeter_monthly()
    with pytest.raises(AttributeError):
        await dev.erase_emeter_stats()


@has_emeter_iot
async def test_get_emeter_realtime(dev):
    emeter = dev.modules[Module.Energy]

    current_emeter = await emeter.get_status()
    # Check realtime query gets the same value as status property
    # iot _query_helper strips out the error code from module responses.
    # but it's not stripped out of the _modular_update queries.
    assert current_emeter == {k: v for k, v in emeter.status.items() if k != "err_code"}
    CURRENT_CONSUMPTION_SCHEMA(current_emeter)


@has_emeter_iot
@pytest.mark.requires_dummy
async def test_get_emeter_daily(dev):
    emeter = dev.modules[Module.Energy]

    assert await emeter.get_daily_stats(year=1900, month=1) == {}

    d = await emeter.get_daily_stats()
    assert len(d) > 0

    k, v = d.popitem()
    assert isinstance(k, int)
    assert isinstance(v, float)

    # Test kwh (energy, energy_wh)
    d = await emeter.get_daily_stats(kwh=False)
    k2, v2 = d.popitem()
    assert v * 1000 == v2


@has_emeter_iot
@pytest.mark.requires_dummy
async def test_get_emeter_monthly(dev):
    emeter = dev.modules[Module.Energy]

    assert await emeter.get_monthly_stats(year=1900) == {}

    d = await emeter.get_monthly_stats()
    assert len(d) > 0

    k, v = d.popitem()
    assert isinstance(k, int)
    assert isinstance(v, float)

    # Test kwh (energy, energy_wh)
    d = await emeter.get_monthly_stats(kwh=False)
    k2, v2 = d.popitem()
    assert v * 1000 == v2


@has_emeter_iot
async def test_emeter_status(dev):
    emeter = dev.modules[Module.Energy]

    d = await emeter.get_status()

    with pytest.raises(KeyError):
        assert d["foo"]

    assert d["power_mw"] == d["power"] * 1000
    # bulbs have only power according to tplink simulator.
    if (
        dev.device_type is not DeviceType.Bulb
        and dev.device_type is not DeviceType.LightStrip
    ):
        assert d["voltage_mv"] == d["voltage"] * 1000

        assert d["current_ma"] == d["current"] * 1000
        assert d["total_wh"] == d["total"] * 1000


@pytest.mark.skip("not clearing your stats..")
@has_emeter_iot
async def test_erase_emeter_stats(dev):
    emeter = dev.modules[Module.Energy]

    await emeter.erase_emeter()


@has_emeter_iot
async def test_current_consumption(dev):
    emeter = dev.modules[Module.Energy]
    x = emeter.current_consumption
    assert isinstance(x, float)
    assert x >= 0.0


async def test_emeterstatus_missing_current():
    """KL125 does not report 'current' for emeter."""
    regular = EmeterStatus(
        {"err_code": 0, "power_mw": 0, "total_wh": 13, "current_ma": 123}
    )
    assert regular["current"] == 0.123

    with pytest.raises(KeyError):
        regular["invalid_key"]

    missing_current = EmeterStatus({"err_code": 0, "power_mw": 0, "total_wh": 13})
    assert missing_current["current"] is None


async def test_emeter_daily():
    """Test fetching the emeter for today.

    This test uses inline data since the fixtures
    will not have data for the current day.
    """
    emeter_data = {
        "get_daystat": {
            "day_list": [{"day": 1, "energy_wh": 8, "month": 1, "year": 2023}],
            "err_code": 0,
        }
    }

    class MockEmeter(Emeter):
        @property
        def data(self):
            return emeter_data

    emeter = MockEmeter(Mock(), "emeter")
    now = datetime.datetime.now()
    emeter_data["get_daystat"]["day_list"].append(
        {"day": now.day, "energy_wh": 500, "month": now.month, "year": now.year}
    )
    assert emeter.consumption_today == 0.500


@has_emeter_iot
async def test_supported(dev: Device):
    energy_module = dev.modules.get(Module.Energy)
    assert energy_module

    info = (
        dev._last_update
        if not isinstance(dev, IotStrip)
        else dev.children[0].internal_state
    )
    emeter = info[energy_module._module]["get_realtime"]
    has_total = "total" in emeter or "total_wh" in emeter
    has_voltage_current = "voltage" in emeter or "voltage_mv" in emeter
    assert energy_module.supports(Energy.ModuleFeature.CONSUMPTION_TOTAL) is has_total
    assert (
        energy_module.supports(Energy.ModuleFeature.VOLTAGE_CURRENT)
        is has_voltage_current
    )
    assert energy_module.supports(Energy.ModuleFeature.PERIODIC_STATS) is True
