"""Tests for the Sonos config flow."""

import asyncio
from http import HTTPStatus
import logging
from unittest.mock import Mock, PropertyMock, patch

from freezegun.api import FrozenDateTimeFactory
import pytest
from requests import Response
from requests.exceptions import HTTPError

from homeassistant import config_entries
from homeassistant.components import sonos
from homeassistant.components.sonos.const import (
    DISCOVERY_INTERVAL,
    SONOS_SPEAKER_ACTIVITY,
    UPNP_ISSUE_ID,
)
from homeassistant.components.sonos.exception import SonosUpdateError
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import entity_registry as er, issue_registry as ir
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util

from .conftest import MockSoCo, SoCoMockFactory

from tests.common import MockConfigEntry, async_fire_time_changed


async def test_creating_entry_sets_up_media_player(
    hass: HomeAssistant, zeroconf_payload: ZeroconfServiceInfo
) -> None:
    """Test setting up Sonos loads the media player."""

    # Initiate a discovery to allow a user config flow
    await hass.config_entries.flow.async_init(
        sonos.DOMAIN,
        context={"source": config_entries.SOURCE_ZEROCONF},
        data=zeroconf_payload,
    )

    with patch(
        "homeassistant.components.sonos.media_player.async_setup_entry",
    ) as mock_setup:
        result = await hass.config_entries.flow.async_init(
            sonos.DOMAIN, context={"source": config_entries.SOURCE_USER}
        )

        # Confirmation form
        assert result["type"] is FlowResultType.FORM

        result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
        assert result["type"] is FlowResultType.CREATE_ENTRY

        await hass.async_block_till_done(wait_background_tasks=True)

    assert len(mock_setup.mock_calls) == 1


async def test_configuring_sonos_creates_entry(hass: HomeAssistant) -> None:
    """Test that specifying config will create an entry."""
    with patch(
        "homeassistant.components.sonos.async_setup_entry",
        return_value=True,
    ) as mock_setup:
        await async_setup_component(
            hass,
            sonos.DOMAIN,
            {"sonos": {"media_player": {"interface_addr": "127.0.0.1"}}},
        )
        await hass.async_block_till_done()

    assert len(mock_setup.mock_calls) == 1


async def test_not_configuring_sonos_not_creates_entry(hass: HomeAssistant) -> None:
    """Test that no config will not create an entry."""
    with patch(
        "homeassistant.components.sonos.async_setup_entry",
        return_value=True,
    ) as mock_setup:
        await async_setup_component(hass, sonos.DOMAIN, {})
        await hass.async_block_till_done()

    assert len(mock_setup.mock_calls) == 0


async def test_upnp_disabled_discovery(
    hass: HomeAssistant, config_entry: MockConfigEntry, soco: MockSoCo
) -> None:
    """Test issue creation when discovery processing fails with 403."""

    resp = Response()
    resp.status_code = HTTPStatus.FORBIDDEN
    http_error = HTTPError(response=resp)

    with patch(
        "tests.components.sonos.conftest.MockSoCo.household_id",
        new_callable=PropertyMock,
        create=True,
        side_effect=http_error,
    ):
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done(wait_background_tasks=True)

    issue_registry = ir.async_get(hass)
    assert (
        issue_registry.async_get_issue(
            sonos.DOMAIN, f"{UPNP_ISSUE_ID}_{soco.ip_address}"
        )
        is not None
    )


async def test_upnp_disabled_manual_hosts(
    hass: HomeAssistant,
    soco_factory: SoCoMockFactory,
) -> None:
    """Test issue creation when manual host processing fails with 403."""

    resp = Response()
    resp.status_code = HTTPStatus.FORBIDDEN
    http_error = HTTPError(response=resp)
    soco = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Bedroom")

    with patch.object(
        type(soco),
        "household_id",
        new_callable=PropertyMock,
        create=True,
        side_effect=http_error,
    ):
        await _setup_hass(hass)

    issue_registry = ir.async_get(hass)
    issue = issue_registry.async_get_issue(
        sonos.DOMAIN, f"{UPNP_ISSUE_ID}_{soco.ip_address}"
    )
    assert issue is not None
    assert issue.translation_placeholders.get("device_ip") == "10.10.10.1"


