"""Tests for HomeComAlt module."""
# pylint: disable=protected-access

from datetime import UTC, datetime, timedelta
from http import HTTPStatus
from unittest.mock import AsyncMock, MagicMock, patch
from urllib.parse import urlencode

import jwt
import pytest
from aiohttp import (
    ClientConnectorError,
    ClientResponseError,
    ClientSession,
    ContentTypeError,
)

from homecom_alt import (
    ApiError,
    AuthFailedError,
    ConnectionOptions,
    HomeComAlt,
    HomeComCommodule,
    HomeComGeneric,
    HomeComK40,
    HomeComRac,
    HomeComWddw2,
    NotRespondingError,
)
from homecom_alt.const import (
    JSON,
    OAUTH_DOMAIN,
    OAUTH_ENDPOINT,
    OAUTH_PARAMS,
    OAUTH_PARAMS_BUDERUS,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def create_test_jwt(expiration: int = 9999999999) -> str:
    """Create a valid JWT token for testing."""
    secret = "test_secret_key"
    payload = {
        "sub": "1234567890",
        "name": "Test User",
        "exp": expiration,
    }
    return jwt.encode(payload, secret, algorithm="HS256")


DEVICE_ID = "test-device-123"


def _make_options(**overrides):  # noqa: ANN003, ANN202
    defaults = {
        "username": "test_user",
        "token": create_test_jwt(),
        "refresh_token": "test_refresh",
        "code": "test_code",
    }
    defaults.update(overrides)
    return ConnectionOptions(**defaults)


def _mock_json_response(data, status=HTTPStatus.OK):  # noqa: ANN001, ANN202
    """Return an AsyncMock that behaves like an aiohttp response."""
    resp = AsyncMock()
    resp.status = status
    resp.json = AsyncMock(return_value=data)
    return resp


# ===========================================================================
# Base class - HTTP layer (existing tests, kept as-is with minor DRY-up)
# ===========================================================================


@pytest.mark.asyncio
async def test_async_http_request_success_json() -> None:
    """Test that _async_http_request returns a successful response for JSON requests."""
    session = ClientSession()
    options = _make_options()
    bhc = await HomeComAlt.create(session, options, auth_provider=True)

    with patch.object(ClientSession, "request", new=AsyncMock()) as mock_request:
        mock_response = _mock_json_response({"key": "value"})
        mock_request.return_value = mock_response

        resp = await bhc._async_http_request("get", "http://test.com", req_type=1)
        assert resp.status == HTTPStatus.OK

    await session.close()


@pytest.mark.asyncio
async def test_async_http_request_success_form() -> None:
    """Test that _async_http_request returns a successful response for form requests."""
    session = ClientSession()
    options = _make_options()
    bhc = await HomeComAlt.create(session, options, auth_provider=True)

    with patch.object(ClientSession, "request", new=AsyncMock()) as mock_request:
        mock_response = _mock_json_response({"key": "value"})
        mock_request.return_value = mock_response

        resp = await bhc._async_http_request("get", "http://test.com", req_type=2)
        assert resp.status == HTTPStatus.OK

    await session.close()


@pytest.mark.asyncio
async def test_async_http_request_unauthorized() -> None:
    """Test that _async_http_request raises AuthFailedError on 401 response."""
    session = ClientSession()
    options = _make_options()
    bhc = await HomeComAlt.create(session, options, auth_provider=True)

    with patch.object(ClientSession, "request", new=AsyncMock()) as mock_request:
        mock_request.side_effect = ClientResponseError(
            None, (), status=HTTPStatus.UNAUTHORIZED
        )
        with pytest.raises(AuthFailedError):
            await bhc._async_http_request("get", "http://test.com")

    await session.close()


@pytest.mark.asyncio
async def test_async_http_request_bad_request() -> None:
    """Test that _async_http_request returns None for a BAD_REQUEST on the token URL."""
    session = ClientSession()
    options = _make_options()
    bhc = await HomeComAlt.create(session, options, auth_provider=True)

    with patch.object(ClientSession, "request", new=AsyncMock()) as mock_request:
        mock_request.side_effect = ClientResponseError(
            None, (), status=HTTPStatus.BAD_REQUEST
        )
        response = await bhc._async_http_request(
            "post", "https://singlekey-id.com/auth/connect/token"
        )
        assert response is None

    await session.close()


@pytest.mark.asyncio
async def test_async_http_request_not_found_returns_empty_dict() -> None:
    """Test that HTTP 404 returns empty dict (unsupported endpoint)."""
    session = ClientSession()
    options = _make_options()
    bhc = await HomeComAlt.create(session, options, auth_provider=True)

    with patch.object(ClientSession, "request", new=AsyncMock()) as mock_request:
        mock_request.side_effect = ClientResponseError(
            None, (), status=HTTPStatus.NOT_FOUND
        )
        response = await bhc._async_http_request("get", "http://test.com/missing")
        assert response == {}

    await session.close()


@pytest.mark.asyncio
async def test_async_http_request_forbidden_returns_empty_dict() -> None:
    """Test that HTTP 403 returns empty dict (unsupported endpoint)."""
    session = ClientSession()
    options = _make_options()
    bhc = await HomeComAlt.create(session, options, auth_provider=True)

    with patch.object(ClientSession, "request", new=AsyncMock()) as mock_request:
        mock_request.side_effect = ClientResponseError(
            None, (), status=HTTPStatus.FORBIDDEN
        )
        response = await bhc._async_http_request("get", "http://test.com")
        assert response == {}

    await session.close()


@pytest.mark.asyncio
async def test_async_http_request_other_client_error_raises_api_error() -> None:
    """Test that non-special HTTP errors raise ApiError."""
    session = ClientSession()
    options = _make_options()
    bhc = await HomeComAlt.create(session, options, auth_provider=True)

    with patch.object(ClientSession, "request", new=AsyncMock()) as mock_request:
        mock_request.side_effect = ClientResponseError(
            None, (), status=HTTPStatus.INTERNAL_SERVER_ERROR
        )
        with pytest.raises(ApiError):
            await bhc._async_http_request("get", "http://test.com")

    await session.close()


@pytest.mark.asyncio
async def test_async_http_request_timeout() -> None:
    """Test that _async_http_request raises NotRespondingError on connection timeout."""
    session = ClientSession()
    options = _make_options()
    bhc = await HomeComAlt.create(session, options, auth_provider=True)

    with patch.object(ClientSession, "request", new=AsyncMock()) as mock_request:
        mock_request.side_effect = ClientConnectorError(None, OSError())
        with pytest.raises(NotRespondingError):
            await bhc._async_http_request("get", "http://test.com")

    await session.close()


@pytest.mark.asyncio
async def test_async_http_request_timeout_error() -> None:
    """Test that TimeoutError raises NotRespondingError."""
    session = ClientSession()
    options = _make_options()
    bhc = await HomeComAlt.create(session, options, auth_provider=True)

    with patch.object(ClientSession, "request", new=AsyncMock()) as mock_request:
        mock_request.side_effect = TimeoutError()
        with pytest.raises(NotRespondingError):
            await bhc._async_http_request("get", "http://test.com")

    await session.close()


@pytest.mark.asyncio
async def test_async_http_request_invalid_status() -> None:
    """Test that ApiError for HTTP errors other than 200/204."""
    session = ClientSession()
    options = _make_options()
    bhc = await HomeComAlt.create(session, options, auth_provider=True)

    with patch.object(ClientSession, "request", new=AsyncMock()) as mock_request:
        mock_response = AsyncMock()
        mock_response.status = HTTPStatus.INTERNAL_SERVER_ERROR
        mock_request.return_value = mock_response
        with pytest.raises(ApiError):
            await bhc._async_http_request("get", "http://test.com")

    await session.close()


@pytest.mark.asyncio
async def test_async_http_request_no_content_success() -> None:
    """Test 204 NO_CONTENT is accepted."""
    session = ClientSession()
    options = _make_options()
    bhc = await HomeComAlt.create(session, options, auth_provider=True)

    with patch.object(ClientSession, "request", new=AsyncMock()) as mock_request:
        mock_response = AsyncMock()
        mock_response.status = HTTPStatus.NO_CONTENT
        mock_request.return_value = mock_response
        resp = await bhc._async_http_request("put", "http://test.com")
        assert resp.status == HTTPStatus.NO_CONTENT

    await session.close()


# ===========================================================================
# _to_data
# ===========================================================================


@pytest.mark.asyncio
async def test_to_data_none_response() -> None:
    """None → None."""
    result = await HomeComAlt._to_data(None)
    assert result is None


@pytest.mark.asyncio
async def test_to_data_empty_response() -> None:
    """Falsy (empty dict / 0 / '') → None."""
    result = await HomeComAlt._to_data({})
    assert result is None


@pytest.mark.asyncio
async def test_to_data_valid_json() -> None:
    """Valid json response → parsed dict."""
    resp = _mock_json_response({"temp": 21.5})
    result = await HomeComAlt._to_data(resp)
    assert result == {"temp": 21.5}


@pytest.mark.asyncio
async def test_to_data_value_error() -> None:
    """ValueError from json() → None (graceful degradation)."""
    resp = AsyncMock()
    resp.json = AsyncMock(side_effect=ValueError("bad"))
    # resp is truthy
    resp.__bool__ = lambda self: True  # noqa: ARG005
    result = await HomeComAlt._to_data(resp)
    assert result is None


# ===========================================================================
# Property getters / setters
# ===========================================================================


@pytest.mark.asyncio
async def test_token_property() -> None:
    """Test token property getter and setter."""
    session = ClientSession()
    bhc = HomeComAlt(session, _make_options(), auth_provider=True)
    original = bhc.token
    assert original is not None
    bhc.token = "new_token"
    assert bhc.token == "new_token"
    await session.close()


@pytest.mark.asyncio
async def test_refresh_token_property() -> None:
    """Test refresh token property getter and setter."""
    session = ClientSession()
    bhc = HomeComAlt(session, _make_options(), auth_provider=True)
    assert bhc.refresh_token == "test_refresh"
    bhc.refresh_token = "new_refresh"
    assert bhc.refresh_token == "new_refresh"
    await session.close()


# ===========================================================================
# check_jwt
# ===========================================================================


@pytest.mark.asyncio
async def test_check_jwt_valid() -> None:
    """Test that check_jwt returns True for a valid, unexpired token."""
    session = ClientSession()
    options = _make_options()
    homecom_alt = await HomeComAlt.create(session, options, auth_provider=True)
    assert homecom_alt.check_jwt() is True
    await session.close()


@pytest.mark.asyncio
async def test_check_jwt_invalid() -> None:
    """Test that check_jwt returns False for an expired token."""
    session = ClientSession()
    options = _make_options()
    homecom_alt = await HomeComAlt.create(session, options, auth_provider=True)
    homecom_alt._options.token = create_test_jwt(
        expiration=int((datetime.now(UTC) - timedelta(days=1)).timestamp())
    )
    assert homecom_alt.check_jwt() is False
    await session.close()


@pytest.mark.asyncio
async def test_check_jwt_no_token() -> None:
    """No token → False."""
    session = ClientSession()
    bhc = HomeComAlt(session, ConnectionOptions(), auth_provider=True)
    assert bhc.check_jwt() is False
    await session.close()


@pytest.mark.asyncio
async def test_check_jwt_decode_error() -> None:
    """Corrupted token → False."""
    session = ClientSession()
    bhc = HomeComAlt(session, ConnectionOptions(token="not.a.jwt"), auth_provider=True)
    assert bhc.check_jwt() is False
    await session.close()


# ===========================================================================
# get_token
# ===========================================================================


@pytest.mark.asyncio
async def test_get_token_valid_jwt() -> None:
    """Test that get_token returns None if the JWT is still valid."""
    session = ClientSession()
    options = _make_options()
    homecom_alt = await HomeComAlt.create(session, options, auth_provider=True)

    with patch.object(homecom_alt, "check_jwt", return_value=True):
        assert await homecom_alt.get_token() is None

    await session.close()


@pytest.mark.asyncio
async def test_get_token_auth_provider_false() -> None:
    """auth_provider=False → returns None immediately."""
    session = ClientSession()
    bhc = HomeComAlt(session, _make_options(), auth_provider=False)
    result = await bhc.get_token()
    assert result is None
    await session.close()


@pytest.mark.asyncio
async def test_get_token_refresh_success() -> None:
    """Refresh token path succeeds."""
    session = ClientSession()
    options = _make_options()
    bhc = HomeComAlt(session, options, auth_provider=True)

    mock_resp = _mock_json_response(
        {
            "access_token": "new_access",
            "refresh_token": "new_refresh",
        }
    )

    with (
        patch.object(bhc, "check_jwt", return_value=False),
        patch.object(bhc, "_async_http_request", new=AsyncMock(return_value=mock_resp)),
    ):
        result = await bhc.get_token()
        assert result is True
        assert bhc.token == "new_access"
        assert bhc.refresh_token == "new_refresh"

    await session.close()


@pytest.mark.asyncio
async def test_get_token_refresh_returns_none_falls_through_to_code() -> None:
    """Refresh returns None, then code exchange succeeds."""
    session = ClientSession()
    options = _make_options()
    bhc = HomeComAlt(session, options, auth_provider=True)

    validate_result = {"access_token": "code_access", "refresh_token": "code_refresh"}

    with (
        patch.object(bhc, "check_jwt", return_value=False),
        patch.object(bhc, "_async_http_request", new=AsyncMock(return_value=None)),
        patch.object(bhc, "validate_auth", new=AsyncMock(return_value=validate_result)),
    ):
        result = await bhc.get_token()
        assert result is True
        assert bhc.token == "code_access"
        assert bhc.refresh_token == "code_refresh"
        assert bhc._options.code is None

    await session.close()


@pytest.mark.asyncio
async def test_get_token_all_fail_raises_auth_failed() -> None:
    """Both refresh and code exchange fail → AuthFailedError."""
    session = ClientSession()
    options = _make_options(refresh_token=None, code=None)
    bhc = HomeComAlt(session, options, auth_provider=True)

    with (
        patch.object(bhc, "check_jwt", return_value=False),
        patch.object(bhc, "_async_http_request", new=AsyncMock(return_value=None)),
        pytest.raises(AuthFailedError),
    ):
        await bhc.get_token()

    await session.close()


# ===========================================================================
# validate_auth
# ===========================================================================


@pytest.mark.asyncio
async def test_validate_auth_success() -> None:
    """Test that validate_auth exchanges a code for access and refresh tokens."""
    session = ClientSession()
    homecom = HomeComAlt(
        session, ConnectionOptions(code="test_code"), auth_provider=True
    )

    with patch.object(homecom, "_async_http_request", new=AsyncMock()) as mock_request:
        mock_response = AsyncMock()
        mock_response.json = AsyncMock(
            return_value={
                "access_token": "test_token",
                "refresh_token": "refresh_token",
            }
        )
        mock_request.return_value = mock_response

        token = await homecom.validate_auth("auth_code", "code_verifier")
        assert token == {"access_token": "test_token", "refresh_token": "refresh_token"}

        mock_request.assert_called_once_with(
            "post",
            OAUTH_DOMAIN + OAUTH_ENDPOINT,
            "code=auth_code&"
            + urlencode(OAUTH_PARAMS)
            + "&code_verifier=code_verifier",
            2,
        )

    await session.close()


@pytest.mark.asyncio
async def test_validate_auth_invalid_response() -> None:
    """Test AuthFailedError if the response is invalid JSON."""
    session = ClientSession()
    homecom = HomeComAlt(
        session, ConnectionOptions(code="test_code"), auth_provider=True
    )

    with patch.object(homecom, "_async_http_request", new=AsyncMock()) as mock_request:
        mock_response = AsyncMock()
        mock_response.json = AsyncMock(side_effect=ValueError("Invalid JSON"))
        mock_request.return_value = mock_response

        with pytest.raises(AuthFailedError, match="Authorization has failed"):
            await homecom.validate_auth("auth_code", "code_verifier")

    await session.close()


# ===========================================================================
# async_get_devices
# ===========================================================================


@pytest.mark.asyncio
async def test_async_get_devices_success() -> None:
    """Test async_get_devices returns device list."""
    session = ClientSession()
    bhc = HomeComAlt(session, _make_options(), auth_provider=False)

    mock_resp = MagicMock()
    mock_resp.json = MagicMock(return_value=[{"id": "gw1"}])

    with patch.object(
        bhc, "_async_http_request", new=AsyncMock(return_value=mock_resp)
    ):
        result = await bhc.async_get_devices()
        assert result == [{"id": "gw1"}]

    await session.close()


# ===========================================================================
# Simple base-class getters
# ===========================================================================


@pytest.mark.asyncio
async def test_async_get_firmware() -> None:
    """Test async_get_firmware returns firmware data."""
    session = ClientSession()
    bhc = HomeComAlt(session, _make_options(), auth_provider=False)
    mock_resp = _mock_json_response({"version": "1.2.3"})

    with patch.object(
        bhc, "_async_http_request", new=AsyncMock(return_value=mock_resp)
    ):
        result = await bhc.async_get_firmware(DEVICE_ID)
        assert result == {"version": "1.2.3"}

    await session.close()


@pytest.mark.asyncio
async def test_async_get_notifications() -> None:
    """Test async_get_notifications returns notification data."""
    session = ClientSession()
    bhc = HomeComAlt(session, _make_options(), auth_provider=False)
    mock_resp = _mock_json_response({"values": [{"msg": "alert"}]})

    with patch.object(
        bhc, "_async_http_request", new=AsyncMock(return_value=mock_resp)
    ):
        result = await bhc.async_get_notifications(DEVICE_ID)
        assert result == {"values": [{"msg": "alert"}]}

    await session.close()


@pytest.mark.asyncio
async def test_async_get_system_info() -> None:
    """Test async_get_system_info returns system info."""
    session = ClientSession()
    bhc = HomeComAlt(session, _make_options(), auth_provider=False)
    mock_resp = _mock_json_response({"brand": "Bosch"})

    with patch.object(
        bhc, "_async_http_request", new=AsyncMock(return_value=mock_resp)
    ):
        result = await bhc.async_get_system_info(DEVICE_ID)
        assert result == {"brand": "Bosch"}

    await session.close()


@pytest.mark.asyncio
async def test_async_get_pv_list() -> None:
    """Test async_get_pv_list returns point value list."""
    session = ClientSession()
    bhc = HomeComAlt(session, _make_options(), auth_provider=False)
    mock_resp = _mock_json_response({"references": []})

    with patch.object(
        bhc, "_async_http_request", new=AsyncMock(return_value=mock_resp)
    ):
        result = await bhc.async_get_pv_list(DEVICE_ID)
        assert result == {"references": []}

    await session.close()


# ===========================================================================
# async_get_time (fallback logic)
# ===========================================================================


@pytest.mark.asyncio
async def test_async_get_time_first_endpoint_success() -> None:
    """First time endpoint works → return data."""
    session = ClientSession()
    bhc = HomeComAlt(session, _make_options(), auth_provider=False)
    mock_resp = _mock_json_response({"value": "2024-01-01T00:00:00"})

    with patch.object(
        bhc, "_async_http_request", new=AsyncMock(return_value=mock_resp)
    ):
        result = await bhc.async_get_time(DEVICE_ID)
        assert result == {"value": "2024-01-01T00:00:00"}

    await session.close()


@pytest.mark.asyncio
async def test_async_get_time_first_fails_second_succeeds() -> None:
    """First endpoint returns empty dict (404), second succeeds."""
    session = ClientSession()
    bhc = HomeComAlt(session, _make_options(), auth_provider=False)
    mock_resp = _mock_json_response({"value": "2024-06-01T12:00:00"})

    call_count = 0

    async def side_effect(*args, **kwargs):  # noqa: ANN002, ANN003, ANN202, ARG001
        nonlocal call_count
        call_count += 1
        if call_count == 1:
            return {}  # 404 mapped to empty dict
        return mock_resp

    with patch.object(
        bhc, "_async_http_request", new=AsyncMock(side_effect=side_effect)
    ):
        result = await bhc.async_get_time(DEVICE_ID)
        assert result == {"value": "2024-06-01T12:00:00"}

    await session.close()


@pytest.mark.asyncio
async def test_async_get_time_both_fail() -> None:
    """Both endpoints fail → raises last exception."""
    session = ClientSession()
    bhc = HomeComAlt(session, _make_options(), auth_provider=False)

    async def side_effect(*args, **kwargs):  # noqa: ANN002, ANN003, ANN202, ARG001
        return {}

    with (
        patch.object(
            bhc, "_async_http_request", new=AsyncMock(side_effect=side_effect)
        ),
        pytest.raises(ApiError),
    ):
        await bhc.async_get_time(DEVICE_ID)

    await session.close()


@pytest.mark.asyncio
async def test_async_get_time_auth_failed_reraises() -> None:
    """AuthFailedError is re-raised immediately, not caught."""
    session = ClientSession()
    bhc = HomeComAlt(session, _make_options(), auth_provider=False)

    with (
        patch.object(
            bhc,
            "_async_http_request",
            new=AsyncMock(side_effect=AuthFailedError("auth")),
        ),
        pytest.raises(AuthFailedError),
    ):
        await bhc.async_get_time(DEVICE_ID)

    await session.close()


# ===========================================================================
# async_action_universal_get
# ===========================================================================


@pytest.mark.asyncio
async def test_async_action_universal_get() -> None:
    """Test async_action_universal_get returns endpoint data."""
    session = ClientSession()
    bhc = HomeComAlt(session, _make_options(), auth_provider=False)
    mock_resp = _mock_json_response({"value": 42})

    with patch.object(
        bhc, "_async_http_request", new=AsyncMock(return_value=mock_resp)
    ):
        result = await bhc.async_action_universal_get(DEVICE_ID, "/some/path")
        assert result == {"value": 42}

    await session.close()


# ===========================================================================
# HomeComGeneric
# ===========================================================================


@pytest.mark.asyncio
async def test_generic_async_update() -> None:
    """Test HomeComGeneric async_update returns device data."""
    session = ClientSession()
    dev = HomeComGeneric(session, _make_options(), DEVICE_ID, auth_provider=False)

    result = await dev.async_update(DEVICE_ID)
    assert result.device == DEVICE_ID
    assert result.firmware == []
    assert result.notifications == []

    await session.close()


# ===========================================================================
# HomeComRac
# ===========================================================================


def _make_rac(session):  # noqa: ANN001, ANN202
    return HomeComRac(session, _make_options(), DEVICE_ID, auth_provider=False)


@pytest.mark.asyncio
async def test_rac_async_update() -> None:
    """Test RAC async_update returns populated device data."""
    session = ClientSession()
    rac = _make_rac(session)

    notifications_resp = _mock_json_response({"values": [{"code": 1}]})
    standard_resp = _mock_json_response({"references": [{"id": "s1"}]})
    advanced_resp = _mock_json_response({"references": [{"id": "a1"}]})
    switch_resp = _mock_json_response({"references": [{"id": "sw1"}]})

    async def route_request(method, url, *args, **kwargs):  # noqa: ANN001, ANN002, ANN003, ANN202, ARG001
        if "notifications" in url:
            return notifications_resp
        if "standardFunctions" in url:
            return standard_resp
        if "advancedFunctions" in url:
            return advanced_resp
        if "switchPrograms/list" in url:
            return switch_resp
        return _mock_json_response({})

    with patch.object(
        rac, "_async_http_request", new=AsyncMock(side_effect=route_request)
    ):
        result = await rac.async_update(DEVICE_ID)

    assert result.device == DEVICE_ID
    assert result.stardard_functions == [{"id": "s1"}]
    assert result.advanced_functions == [{"id": "a1"}]
    assert result.switch_programs == [{"id": "sw1"}]
    assert result.notifications == [{"code": 1}]

    await session.close()


@pytest.mark.asyncio
async def test_rac_async_control() -> None:
    """Test RAC async_control sends put request with value."""
    session = ClientSession()
    rac = _make_rac(session)

    with patch.object(rac, "_async_http_request", new=AsyncMock()) as mock_req:
        await rac.async_control(DEVICE_ID, "on")
        mock_req.assert_called_once()
        call_args = mock_req.call_args
        assert call_args[0][0] == "put"
        assert call_args[0][2] == {"value": "on"}
        assert call_args[0][3] == JSON

    await session.close()


@pytest.mark.asyncio
async def test_rac_async_turn_on() -> None:
    """Test RAC async_turn_on sends on control."""
    session = ClientSession()
    rac = _make_rac(session)

    with patch.object(rac, "async_control", new=AsyncMock()) as mock_ctrl:
        await rac.async_turn_on(DEVICE_ID)
        mock_ctrl.assert_called_once_with(DEVICE_ID, "on")

    await session.close()


@pytest.mark.asyncio
async def test_rac_async_turn_off() -> None:
    """Test RAC async_turn_off sends off control."""
    session = ClientSession()
    rac = _make_rac(session)

    with patch.object(rac, "async_control", new=AsyncMock()) as mock_ctrl:
        await rac.async_turn_off(DEVICE_ID)
        mock_ctrl.assert_called_once_with(DEVICE_ID, "off")

    await session.close()


@pytest.mark.asyncio
async def test_rac_set_temperature_rounds() -> None:
    """Test RAC set temperature rounds to nearest 0.5."""
    session = ClientSession()
    rac = _make_rac(session)

    with patch.object(rac, "_async_http_request", new=AsyncMock()) as mock_req:
        await rac.async_set_temperature(DEVICE_ID, 22.456)
        call_args = mock_req.call_args
        assert call_args[0][2] == {"value": 22.5}

    await session.close()


@pytest.mark.asyncio
async def test_rac_set_hvac_mode_off() -> None:
    """'off' mode sends 'off' control."""
    session = ClientSession()
    rac = _make_rac(session)

    with (
        patch.object(rac, "async_control", new=AsyncMock()) as mock_ctrl,
        patch.object(rac, "_async_http_request", new=AsyncMock()),
    ):
        await rac.async_set_hvac_mode(DEVICE_ID, "off")
        mock_ctrl.assert_called_once_with(DEVICE_ID, "off")

    await session.close()


@pytest.mark.asyncio
async def test_rac_set_hvac_mode_heating() -> None:
    """Non-off mode sends 'on' control then sets mode."""
    session = ClientSession()
    rac = _make_rac(session)

    with (
        patch.object(rac, "async_control", new=AsyncMock()) as mock_ctrl,
        patch.object(rac, "_async_http_request", new=AsyncMock()) as mock_req,
    ):
        await rac.async_set_hvac_mode(DEVICE_ID, "heating")
        mock_ctrl.assert_called_once_with(DEVICE_ID, "on")
        assert mock_req.call_args[0][2] == {"value": "heating"}

    await session.close()


@pytest.mark.asyncio
async def test_rac_set_plasmacluster_true() -> None:
    """Test RAC set plasmacluster to on when True."""
    session = ClientSession()
    rac = _make_rac(session)

    with patch.object(rac, "_async_http_request", new=AsyncMock()) as mock_req:
        await rac.async_set_plasmacluster(DEVICE_ID, True)  # noqa: FBT003
        assert mock_req.call_args[0][2] == {"value": "on"}

    await session.close()


@pytest.mark.asyncio
async def test_rac_set_plasmacluster_false() -> None:
    """Test RAC set plasmacluster to off when False."""
    session = ClientSession()
    rac = _make_rac(session)

    with patch.object(rac, "_async_http_request", new=AsyncMock()) as mock_req:
        await rac.async_set_plasmacluster(DEVICE_ID, False)  # noqa: FBT003
        assert mock_req.call_args[0][2] == {"value": "off"}

    await session.close()


@pytest.mark.asyncio
async def test_rac_set_boost() -> None:
    """Test RAC set boost mode."""
    session = ClientSession()
    rac = _make_rac(session)

    with patch.object(rac, "_async_http_request", new=AsyncMock()) as mock_req:
        await rac.async_set_boost(DEVICE_ID, True)  # noqa: FBT003
        assert mock_req.call_args[0][2] == {"value": "on"}

    await session.close()


@pytest.mark.asyncio
async def test_rac_set_eco() -> None:
    """Test RAC set eco mode."""
    session = ClientSession()
    rac = _make_rac(session)

    with patch.object(rac, "_async_http_request", new=AsyncMock()) as mock_req:
        await rac.async_set_eco(DEVICE_ID, False)  # noqa: FBT003
        assert mock_req.call_args[0][2] == {"value": "off"}

    await session.close()


@pytest.mark.asyncio
async def test_rac_set_fan_mode() -> None:
    """Test RAC set fan mode."""
    session = ClientSession()
    rac = _make_rac(session)

    with patch.object(rac, "_async_http_request", new=AsyncMock()) as mock_req:
        await rac.async_set_fan_mode(DEVICE_ID, "high")
        assert mock_req.call_args[0][2] == {"value": "high"}

    await session.close()


@pytest.mark.asyncio
async def test_rac_set_vertical_swing() -> None:
    """Test RAC set vertical swing mode."""
    session = ClientSession()
    rac = _make_rac(session)

    with patch.object(rac, "_async_http_request", new=AsyncMock()) as mock_req:
        await rac.async_set_vertical_swing_mode(DEVICE_ID, "swing")
        assert mock_req.call_args[0][2] == {"value": "swing"}

    await session.close()


@pytest.mark.asyncio
async def test_rac_set_horizontal_swing() -> None:
    """Test RAC set horizontal swing mode."""
    session = ClientSession()
    rac = _make_rac(session)

    with patch.object(rac, "_async_http_request", new=AsyncMock()) as mock_req:
        await rac.async_set_horizontal_swing_mode(DEVICE_ID, "auto")
        assert mock_req.call_args[0][2] == {"value": "auto"}

    await session.close()


@pytest.mark.asyncio
async def test_rac_switch_program() -> None:
    """Test RAC switch program."""
    session = ClientSession()
    rac = _make_rac(session)

    with patch.object(rac, "_async_http_request", new=AsyncMock()) as mock_req:
        await rac.async_switch_program(DEVICE_ID, "prog1")
        assert mock_req.call_args[0][2] == {"value": "prog1"}

    await session.close()


@pytest.mark.asyncio
async def test_rac_control_program() -> None:
    """Test RAC control program."""
    session = ClientSession()
    rac = _make_rac(session)

    with patch.object(rac, "_async_http_request", new=AsyncMock()) as mock_req:
        await rac.async_control_program(DEVICE_ID, "on")
        assert mock_req.call_args[0][2] == {"value": "on"}

    await session.close()


@pytest.mark.asyncio
async def test_rac_time_on() -> None:
    """Test RAC time on setting."""
    session = ClientSession()
    rac = _make_rac(session)

    with patch.object(rac, "_async_http_request", new=AsyncMock()) as mock_req:
        await rac.async_time_on(DEVICE_ID, 30)
        assert mock_req.call_args[0][2] == {"value": 30}
        assert "/on" in mock_req.call_args[0][1]

    await session.close()


@pytest.mark.asyncio
async def test_rac_time_off() -> None:
    """Test RAC time off setting."""
    session = ClientSession()
    rac = _make_rac(session)

    with patch.object(rac, "_async_http_request", new=AsyncMock()) as mock_req:
        await rac.async_time_off(DEVICE_ID, 60)
        assert mock_req.call_args[0][2] == {"value": 60}
        assert "/off" in mock_req.call_args[0][1]

    await session.close()


# ===========================================================================
# HomeComK40
# ===========================================================================


def _make_k40(session):  # noqa: ANN001, ANN202
    return HomeComK40(session, _make_options(), DEVICE_ID, auth_provider=False)


@pytest.mark.asyncio
async def test_k40_async_update_with_dhw_and_hc() -> None:  # noqa: C901, PLR0915
    """Test full K40 update with DHW and HC circuits."""
    session = ClientSession()
    k40 = _make_k40(session)

    async def route(method, url, *args, **kwargs):  # noqa: ANN001, ANN002, ANN003, ANN202, ARG001, C901, PLR0911, PLR0912, PLR0915
        if "notifications" in url:
            return _mock_json_response({"values": []})
        if "dhwCircuits" in url and "operationMode" in url:
            return _mock_json_response({"value": "auto"})
        if "dhwCircuits" in url and "actualTemp" in url:
            return _mock_json_response({"value": 45.0})
        if "dhwCircuits" in url and "chargeRemainingTime" in url:
            return _mock_json_response({"value": 10})
        if "dhwCircuits" in url and "singleChargeSetpoint" in url:
            return _mock_json_response({"value": 55})
        if "dhwCircuits" in url and "currentTemperatureLevel" in url:
            return _mock_json_response(
                {"value": "high", "allowedValues": ["off", "high", "low"]}
            )
        if "dhwCircuits" in url and "temperatureLevels" in url:
            level = url.split("/")[-1]
            return _mock_json_response({"value": 60 if level == "high" else 40})
        if (
            "dhwCircuits" in url
            and "charge" in url
            and "Duration" not in url
            and "Setpoint" not in url
            and "Remaining" not in url
        ):
            return _mock_json_response({"value": "off"})
        if url.endswith("/resource/dhwCircuits"):
            return _mock_json_response(
                {
                    "references": [{"id": "/dhwCircuits/dhw1"}],
                }
            )
        if "heatingCircuits" in url and "operationMode" in url:
            return _mock_json_response({"value": "auto"})
        if "heatingCircuits" in url and "currentSuWiMode" in url:
            return _mock_json_response({"value": "summer"})
        if "heatingCircuits" in url and "heatCoolMode" in url:
            return _mock_json_response({"value": "heat"})
        if "heatingCircuits" in url and "roomtemperature" in url:
            return _mock_json_response({"value": 21.5})
        if "heatingCircuits" in url and "actualHumidity" in url:
            return _mock_json_response({"value": 55})
        if "heatingCircuits" in url and "manualRoomSetpoint" in url:
            return _mock_json_response({"value": 20})
        if "heatingCircuits" in url and "currentRoomSetpoint" in url:
            return _mock_json_response({"value": 21})
        if "heatingCircuits" in url and "cooling/roomTempSetpoint" in url:
            return _mock_json_response({"value": 25})
        if "heatingCircuits" in url and "maxSupply" in url:
            return _mock_json_response({"value": 90})
        if "heatingCircuits" in url and "minSupply" in url:
            return _mock_json_response({"value": 20})
        if "heatingCircuits" in url and "heatCurveMax" in url:
            return _mock_json_response({"value": 75})
        if "heatingCircuits" in url and "heatCurveMin" in url:
            return _mock_json_response({"value": 20})
        if "heatingCircuits" in url and "supplyTemperatureSetpoint" in url:
            return _mock_json_response({"value": 39})
        if "heatingCircuits" in url and "nightSwitchMode" in url:
            return _mock_json_response({"value": "off"})
        if "heatingCircuits" in url and "control" in url:
            return _mock_json_response({"value": "weather"})
        if "heatingCircuits" in url and "nightThreshold" in url:
            return _mock_json_response({"value": 21})
        if "heatingCircuits" in url and "roomInfluence" in url:
            return _mock_json_response({"value": 3})
        if url.endswith("/resource/heatingCircuits"):
            return _mock_json_response(
                {
                    "references": [{"id": "/heatingCircuits/hc1"}],
                }
            )
        if "heatPumpType" in url:
            return _mock_json_response({"value": "airToWater"})
        if "numberOfStarts" in url:
            return _mock_json_response({"value": 100})
        if "returnTemperature" in url:
            return _mock_json_response({"value": 30})
        if "actualSupplyTemperature" in url:
            return _mock_json_response({"value": 35})
        if "actualModulation" in url:
            return _mock_json_response({"value": 50})
        if "collectorInflowTemp" in url:
            return _mock_json_response({"value": 10})
        if "collectorOutflowTemp" in url:
            return _mock_json_response({"value": 8})
        if "actualHeatDemand" in url:
            return _mock_json_response({"value": 80})
        if "workingTime" in url:
            return _mock_json_response({"value": 5000})
        if "totalConsumption" in url:
            return _mock_json_response({"value": 1234})
        if "systemPressure" in url:
            return _mock_json_response({"value": 1.5})
        if "flameIndication" in url:
            return _mock_json_response({"value": "ch"})
        if "holidayMode" in url:
            return _mock_json_response({"value": "off"})
        if "awayMode" in url:
            return _mock_json_response({"value": "off"})
        if "powerLimitation" in url:
            return _mock_json_response({"value": "off"})
        if "outdoor_t1" in url:
            return _mock_json_response({"value": 5.0})
        if "indoor_h1" in url:
            return _mock_json_response({"value": 43.5})
        if "ventilation" in url:
            return _mock_json_response({"references": []})
        if url.endswith("/resource/zones"):
            return _mock_json_response({"references": []})
        if "energy/historyEntries" in url:
            return _mock_json_response({"value": 1})
        if "energy/historyHourly" in url:
            return _mock_json_response({"value": []})
        if "energy/history" in url:
            return _mock_json_response({"value": []})
        if url.endswith("/resource/devices"):
            return _mock_json_response({"references": []})
        if "bulk" in url:
            return _mock_json_response(
                [{"resourcePaths": [{"gatewayResponse": {"payload": {"value": 99}}}]}]
            )
        return _mock_json_response({})

    with patch.object(k40, "_async_http_request", new=AsyncMock(side_effect=route)):
        result = await k40.async_update(DEVICE_ID)

    assert result.device == DEVICE_ID
    assert isinstance(result.dhw_circuits, list)
    assert len(result.dhw_circuits) == 1
    # tempLevel should have 'high' and 'low' (not 'off')
    assert "high" in result.dhw_circuits[0]["tempLevel"]
    assert "low" in result.dhw_circuits[0]["tempLevel"]
    assert "off" not in result.dhw_circuits[0]["tempLevel"]
    assert isinstance(result.heating_circuits, list)
    assert len(result.heating_circuits) == 1
    hc = result.heating_circuits[0]
    assert hc["maxSupply"] == {"value": 90}
    assert hc["minSupply"] == {"value": 20}
    assert hc["heatCurveMax"] == {"value": 75}
    assert hc["heatCurveMin"] == {"value": 20}
    assert hc["supplyTemperatureSetpoint"] == {"value": 39}
    assert hc["nightSwitchMode"] == {"value": "off"}
    assert hc["control"] == {"value": "weather"}
    assert hc["nightThreshold"] == {"value": 21}
    assert hc["roomInfluence"] == {"value": 3}
    assert result.flame_indication == {"value": "ch"}
    assert result.indoor_humidity == {"value": 43.5}
    assert result.energy_history == {"value": []}
    assert result.zones == {}
    assert result.devices == {}

    await session.close()


@pytest.mark.asyncio
async def test_k40_async_update_empty_references() -> None:
    """K40 update when DHW, HC, ventilation, zones, and devices have no references."""
    session = ClientSession()
    k40 = _make_k40(session)

    async def route(method, url, *args, **kwargs):  # noqa: ANN001, ANN002, ANN003, ANN202, ARG001, PLR0911
        if "notifications" in url:
            return _mock_json_response({"values": []})
        if url.endswith("/resource/dhwCircuits"):
            return _mock_json_response({"references": []})
        if url.endswith("/resource/heatingCircuits"):
            return _mock_json_response({"references": []})
        if "ventilation" in url:
            return _mock_json_response({"references": []})
        if url.endswith("/resource/zones"):
            return _mock_json_response({"references": []})
        if url.endswith("/resource/devices"):
            return _mock_json_response({"references": []})
        if "bulk" in url:
            return _mock_json_response(
                [{"resourcePaths": [{"gatewayResponse": {"payload": {"value": 0}}}]}]
            )
        return _mock_json_response({})

    with patch.object(k40, "_async_http_request", new=AsyncMock(side_effect=route)):
        result = await k40.async_update(DEVICE_ID)

    assert result.dhw_circuits == {}
    assert result.heating_circuits == {}
    assert result.ventilation == {}
    assert result.zones == {}
    assert result.devices == {}

    await session.close()


@pytest.mark.asyncio
async def test_k40_async_update_with_ventilation() -> None:
    """K40 update with ventilation zones populated."""
    session = ClientSession()
    k40 = _make_k40(session)

    async def route(method, url, *args, **kwargs):  # noqa: ANN001, ANN002, ANN003, ANN202, ARG001, PLR0911, PLR0912
        if "notifications" in url:
            return _mock_json_response({"values": []})
        if url.endswith("/resource/dhwCircuits"):
            return _mock_json_response({"references": []})
        if url.endswith("/resource/heatingCircuits"):
            return _mock_json_response({"references": []})
        if url.endswith("/resource/ventilation"):
            return _mock_json_response(
                {
                    "references": [{"id": "/ventilation/zone1"}],
                }
            )
        if "ventilation" in url and "exhaustFanLevel" in url:
            return _mock_json_response({"value": 3})
        if "ventilation" in url and "maxIndoorAirQuality" in url:
            return _mock_json_response({"value": 80})
        if "ventilation" in url and "maxRelativeHumidity" in url:
            return _mock_json_response({"value": 60})
        if "ventilation" in url and "operationMode" in url:
            return _mock_json_response({"value": "auto"})
        if "ventilation" in url and "exhaustTemp" in url:
            return _mock_json_response({"value": 22})
        if "ventilation" in url and "extractTemp" in url:
            return _mock_json_response({"value": 23})
        if "ventilation" in url and "internalAirQuality" in url:
            return _mock_json_response({"value": 90})
        if "ventilation" in url and "internalHumidity" in url:
            return _mock_json_response({"value": 50})
        if "ventilation" in url and "outdoorTemp" in url:
            return _mock_json_response({"value": 5})
        if "ventilation" in url and "supplyTemp" in url:
            return _mock_json_response({"value": 20})
        if "ventilation" in url and "summerBypass/enable" in url:
            return _mock_json_response({"value": "off"})
        if "ventilation" in url and "summerBypass/duration" in url:
            return _mock_json_response({"value": 120})
        if "ventilation" in url and "demand/indoorAirQuality" in url:
            return _mock_json_response({"value": 70})
        if "ventilation" in url and "demand/relativeHumidity" in url:
            return _mock_json_response({"value": 55})
        if url.endswith("/resource/zones"):
            return _mock_json_response({"references": []})
        if url.endswith("/resource/devices"):
            return _mock_json_response({"references": []})
        if "bulk" in url:
            return _mock_json_response(
                [{"resourcePaths": [{"gatewayResponse": {"payload": {"value": 0}}}]}]
            )
        return _mock_json_response({})

    with patch.object(k40, "_async_http_request", new=AsyncMock(side_effect=route)):
        result = await k40.async_update(DEVICE_ID)

    assert isinstance(result.ventilation, list)
    assert len(result.ventilation) == 1
    assert result.ventilation[0]["exhaustFanLevel"] == {"value": 3}

    await session.close()


# ---------------------------------------------------------------------------
# K40 individual methods
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_k40_dhw_operation_mode_get_put() -> None:
    """Test K40 DHW operation mode get and put."""
    session = ClientSession()
    k40 = _make_k40(session)

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": "auto"})),
    ) as mock_req:
        result = await k40.async_get_dhw_operation_mode(DEVICE_ID, "dhw1")
        assert result == {"value": "auto"}

    with patch.object(k40, "_async_http_request", new=AsyncMock()) as mock_req:
        await k40.async_put_dhw_operation_mode(DEVICE_ID, "dhw1", "manual")
        assert mock_req.call_args[0][2] == {"value": "manual"}

    await session.close()


