"""Tests for the Withings component."""

from datetime import timedelta
from typing import Any
from unittest.mock import AsyncMock, patch
from urllib.parse import urlparse

from aiohttp import ClientConnectionError
from aiohttp.hdrs import METH_HEAD
from aiowithings import (
    NotificationCategory,
    WithingsAuthenticationFailedError,
    WithingsUnauthorizedError,
)
from freezegun.api import FrozenDateTimeFactory
import pytest

from homeassistant import config_entries
from homeassistant.components import cloud
from homeassistant.components.cloud import CloudNotAvailable
from homeassistant.components.webhook import async_generate_url
from homeassistant.components.withings.const import DOMAIN
from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util

from . import call_webhook, prepare_webhook_setup, setup_integration
from .conftest import USER_ID, WEBHOOK_ID

from tests.common import (
    MockConfigEntry,
    async_fire_time_changed,
    async_mock_cloud_connection_status,
)
from tests.components.cloud import mock_cloud
from tests.typing import ClientSessionGenerator


async def test_data_manager_webhook_subscription(
    hass: HomeAssistant,
    withings: AsyncMock,
    webhook_config_entry: MockConfigEntry,
) -> None:
    """Test data manager webhook subscriptions."""
    await setup_integration(hass, webhook_config_entry)
    await hass.async_block_till_done()
    async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1))
    await hass.async_block_till_done()

    assert withings.subscribe_notification.call_count == 6

    webhook_url = "https://example.com/api/webhook/55a7335ea8dee830eed4ef8f84cda8f6d80b83af0847dc74032e86120bffed5e"

    withings.subscribe_notification.assert_any_call(
        webhook_url, NotificationCategory.WEIGHT
    )
    withings.subscribe_notification.assert_any_call(
        webhook_url, NotificationCategory.PRESSURE
    )
    withings.subscribe_notification.assert_any_call(
        webhook_url, NotificationCategory.ACTIVITY
    )
    withings.subscribe_notification.assert_any_call(
        webhook_url, NotificationCategory.SLEEP
    )

    withings.revoke_notification_configurations.assert_any_call(
        webhook_url, NotificationCategory.IN_BED
    )
    withings.revoke_notification_configurations.assert_any_call(
        webhook_url, NotificationCategory.OUT_BED
    )