async def test_discovery_exception(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test exception handling during discovery processing."""

    with patch(
        "tests.components.sonos.conftest.MockSoCo.household_id",
        new_callable=PropertyMock,
        create=True,
        side_effect=OSError("This is a test"),
    ):
        caplog.set_level(logging.ERROR)
        caplog.clear()
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done(wait_background_tasks=True)
        assert "This is a test" in caplog.text


async def test_async_poll_manual_hosts_warnings(
    hass: HomeAssistant,
    caplog: pytest.LogCaptureFixture,
    soco_factory: SoCoMockFactory,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test that host warnings are not logged repeatedly."""

    soco = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Bedroom")
    with (
        caplog.at_level(logging.DEBUG),
        patch.object(
            type(soco), "visible_zones", new_callable=PropertyMock
        ) as mock_visible_zones,
    ):
        # First call fails, it should be logged as a WARNING message
        mock_visible_zones.side_effect = OSError()
        caplog.clear()
        await _setup_hass(hass)
        assert [
            rec.levelname
            for rec in caplog.records
            if "Could not get visible Sonos devices from" in rec.message
        ] == ["WARNING"]

        # Second call fails again, it should be logged as a DEBUG message
        mock_visible_zones.side_effect = OSError()
        caplog.clear()
        freezer.tick(DISCOVERY_INTERVAL)
        async_fire_time_changed(hass)
        await hass.async_block_till_done(wait_background_tasks=True)
        assert [
            rec.levelname
            for rec in caplog.records
            if "Could not get visible Sonos devices from" in rec.message
        ] == ["DEBUG"]

        # Third call succeeds, logs message indicating reconnect
        mock_visible_zones.return_value = {soco}
        mock_visible_zones.side_effect = None
        caplog.clear()
        freezer.tick(DISCOVERY_INTERVAL)
        async_fire_time_changed(hass)
        await hass.async_block_till_done(wait_background_tasks=True)
        assert [
            rec.levelname
            for rec in caplog.records
            if "Connection reestablished to Sonos device" in rec.message
        ] == ["WARNING"]

        # Fourth call succeeds, it should log nothing
        caplog.clear()
        freezer.tick(DISCOVERY_INTERVAL)
        async_fire_time_changed(hass)
        await hass.async_block_till_done(wait_background_tasks=True)
        assert "Connection reestablished to Sonos device" not in caplog.text

        # Fifth call fails again again, should be logged as a WARNING message
        mock_visible_zones.side_effect = OSError()
        caplog.clear()
        freezer.tick(DISCOVERY_INTERVAL)
        async_fire_time_changed(hass)
        await hass.async_block_till_done()
        assert [
            rec.levelname
            for rec in caplog.records
            if "Could not get visible Sonos devices from" in rec.message
        ] == ["WARNING"]


class _MockSoCoOsError(MockSoCo):
    @property
    def visible_zones(self):
        raise OSError


class _MockSoCoVisibleZones(MockSoCo):
    def set_visible_zones(self, visible_zones) -> None:
        """Set visible zones."""
        self.vz_return = visible_zones  # pylint: disable=attribute-defined-outside-init

    @property
    def visible_zones(self):
        return self.vz_return


async def _setup_hass(hass: HomeAssistant):
    await async_setup_component(
        hass,
        sonos.DOMAIN,
        {
            "sonos": {
                "media_player": {
                    "interface_addr": "127.0.0.1",
                    "hosts": ["10.10.10.1", "10.10.10.2"],
                }
            }
        },
    )
    await hass.async_block_till_done()


async def test_async_poll_manual_hosts_1(
    hass: HomeAssistant,
    soco_factory: SoCoMockFactory,
    entity_registry: er.EntityRegistry,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Tests first device fails, second device successful, speakers do not exist."""
    soco_1 = soco_factory.cache_mock(_MockSoCoOsError(), "10.10.10.1", "Living Room")
    soco_2 = soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom")

    with caplog.at_level(logging.WARNING):
        await _setup_hass(hass)
        assert "media_player.bedroom" in entity_registry.entities
        assert "media_player.living_room" not in entity_registry.entities
        assert (
            f"Could not get visible Sonos devices from {soco_1.ip_address}"
            in caplog.text
        )
        assert (
            f"Could not get visible Sonos devices from {soco_2.ip_address}"
            not in caplog.text
        )

    await hass.async_block_till_done(wait_background_tasks=True)


async def test_async_poll_manual_hosts_2(
    hass: HomeAssistant,
    soco_factory: SoCoMockFactory,
    entity_registry: er.EntityRegistry,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test first device success, second device fails, speakers do not exist."""
    soco_1 = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Living Room")
    soco_2 = soco_factory.cache_mock(_MockSoCoOsError(), "10.10.10.2", "Bedroom")

    with caplog.at_level(logging.WARNING):
        await _setup_hass(hass)
        assert "media_player.bedroom" not in entity_registry.entities
        assert "media_player.living_room" in entity_registry.entities
        assert (
            f"Could not get visible Sonos devices from {soco_1.ip_address}"
            not in caplog.text
        )
        assert (
            f"Could not get visible Sonos devices from {soco_2.ip_address}"
            in caplog.text
        )

    await hass.async_block_till_done(wait_background_tasks=True)


async def test_async_poll_manual_hosts_3(
    hass: HomeAssistant,
    soco_factory: SoCoMockFactory,
    entity_registry: er.EntityRegistry,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test both devices fail, speakers do not exist."""
    soco_1 = soco_factory.cache_mock(_MockSoCoOsError(), "10.10.10.1", "Living Room")
    soco_2 = soco_factory.cache_mock(_MockSoCoOsError(), "10.10.10.2", "Bedroom")

    with caplog.at_level(logging.WARNING):
        await _setup_hass(hass)
        assert "media_player.bedroom" not in entity_registry.entities
        assert "media_player.living_room" not in entity_registry.entities
        assert (
            f"Could not get visible Sonos devices from {soco_1.ip_address}"
            in caplog.text
        )
        assert (
            f"Could not get visible Sonos devices from {soco_2.ip_address}"
            in caplog.text
        )

    await hass.async_block_till_done(wait_background_tasks=True)


async def test_async_poll_manual_hosts_4(
    hass: HomeAssistant,
    soco_factory: SoCoMockFactory,
    entity_registry: er.EntityRegistry,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test both devices are successful, speakers do not exist."""
    soco_1 = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Living Room")
    soco_2 = soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom")

    with caplog.at_level(logging.WARNING):
        await _setup_hass(hass)
        assert "media_player.bedroom" in entity_registry.entities
        assert "media_player.living_room" in entity_registry.entities
        assert (
            f"Could not get visible Sonos devices from {soco_1.ip_address}"
            not in caplog.text
        )
        assert (
            f"Could not get visible Sonos devices from {soco_2.ip_address}"
            not in caplog.text
        )

    await hass.async_block_till_done(wait_background_tasks=True)


class SpeakerActivity:
    """Unit test class to track speaker activity messages."""

    def __init__(self, hass: HomeAssistant, soco: MockSoCo) -> None:
        """Create the object from soco."""
        self.soco = soco
        self.hass = hass
        self.call_count: int = 0
        self.event = asyncio.Event()
        async_dispatcher_connect(
            self.hass,
            f"{SONOS_SPEAKER_ACTIVITY}-{self.soco.uid}",
            self.speaker_activity,
        )

    @callback
    def speaker_activity(self, source: str) -> None:
        """Track the last activity on this speaker, set availability and resubscribe."""
        if source == "manual zone scan":
            self.event.set()
            self.call_count += 1


async def test_async_poll_manual_hosts_5(
    hass: HomeAssistant,
    soco_factory: SoCoMockFactory,
    entity_registry: er.EntityRegistry,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test both succeed, speakers exist and unavailable, ping succeeds."""
    soco_1 = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Living Room")
    soco_1.renderingControl = Mock()
    soco_1.renderingControl.GetVolume = Mock()
    speaker_1_activity = SpeakerActivity(hass, soco_1)
    soco_2 = soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom")
    soco_2.renderingControl = Mock()
    soco_2.renderingControl.GetVolume = Mock()
    speaker_2_activity = SpeakerActivity(hass, soco_2)

    with caplog.at_level(logging.DEBUG):
        caplog.clear()

        await _setup_hass(hass)

        assert "media_player.bedroom" in entity_registry.entities
        assert "media_player.living_room" in entity_registry.entities

        async_fire_time_changed(hass, dt_util.utcnow() + DISCOVERY_INTERVAL)
        await hass.async_block_till_done()
        await asyncio.gather(
            *[speaker_1_activity.event.wait(), speaker_2_activity.event.wait()]
        )
        assert speaker_1_activity.call_count == 1
        assert speaker_2_activity.call_count == 1
        assert "Activity on Living Room" in caplog.text
        assert "Activity on Bedroom" in caplog.text

    await hass.async_block_till_done(wait_background_tasks=True)


async def test_async_poll_manual_hosts_6(
    hass: HomeAssistant,
    soco_factory: SoCoMockFactory,
    entity_registry: er.EntityRegistry,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test both succeed, speakers exist and unavailable, pings fail."""
    soco_1 = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Living Room")
    # Rendering Control Get Volume is what speaker ping calls.
    soco_1.renderingControl = Mock()
    soco_1.renderingControl.GetVolume = Mock()
    soco_1.renderingControl.GetVolume.side_effect = SonosUpdateError()
    speaker_1_activity = SpeakerActivity(hass, soco_1)
    soco_2 = soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom")
    soco_2.renderingControl = Mock()
    soco_2.renderingControl.GetVolume = Mock()
    soco_2.renderingControl.GetVolume.side_effect = SonosUpdateError()
    speaker_2_activity = SpeakerActivity(hass, soco_2)

    with patch(
        "homeassistant.components.sonos.DISCOVERY_INTERVAL"
    ) as mock_discovery_interval:
        # Speed up manual discovery interval so second iteration runs sooner
        mock_discovery_interval.total_seconds = Mock(side_effect=[0.0, 60])
        await _setup_hass(hass)

        assert "media_player.bedroom" in entity_registry.entities
        assert "media_player.living_room" in entity_registry.entities

        with caplog.at_level(logging.DEBUG):
            caplog.clear()
            await hass.async_block_till_done()
            assert "Activity on Living Room" not in caplog.text
            assert "Activity on Bedroom" not in caplog.text
            assert speaker_1_activity.call_count == 0
            assert speaker_2_activity.call_count == 0

    await hass.async_block_till_done(wait_background_tasks=True)


async def test_async_poll_manual_hosts_7(
    hass: HomeAssistant,
    soco_factory: SoCoMockFactory,
    entity_registry: er.EntityRegistry,
) -> None:
    """Test both succeed, speaker do not exist, new hosts found in visible zones."""
    soco_1 = soco_factory.cache_mock(
        _MockSoCoVisibleZones(), "10.10.10.1", "Living Room"
    )
    soco_2 = soco_factory.cache_mock(_MockSoCoVisibleZones(), "10.10.10.2", "Bedroom")
    soco_3 = soco_factory.cache_mock(MockSoCo(), "10.10.10.3", "Basement")
    soco_4 = soco_factory.cache_mock(MockSoCo(), "10.10.10.4", "Garage")
    soco_5 = soco_factory.cache_mock(MockSoCo(), "10.10.10.5", "Studio")

    soco_1.set_visible_zones({soco_1, soco_2, soco_3, soco_4, soco_5})
    soco_2.set_visible_zones({soco_1, soco_2, soco_3, soco_4, soco_5})

    await _setup_hass(hass)
    await hass.async_block_till_done()

    assert "media_player.bedroom" in entity_registry.entities
    assert "media_player.living_room" in entity_registry.entities
    assert "media_player.basement" in entity_registry.entities
    assert "media_player.garage" in entity_registry.entities
    assert "media_player.studio" in entity_registry.entities

    await hass.async_block_till_done(wait_background_tasks=True)


async def test_async_poll_manual_hosts_8(
    hass: HomeAssistant,
    soco_factory: SoCoMockFactory,
    entity_registry: er.EntityRegistry,
) -> None:
    """Test both succeed, speaker do not exist, invisible zone."""
    soco_1 = soco_factory.cache_mock(
        _MockSoCoVisibleZones(), "10.10.10.1", "Living Room"
    )
    soco_2 = soco_factory.cache_mock(_MockSoCoVisibleZones(), "10.10.10.2", "Bedroom")
    soco_3 = soco_factory.cache_mock(MockSoCo(), "10.10.10.3", "Basement")
    soco_4 = soco_factory.cache_mock(MockSoCo(), "10.10.10.4", "Garage")
    soco_5 = soco_factory.cache_mock(MockSoCo(), "10.10.10.5", "Studio")

    soco_1.set_visible_zones({soco_2, soco_3, soco_4, soco_5})
    soco_2.set_visible_zones({soco_2, soco_3, soco_4, soco_5})

    await _setup_hass(hass)
    await hass.async_block_till_done()

    assert "media_player.bedroom" in entity_registry.entities
    assert "media_player.living_room" not in entity_registry.entities
    assert "media_player.basement" in entity_registry.entities
    assert "media_player.garage" in entity_registry.entities
    assert "media_player.studio" in entity_registry.entities
    await hass.async_block_till_done(wait_background_tasks=True)


async def _setup_hass_ipv6_address_not_supported(hass: HomeAssistant):
    await async_setup_component(
        hass,
        sonos.DOMAIN,
        {
            "sonos": {
                "media_player": {
                    "interface_addr": "127.0.0.1",
                    "hosts": ["2001:db8:3333:4444:5555:6666:7777:8888"],
                }
            }
        },
    )
    await hass.async_block_till_done()


async def test_ipv6_not_supported(
    hass: HomeAssistant,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Tests that invalid ipv4 addresses do not generate stack dump."""
    with caplog.at_level(logging.DEBUG):
        caplog.clear()
        await _setup_hass_ipv6_address_not_supported(hass)
        await hass.async_block_till_done()
    assert "invalid ip_address received" in caplog.text
    assert "2001:db8:3333:4444:5555:6666:7777:8888" in caplog.text
