"""The tests for the Google Pub/Sub component."""

from collections.abc import Generator
from dataclasses import dataclass
from datetime import datetime
import os
from typing import Any
from unittest.mock import MagicMock, Mock, patch

import pytest

from homeassistant.components import google_pubsub
from homeassistant.components.google_pubsub import DateTimeJSONEncoder as victim
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component

GOOGLE_PUBSUB_PATH = "homeassistant.components.google_pubsub"


@dataclass
class FilterTest:
    """Class for capturing a filter test."""

    id: str
    should_pass: bool


async def test_datetime() -> None:
    """Test datetime encoding."""
    time = datetime(2019, 1, 13, 12, 30, 5)
    assert victim().encode(time) == '"2019-01-13T12:30:05"'


async def test_no_datetime() -> None:
    """Test integer encoding."""
    assert victim().encode(42) == "42"


async def test_nested() -> None:
    """Test dictionary encoding."""
    assert victim().encode({"foo": "bar"}) == '{"foo": "bar"}'


@pytest.fixture(autouse=True, name="mock_client")
def mock_client_fixture() -> Generator[MagicMock]:
    """Mock the pubsub client."""
    with patch(f"{GOOGLE_PUBSUB_PATH}.PublisherClient") as client:
        client.from_service_account_json = MagicMock(return_value=MagicMock())
        yield client


@pytest.fixture(autouse=True, name="mock_is_file")
def mock_is_file_fixture() -> Generator[MagicMock]:
    """Mock os.path.isfile."""
    with patch(f"{GOOGLE_PUBSUB_PATH}.os.path.isfile") as is_file:
        is_file.return_value = True
        yield is_file


@pytest.fixture(autouse=True)
def mock_json(monkeypatch: pytest.MonkeyPatch) -> None:
    """Mock the event bus listener and os component."""
    monkeypatch.setattr(
        f"{GOOGLE_PUBSUB_PATH}.json.dumps", Mock(return_value=MagicMock())
    )


async def test_minimal_config(hass: HomeAssistant, mock_client) -> None:
    """Test the minimal config and defaults of component."""
    config = {
        google_pubsub.DOMAIN: {
            "project_id": "proj",
            "topic_name": "topic",
            "credentials_json": "creds",
            "filter": {},
        }
    }
    assert await async_setup_component(hass, google_pubsub.DOMAIN, config)
    await hass.async_block_till_done()
    assert mock_client.from_service_account_json.call_count == 1
    assert mock_client.from_service_account_json.call_args[0][0] == os.path.join(
        hass.config.config_dir, "creds"
    )


async def test_full_config(hass: HomeAssistant, mock_client) -> None:
    """Test the full config of the component."""
    config = {
        google_pubsub.DOMAIN: {
            "project_id": "proj",
            "topic_name": "topic",
            "credentials_json": "creds",
            "filter": {
                "include_domains": ["light"],
                "include_entity_globs": ["sensor.included_*"],
                "include_entities": ["binary_sensor.included"],
                "exclude_domains": ["light"],
                "exclude_entity_globs": ["sensor.excluded_*"],
                "exclude_entities": ["binary_sensor.excluded"],
            },
        }
    }
    assert await async_setup_component(hass, google_pubsub.DOMAIN, config)
    await hass.async_block_till_done()
    assert mock_client.from_service_account_json.call_count == 1
    assert mock_client.from_service_account_json.call_args[0][0] == os.path.join(
        hass.config.config_dir, "creds"
    )


async def _setup(hass: HomeAssistant, filter_config: dict[str, Any]) -> None:
    """Shared set up for filtering tests."""
    config = {
        google_pubsub.DOMAIN: {
            "project_id": "proj",
            "topic_name": "topic",
            "credentials_json": "creds",
            "filter": filter_config,
        }
    }
    assert await async_setup_component(hass, google_pubsub.DOMAIN, config)
    await hass.async_block_till_done()


async def test_allowlist(hass: HomeAssistant, mock_client) -> None:
    """Test an allowlist only config."""
    await _setup(
        hass,
        {
            "include_domains": ["light"],
            "include_entity_globs": ["sensor.included_*"],
            "include_entities": ["binary_sensor.included"],
        },
    )
    publish_client = mock_client.from_service_account_json("path")

    tests = [
        FilterTest("climate.excluded", False),
        FilterTest("light.included", True),
        FilterTest("sensor.excluded_test", False),
        FilterTest("sensor.included_test", True),
        FilterTest("binary_sensor.included", True),
        FilterTest("binary_sensor.excluded", False),
    ]

    for test in tests:
        hass.states.async_set(test.id, "on")
        await hass.async_block_till_done()

        was_called = publish_client.publish.call_count == 1
        assert test.should_pass == was_called
        publish_client.publish.reset_mock()


async def test_denylist(hass: HomeAssistant, mock_client) -> None:
    """Test a denylist only config."""
    await _setup(
        hass,
        {
            "exclude_domains": ["climate"],
            "exclude_entity_globs": ["sensor.excluded_*"],
            "exclude_entities": ["binary_sensor.excluded"],
        },
    )
    publish_client = mock_client.from_service_account_json("path")

    tests = [
        FilterTest("climate.excluded", False),
        FilterTest("light.included", True),
        FilterTest("sensor.excluded_test", False),
        FilterTest("sensor.included_test", True),
        FilterTest("binary_sensor.included", True),
        FilterTest("binary_sensor.excluded", False),
    ]

    for test in tests:
        hass.states.async_set(test.id, "on")
        await hass.async_block_till_done()

        was_called = publish_client.publish.call_count == 1
        assert test.should_pass == was_called
        publish_client.publish.reset_mock()


async def test_filtered_allowlist(hass: HomeAssistant, mock_client) -> None:
    """Test an allowlist config with a filtering denylist."""
    await _setup(
        hass,
        {
            "include_domains": ["light"],
            "include_entity_globs": ["*.included_*"],
            "exclude_domains": ["climate"],
            "exclude_entity_globs": ["*.excluded_*"],
            "exclude_entities": ["light.excluded"],
        },
    )
    publish_client = mock_client.from_service_account_json("path")

    tests = [
        FilterTest("light.included", True),
        FilterTest("light.excluded_test", False),
        FilterTest("light.excluded", False),
        FilterTest("sensor.included_test", True),
        FilterTest("climate.included_test", True),
    ]

    for test in tests:
        hass.states.async_set(test.id, "not blank")
        await hass.async_block_till_done()

        was_called = publish_client.publish.call_count == 1
        assert test.should_pass == was_called
        publish_client.publish.reset_mock()


async def test_filtered_denylist(hass: HomeAssistant, mock_client) -> None:
    """Test a denylist config with a filtering allowlist."""
    await _setup(
        hass,
        {
            "include_entities": ["climate.included", "sensor.excluded_test"],
            "exclude_domains": ["climate"],
            "exclude_entity_globs": ["*.excluded_*"],
            "exclude_entities": ["light.excluded"],
        },
    )
    publish_client = mock_client.from_service_account_json("path")

    tests = [
        FilterTest("climate.excluded", False),
        FilterTest("climate.included", True),
        FilterTest("switch.excluded_test", False),
        FilterTest("sensor.excluded_test", True),
        FilterTest("light.excluded", False),
        FilterTest("light.included", True),
    ]

    for test in tests:
        hass.states.async_set(test.id, "not blank")
        await hass.async_block_till_done()

        was_called = publish_client.publish.call_count == 1
        assert test.should_pass == was_called
        publish_client.publish.reset_mock()