async def test_webhook_subscription_polling_config(
    hass: HomeAssistant,
    withings: AsyncMock,
    polling_config_entry: MockConfigEntry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test webhook subscriptions not run when polling."""
    await setup_integration(hass, polling_config_entry, False)
    await hass.async_block_till_done()
    freezer.tick(timedelta(seconds=1))
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    assert withings.revoke_notification_configurations.call_count == 0
    assert withings.subscribe_notification.call_count == 0
    assert withings.list_notification_configurations.call_count == 0


async def test_head_request(
    hass: HomeAssistant,
    withings: AsyncMock,
    webhook_config_entry: MockConfigEntry,
    hass_client_no_auth: ClientSessionGenerator,
) -> None:
    """Test we handle head requests Withings sends."""
    await setup_integration(hass, webhook_config_entry)
    client = await hass_client_no_auth()
    webhook_url = async_generate_url(hass, WEBHOOK_ID)

    response = await client.request(
        method=METH_HEAD,
        path=urlparse(webhook_url).path,
    )
    assert response.status == 200


async def test_webhooks_request_data(
    hass: HomeAssistant,
    withings: AsyncMock,
    webhook_config_entry: MockConfigEntry,
    hass_client_no_auth: ClientSessionGenerator,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test calling a webhook requests data."""
    await setup_integration(hass, webhook_config_entry)
    await prepare_webhook_setup(hass, freezer)

    client = await hass_client_no_auth()

    assert withings.get_measurement_since.call_count == 0
    assert withings.get_measurement_in_period.call_count == 1

    await call_webhook(
        hass,
        WEBHOOK_ID,
        {"userid": USER_ID, "appli": NotificationCategory.WEIGHT},
        client,
    )
    assert withings.get_measurement_since.call_count == 1
    assert withings.get_measurement_in_period.call_count == 1


@pytest.mark.parametrize(
    "error",
    [
        WithingsUnauthorizedError(401),
        WithingsAuthenticationFailedError(500),
    ],
)
async def test_triggering_reauth(
    hass: HomeAssistant,
    withings: AsyncMock,
    polling_config_entry: MockConfigEntry,
    error: Exception,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test triggering reauth."""
    await setup_integration(hass, polling_config_entry, False)

    withings.get_measurement_since.side_effect = error
    freezer.tick(timedelta(minutes=10))
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    flows = hass.config_entries.flow.async_progress()

    assert len(flows) == 1
    flow = flows[0]
    assert flow["step_id"] == "reauth_confirm"
    assert flow["handler"] == DOMAIN
    assert flow["context"]["source"] == config_entries.SOURCE_REAUTH


@pytest.mark.parametrize(
    ("config_entry"),
    [
        MockConfigEntry(
            domain=DOMAIN,
            unique_id="123",
            data={
                "token": {"userid": 123},
                "profile": "henk",
                "use_webhook": False,
                "webhook_id": "3290798afaebd28519c4883d3d411c7197572e0cc9b8d507471f59a700a61a55",
            },
        ),
        MockConfigEntry(
            domain=DOMAIN,
            unique_id="123",
            data={
                "token": {"userid": 123},
                "profile": "henk",
                "use_webhook": False,
            },
        ),
    ],
)
async def test_config_flow_upgrade(
    hass: HomeAssistant, config_entry: MockConfigEntry
) -> None:
    """Test config flow upgrade."""
    config_entry.add_to_hass(hass)

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

    entry = hass.config_entries.async_get_entry(config_entry.entry_id)

    assert entry.unique_id == "123"
    assert entry.data["token"]["userid"] == 123
    assert CONF_WEBHOOK_ID in entry.data


async def test_setup_with_cloudhook(
    hass: HomeAssistant, cloudhook_config_entry: MockConfigEntry, withings: AsyncMock
) -> None:
    """Test if set up with active cloud subscription and cloud hook."""

    await mock_cloud(hass)
    await hass.async_block_till_done()

    with (
        patch("homeassistant.components.cloud.async_is_logged_in", return_value=True),
        patch("homeassistant.components.cloud.async_is_connected", return_value=True),
        patch.object(cloud, "async_active_subscription", return_value=True),
        patch(
            "homeassistant.components.cloud.async_create_cloudhook",
            return_value="https://hooks.nabu.casa/ABCD",
        ) as fake_create_cloudhook,
        patch(
            "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
        ),
        patch(
            "homeassistant.components.cloud.async_delete_cloudhook"
        ) as fake_delete_cloudhook,
        patch("homeassistant.components.withings.webhook_generate_url"),
    ):
        await setup_integration(hass, cloudhook_config_entry)

        assert cloud.async_active_subscription(hass) is True

        assert (
            hass.config_entries.async_entries(DOMAIN)[0].data["cloudhook_url"]
            == "https://hooks.nabu.casa/ABCD"
        )

        await hass.async_block_till_done()
        assert hass.config_entries.async_entries(DOMAIN)
        fake_create_cloudhook.assert_not_called()

        for config_entry in hass.config_entries.async_entries(DOMAIN):
            await hass.config_entries.async_remove(config_entry.entry_id)
            fake_delete_cloudhook.assert_called_once()

        await hass.async_block_till_done()
        assert not hass.config_entries.async_entries(DOMAIN)


async def test_removing_entry_with_cloud_unavailable(
    hass: HomeAssistant, cloudhook_config_entry: MockConfigEntry, withings: AsyncMock
) -> None:
    """Test handling cloud unavailable when deleting entry."""

    await mock_cloud(hass)
    await hass.async_block_till_done()

    with (
        patch("homeassistant.components.cloud.async_is_logged_in", return_value=True),
        patch("homeassistant.components.cloud.async_is_connected", return_value=True),
        patch.object(cloud, "async_active_subscription", return_value=True),
        patch(
            "homeassistant.components.cloud.async_create_cloudhook",
            return_value="https://hooks.nabu.casa/ABCD",
        ),
        patch(
            "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
        ),
        patch(
            "homeassistant.components.cloud.async_delete_cloudhook",
            side_effect=CloudNotAvailable(),
        ),
        patch(
            "homeassistant.components.withings.webhook_generate_url",
        ),
    ):
        await setup_integration(hass, cloudhook_config_entry)

        assert cloud.async_active_subscription(hass) is True

        await hass.async_block_till_done()
        assert hass.config_entries.async_entries(DOMAIN)

        for config_entry in hass.config_entries.async_entries(DOMAIN):
            await hass.config_entries.async_remove(config_entry.entry_id)

        await hass.async_block_till_done()
        assert not hass.config_entries.async_entries(DOMAIN)


async def test_setup_with_cloud(
    hass: HomeAssistant,
    webhook_config_entry: MockConfigEntry,
    withings: AsyncMock,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test if set up with active cloud subscription."""
    await mock_cloud(hass)
    await hass.async_block_till_done()

    with (
        patch("homeassistant.components.cloud.async_is_logged_in", return_value=True),
        patch.object(cloud, "async_is_connected", return_value=True),
        patch.object(cloud, "async_active_subscription", return_value=True),
        patch(
            "homeassistant.components.cloud.async_create_cloudhook",
            return_value="https://hooks.nabu.casa/ABCD",
        ) as fake_create_cloudhook,
        patch(
            "homeassistant.components.withings.async_get_config_entry_implementation",
        ),
        patch(
            "homeassistant.components.cloud.async_delete_cloudhook"
        ) as fake_delete_cloudhook,
        patch("homeassistant.components.withings.webhook_generate_url"),
    ):
        await setup_integration(hass, webhook_config_entry)
        await prepare_webhook_setup(hass, freezer)

        assert cloud.async_active_subscription(hass) is True
        assert cloud.async_is_connected(hass) is True
        fake_create_cloudhook.assert_called_once()
        fake_delete_cloudhook.assert_called_once()

        assert (
            hass.config_entries.async_entries("withings")[0].data["cloudhook_url"]
            == "https://hooks.nabu.casa/ABCD"
        )

        await hass.async_block_till_done()
        assert hass.config_entries.async_entries(DOMAIN)

        for config_entry in hass.config_entries.async_entries("withings"):
            await hass.config_entries.async_remove(config_entry.entry_id)
            assert fake_delete_cloudhook.call_count == 2

        await hass.async_block_till_done()
        assert not hass.config_entries.async_entries(DOMAIN)


@pytest.mark.parametrize("url", ["http://example.com", "https://example.com:444"])
async def test_setup_no_webhook(
    hass: HomeAssistant,
    webhook_config_entry: MockConfigEntry,
    withings: AsyncMock,
    caplog: pytest.LogCaptureFixture,
    freezer: FrozenDateTimeFactory,
    url: str,
) -> None:
    """Test if set up with cloud link and without https."""
    hass.config.components.add("cloud")
    with (
        patch(
            "homeassistant.helpers.network.get_url",
            return_value="http://example.nabu.casa",
        ),
        patch(
            "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
        ),
        patch(
            "homeassistant.components.withings.webhook_generate_url"
        ) as mock_async_generate_url,
    ):
        mock_async_generate_url.return_value = url
        await setup_integration(hass, webhook_config_entry)
        await prepare_webhook_setup(hass, freezer)

        await hass.async_block_till_done()
        mock_async_generate_url.assert_called_once()

    assert "https and port 443 is required to register the webhook" in caplog.text


async def test_cloud_disconnect(
    hass: HomeAssistant,
    withings: AsyncMock,
    webhook_config_entry: MockConfigEntry,
    hass_client_no_auth: ClientSessionGenerator,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test disconnecting from the cloud."""
    await mock_cloud(hass)
    await hass.async_block_till_done()

    with (
        patch("homeassistant.components.cloud.async_is_logged_in", return_value=True),
        patch.object(cloud, "async_is_connected", return_value=True),
        patch.object(cloud, "async_active_subscription", return_value=True),
        patch(
            "homeassistant.components.cloud.async_create_cloudhook",
            return_value="https://hooks.nabu.casa/ABCD",
        ),
        patch(
            "homeassistant.components.withings.async_get_config_entry_implementation",
        ),
        patch(
            "homeassistant.components.cloud.async_delete_cloudhook",
        ),
        patch(
            "homeassistant.components.withings.webhook_generate_url",
        ),
    ):
        await setup_integration(hass, webhook_config_entry)
        await prepare_webhook_setup(hass, freezer)

        assert cloud.async_active_subscription(hass) is True
        assert cloud.async_is_connected(hass) is True

        await hass.async_block_till_done()

        withings.list_notification_configurations.return_value = []

        assert withings.subscribe_notification.call_count == 6

        async_mock_cloud_connection_status(hass, False)
        await hass.async_block_till_done()

        assert withings.revoke_notification_configurations.call_count == 3

        async_mock_cloud_connection_status(hass, True)
        await hass.async_block_till_done()

        assert withings.subscribe_notification.call_count == 12


async def test_internet_disconnect(
    hass: HomeAssistant,
    withings: AsyncMock,
    webhook_config_entry: MockConfigEntry,
    hass_client_no_auth: ClientSessionGenerator,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test we can recover from internet disconnects."""
    await mock_cloud(hass)
    await hass.async_block_till_done()

    with (
        patch("homeassistant.components.cloud.async_is_logged_in", return_value=True),
        patch.object(cloud, "async_is_connected", return_value=True),
        patch.object(cloud, "async_active_subscription", return_value=True),
        patch(
            "homeassistant.components.cloud.async_create_cloudhook",
            return_value="https://hooks.nabu.casa/ABCD",
        ),
        patch(
            "homeassistant.components.withings.async_get_config_entry_implementation",
        ),
        patch(
            "homeassistant.components.cloud.async_delete_cloudhook",
        ),
        patch(
            "homeassistant.components.withings.webhook_generate_url",
        ),
    ):
        await setup_integration(hass, webhook_config_entry)
        await prepare_webhook_setup(hass, freezer)

        assert cloud.async_active_subscription(hass) is True
        assert cloud.async_is_connected(hass) is True
        assert withings.revoke_notification_configurations.call_count == 3
        assert withings.subscribe_notification.call_count == 6

        await hass.async_block_till_done()

        withings.list_notification_configurations.side_effect = ClientConnectionError

        async_mock_cloud_connection_status(hass, False)
        await hass.async_block_till_done()

        assert withings.revoke_notification_configurations.call_count == 3

        async_mock_cloud_connection_status(hass, True)
        await hass.async_block_till_done()

        assert withings.subscribe_notification.call_count == 12


async def test_cloud_disconnect_retry(
    hass: HomeAssistant,
    withings: AsyncMock,
    webhook_config_entry: MockConfigEntry,
    hass_client_no_auth: ClientSessionGenerator,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test we retry to create webhook connection again after cloud disconnects."""
    await mock_cloud(hass)
    await hass.async_block_till_done()

    with (
        patch("homeassistant.components.cloud.async_is_logged_in", return_value=True),
        patch.object(cloud, "async_is_connected", return_value=True),
        patch.object(
            cloud, "async_active_subscription", return_value=True
        ) as mock_async_active_subscription,
        patch(
            "homeassistant.components.cloud.async_create_cloudhook",
            return_value="https://hooks.nabu.casa/ABCD",
        ),
        patch(
            "homeassistant.components.withings.async_get_config_entry_implementation",
        ),
        patch(
            "homeassistant.components.cloud.async_delete_cloudhook",
        ),
        patch(
            "homeassistant.components.withings.webhook_generate_url",
        ),
    ):
        await setup_integration(hass, webhook_config_entry)
        await prepare_webhook_setup(hass, freezer)

        assert cloud.async_active_subscription(hass) is True
        assert cloud.async_is_connected(hass) is True
        assert mock_async_active_subscription.call_count == 3

        await hass.async_block_till_done()

        async_mock_cloud_connection_status(hass, False)
        await hass.async_block_till_done()

        assert mock_async_active_subscription.call_count == 3

        freezer.tick(timedelta(seconds=30))
        async_fire_time_changed(hass)
        await hass.async_block_till_done()

        assert mock_async_active_subscription.call_count == 4


@pytest.mark.parametrize(
    ("body", "expected_code"),
    [
        ({"userid": 0, "appli": NotificationCategory.WEIGHT.value}, 0),  # Success
        ({"userid": None, "appli": 1}, 0),  # Success, we ignore the user_id.
        ({}, 12),  # No request body.
        ({"userid": "GG"}, 20),  # appli not provided.
        ({"userid": 0}, 20),  # appli not provided.
        (
            {"userid": 11, "appli": NotificationCategory.WEIGHT.value},
            0,
        ),  # Success, we ignore the user_id
    ],
)
@pytest.mark.usefixtures("current_request_with_host")
async def test_webhook_post(
    hass: HomeAssistant,
    withings: AsyncMock,
    webhook_config_entry: MockConfigEntry,
    hass_client_no_auth: ClientSessionGenerator,
    body: dict[str, Any],
    expected_code: int,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test webhook callback."""
    await setup_integration(hass, webhook_config_entry)
    await prepare_webhook_setup(hass, freezer)
    client = await hass_client_no_auth()
    webhook_url = async_generate_url(hass, WEBHOOK_ID)

    resp = await client.post(urlparse(webhook_url).path, data=body)

    # Wait for remaining tasks to complete.
    await hass.async_block_till_done()

    data = await resp.json()
    resp.close()

    assert data["code"] == expected_code
