"""Test the Felicity implementation."""

from collections.abc import Buffer
from typing import Final
from uuid import UUID

from bleak.backends.characteristic import BleakGATTCharacteristic
from bleak.uuids import normalize_uuid_str
import pytest

from aiobmsble import BMSSample
from aiobmsble.bms.felicity_bms import BMS
from tests.bluetooth import generate_ble_device
from tests.conftest import MockBleakClient
from tests.test_basebms import BMSBasicTests

BT_FRAME_SIZE = 35

RESP_VALUE: Final[dict[str, bytearray]] = {
    "dat": bytearray(
        b'{"CommVer":1,"wifiSN":"F100011002424470238","iotType":3,"dateTime":"20210101010459",'
        b'"timeZMin":480}'
    ),
    "rt": bytearray(
        b'{"CommVer":1,"wifiSN":"F100011002424470238","modID":1,"date":"20210101010501",'
        b'"DevSN":"100011002424470238","Type":112,"SubType":7300,"Estate":960,"Bfault":0,'
        b'"Bwarn":0,"Bstate":960,"BBfault":0,"BBwarn":0,"BTemp":[[130,130],[256,256]],"Batt":'
        b'[[52800],[-1],[null]],"Batsoc":[[3300,1000,300000]],"Templist":[[130,130],[0,0],'
        b'[65535,65535],[65535,65535]],"BattList":[[52750,65535],[-1,-1]],"BatsocList":'
        b'[[3300,1000,300000]],"BatcelList":[[3296,3296,3297,3297,3297,3297,3297,3297,3297,'
        b"3297,3296,3297,3297,3297,3297,3297],[65535,65535,65535,65535,65535,65535,65535,"
        b'65535,65535,65535,65535,65535,65535,65535,65535,65535]],"EMSpara":[[1,2]],"BMaxMin":'
        b'[[3297,3296],[2,0]],"LVolCur":[[576,480],[1500,1500]],"BMSpara":[[1,2]],"BLVolCu":'
        b'[[576,480],[1500,1500]],"BtemList":[[130,130,130,130,32767,32767,32767,32767]]}'
    ),
    "bas": bytearray(
        b'{"CommVer":1,"version":"2.06","wifiSN":"F100011002424470238","COM":3,"iotType":3,'
        b'"modID":1,"DevSN":"100011002424470238","Type":112,"SubType":7300,"DSwVer":65535,'
        b'"M1SwVer":519,"M2SwVer":16,"DHwVer":0,"CtHwVer":0,"PwHwVer":65535}'
    ),
}


def ref_value() -> BMSSample:
    """Return reference value for mock Seplos BMS."""
    return {
        "voltage": 52.8,
        "current": -0.1,
        "battery_level": 33.0,
        "cycle_charge": 99.0,
        "temperature": 13.0,
        "cycle_capacity": 5227.2,
        "power": -5.28,
        "battery_charging": False,
        "cell_count": 16,
        "cell_voltages": [
            3.296,
            3.296,
            3.297,
            3.297,
            3.297,
            3.297,
            3.297,
            3.297,
            3.297,
            3.297,
            3.296,
            3.297,
            3.297,
            3.297,
            3.297,
            3.297,
        ],
        "temp_values": [
            13.0,
            13.0,
            13.0,
            13.0,
        ],
        "delta_voltage": 0.001,
        "runtime": 3564000,
        "problem": False,
        "problem_code": 0,
    }


class TestBasicBMS(BMSBasicTests):
    """Test the basic BMS functionality."""

    bms_class = BMS