@pytest.mark.asyncio
async def test_k40_hc_operation_mode_get_put() -> None:
    """Test K40 HC operation mode get and put."""
    session = ClientSession()
    k40 = _make_k40(session)

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": "auto"})),
    ) as mock_req:
        result = await k40.async_get_hc_operation_mode(DEVICE_ID, "hc1")
        assert result == {"value": "auto"}

    with patch.object(k40, "_async_http_request", new=AsyncMock()) as mock_req:
        await k40.async_put_hc_operation_mode(DEVICE_ID, "hc1", "manual")
        assert mock_req.call_args[0][2] == {"value": "manual"}

    await session.close()


@pytest.mark.asyncio
async def test_k40_hc_suwi_mode() -> None:
    """Test K40 HC summer/winter mode get and put."""
    session = ClientSession()
    k40 = _make_k40(session)

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": "summer"})),
    ) as mock_req:
        result = await k40.async_get_hc_suwi_mode(DEVICE_ID, "hc1")
        assert result == {"value": "summer"}

    with patch.object(k40, "_async_http_request", new=AsyncMock()) as mock_req:
        await k40.async_put_hc_suwi_mode(DEVICE_ID, "hc1", "winter")
        assert mock_req.call_args[0][2] == {"value": "winter"}

    await session.close()


