"""Test ViCare initialization and migration."""

from unittest.mock import Mock, patch

from aiohttp import ClientError
import pytest
from PyViCare.PyViCareUtils import (
    PyViCareInvalidConfigurationError,
    PyViCareInvalidCredentialsError,
)

from homeassistant.components.vicare.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_CLIENT_ID, CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
    OAuth2TokenRequestReauthError,
    OAuth2TokenRequestTransientError,
)
from homeassistant.helpers import (
    device_registry as dr,
    entity_registry as er,
    issue_registry as ir,
)

from . import MODULE, setup_integration
from .conftest import Fixture, MockPyViCare

from tests.common import MockConfigEntry

# 16-character zigbee IEEE address shared by the FHT fixtures.
ZIGBEE_IEEE = "#" * 16


@pytest.mark.usefixtures("mock_setup_entry")
async def test_migrate_entry_v1_1_to_v2_1(hass: HomeAssistant) -> None:
    """Test migration of config entry from v1.1 through to v2.1."""
    mock_token = {
        "access_token": "mock-access-token",
        "refresh_token": "mock-refresh-token",
        "expires_at": 9999999999.0,
        "token_type": "Bearer",
    }
    config_entry = MockConfigEntry(
        domain=DOMAIN,
        unique_id="ViCare",
        data={
            CONF_USERNAME: "foo@bar.com",
            CONF_PASSWORD: "1234",
            CONF_CLIENT_ID: "5678",
            "heating_type": "auto",
        },
        version=1,
        minor_version=1,
    )
    config_entry.add_to_hass(hass)

    with patch(
        f"{MODULE}.obtain_token_via_basic_auth_pkce",
        return_value=mock_token,
    ):
        await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    assert config_entry.version == 2
    assert config_entry.minor_version == 1
    assert "heating_type" not in config_entry.data
    assert "username" not in config_entry.data
    assert "password" not in config_entry.data
    assert "client_id" not in config_entry.data
    assert config_entry.data["auth_implementation"] == DOMAIN
    assert config_entry.data["token"]["refresh_token"] == "mock-refresh-token"


@pytest.mark.usefixtures("mock_setup_entry")
async def test_migrate_entry_v1_2_to_v2_1(hass: HomeAssistant) -> None:
    """Test migration of config entry from v1.2 to v2.1."""
    mock_token = {
        "access_token": "mock-access-token",
        "refresh_token": "mock-refresh-token",
        "expires_at": 9999999999.0,
        "token_type": "Bearer",
    }
    config_entry = MockConfigEntry(
        domain=DOMAIN,
        unique_id="ViCare",
        data={
            CONF_USERNAME: "foo@bar.com",
            CONF_PASSWORD: "1234",
            CONF_CLIENT_ID: "5678",
        },
        version=1,
        minor_version=2,
    )
    config_entry.add_to_hass(hass)

    with patch(
        f"{MODULE}.obtain_token_via_basic_auth_pkce",
        return_value=mock_token,
    ):
        await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    assert config_entry.version == 2
    assert config_entry.minor_version == 1
    assert "username" not in config_entry.data
    assert "password" not in config_entry.data
    assert "client_id" not in config_entry.data
    assert config_entry.data["auth_implementation"] == DOMAIN
    assert config_entry.data["token"]["refresh_token"] == "mock-refresh-token"


@pytest.mark.usefixtures("mock_setup_entry")
async def test_migrate_entry_token_failure(hass: HomeAssistant) -> None:
    """Test migration completes even when token cannot be obtained."""
    config_entry = MockConfigEntry(
        domain=DOMAIN,
        unique_id="ViCare",
        data={
            CONF_USERNAME: "foo@bar.com",
            CONF_PASSWORD: "1234",
            CONF_CLIENT_ID: "5678",
        },
        version=1,
        minor_version=2,
    )
    config_entry.add_to_hass(hass)

    with patch(
        f"{MODULE}.obtain_token_via_basic_auth_pkce",
        return_value={},
    ):
        await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    assert config_entry.version == 2
    assert config_entry.minor_version == 1
    assert "username" not in config_entry.data
    assert "password" not in config_entry.data
    assert "client_id" not in config_entry.data
    assert config_entry.data["auth_implementation"] == DOMAIN
    assert config_entry.data["token"] == {}