class MockFelicityBleakClient(MockBleakClient):
    """Emulate a Felicity BMS BleakClient."""

    HEAD_CMD: Final[int] = 0x7B
    TAIL_CMD: Final[int] = 0x7D
    CMDS: Final[dict[str, bytearray]] = {
        "dat": bytearray(b"wifilocalMonitor:get Date"),
        "bas": bytearray(b"wifilocalMonitor:get dev basice infor"),
        "rt": bytearray(b"wifilocalMonitor:get dev real infor"),
    }
    RESP: Final[dict[str, bytearray]] = RESP_VALUE

    def _response(
        self, char_specifier: BleakGATTCharacteristic | int | str | UUID, data: Buffer
    ) -> bytearray:

        if isinstance(char_specifier, str) and normalize_uuid_str(
            char_specifier
        ) == normalize_uuid_str("49535258-184d-4bd9-bc61-20c647249616"):
            for k, v in self.CMDS.items():
                if bytearray(data).startswith(v):
                    return self.RESP[k]

        return bytearray()

    async def write_gatt_char(
        self,
        char_specifier: BleakGATTCharacteristic | int | str | UUID,
        data: Buffer,
        response: bool | None = None,
    ) -> None:
        """Issue write command to GATT."""
        await super().write_gatt_char(char_specifier, data)

        assert (
            self._notify_callback
        ), "write to characteristics but notification not enabled"

        resp: bytearray = self._response(char_specifier, data)
        for notify_data in [
            resp[i : i + BT_FRAME_SIZE] for i in range(0, len(resp), BT_FRAME_SIZE)
        ]:
            self._notify_callback("MockFelicityBleakClient", notify_data)


async def test_update(patch_bleak_client, keep_alive_fixture) -> None:
    """Test Felicity BMS data update."""

    patch_bleak_client(MockFelicityBleakClient)

    bms = BMS(generate_ble_device(), keep_alive_fixture)

    assert await bms.async_update() == ref_value()

    # query again to check already connected state
    await bms.async_update()
    assert bms.is_connected is keep_alive_fixture

    await bms.disconnect()


async def test_device_info(patch_bleak_client) -> None:
    """Test that the BMS returns initialized dynamic device information."""
    patch_bleak_client(MockFelicityBleakClient)
    bms = BMS(generate_ble_device())
    assert await bms.device_info() == {
        "fw_version": 519,
        "sw_version": "2.06",
        "model_id": 112,
        "serial_number": "100011002424470238",
    }


async def test_problem_response(monkeypatch: pytest.MonkeyPatch, patch_bleak_client) -> None:
    """Test Felicity BMS data update with problem response."""

    prb_resp: dict[str, bytearray] = RESP_VALUE.copy()
    prb_resp["rt"][146:166] = b'"Bfault":1,"Bwarn":10'  # patch problem codes

    patch_bleak_client(MockFelicityBleakClient)

    monkeypatch.setattr(MockFelicityBleakClient, "RESP", prb_resp)

    bms = BMS(generate_ble_device(), False)

    assert await bms.async_update() == ref_value() | {
        "problem": True,
        "problem_code": 11,
    }

    await bms.disconnect()


@pytest.fixture(
    name="wrong_response",
    params=[
        (b'"CommVer":1,"wifiSN":"F100011002424470238"}', "invalid frame start"),
        (b'{"CommVer":1,"wifiSN":"F100011002424470238"', "invalid frame end"),
        (b'{"CommVer":2,"wifiSN":"F100011002424470238"}', "invalid protocol"),
    ],
    ids=lambda param: param[1],
)
def fix_response(request: pytest.FixtureRequest) -> bytes:
    """Return faulty response frame."""
    assert isinstance(request.param[0], bytes)
    return request.param[0]


async def test_invalid_response(
    monkeypatch: pytest.MonkeyPatch,
    patch_bleak_client,
    patch_bms_timeout,
    wrong_response: bytes,
) -> None:
    """Test data up date with BMS returning invalid data."""

    patch_bms_timeout()
    monkeypatch.setattr(
        MockFelicityBleakClient, "_response", lambda _s, _c_, d: wrong_response
    )
    patch_bleak_client(MockFelicityBleakClient)

    bms = BMS(generate_ble_device())

    result: BMSSample = {}
    with pytest.raises(TimeoutError):
        result = await bms.async_update()

    assert not result
    await bms.disconnect()