@pytest.mark.asyncio
async def test_k40_hc_heatcool_mode() -> None:
    """Test K40 HC heat/cool mode get and put."""
    session = ClientSession()
    k40 = _make_k40(session)

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": "heat"})),
    ) as mock_req:
        result = await k40.async_get_hc_heatcool_mode(DEVICE_ID, "hc1")
        assert result == {"value": "heat"}

    with patch.object(k40, "_async_http_request", new=AsyncMock()) as mock_req:
        await k40.async_put_hc_heatcool_mode(DEVICE_ID, "hc1", "cool")
        assert mock_req.call_args[0][2] == {"value": "cool"}

    await session.close()


@pytest.mark.asyncio
async def test_k40_away_holiday_mode() -> None:
    """Test K40 away and holiday mode get and put."""
    session = ClientSession()
    k40 = _make_k40(session)

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": "on"})),
    ):
        result = await k40.async_get_away_mode(DEVICE_ID)
        assert result == {"value": "on"}

    with patch.object(k40, "_async_http_request", new=AsyncMock()) as mock_req:
        await k40.async_put_away_mode(DEVICE_ID, "off")
        assert mock_req.call_args[0][2] == {"value": "off"}

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": "off"})),
    ):
        result = await k40.async_get_holiday_mode(DEVICE_ID)
        assert result == {"value": "off"}

    with patch.object(k40, "_async_http_request", new=AsyncMock()) as mock_req:
        await k40.async_put_holiday_mode(DEVICE_ID, "on")
        assert mock_req.call_args[0][2] == {"value": "on"}

    await session.close()