@pytest.mark.usefixtures("mock_setup_entry")
async def test_migrate_entry_creates_repair_issue(hass: HomeAssistant) -> None:
    """Test migration creates a repair issue for redirect URI update."""
    config_entry = MockConfigEntry(
        domain=DOMAIN,
        unique_id="ViCare",
        data={
            CONF_USERNAME: "foo@bar.com",
            CONF_PASSWORD: "1234",
            CONF_CLIENT_ID: "5678",
        },
        version=1,
        minor_version=2,
    )
    config_entry.add_to_hass(hass)

    with patch(
        f"{MODULE}.obtain_token_via_basic_auth_pkce",
        return_value={
            "access_token": "a",
            "refresh_token": "r",
            "expires_at": 9999999999.0,
            "token_type": "Bearer",
        },
    ):
        await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    issue = ir.async_get(hass).async_get_issue(DOMAIN, "update_redirect_uri")  # pylint: disable=home-assistant-tests-registry-fixtures
    assert issue is not None
    assert issue.severity == ir.IssueSeverity.WARNING


@pytest.mark.usefixtures("mock_setup_entry")
async def test_migrate_entry_v1_3_stamp_bump(hass: HomeAssistant) -> None:
    """Test pre-merge v1.3 entries are promoted to v2.1 without re-running migration."""
    token = {
        "access_token": "a",
        "refresh_token": "r",
        "expires_at": 9999999999.0,
        "token_type": "Bearer",
    }
    config_entry = MockConfigEntry(
        domain=DOMAIN,
        unique_id="ViCare",
        data={"auth_implementation": DOMAIN, "token": token},
        version=1,
        minor_version=3,
    )
    config_entry.add_to_hass(hass)

    await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    assert config_entry.version == 2
    assert config_entry.minor_version == 1
    assert config_entry.data["auth_implementation"] == DOMAIN
    assert config_entry.data["token"] == token


