"""Test the cloud component."""

from collections.abc import Callable, Coroutine
from typing import Any
from unittest.mock import MagicMock, patch

import pytest

from homeassistant.components.cloud import (
    CloudConnectionState,
    CloudNotAvailable,
    CloudNotConnected,
    async_get_or_create_cloudhook,
    async_listen_cloudhook_change,
    async_listen_connection_change,
    async_remote_ui_url,
)
from homeassistant.components.cloud.const import (
    DATA_CLOUD,
    DOMAIN,
    MODE_DEV,
    PREF_CLOUDHOOKS,
)
from homeassistant.components.cloud.prefs import STORAGE_KEY
from homeassistant.const import CONF_MODE, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Context, HomeAssistant
from homeassistant.exceptions import Unauthorized
from homeassistant.setup import async_setup_component

from tests.common import MockConfigEntry, MockUser


async def test_constructor_loads_info_from_config(hass: HomeAssistant) -> None:
    """Test non-dev mode loads info from SERVERS constant."""
    with patch("hass_nabucasa.Cloud.initialize"):
        result = await async_setup_component(
            hass,
            "cloud",
            {
                "http": {},
                "cloud": {
                    CONF_MODE: MODE_DEV,
                    "cognito_client_id": "test-cognito_client_id",
                    "user_pool_id": "test-user_pool_id",
                    "region": "test-region",
                    "api_server": "test-api-server",
                    "relayer_server": "test-relayer-server",
                    "acme_server": "test-acme-server",
                    "remotestate_server": "test-remotestate-server",
                    "discovery_service_actions": {
                        "lorem_ipsum": "https://lorem.ipsum/test-url"
                    },
                },
            },
        )
        assert result

    cl = hass.data[DATA_CLOUD]
    assert cl.mode == MODE_DEV
    assert cl.cognito_client_id == "test-cognito_client_id"
    assert cl.user_pool_id == "test-user_pool_id"
    assert cl.region == "test-region"
    assert cl.relayer_server == "test-relayer-server"
    assert cl.iot.ws_server_url == "wss://test-relayer-server/websocket"
    assert cl.acme_server == "test-acme-server"
    assert cl.api_server == "test-api-server"
    assert cl.remotestate_server == "test-remotestate-server"
    assert (
        cl.service_discovery._action_overrides["lorem_ipsum"]
        == "https://lorem.ipsum/test-url"
    )


@pytest.mark.usefixtures("mock_cloud_fixture")
async def test_remote_services(
    hass: HomeAssistant, hass_read_only_user: MockUser
) -> None:
    """Setup cloud component and test services."""
    cloud = hass.data[DATA_CLOUD]

    assert hass.services.has_service(DOMAIN, "remote_connect")
    assert hass.services.has_service(DOMAIN, "remote_disconnect")

    with patch("hass_nabucasa.remote.RemoteUI.connect") as mock_connect:
        await hass.services.async_call(DOMAIN, "remote_connect", blocking=True)
        await hass.async_block_till_done()

    assert mock_connect.called
    assert cloud.client.remote_autostart

    with patch("hass_nabucasa.remote.RemoteUI.disconnect") as mock_disconnect:
        await hass.services.async_call(DOMAIN, "remote_disconnect", blocking=True)
        await hass.async_block_till_done()

    assert mock_disconnect.called
    assert not cloud.client.remote_autostart

    # Test admin access required
    non_admin_context = Context(user_id=hass_read_only_user.id)

    with (
        patch("hass_nabucasa.remote.RemoteUI.connect") as mock_connect,
        pytest.raises(Unauthorized),
    ):
        await hass.services.async_call(
            DOMAIN, "remote_connect", blocking=True, context=non_admin_context
        )

    assert mock_connect.called is False

    with (
        patch("hass_nabucasa.remote.RemoteUI.disconnect") as mock_disconnect,
        pytest.raises(Unauthorized),
    ):
        await hass.services.async_call(
            DOMAIN, "remote_disconnect", blocking=True, context=non_admin_context
        )

    assert mock_disconnect.called is False


@pytest.mark.usefixtures("mock_cloud_fixture")
async def test_shutdown_event(hass: HomeAssistant) -> None:
    """Test if the cloud will stop on shutdown event."""
    with patch("hass_nabucasa.Cloud.stop") as mock_stop:
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
        await hass.async_block_till_done()

    assert mock_stop.called


async def test_setup_existing_cloud_user(
    hass: HomeAssistant, hass_storage: dict[str, Any]
) -> None:
    """Test setup with API push default data."""
    user = await hass.auth.async_create_system_user("Cloud test")
    hass_storage[STORAGE_KEY] = {"version": 1, "data": {"cloud_user": user.id}}
    with patch("hass_nabucasa.Cloud.initialize"):
        result = await async_setup_component(
            hass,
            "cloud",
            {
                "http": {},
                "cloud": {
                    CONF_MODE: MODE_DEV,
                    "cognito_client_id": "test-cognito_client_id",
                    "user_pool_id": "test-user_pool_id",
                    "region": "test-region",
                    "relayer_server": "test-relayer-serer",
                    "api_server": "test-api-server",
                },
            },
        )
        assert result

    assert hass_storage[STORAGE_KEY]["data"]["cloud_user"] == user.id