@pytest.mark.asyncio
async def test_k40_heat_source_getters() -> None:
    """Test K40 heat source getter methods."""
    session = ClientSession()
    k40 = _make_k40(session)

    for method_name in (
        "async_get_hs_type",
        "async_get_hs_pump_type",
        "async_get_hs_starts",
        "async_get_hs_return_temp",
        "async_get_hs_supply_temp",
        "async_get_hs_modulation",
        "async_get_hs_brine_inflow_temp",
        "async_get_hs_brine_outflow_temp",
        "async_get_hs_heat_demand",
        "async_get_hs_working_time",
        "async_get_hs_total_consumption",
        "async_get_hs_system_pressure",
        "async_get_power_limitation",
        "async_get_outdoor_temp",
    ):
        with patch.object(
            k40,
            "_async_http_request",
            new=AsyncMock(return_value=_mock_json_response({"value": 42})),
        ):
            result = await getattr(k40, method_name)(DEVICE_ID)
            assert result == {"value": 42}, f"{method_name} failed"

    await session.close()


@pytest.mark.asyncio
async def test_k40_hc_room_temp_and_setpoints() -> None:
    """Test K40 HC room temperature and setpoint methods."""
    session = ClientSession()
    k40 = _make_k40(session)

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": 21.5})),
    ):
        assert await k40.async_get_hc_room_temp(DEVICE_ID, "hc1") == {"value": 21.5}

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": 55})),
    ):
        assert await k40.async_get_hc_actual_humidity(DEVICE_ID, "hc1") == {"value": 55}

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": 20})),
    ):
        assert await k40.async_get_hc_manual_room_setpoint(DEVICE_ID, "hc1") == {
            "value": 20
        }

    with patch.object(k40, "_async_http_request", new=AsyncMock()) as mock_req:
        await k40.async_set_hc_manual_room_setpoint(DEVICE_ID, "hc1", "22")
        assert mock_req.call_args[0][2] == {"value": "22"}

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": 21})),
    ):
        assert await k40.async_get_hc_current_room_setpoint(DEVICE_ID, "hc1") == {
            "value": 21
        }

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": 25})),
    ):
        assert await k40.async_get_hc_cooling_room_temp_setpoint(DEVICE_ID, "hc1") == {
            "value": 25
        }

    with patch.object(k40, "_async_http_request", new=AsyncMock()) as mock_req:
        await k40.async_set_hc_cooling_room_temp_setpoint(DEVICE_ID, "hc1", "24")
        assert mock_req.call_args[0][2] == {"value": "24"}

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": "underfloor"})),
    ):
        assert await k40.async_get_hc_heating_type(DEVICE_ID, "hc1") == {
            "value": "underfloor"
        }

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": "room"})),
    ):
        assert await k40.async_get_hc_control_type(DEVICE_ID, "hc1") == {
            "value": "room"
        }

    await session.close()