async def test_setup_entry_token_invalid(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test setup raises ConfigEntryAuthFailed on invalid token."""
    mock_config_entry.add_to_hass(hass)

    with patch(
        "homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
        side_effect=KeyError("token"),
    ):
        await hass.config_entries.async_setup(mock_config_entry.entry_id)
        await hass.async_block_till_done()

    assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR


async def test_setup_entry_implementation_unavailable(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test setup raises ConfigEntryAuthFailed when OAuth2 implementation is missing."""
    mock_config_entry.add_to_hass(hass)

    with patch(
        "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
        side_effect=ValueError("Implementation not available"),
    ):
        await hass.config_entries.async_setup(mock_config_entry.entry_id)
        await hass.async_block_till_done()

    assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR


async def test_setup_entry_token_refresh_transient(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test setup raises ConfigEntryNotReady on transient token refresh error."""
    mock_config_entry.add_to_hass(hass)

    request_info = Mock()
    request_info.real_url = "https://example.com"
    transient = OAuth2TokenRequestTransientError(
        domain=DOMAIN, request_info=request_info, status=503
    )
    with patch(
        "homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
        side_effect=transient,
    ):
        await hass.config_entries.async_setup(mock_config_entry.entry_id)
        await hass.async_block_till_done()

    assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY


async def test_setup_entry_token_refresh_reauth(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test setup raises ConfigEntryAuthFailed when token refresh requires re-auth."""
    mock_config_entry.add_to_hass(hass)

    request_info = Mock()
    request_info.real_url = "https://example.com"
    reauth = OAuth2TokenRequestReauthError(
        domain=DOMAIN, request_info=request_info, status=401
    )
    with patch(
        "homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
        side_effect=reauth,
    ):
        await hass.config_entries.async_setup(mock_config_entry.entry_id)
        await hass.async_block_till_done()

    assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR


async def test_setup_entry_transient_error(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test setup raises ConfigEntryNotReady on transient auth error."""
    mock_config_entry.add_to_hass(hass)

    with patch(
        "homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
        side_effect=ClientError("connection failed"),
    ):
        await hass.config_entries.async_setup(mock_config_entry.entry_id)
        await hass.async_block_till_done()

    assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY


async def test_setup_entry_invalid_credentials(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test setup raises ConfigEntryAuthFailed on PyViCare credentials error."""
    mock_config_entry.add_to_hass(hass)

    with (
        patch(
            "homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
        ),
        patch(
            f"{MODULE}._setup_vicare_api",
            side_effect=PyViCareInvalidCredentialsError,
        ),
    ):
        await hass.config_entries.async_setup(mock_config_entry.entry_id)
        await hass.async_block_till_done()

    assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR


async def test_setup_entry_invalid_configuration(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test setup raises ConfigEntryAuthFailed on PyViCare config error."""
    mock_config_entry.add_to_hass(hass)

    with (
        patch(
            "homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
        ),
        patch(
            f"{MODULE}._setup_vicare_api",
            side_effect=PyViCareInvalidConfigurationError,
        ),
    ):
        await hass.config_entries.async_setup(mock_config_entry.entry_id)
        await hass.async_block_till_done()

    assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR


# Device migration test can be removed in 2025.4.0
async def test_device_and_entity_migration(
    hass: HomeAssistant,
    device_registry: dr.DeviceRegistry,
    entity_registry: er.EntityRegistry,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test that the device registry is updated correctly."""
    fixtures: list[Fixture] = [
        Fixture({"type:boiler"}, "vicare/Vitodens300W.json"),
        Fixture({"type:boiler"}, "vicare/dummy-device-no-serial.json"),
    ]
    with (
        patch(
            "homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
        ),
        patch(
            f"{MODULE}._setup_vicare_api",
            return_value=MockPyViCare(fixtures).as_vicare_data(),
        ),
        patch(f"{MODULE}.PLATFORMS", [Platform.CLIMATE]),
    ):
        mock_config_entry.add_to_hass(hass)

        # device with serial data point
        device0 = device_registry.async_get_or_create(
            config_entry_id=mock_config_entry.entry_id,
            identifiers={
                (DOMAIN, "gateway0"),
            },
            model="model0",
        )
        entry0 = entity_registry.async_get_or_create(
            domain=Platform.CLIMATE,
            platform=DOMAIN,
            config_entry=mock_config_entry,
            unique_id="gateway0-0",
            translation_key="heating",
            device_id=device0.id,
        )
        entry1 = entity_registry.async_get_or_create(
            domain=Platform.CLIMATE,
            platform=DOMAIN,
            config_entry=mock_config_entry,
            unique_id="gateway0_deviceSerialVitodens300W-heating-1",
            translation_key="heating",
            device_id=device0.id,
        )
        # device without serial data point
        device1 = device_registry.async_get_or_create(
            config_entry_id=mock_config_entry.entry_id,
            identifiers={
                (DOMAIN, "gateway1"),
            },
            model="model1",
        )
        entry2 = entity_registry.async_get_or_create(
            domain=Platform.CLIMATE,
            platform=DOMAIN,
            config_entry=mock_config_entry,
            unique_id="gateway1-0",
            translation_key="heating",
            device_id=device1.id,
        )
        # device is not provided by api
        device2 = device_registry.async_get_or_create(
            config_entry_id=mock_config_entry.entry_id,
            identifiers={
                (DOMAIN, "gateway2"),
            },
            model="model2",
        )
        entry3 = entity_registry.async_get_or_create(
            domain=Platform.CLIMATE,
            platform=DOMAIN,
            config_entry=mock_config_entry,
            unique_id="gateway2-0",
            translation_key="heating",
            device_id=device2.id,
        )

        await hass.config_entries.async_setup(mock_config_entry.entry_id)

        await hass.async_block_till_done()

    assert (
        entity_registry.async_get(entry0.entity_id).unique_id
        == "gateway0_deviceSerialVitodens300W-heating-0"
    )
    assert (
        entity_registry.async_get(entry1.entity_id).unique_id
        == "gateway0_deviceSerialVitodens300W-heating-1"
    )
    assert (
        entity_registry.async_get(entry2.entity_id).unique_id
        == "gateway1_deviceId1-heating-0"
    )
    assert entity_registry.async_get(entry3.entity_id).unique_id == "gateway2-0"


async def test_device_via_device_links(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
    device_registry: dr.DeviceRegistry,
) -> None:
    """Test that a zigbee sub-device links to its gateway via via_device_id."""
    fixtures: list[Fixture] = [
        Fixture({"type:fhtMain"}, "vicare/FHTMain.json", gateway_id="gateway0"),
        Fixture({"type:fhtChannel"}, "vicare/FHTChannel.json", gateway_id="gateway0"),
    ]
    with (
        patch(
            "homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
        ),
        patch(
            f"{MODULE}._setup_vicare_api",
            return_value=MockPyViCare(fixtures).as_vicare_data(),
        ),
    ):
        await setup_integration(hass, mock_config_entry)

    gateway_device = device_registry.async_get_device_by_identifier(
        (DOMAIN, f"gateway0_zigbee_{ZIGBEE_IEEE}"), mock_config_entry.entry_id
    )
    assert gateway_device is not None

    channel_device = device_registry.async_get_device_by_identifier(
        (DOMAIN, f"gateway0_zigbee_{ZIGBEE_IEEE}_2"), mock_config_entry.entry_id
    )
    assert channel_device is not None
    assert channel_device.via_device_id == gateway_device.id
