"""Tests for VictronClient with mocked pymodbus."""

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from aiovictron.client import VictronClient
from aiovictron.exceptions import (
    VictronConnectionError,
    VictronModbusError,
    VictronNoDevicesError,
)


def _make_register_result(registers: list[int]):
    """Create a mock Modbus response with register values."""
    result = MagicMock()
    result.isError.return_value = False
    result.registers = registers
    return result


def _make_error_result():
    """Create a mock Modbus error response."""
    result = MagicMock()
    result.isError.return_value = True
    return result


@pytest.fixture
def mock_modbus():
    """Patch AsyncModbusTcpClient."""
    with patch("aiovictron.client.AsyncModbusTcpClient") as mock_cls:
        mock_client = AsyncMock()
        mock_client.connected = True
        mock_client.connect = AsyncMock(return_value=True)
        mock_client.close = MagicMock()
        mock_cls.return_value = mock_client
        yield mock_client


@pytest.mark.asyncio
async def test_connect_success(mock_modbus):
    client = VictronClient("192.168.1.153")
    await client.connect()
    mock_modbus.connect.assert_awaited_once()


@pytest.mark.asyncio
async def test_connect_failure(mock_modbus):
    mock_modbus.connect = AsyncMock(return_value=False)
    client = VictronClient("192.168.1.153")
    with pytest.raises(VictronConnectionError):
        await client.connect()


@pytest.mark.asyncio
async def test_context_manager(mock_modbus):
    async with VictronClient("192.168.1.153") as client:
        assert client.connected
    mock_modbus.close.assert_called_once()


@pytest.mark.asyncio
async def test_discover_devices(mock_modbus):
    # System (unit 100) responds, solar charger (223) responds, VE.Bus (227) errors
    def side_effect(address, *, count=1, device_id=0, no_response_expected=False):
        if device_id == 100:
            return _make_register_result([265])
        if device_id == 223:
            return _make_register_result([3])
        return _make_error_result()

    mock_modbus.read_holding_registers = AsyncMock(side_effect=side_effect)

    client = VictronClient("192.168.1.153")
    await client.connect()
    devices = await client.discover_devices()

    assert len(devices) == 2
    assert devices[0].unit_id == 100
    assert devices[0].device_type == "system"
    assert devices[1].unit_id == 223
    assert devices[1].device_type == "solar_charger"


@pytest.mark.asyncio
async def test_discover_no_devices(mock_modbus):
    mock_modbus.read_holding_registers = AsyncMock(
        return_value=_make_error_result()
    )

    client = VictronClient("192.168.1.153")
    await client.connect()
    devices = await client.discover_devices()
    assert len(devices) == 0


@pytest.mark.asyncio
async def test_validate_connection_raises_when_no_devices(mock_modbus):
    mock_modbus.read_holding_registers = AsyncMock(
        return_value=_make_error_result()
    )

    client = VictronClient("192.168.1.153")
    await client.connect()
    with pytest.raises(VictronNoDevicesError):
        await client.validate_connection()


@pytest.mark.asyncio
async def test_get_data_system_only(mock_modbus):
    # Only system (unit 100) discovered
    probed = set()

    def side_effect(address, *, count=1, device_id=0, no_response_expected=False):
        if device_id == 100:
            if device_id not in probed and address == 817 and count == 1:
                # Discovery probe (first register of system parser)
                probed.add(device_id)
                return _make_register_result([0])
            # Full read: system has 3 blocks (817-820, 840-844, 850)
            regs = [0] * count
            base = address
            mapping = {
                817: 0,       # ac_consumption_l1
                820: 120,     # grid_power_l1
                840: 265,     # battery_voltage raw (26.5V)
                841: 0,       # battery_current
                842: 0,       # battery_power
                843: 78,      # battery_soc
                844: 4,       # battery_state
                850: 350,     # pv_power
            }
            for addr, val in mapping.items():
                idx = addr - base
                if 0 <= idx < count:
                    regs[idx] = val
            return _make_register_result(regs)

        return _make_error_result()

    mock_modbus.read_holding_registers = AsyncMock(side_effect=side_effect)

    client = VictronClient("192.168.1.153")
    await client.connect()
    data = await client.get_data()

    assert data.system is not None
    assert data.system.battery_voltage == 26.5
    assert data.system.battery_soc == 78.0
    assert data.system.pv_power == 350.0
    assert data.system.grid_power_l1 == 120.0
    assert data.system.ac_consumption_l1 == 0.0
    assert data.solar_charger is None
    assert data.vebus is None


@pytest.mark.asyncio
async def test_get_data_modbus_error_on_read(mock_modbus):
    # Discovery succeeds, but full read returns error
    probe_done = {}

    def side_effect(address, *, count=1, device_id=0, no_response_expected=False):
        if device_id == 100:
            if device_id not in probe_done:
                probe_done[device_id] = True
                return _make_register_result([265])
            return _make_error_result()
        return _make_error_result()

    mock_modbus.read_holding_registers = AsyncMock(side_effect=side_effect)

    client = VictronClient("192.168.1.153")
    await client.connect()
    with pytest.raises(VictronModbusError):
        await client.get_data()