@pytest.mark.usefixtures("mock_cloud_fixture")
async def test_on_connect(hass: HomeAssistant) -> None:
    """Test cloud on connect triggers."""
    cl = hass.data[DATA_CLOUD]

    assert len(cl.iot._on_connect) == 3

    assert len(hass.states.async_entity_ids("binary_sensor")) == 0

    cloud_states = []

    def handle_state(cloud_state):
        nonlocal cloud_states
        cloud_states.append(cloud_state)

    async_listen_connection_change(hass, handle_state)

    assert "async_setup" in str(cl.iot._on_connect[-1])
    await cl.iot._on_connect[-1]()
    await hass.async_block_till_done()

    assert len(hass.states.async_entity_ids("binary_sensor")) == 0

    # The on_start callback discovers the binary sensor platform
    assert "async_setup" in str(cl._on_start[-1])
    await cl._on_start[-1]()
    await hass.async_block_till_done()

    assert len(hass.states.async_entity_ids("binary_sensor")) == 1

    with patch("homeassistant.helpers.discovery.async_load_platform") as mock_load:
        await cl._on_start[-1]()
        await hass.async_block_till_done()

    assert len(mock_load.mock_calls) == 0

    assert len(cloud_states) == 1
    assert cloud_states[-1] == CloudConnectionState.CLOUD_CONNECTED

    await cl.iot._on_connect[-1]()
    await hass.async_block_till_done()
    assert len(cloud_states) == 2
    assert cloud_states[-1] == CloudConnectionState.CLOUD_CONNECTED

    assert len(cl.iot._on_disconnect) == 2
    assert "async_setup" in str(cl.iot._on_disconnect[-1])
    await cl.iot._on_disconnect[-1]()
    await hass.async_block_till_done()

    assert len(cloud_states) == 3
    assert cloud_states[-1] == CloudConnectionState.CLOUD_DISCONNECTED

    await cl.iot._on_disconnect[-1]()
    await hass.async_block_till_done()
    assert len(cloud_states) == 4
    assert cloud_states[-1] == CloudConnectionState.CLOUD_DISCONNECTED


@pytest.mark.usefixtures("mock_cloud_fixture")
async def test_remote_ui_url(hass: HomeAssistant) -> None:
    """Test getting remote ui url."""
    cl = hass.data[DATA_CLOUD]

    # Not logged in
    with pytest.raises(CloudNotAvailable):
        async_remote_ui_url(hass)

    with patch("homeassistant.components.cloud.async_is_logged_in", return_value=True):
        # Remote not enabled
        with pytest.raises(CloudNotAvailable):
            async_remote_ui_url(hass)

        with patch.object(cl.remote, "connect"):
            await cl.client.prefs.async_update(remote_enabled=True)
            await hass.async_block_till_done()

        # No instance domain
        with pytest.raises(CloudNotAvailable):
            async_remote_ui_url(hass)

        # Remote finished initializing
        cl.client.prefs._prefs["remote_domain"] = "example.com"

        assert async_remote_ui_url(hass) == "https://example.com"


async def test_async_get_or_create_cloudhook(
    hass: HomeAssistant,
    cloud: MagicMock,
    set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]],
) -> None:
    """Test async_get_or_create_cloudhook."""
    assert await async_setup_component(hass, "cloud", {"cloud": {}})
    await hass.async_block_till_done()
    await cloud.login("test-user", "test-pass")

    webhook_id = "mock-webhook-id"
    cloudhook_url = "https://cloudhook.nabu.casa/abcdefg"

    with patch(
        "homeassistant.components.cloud.async_create_cloudhook",
        return_value=cloudhook_url,
    ) as async_create_cloudhook_mock:
        # create cloudhook as it does not exist
        assert (await async_get_or_create_cloudhook(hass, webhook_id)) == cloudhook_url
        async_create_cloudhook_mock.assert_called_once_with(hass, webhook_id)

        await set_cloud_prefs(
            {
                PREF_CLOUDHOOKS: {
                    webhook_id: {
                        "webhook_id": webhook_id,
                        "cloudhook_id": "random-id",
                        "cloudhook_url": cloudhook_url,
                        "managed": True,
                    }
                }
            }
        )

        async_create_cloudhook_mock.reset_mock()

        # get cloudhook as it exists
        assert await async_get_or_create_cloudhook(hass, webhook_id) == cloudhook_url
        async_create_cloudhook_mock.assert_not_called()

    # Simulate logged out
    await cloud.logout()

    # Not logged in
    with pytest.raises(CloudNotAvailable):
        await async_get_or_create_cloudhook(hass, webhook_id)

    # Simulate disconnected
    cloud.iot.state = "disconnected"

    # Not connected
    with pytest.raises(CloudNotConnected):
        await async_get_or_create_cloudhook(hass, webhook_id)