@pytest.mark.asyncio
async def test_k40_dhw_charge_methods() -> None:
    """Test K40 DHW charge-related methods."""
    session = ClientSession()
    k40 = _make_k40(session)

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": "off"})),
    ):
        assert await k40.async_get_dhw_charge(DEVICE_ID, "dhw1") == {"value": "off"}

    with patch.object(k40, "_async_http_request", new=AsyncMock()) as mock_req:
        await k40.async_set_dhw_charge(DEVICE_ID, "dhw1", "on")
        assert mock_req.call_args[0][2] == {"value": "on"}

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": 10})),
    ):
        assert await k40.async_get_dhw_charge_remaining_time(DEVICE_ID, "dhw1") == {
            "value": 10
        }

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": 30})),
    ):
        assert await k40.async_get_dhw_charge_duration(DEVICE_ID, "dhw1") == {
            "value": 30
        }

    with patch.object(k40, "_async_http_request", new=AsyncMock()) as mock_req:
        await k40.async_set_dhw_charge_duration(DEVICE_ID, "dhw1", "45")
        assert mock_req.call_args[0][2] == {"value": "45"}

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": 55})),
    ):
        assert await k40.async_get_dhw_charge_setpoint(DEVICE_ID, "dhw1") == {
            "value": 55
        }

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": 45})),
    ):
        assert await k40.async_get_dhw_actual_temp(DEVICE_ID, "dhw1") == {"value": 45}

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": "high"})),
    ):
        assert await k40.async_get_dhw_current_temp_level(DEVICE_ID, "dhw1") == {
            "value": "high"
        }

    with patch.object(k40, "_async_http_request", new=AsyncMock()) as mock_req:
        await k40.async_put_dhw_current_temp_level(DEVICE_ID, "dhw1", "low")
        assert mock_req.call_args[0][2] == {"value": "low"}

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": 60})),
    ):
        assert await k40.async_get_dhw_temp_level(DEVICE_ID, "dhw1", "high") == {
            "value": 60
        }

    with patch.object(k40, "_async_http_request", new=AsyncMock()) as mock_req:
        await k40.async_set_dhw_temp_level(DEVICE_ID, "dhw1", "high", "65")
        assert mock_req.call_args[0][2] == {"value": "65"}

    await session.close()


@pytest.mark.asyncio
async def test_k40_get_consumption_payload_present() -> None:
    """Test consumption when payload key is present."""
    session = ClientSession()
    k40 = _make_k40(session)
    bulk_resp = _mock_json_response(
        [
            {
                "resourcePaths": [
                    {
                        "gatewayResponse": {
                            "payload": {"value": 123},
                        },
                    }
                ],
            }
        ]
    )

    with patch.object(
        k40, "_async_http_request", new=AsyncMock(return_value=bulk_resp)
    ):
        result = await k40.async_get_consumption(DEVICE_ID, "dhw", "2024-01-01")
        assert result == {"value": 123}

    await session.close()


@pytest.mark.asyncio
async def test_k40_get_consumption_no_payload() -> None:
    """Test consumption when payload key is absent (falls to except)."""
    session = ClientSession()
    k40 = _make_k40(session)
    bulk_resp = _mock_json_response(
        [
            {
                "resourcePaths": [
                    {
                        "gatewayResponse": {
                            "errorCode": 404,
                        },
                    }
                ],
            }
        ]
    )

    with patch.object(
        k40, "_async_http_request", new=AsyncMock(return_value=bulk_resp)
    ):
        result = await k40.async_get_consumption(DEVICE_ID, "total", "2024-01")
        assert result == {"errorCode": 404}

    await session.close()


@pytest.mark.asyncio
async def test_k40_ventilation_setters() -> None:
    """Test ventilation setter methods."""
    session = ClientSession()
    k40 = _make_k40(session)

    with patch.object(k40, "_async_http_request", new=AsyncMock()) as mock_req:
        await k40.async_set_ventilation_mode(DEVICE_ID, "zone1", "manual")
        assert mock_req.call_args[0][2] == {"value": "manual"}

    with patch.object(k40, "_async_http_request", new=AsyncMock()) as mock_req:
        await k40.async_set_ventilation_summer_enable(DEVICE_ID, "zone1", "on")
        assert mock_req.call_args[0][2] == {"value": "on"}

    with patch.object(k40, "_async_http_request", new=AsyncMock()) as mock_req:
        await k40.async_set_ventilation_summer_duration(DEVICE_ID, "zone1", "180")
        assert mock_req.call_args[0][2] == {"value": "180"}

    with patch.object(k40, "_async_http_request", new=AsyncMock()) as mock_req:
        await k40.async_set_ventilation_demand_quality(DEVICE_ID, "zone1", "high")
        assert mock_req.call_args[0][2] == {"value": "high"}

    with patch.object(k40, "_async_http_request", new=AsyncMock()) as mock_req:
        await k40.async_set_ventilation_demand_humidity(DEVICE_ID, "zone1", "60")
        assert mock_req.call_args[0][2] == {"value": "60"}

    await session.close()


# ---------------------------------------------------------------------------
# K40 new API methods - zones, HC extensions, flame, energy, humidity, devices
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_k40_zone_methods() -> None:
    """Test K40 zone getter and setter methods."""
    session = ClientSession()
    k40 = _make_k40(session)

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(
            return_value=_mock_json_response({"references": [{"id": "/zones/zn1"}]})
        ),
    ):
        result = await k40.async_get_zones(DEVICE_ID)
        assert result == {"references": [{"id": "/zones/zn1"}]}

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": 23.5})),
    ):
        result = await k40.async_get_zone_temp_actual(DEVICE_ID, "zn1")
        assert result == {"value": 23.5}

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": 21})),
    ):
        result = await k40.async_get_zone_manual_temp_heating(DEVICE_ID, "zn1")
        assert result == {"value": 21}

    with patch.object(k40, "_async_http_request", new=AsyncMock()) as mock_req:
        await k40.async_set_zone_manual_temp_heating(DEVICE_ID, "zn1", 22.0)
        assert mock_req.call_args[0][2] == {"value": 22.0}

    await session.close()


@pytest.mark.asyncio
async def test_k40_hc_new_getters() -> None:
    """Test K40 new HC getter methods."""
    session = ClientSession()
    k40 = _make_k40(session)

    for method_name in (
        "async_get_hc_max_supply",
        "async_get_hc_min_supply",
        "async_get_hc_heat_curve_max",
        "async_get_hc_heat_curve_min",
        "async_get_hc_supply_temp_setpoint",
        "async_get_hc_night_switch_mode",
        "async_get_hc_control",
        "async_get_hc_night_threshold",
        "async_get_hc_room_influence",
    ):
        with patch.object(
            k40,
            "_async_http_request",
            new=AsyncMock(return_value=_mock_json_response({"value": 42})),
        ):
            result = await getattr(k40, method_name)(DEVICE_ID, "hc1")
            assert result == {"value": 42}, f"{method_name} failed"

    await session.close()


@pytest.mark.asyncio
async def test_k40_hc_new_setters() -> None:
    """Test K40 new HC setter methods."""
    session = ClientSession()
    k40 = _make_k40(session)

    setters = (
        ("async_set_hc_max_supply", "90"),
        ("async_set_hc_min_supply", "20"),
        ("async_set_hc_heat_curve_max", "75"),
        ("async_set_hc_heat_curve_min", "20"),
        ("async_set_hc_night_switch_mode", "on"),
        ("async_set_hc_control", "weather"),
        ("async_set_hc_night_threshold", "21"),
        ("async_set_hc_room_influence", "3"),
    )

    for method_name, val in setters:
        with patch.object(k40, "_async_http_request", new=AsyncMock()) as mock_req:
            await getattr(k40, method_name)(DEVICE_ID, "hc1", val)
            assert mock_req.call_args[0][2] == {"value": val}, f"{method_name} failed"

    await session.close()


@pytest.mark.asyncio
async def test_k40_hs_flame_indication() -> None:
    """Test K40 flame indication getter."""
    session = ClientSession()
    k40 = _make_k40(session)

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": "ch"})),
    ):
        result = await k40.async_get_hs_flame_indication(DEVICE_ID)
        assert result == {"value": "ch"}

    await session.close()


@pytest.mark.asyncio
async def test_k40_energy_history() -> None:
    """Test K40 energy history getter."""
    session = ClientSession()
    k40 = _make_k40(session)

    async def route(method, url, *args, **kwargs):  # noqa: ANN001, ANN002, ANN003, ANN202, ARG001
        if "historyEntries" in url:
            return _mock_json_response({"value": 5})
        return _mock_json_response(
            {"value": [{"d": "19-02-2026", "T": 5.5, "gCh": 106.38}]}
        )

    with patch.object(k40, "_async_http_request", new=AsyncMock(side_effect=route)):
        result = await k40.async_get_energy_history(DEVICE_ID)
        assert result == {"value": [{"d": "19-02-2026", "T": 5.5, "gCh": 106.38}]}

    await session.close()


@pytest.mark.asyncio
async def test_k40_indoor_humidity() -> None:
    """Test K40 indoor humidity getter."""
    session = ClientSession()
    k40 = _make_k40(session)

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": 43.5})),
    ):
        result = await k40.async_get_indoor_humidity(DEVICE_ID)
        assert result == {"value": 43.5}

    await session.close()


@pytest.mark.asyncio
async def test_k40_devices_and_child_lock() -> None:
    """Test K40 devices list and child lock get/set."""
    session = ClientSession()
    k40 = _make_k40(session)

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(
            return_value=_mock_json_response(
                {"references": [{"id": "/devices/device1"}]}
            )
        ),
    ):
        result = await k40.async_get_devices_list(DEVICE_ID)
        assert result == {"references": [{"id": "/devices/device1"}]}

    with patch.object(
        k40,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": "false"})),
    ):
        result = await k40.async_get_child_lock(DEVICE_ID, "device1")
        assert result == {"value": "false"}

    with patch.object(k40, "_async_http_request", new=AsyncMock()) as mock_req:
        await k40.async_set_child_lock(DEVICE_ID, "device1", "true")
        assert mock_req.call_args[0][2] == {"value": "true"}

    await session.close()


@pytest.mark.asyncio
async def test_k40_async_update_with_zones_and_devices() -> None:
    """K40 update with zones and devices populated."""
    session = ClientSession()
    k40 = _make_k40(session)

    async def route(method, url, *args, **kwargs):  # noqa: ANN001, ANN002, ANN003, ANN202, ARG001, PLR0911, PLR0912
        if "notifications" in url:
            return _mock_json_response({"values": []})
        if url.endswith("/resource/dhwCircuits"):
            return _mock_json_response({"references": []})
        if url.endswith("/resource/heatingCircuits"):
            return _mock_json_response({"references": []})
        if "ventilation" in url:
            return _mock_json_response({"references": []})
        if url.endswith("/resource/zones"):
            return _mock_json_response({"references": [{"id": "/zones/zn1"}]})
        if "zones" in url and "temperatureActual" in url:
            return _mock_json_response({"value": 23.5})
        if "zones" in url and "manualTemperatureHeating" in url:
            return _mock_json_response({"value": 21})
        if url.endswith("/resource/devices"):
            return _mock_json_response({"references": [{"id": "/devices/device1"}]})
        if "devices" in url and "childLock" in url:
            return _mock_json_response({"value": "false"})
        if "flameIndication" in url:
            return _mock_json_response({"value": "ch"})
        if "energy/historyEntries" in url:
            return _mock_json_response({"value": 1})
        if "energy/historyHourly" in url:
            return _mock_json_response({"value": []})
        if "energy/history" in url:
            return _mock_json_response({"value": []})
        if "indoor_h1" in url:
            return _mock_json_response({"value": 43.5})
        if "bulk" in url:
            return _mock_json_response(
                [{"resourcePaths": [{"gatewayResponse": {"payload": {"value": 0}}}]}]
            )
        return _mock_json_response({})

    with patch.object(k40, "_async_http_request", new=AsyncMock(side_effect=route)):
        result = await k40.async_update(DEVICE_ID)

    assert isinstance(result.zones, list)
    assert len(result.zones) == 1
    assert result.zones[0]["temperatureActual"] == {"value": 23.5}
    assert result.zones[0]["manualTemperatureHeating"] == {"value": 21}
    assert isinstance(result.devices, list)
    assert len(result.devices) == 1
    assert result.devices[0]["childLock"] == {"value": "false"}
    assert result.flame_indication == {"value": "ch"}
    assert result.energy_history == {"value": []}
    assert result.indoor_humidity == {"value": 43.5}

    await session.close()


# ===========================================================================
# HomeComWddw2
# ===========================================================================


def _make_wddw2(session):  # noqa: ANN001, ANN202
    return HomeComWddw2(session, _make_options(), DEVICE_ID, auth_provider=False)


@pytest.mark.asyncio
async def test_wddw2_async_update_with_dhw() -> None:
    """Full wddw2 update with dhw circuits and regex matching."""
    session = ClientSession()
    wddw2 = _make_wddw2(session)

    async def route(method, url, *args, **kwargs):  # noqa: ANN001, ANN002, ANN003, ANN202, ARG001, PLR0911
        if "notifications" in url:
            return _mock_json_response({"values": [{"code": "warn"}]})
        if url.endswith("/resource/dhwCircuits"):
            return _mock_json_response(
                {
                    "references": [{"id": "/dhwCircuits/dhw1"}],
                }
            )
        if "dhwCircuits" in url and "operationMode" in url:
            return _mock_json_response(
                {"value": "auto", "allowedValues": ["off", "manual", "auto"]}
            )
        if "dhwCircuits" in url and "airBoxTemperature" in url:
            return _mock_json_response({"value": 28})
        if "dhwCircuits" in url and "fanSpeed" in url:
            return _mock_json_response({"value": 1200})
        if "dhwCircuits" in url and "inletTemperature" in url:
            return _mock_json_response({"value": 15})
        if "dhwCircuits" in url and "outletTemperature" in url:
            return _mock_json_response({"value": 45})
        if "dhwCircuits" in url and "waterFlow" in url:
            return _mock_json_response({"value": 5.2})
        if "numberOfStarts" in url:
            return _mock_json_response({"value": 200})
        if "dhwCircuits" in url and "manualsetpoint" in url:
            return _mock_json_response({"value": 50})
        if "dhwCircuits" in url and "temperatureLevels" in url:
            return _mock_json_response({"value": 55})
        return _mock_json_response({})

    with patch.object(wddw2, "_async_http_request", new=AsyncMock(side_effect=route)):
        result = await wddw2.async_update(DEVICE_ID)

    assert result.device == DEVICE_ID
    assert result.notifications == [{"code": "warn"}]
    assert isinstance(result.dhw_circuits, list)
    assert len(result.dhw_circuits) == 1
    ref = result.dhw_circuits[0]
    assert ref["operationMode"]["value"] == "auto"
    assert ref["airBoxTemperature"] == {"value": 28}
    # tempLevel should have 'manual' and 'auto' (not 'off')
    assert "manual" in ref["tempLevel"]
    assert "auto" in ref["tempLevel"]
    assert "off" not in ref["tempLevel"]

    await session.close()


@pytest.mark.asyncio
async def test_wddw2_async_update_non_matching_dhw_id() -> None:
    r"""DHW ID that doesn't match regex dhw\\d is skipped."""
    session = ClientSession()
    wddw2 = _make_wddw2(session)

    async def route(method, url, *args, **kwargs):  # noqa: ANN001, ANN002, ANN003, ANN202, ARG001
        if "notifications" in url:
            return _mock_json_response({"values": []})
        if url.endswith("/resource/dhwCircuits"):
            return _mock_json_response(
                {
                    "references": [{"id": "/dhwCircuits/dhw_extra_thing"}],
                }
            )
        return _mock_json_response({})

    with patch.object(wddw2, "_async_http_request", new=AsyncMock(side_effect=route)):
        result = await wddw2.async_update(DEVICE_ID)

    # The reference is still there but was not enriched
    assert isinstance(result.dhw_circuits, list)
    assert len(result.dhw_circuits) == 1
    assert "operationMode" not in result.dhw_circuits[0]

    await session.close()


@pytest.mark.asyncio
async def test_wddw2_async_update_empty_references() -> None:
    """Empty references - references set to empty dict."""
    session = ClientSession()
    wddw2 = _make_wddw2(session)

    async def route(method, url, *args, **kwargs):  # noqa: ANN001, ANN002, ANN003, ANN202, ARG001
        if "notifications" in url:
            return _mock_json_response({"values": []})
        if url.endswith("/resource/dhwCircuits"):
            return _mock_json_response({"references": []})
        return _mock_json_response({})

    with patch.object(wddw2, "_async_http_request", new=AsyncMock(side_effect=route)):
        result = await wddw2.async_update(DEVICE_ID)

    assert result.dhw_circuits == {}

    await session.close()