async def test_cloud_logout(
    hass: HomeAssistant,
    cloud: MagicMock,
) -> None:
    """Test cloud setup with existing config entry when user is logged out."""
    assert cloud.is_logged_in is False

    mock_config_entry = MockConfigEntry(domain=DOMAIN)
    mock_config_entry.add_to_hass(hass)
    assert await async_setup_component(hass, DOMAIN, {"cloud": {}})
    await hass.async_block_till_done()

    assert cloud.is_logged_in is False


async def test_async_listen_cloudhook_change(
    hass: HomeAssistant,
    cloud: MagicMock,
    set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]],
) -> None:
    """Test async_listen_cloudhook_change."""
    assert await async_setup_component(hass, "cloud", {"cloud": {}})
    await hass.async_block_till_done()
    await cloud.login("test-user", "test-pass")

    webhook_id = "mock-webhook-id"
    cloudhook_url = "https://cloudhook.nabu.casa/abcdefg"

    # Set up initial cloudhooks state
    await set_cloud_prefs(
        {
            PREF_CLOUDHOOKS: {
                webhook_id: {
                    "webhook_id": webhook_id,
                    "cloudhook_id": "random-id",
                    "cloudhook_url": cloudhook_url,
                    "managed": True,
                }
            }
        }
    )

    # Track cloudhook changes
    changes = []
    changeInvoked = False

    def on_change(cloudhook: dict[str, Any] | None) -> None:
        """Handle cloudhook change."""
        nonlocal changeInvoked
        changes.append(cloudhook)
        changeInvoked = True

    # Register the change listener
    unsubscribe = async_listen_cloudhook_change(hass, webhook_id, on_change)

    # Verify no changes yet
    assert len(changes) == 0
    assert changeInvoked is False

    # Delete the cloudhook by updating prefs
    await set_cloud_prefs({PREF_CLOUDHOOKS: {}})
    await hass.async_block_till_done()

    # Verify deletion callback was called with None
    assert len(changes) == 1
    assert changes[-1] is None
    assert changeInvoked is True

    # Reset changeInvoked to detect next change
    changeInvoked = False

    # Add cloudhook back
    cloudhook_data = {
        "webhook_id": webhook_id,
        "cloudhook_id": "random-id",
        "cloudhook_url": cloudhook_url,
        "managed": True,
    }
    await set_cloud_prefs({PREF_CLOUDHOOKS: {webhook_id: cloudhook_data}})
    await hass.async_block_till_done()

    # Verify callback called with cloudhook data
    assert len(changes) == 2
    assert changes[-1] == cloudhook_data
    assert changeInvoked is True

    # Reset changeInvoked to detect next change
    changeInvoked = False

    # Update cloudhook data with same cloudhook should not trigger callback
    await set_cloud_prefs({PREF_CLOUDHOOKS: {webhook_id: cloudhook_data}})
    await hass.async_block_till_done()

    assert changeInvoked is False

    # Unsubscribe from listener
    unsubscribe()

    # Delete cloudhook again
    await set_cloud_prefs({PREF_CLOUDHOOKS: {}})
    await hass.async_block_till_done()

    # Verify change callback was NOT called after unsubscribe
    assert len(changes) == 2
    assert changeInvoked is False


async def test_async_listen_cloudhook_change_cloud_setup_later(
    hass: HomeAssistant,
    cloud: MagicMock,
    set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]],
) -> None:
    """Test cloudhook change listener with late cloud setup."""
    webhook_id = "mock-webhook-id"
    cloudhook_url = "https://cloudhook.nabu.casa/abcdefg"

    # Track cloudhook changes
    changes: list[dict[str, Any] | None] = []

    def on_change(cloudhook: dict[str, Any] | None) -> None:
        """Handle cloudhook change."""
        changes.append(cloudhook)

    # Register listener BEFORE cloud is set up
    unsubscribe = async_listen_cloudhook_change(hass, webhook_id, on_change)

    # Verify it returns a callable
    assert callable(unsubscribe)

    # No changes yet since cloud isn't set up
    assert len(changes) == 0

    # Now set up cloud
    assert await async_setup_component(hass, "cloud", {"cloud": {}})
    await hass.async_block_till_done()
    await cloud.login("test-user", "test-pass")

    # Add a cloudhook - this should trigger the listener
    cloudhook_data = {
        "webhook_id": webhook_id,
        "cloudhook_id": "random-id",
        "cloudhook_url": cloudhook_url,
        "managed": True,
    }
    await set_cloud_prefs({PREF_CLOUDHOOKS: {webhook_id: cloudhook_data}})
    await hass.async_block_till_done()

    # Verify the listener received the update
    assert len(changes) == 1
    assert changes[-1] == cloudhook_data

    # Unsubscribe and verify no more updates
    unsubscribe()

    await set_cloud_prefs({PREF_CLOUDHOOKS: {}})
    await hass.async_block_till_done()

    # Should not receive update after unsubscribe
    assert len(changes) == 1