@pytest.mark.asyncio
async def test_wddw2_dhw_temp_level_manual() -> None:
    """level='manual' uses the manual endpoint."""
    session = ClientSession()
    wddw2 = _make_wddw2(session)

    with patch.object(
        wddw2,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": 50})),
    ) as mock_req:
        result = await wddw2.async_get_dhw_temp_level(DEVICE_ID, "dhw1", "manual")
        assert result == {"value": 50}
        assert "manualsetpoint" in mock_req.call_args[0][1]

    await session.close()


@pytest.mark.asyncio
async def test_wddw2_dhw_temp_level_other() -> None:
    """Level != 'manual' uses the normal temperatureLevels endpoint."""
    session = ClientSession()
    wddw2 = _make_wddw2(session)

    with patch.object(
        wddw2,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": 60})),
    ) as mock_req:
        result = await wddw2.async_get_dhw_temp_level(DEVICE_ID, "dhw1", "high")
        assert result == {"value": 60}
        assert "temperatureLevels/high" in mock_req.call_args[0][1]

    await session.close()


@pytest.mark.asyncio
async def test_wddw2_set_dhw_temp_level() -> None:
    """Test WDDW2 set DHW temperature level rounds value."""
    session = ClientSession()
    wddw2 = _make_wddw2(session)

    with patch.object(wddw2, "_async_http_request", new=AsyncMock()) as mock_req:
        await wddw2.async_set_dhw_temp_level(DEVICE_ID, "dhw1", "manual", 50.56)
        assert mock_req.call_args[0][2] == {"value": 50.6}

    await session.close()


@pytest.mark.asyncio
async def test_wddw2_sensor_getters() -> None:
    """Test all wddw2 sensor getter methods."""
    session = ClientSession()
    wddw2 = _make_wddw2(session)

    for method_name in (
        "async_get_dhw_airbox_temp",
        "async_get_dhw_fan_speed",
        "async_get_dhw_inlet_temp",
        "async_get_dhw_outlet_temp",
        "async_get_dhw_water_flow",
    ):
        with patch.object(
            wddw2,
            "_async_http_request",
            new=AsyncMock(return_value=_mock_json_response({"value": 42})),
        ):
            result = await getattr(wddw2, method_name)(DEVICE_ID, "dhw1")
            assert result == {"value": 42}, f"{method_name} failed"

    with patch.object(
        wddw2,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": 100})),
    ):
        result = await wddw2.async_get_hs_starts(DEVICE_ID)
        assert result == {"value": 100}

    await session.close()


@pytest.mark.asyncio
async def test_wddw2_dhw_operation_mode_put() -> None:
    """Test WDDW2 DHW operation mode put."""
    session = ClientSession()
    wddw2 = _make_wddw2(session)

    with patch.object(wddw2, "_async_http_request", new=AsyncMock()) as mock_req:
        await wddw2.async_put_dhw_operation_mode(DEVICE_ID, "dhw1", "manual")
        assert mock_req.call_args[0][2] == {"value": "manual"}

    await session.close()


# ===========================================================================
# HomeComCommodule (wallbox / EV charger)
# ===========================================================================


def _make_commodule(session):  # noqa: ANN001, ANN202
    return HomeComCommodule(session, _make_options(), DEVICE_ID, auth_provider=False)


@pytest.mark.asyncio
async def test_commodule_async_update() -> None:
    """Full commodule update with charge points and regex matching."""
    session = ClientSession()
    cm = _make_commodule(session)

    async def route(method, url, *args, **kwargs):  # noqa: ANN001, ANN002, ANN003, ANN202, ARG001, PLR0911
        if "notifications" in url:
            return _mock_json_response({"values": [{"code": "info"}]})
        if "eth0/state" in url:
            return _mock_json_response({"value": "connected"})
        if url.endswith("/resource/rest/v1"):
            return _mock_json_response({"references": [{"id": "/rest/v1/cp0"}]})
        if "/cp0/conf/price" in url:
            return _mock_json_response({"value": 0.29})
        if "/cp0/conf/locked" in url:
            return _mock_json_response({"value": "off"})
        if "/cp0/conf/auth" in url:
            return _mock_json_response({"value": "on"})
        if "/cp0/conf/rfid/secure" in url:
            return _mock_json_response({"value": "off"})
        if "/cp0/conf" in url:
            return _mock_json_response({"maxCurrent": 32})
        if "/cp0/info" in url:
            return _mock_json_response({"product": "Power Charge 7000i"})
        if "/cp0/telemetry" in url:
            return _mock_json_response({"wbState": "Available", "actualPower": 0})
        if "/cp0/chargelog" in url:
            return _mock_json_response({"sessions": []})
        return _mock_json_response({})

    with patch.object(cm, "_async_http_request", new=AsyncMock(side_effect=route)):
        result = await cm.async_update(DEVICE_ID)

    assert result.device == DEVICE_ID
    assert result.notifications == [{"code": "info"}]
    assert result.eth0_state == {"value": "connected"}
    assert isinstance(result.charge_points, list)
    assert len(result.charge_points) == 1
    ref = result.charge_points[0]
    assert ref["conf"] == {"maxCurrent": 32}
    assert ref["info"] == {"product": "Power Charge 7000i"}
    assert ref["telemetry"] == {"wbState": "Available", "actualPower": 0}
    assert ref["chargelog"] == {"sessions": []}
    assert ref["price"] == {"value": 0.29}
    assert ref["locked"] == {"value": "off"}
    assert ref["auth"] == {"value": "on"}
    assert ref["rfidSecure"] == {"value": "off"}

    await session.close()


@pytest.mark.asyncio
async def test_commodule_async_update_no_references() -> None:
    """Empty references - references set to empty dict."""
    session = ClientSession()
    cm = _make_commodule(session)

    async def route(method, url, *args, **kwargs):  # noqa: ANN001, ANN002, ANN003, ANN202, ARG001
        if "notifications" in url:
            return _mock_json_response({"values": []})
        if "eth0/state" in url:
            return _mock_json_response({"value": "disconnected"})
        if url.endswith("/resource/rest/v1"):
            return _mock_json_response({"references": []})
        return _mock_json_response({})

    with patch.object(cm, "_async_http_request", new=AsyncMock(side_effect=route)):
        result = await cm.async_update(DEVICE_ID)

    assert result.charge_points == {}

    await session.close()


@pytest.mark.asyncio
async def test_commodule_get_cp_conf() -> None:
    """Test commodule get charge point config."""
    session = ClientSession()
    cm = _make_commodule(session)

    with patch.object(
        cm,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"maxCurrent": 32})),
    ):
        result = await cm.async_get_cp_conf(DEVICE_ID, "cp0")
        assert result == {"maxCurrent": 32}

    await session.close()


@pytest.mark.asyncio
async def test_commodule_get_cp_telemetry() -> None:
    """Test commodule get charge point telemetry."""
    session = ClientSession()
    cm = _make_commodule(session)

    with patch.object(
        cm,
        "_async_http_request",
        new=AsyncMock(
            return_value=_mock_json_response(
                {"wbState": "Charging", "actualPower": 7.4}
            )
        ),
    ):
        result = await cm.async_get_cp_telemetry(DEVICE_ID, "cp0")
        assert result == {"wbState": "Charging", "actualPower": 7.4}

    await session.close()


@pytest.mark.asyncio
async def test_commodule_get_cp_chargelog() -> None:
    """Test commodule get charge point charge log."""
    session = ClientSession()
    cm = _make_commodule(session)

    with patch.object(
        cm,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"sessions": [{"energy": 10}]})),
    ):
        result = await cm.async_get_cp_chargelog(DEVICE_ID, "cp0")
        assert result == {"sessions": [{"energy": 10}]}

    await session.close()


@pytest.mark.asyncio
async def test_commodule_get_cp_info() -> None:
    """Test commodule get charge point info."""
    session = ClientSession()
    cm = _make_commodule(session)

    with patch.object(
        cm,
        "_async_http_request",
        new=AsyncMock(
            return_value=_mock_json_response({"product": "Power Charge 7000i"})
        ),
    ):
        result = await cm.async_get_cp_info(DEVICE_ID, "cp0")
        assert result == {"product": "Power Charge 7000i"}

    await session.close()


@pytest.mark.asyncio
async def test_commodule_get_eth0_state() -> None:
    """Test commodule get ethernet state."""
    session = ClientSession()
    cm = _make_commodule(session)

    with patch.object(
        cm,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"value": "connected"})),
    ):
        result = await cm.async_get_eth0_state(DEVICE_ID)
        assert result == {"value": "connected"}

    await session.close()


@pytest.mark.asyncio
async def test_commodule_put_conf_price() -> None:
    """Test commodule set electricity price."""
    session = ClientSession()
    cm = _make_commodule(session)

    with patch.object(cm, "_async_http_request", new=AsyncMock()) as mock_req:
        await cm.async_put_cp_conf_price(DEVICE_ID, "cp0", 0.293)
        assert mock_req.call_args[0][2] == {"value": 0.29}

    await session.close()


@pytest.mark.asyncio
async def test_commodule_put_conf_locked() -> None:
    """Test commodule set lock state."""
    session = ClientSession()
    cm = _make_commodule(session)

    with patch.object(cm, "_async_http_request", new=AsyncMock()) as mock_req:
        await cm.async_put_cp_conf_locked(DEVICE_ID, "cp0", "on")
        assert mock_req.call_args[0][2] == {"value": "on"}

    await session.close()


@pytest.mark.asyncio
async def test_commodule_put_conf_auth() -> None:
    """Test commodule set auth setting."""
    session = ClientSession()
    cm = _make_commodule(session)

    with patch.object(cm, "_async_http_request", new=AsyncMock()) as mock_req:
        await cm.async_put_cp_conf_auth(DEVICE_ID, "cp0", "on")
        assert mock_req.call_args[0][2] == {"value": "on"}

    await session.close()


@pytest.mark.asyncio
async def test_commodule_put_conf_rfid_secure() -> None:
    """Test commodule set RFID security."""
    session = ClientSession()
    cm = _make_commodule(session)

    with patch.object(cm, "_async_http_request", new=AsyncMock()) as mock_req:
        await cm.async_put_cp_conf_rfid_secure(DEVICE_ID, "cp0", "on")
        assert mock_req.call_args[0][2] == {"value": "on"}

    await session.close()


@pytest.mark.asyncio
async def test_commodule_get_cp_label() -> None:
    """Test charge point label retrieval."""
    session = ClientSession()
    cm = _make_commodule(session)

    with patch.object(
        cm,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"label": "MyWallbox"})),
    ):
        label = await cm.async_get_cp_label(DEVICE_ID, "cp0")
        assert label == "MyWallbox"

    await session.close()


@pytest.mark.asyncio
async def test_commodule_get_cp_label_fallback_none() -> None:
    """Test charge point label falls back to Wallbox when label is None."""
    session = ClientSession()
    cm = _make_commodule(session)

    with patch.object(
        cm,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({"label": None})),
    ):
        label = await cm.async_get_cp_label(DEVICE_ID, "cp0")
        assert label == "Wallbox"

    await session.close()


@pytest.mark.asyncio
async def test_commodule_get_cp_label_fallback_empty() -> None:
    """Test charge point label falls back to Wallbox when label is empty."""
    session = ClientSession()
    cm = _make_commodule(session)

    with patch.object(
        cm,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({})),
    ):
        label = await cm.async_get_cp_label(DEVICE_ID, "cp0")
        assert label == "Wallbox"

    await session.close()


@pytest.mark.asyncio
async def test_commodule_cp_start_charging() -> None:
    """Test start charging command."""
    session = ClientSession()
    cm = _make_commodule(session)

    async def route(method, url, *args, **kwargs):  # noqa: ANN001, ANN002, ANN003, ANN202, ARG001
        if "/cp0/conf" in url:
            return _mock_json_response({"label": "MyWallbox"})
        return _mock_json_response({})

    with patch.object(
        cm, "_async_http_request", new=AsyncMock(side_effect=route)
    ) as mock_req:
        await cm.async_cp_start_charging(DEVICE_ID, "cp0")
        assert mock_req.call_count == 2
        assert "/cmd/start" in mock_req.call_args_list[1][0][1]
        assert mock_req.call_args_list[1][0][2] == {"name": "MyWallbox"}

    await session.close()


@pytest.mark.asyncio
async def test_commodule_cp_pause_charging() -> None:
    """Test pause charging command."""
    session = ClientSession()
    cm = _make_commodule(session)

    async def route(method, url, *args, **kwargs):  # noqa: ANN001, ANN002, ANN003, ANN202, ARG001
        if "/cp0/conf" in url:
            return _mock_json_response({"label": "MyWallbox"})
        return _mock_json_response({})

    with patch.object(
        cm, "_async_http_request", new=AsyncMock(side_effect=route)
    ) as mock_req:
        await cm.async_cp_pause_charging(DEVICE_ID, "cp0")
        assert mock_req.call_count == 2
        assert "/cmd/pause" in mock_req.call_args_list[1][0][1]
        assert mock_req.call_args_list[1][0][2] == {"name": "MyWallbox"}

    await session.close()


@pytest.mark.asyncio
async def test_commodule_cp_start_charging_label_fallback() -> None:
    """Test start charging uses Wallbox as fallback label."""
    session = ClientSession()
    cm = _make_commodule(session)

    with patch.object(
        cm,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({})),
    ) as mock_req:
        await cm.async_cp_start_charging(DEVICE_ID, "cp0")
        assert mock_req.call_args_list[1][0][2] == {"name": "Wallbox"}

    await session.close()


@pytest.mark.asyncio
async def test_commodule_cp_pause_charging_label_fallback() -> None:
    """Test pause charging uses Wallbox as fallback label."""
    session = ClientSession()
    cm = _make_commodule(session)

    with patch.object(
        cm,
        "_async_http_request",
        new=AsyncMock(return_value=_mock_json_response({})),
    ) as mock_req:
        await cm.async_cp_pause_charging(DEVICE_ID, "cp0")
        assert mock_req.call_args_list[1][0][2] == {"name": "Wallbox"}

    await session.close()


# ===========================================================================
# Brand selection (Bosch / Buderus)
# ===========================================================================


@pytest.mark.asyncio
async def test_brand_default_is_bosch() -> None:
    """Default brand selects Bosch OAuth params."""
    session = ClientSession()
    options = _make_options()
    bhc = HomeComAlt(session, options, auth_provider=True)
    assert bhc._oauth_params is OAUTH_PARAMS
    await session.close()


@pytest.mark.asyncio
async def test_brand_buderus_selects_buderus_oauth_params() -> None:
    """brand='buderus' selects Buderus OAuth params."""
    session = ClientSession()
    options = _make_options(brand="buderus")
    bhc = HomeComAlt(session, options, auth_provider=True)
    assert bhc._oauth_params is OAUTH_PARAMS_BUDERUS
    await session.close()


@pytest.mark.asyncio
async def test_validate_auth_uses_buderus_params() -> None:
    """validate_auth encodes Buderus redirect_uri when brand='buderus'."""
    session = ClientSession()
    options = ConnectionOptions(code="test_code", brand="buderus")
    homecom = HomeComAlt(session, options, auth_provider=True)

    with patch.object(homecom, "_async_http_request", new=AsyncMock()) as mock_request:
        mock_response = AsyncMock()
        mock_response.json = AsyncMock(
            return_value={
                "access_token": "test_token",
                "refresh_token": "refresh_token",
            }
        )
        mock_request.return_value = mock_response

        await homecom.validate_auth("auth_code", "code_verifier")
        call_data = mock_request.call_args[0][2]
        assert "com.buderus.tt.dashtt" in call_data
        assert "com.bosch.tt.dashtt" not in call_data

    await session.close()


@pytest.mark.asyncio
async def test_get_token_refresh_uses_instance_params() -> None:
    """get_token refresh path uses instance _oauth_refresh_params."""
    session = ClientSession()
    options = _make_options(brand="buderus")
    bhc = HomeComAlt(session, options, auth_provider=True)

    mock_resp = _mock_json_response(
        {
            "access_token": "new_access",
            "refresh_token": "new_refresh",
        }
    )

    with (
        patch.object(bhc, "check_jwt", return_value=False),
        patch.object(bhc, "_async_http_request", new=AsyncMock(return_value=mock_resp)),
    ):
        result = await bhc.get_token()
        assert result is True
        assert bhc.token == "new_access"
        assert bhc.refresh_token == "new_refresh"

    await session.close()


@pytest.mark.asyncio
async def test_connection_options_brand_default() -> None:
    """ConnectionOptions defaults brand to 'bosch'."""
    opts = ConnectionOptions()
    assert opts.brand == "bosch"


# ===========================================================================
# Resilient API error handling
# ===========================================================================


@pytest.mark.asyncio
async def test_async_http_request_bad_gateway_returns_empty_dict() -> None:
    """Test that HTTP 502 returns empty dict (transient gateway error)."""
    session = ClientSession()
    options = _make_options()
    bhc = await HomeComAlt.create(session, options, auth_provider=True)

    with patch.object(ClientSession, "request", new=AsyncMock()) as mock_request:
        mock_request.side_effect = ClientResponseError(
            None, (), status=HTTPStatus.BAD_GATEWAY
        )
        response = await bhc._async_http_request("get", "http://test.com/api")
        assert response == {}

    await session.close()


@pytest.mark.asyncio
async def test_async_http_request_gateway_timeout_returns_empty_dict() -> None:
    """Test that HTTP 504 returns empty dict (transient gateway timeout)."""
    session = ClientSession()
    options = _make_options()
    bhc = await HomeComAlt.create(session, options, auth_provider=True)

    with patch.object(ClientSession, "request", new=AsyncMock()) as mock_request:
        mock_request.side_effect = ClientResponseError(
            None, (), status=HTTPStatus.GATEWAY_TIMEOUT
        )
        response = await bhc._async_http_request("get", "http://test.com/api")
        assert response == {}

    await session.close()


@pytest.mark.asyncio
async def test_to_data_content_type_error_returns_none() -> None:
    """ContentTypeError from json() → None (not a crash)."""
    resp = AsyncMock()
    resp.json = AsyncMock(
        side_effect=ContentTypeError(MagicMock(), (), message="text/html")
    )
    resp.__bool__ = lambda self: True  # noqa: ARG005
    result = await HomeComAlt._to_data(resp)
    assert result is None


@pytest.mark.asyncio
async def test_rac_async_update_none_endpoints() -> None:
    """RAC async_update completes when endpoints return None."""
    session = ClientSession()
    rac = _make_rac(session)

    async def route(method, url, *args, **kwargs):  # noqa: ANN001, ANN002, ANN003, ANN202, ARG001
        if "notifications" in url:
            return _mock_json_response(None)
        if "standardFunctions" in url:
            return _mock_json_response(None)
        if "advancedFunctions" in url:
            return _mock_json_response(None)
        if "switchPrograms/list" in url:
            return _mock_json_response(None)
        return _mock_json_response({})

    with patch.object(rac, "_async_http_request", new=AsyncMock(side_effect=route)):
        result = await rac.async_update(DEVICE_ID)

    assert result.device == DEVICE_ID
    assert result.stardard_functions == []
    assert result.advanced_functions == []
    assert result.switch_programs == []
    assert result.notifications == []

    await session.close()


@pytest.mark.asyncio
async def test_k40_async_update_none_dhw_and_hc() -> None:
    """K40 async_update completes when DHW and HC return None."""
    session = ClientSession()
    k40 = _make_k40(session)

    async def route(method, url, *args, **kwargs):  # noqa: ANN001, ANN002, ANN003, ANN202, ARG001, PLR0911
        if "notifications" in url:
            return _mock_json_response({"values": []})
        if url.endswith("/resource/dhwCircuits"):
            return _mock_json_response(None)
        if url.endswith("/resource/heatingCircuits"):
            return _mock_json_response(None)
        if "ventilation" in url:
            return _mock_json_response({"references": []})
        if url.endswith("/resource/zones"):
            return _mock_json_response({"references": []})
        if url.endswith("/resource/devices"):
            return _mock_json_response({"references": []})
        if "bulk" in url:
            return _mock_json_response(
                [{"resourcePaths": [{"gatewayResponse": {"payload": {"value": 0}}}]}]
            )
        return _mock_json_response({})

    with patch.object(k40, "_async_http_request", new=AsyncMock(side_effect=route)):
        result = await k40.async_update(DEVICE_ID)

    assert result.dhw_circuits == {}
    assert result.heating_circuits == {}

    await session.close()


@pytest.mark.asyncio
async def test_wddw2_async_update_none_dhw() -> None:
    """Wddw2 async_update completes when async_get_dhw returns None."""
    session = ClientSession()
    wddw2 = _make_wddw2(session)

    async def route(method, url, *args, **kwargs):  # noqa: ANN001, ANN002, ANN003, ANN202, ARG001
        if "notifications" in url:
            return _mock_json_response({"values": []})
        if url.endswith("/resource/dhwCircuits"):
            return _mock_json_response(None)
        return _mock_json_response({})

    with patch.object(wddw2, "_async_http_request", new=AsyncMock(side_effect=route)):
        result = await wddw2.async_update(DEVICE_ID)

    assert result.dhw_circuits == {}

    await session.close()


@pytest.mark.asyncio
async def test_commodule_async_update_none_charge_points() -> None:
    """Commodule async_update completes when async_get_charge_points returns None."""
    session = ClientSession()
    cm = _make_commodule(session)

    async def route(method, url, *args, **kwargs):  # noqa: ANN001, ANN002, ANN003, ANN202, ARG001
        if "notifications" in url:
            return _mock_json_response({"values": []})
        if "eth0/state" in url:
            return _mock_json_response({"value": "disconnected"})
        if url.endswith("/resource/rest/v1"):
            return _mock_json_response(None)
        return _mock_json_response({})

    with patch.object(cm, "_async_http_request", new=AsyncMock(side_effect=route)):
        result = await cm.async_update(DEVICE_ID)

    assert result.charge_points == {}

    await session.close()
