"""The tests for the MQTT statestream component."""
from unittest.mock import ANY, call, patch

import homeassistant.components.mqtt_statestream as statestream
from homeassistant.core import State
from homeassistant.setup import setup_component

from tests.common import (
    get_test_home_assistant,
    mock_mqtt_component,
    mock_state_change_event,
)


class TestMqttStateStream:
    """Test the MQTT statestream module."""

    def setup_method(self):
        """Set up things to be run when tests are started."""
        self.hass = get_test_home_assistant()
        self.mock_mqtt = mock_mqtt_component(self.hass)

    def teardown_method(self):
        """Stop everything that was started."""
        self.hass.stop()

    def add_statestream(
        self,
        base_topic=None,
        publish_attributes=None,
        publish_timestamps=None,
        publish_include=None,
        publish_exclude=None,
    ):
        """Add a mqtt_statestream component."""
        config = {}
        if base_topic:
            config["base_topic"] = base_topic
        if publish_attributes:
            config["publish_attributes"] = publish_attributes
        if publish_timestamps:
            config["publish_timestamps"] = publish_timestamps
        if publish_include:
            config["include"] = publish_include
        if publish_exclude:
            config["exclude"] = publish_exclude
        return setup_component(
            self.hass, statestream.DOMAIN, {statestream.DOMAIN: config}
        )

    def test_fails_with_no_base(self):
        """Setup should fail if no base_topic is set."""
        assert self.add_statestream() is False

    def test_setup_succeeds_without_attributes(self):
        """Test the success of the setup with a valid base_topic."""
        assert self.add_statestream(base_topic="pub")

    def test_setup_succeeds_with_attributes(self):
        """Test setup with a valid base_topic and publish_attributes."""
        assert self.add_statestream(base_topic="pub", publish_attributes=True)

    @patch("homeassistant.components.mqtt.async_publish")
    @patch("homeassistant.core.dt_util.utcnow")
    def test_state_changed_event_sends_message(self, mock_utcnow, mock_pub):
        """Test the sending of a new message if event changed."""
        e_id = "fake.entity"
        base_topic = "pub"

        # Add the statestream component for publishing state updates
        assert self.add_statestream(base_topic=base_topic)
        self.hass.block_till_done()

        # Reset the mock because it will have already gotten calls for the
        # mqtt_statestream state change on initialization, etc.
        mock_pub.reset_mock()

        # Set a state of an entity
        mock_state_change_event(self.hass, State(e_id, "on"))
        self.hass.block_till_done()

        # Make sure 'on' was published to pub/fake/entity/state
        mock_pub.assert_called_with(self.hass, "pub/fake/entity/state", "on", 1, True)
        assert mock_pub.called

    @patch("homeassistant.components.mqtt.async_publish")
    @patch("homeassistant.core.dt_util.utcnow")
    def test_state_changed_event_sends_message_and_timestamp(
        self, mock_utcnow, mock_pub
    ):
        """Test the sending of a message and timestamps if event changed."""
        e_id = "another.entity"
        base_topic = "pub"

        # Add the statestream component for publishing state updates
        assert self.add_statestream(
            base_topic=base_topic, publish_attributes=None, publish_timestamps=True
        )
        self.hass.block_till_done()

        # Reset the mock because it will have already gotten calls for the
        # mqtt_statestream state change on initialization, etc.
        mock_pub.reset_mock()

        # Set a state of an entity
        mock_state_change_event(self.hass, State(e_id, "on"))
        self.hass.block_till_done()

        # Make sure 'on' was published to pub/fake/entity/state
        calls = [
            call.async_publish(self.hass, "pub/another/entity/state", "on", 1, True),
            call.async_publish(
                self.hass, "pub/another/entity/last_changed", ANY, 1, True
            ),
            call.async_publish(
                self.hass, "pub/another/entity/last_updated", ANY, 1, True
            ),
        ]

        mock_pub.assert_has_calls(calls, any_order=True)
        assert mock_pub.called

    @patch("homeassistant.components.mqtt.async_publish")
    @patch("homeassistant.core.dt_util.utcnow")
    def test_state_changed_attr_sends_message(self, mock_utcnow, mock_pub):
        """Test the sending of a new message if attribute changed."""
        e_id = "fake.entity"
        base_topic = "pub"

        # Add the statestream component for publishing state updates
        assert self.add_statestream(base_topic=base_topic, publish_attributes=True)
        self.hass.block_till_done()

        # Reset the mock because it will have already gotten calls for the
        # mqtt_statestream state change on initialization, etc.
        mock_pub.reset_mock()

        test_attributes = {"testing": "YES", "list": ["a", "b", "c"], "bool": False}

        # Set a state of an entity
        mock_state_change_event(
            self.hass, State(e_id, "off", attributes=test_attributes)
        )
        self.hass.block_till_done()

        # Make sure 'on' was published to pub/fake/entity/state
        calls = [
            call.async_publish(self.hass, "pub/fake/entity/state", "off", 1, True),
            call.async_publish(self.hass, "pub/fake/entity/testing", '"YES"', 1, True),
            call.async_publish(
                self.hass, "pub/fake/entity/list", '["a", "b", "c"]', 1, True
            ),
            call.async_publish(self.hass, "pub/fake/entity/bool", "false", 1, True),
        ]

        mock_pub.assert_has_calls(calls, any_order=True)
        assert mock_pub.called

    @patch("homeassistant.components.mqtt.async_publish")
    @patch("homeassistant.core.dt_util.utcnow")
    def test_state_changed_event_include_domain(self, mock_utcnow, mock_pub):
        """Test that filtering on included domain works as expected."""
        base_topic = "pub"

        incl = {"domains": ["fake"]}
        excl = {}

        # Add the statestream component for publishing state updates
        # Set the filter to allow fake.* items
        assert self.add_statestream(
            base_topic=base_topic, publish_include=incl, publish_exclude=excl
        )
        self.hass.block_till_done()

        # Reset the mock because it will have already gotten calls for the
        # mqtt_statestream state change on initialization, etc.
        mock_pub.reset_mock()

        # Set a state of an entity
        mock_state_change_event(self.hass, State("fake.entity", "on"))
        self.hass.block_till_done()

        # Make sure 'on' was published to pub/fake/entity/state
        mock_pub.assert_called_with(self.hass, "pub/fake/entity/state", "on", 1, True)
        assert mock_pub.called

        mock_pub.reset_mock()
        # Set a state of an entity that shouldn't be included
        mock_state_change_event(self.hass, State("fake2.entity", "on"))
        self.hass.block_till_done()

        assert not mock_pub.called

    @patch("homeassistant.components.mqtt.async_publish")
    @patch("homeassistant.core.dt_util.utcnow")
    def test_state_changed_event_include_entity(self, mock_utcnow, mock_pub):
        """Test that filtering on included entity works as expected."""
        base_topic = "pub"

        incl = {"entities": ["fake.entity"]}
        excl = {}

        # Add the statestream component for publishing state updates
        # Set the filter to allow fake.* items
        assert self.add_statestream(
            base_topic=base_topic, publish_include=incl, publish_exclude=excl
        )
        self.hass.block_till_done()

        # Reset the mock because it will have already gotten calls for the
        # mqtt_statestream state change on initialization, etc.
        mock_pub.reset_mock()

        # Set a state of an entity
        mock_state_change_event(self.hass, State("fake.entity", "on"))
        self.hass.block_till_done()

        # Make sure 'on' was published to pub/fake/entity/state
        mock_pub.assert_called_with(self.hass, "pub/fake/entity/state", "on", 1, True)
        assert mock_pub.called

        mock_pub.reset_mock()
        # Set a state of an entity that shouldn't be included
        mock_state_change_event(self.hass, State("fake.entity2", "on"))
        self.hass.block_till_done()

        assert not mock_pub.called

    @patch("homeassistant.components.mqtt.async_publish")
    @patch("homeassistant.core.dt_util.utcnow")
    def test_state_changed_event_exclude_domain(self, mock_utcnow, mock_pub):
        """Test that filtering on excluded domain works as expected."""
        base_topic = "pub"

        incl = {}
        excl = {"domains": ["fake2"]}

        # Add the statestream component for publishing state updates
        # Set the filter to allow fake.* items
        assert self.add_statestream(
            base_topic=base_topic, publish_include=incl, publish_exclude=excl
        )
        self.hass.block_till_done()

        # Reset the mock because it will have already gotten calls for the
        # mqtt_statestream state change on initialization, etc.
        mock_pub.reset_mock()

        # Set a state of an entity
        mock_state_change_event(self.hass, State("fake.entity", "on"))
        self.hass.block_till_done()

        # Make sure 'on' was published to pub/fake/entity/state
        mock_pub.assert_called_with(self.hass, "pub/fake/entity/state", "on", 1, True)
        assert mock_pub.called

        mock_pub.reset_mock()
        # Set a state of an entity that shouldn't be included
        mock_state_change_event(self.hass, State("fake2.entity", "on"))
        self.hass.block_till_done()

        assert not mock_pub.called

    @patch("homeassistant.components.mqtt.async_publish")
    @patch("homeassistant.core.dt_util.utcnow")
    def test_state_changed_event_exclude_entity(self, mock_utcnow, mock_pub):
        """Test that filtering on excluded entity works as expected."""
        base_topic = "pub"

        incl = {}
        excl = {"entities": ["fake.entity2"]}

        # Add the statestream component for publishing state updates
        # Set the filter to allow fake.* items
        assert self.add_statestream(
            base_topic=base_topic, publish_include=incl, publish_exclude=excl
        )
        self.hass.block_till_done()

        # Reset the mock because it will have already gotten calls for the
        # mqtt_statestream state change on initialization, etc.
        mock_pub.reset_mock()

        # Set a state of an entity
        mock_state_change_event(self.hass, State("fake.entity", "on"))
        self.hass.block_till_done()

        # Make sure 'on' was published to pub/fake/entity/state
        mock_pub.assert_called_with(self.hass, "pub/fake/entity/state", "on", 1, True)
        assert mock_pub.called

        mock_pub.reset_mock()
        # Set a state of an entity that shouldn't be included
        mock_state_change_event(self.hass, State("fake.entity2", "on"))
        self.hass.block_till_done()

        assert not mock_pub.called

    @patch("homeassistant.components.mqtt.async_publish")
    @patch("homeassistant.core.dt_util.utcnow")
    def test_state_changed_event_exclude_domain_include_entity(
        self, mock_utcnow, mock_pub
    ):
        """Test filtering with excluded domain and included entity."""
        base_topic = "pub"

        incl = {"entities": ["fake.entity"]}
        excl = {"domains": ["fake"]}

        # Add the statestream component for publishing state updates
        # Set the filter to allow fake.* items
        assert self.add_statestream(
            base_topic=base_topic, publish_include=incl, publish_exclude=excl
        )
        self.hass.block_till_done()

        # Reset the mock because it will have already gotten calls for the
        # mqtt_statestream state change on initialization, etc.
        mock_pub.reset_mock()

        # Set a state of an entity
        mock_state_change_event(self.hass, State("fake.entity", "on"))
        self.hass.block_till_done()

        # Make sure 'on' was published to pub/fake/entity/state
        mock_pub.assert_called_with(self.hass, "pub/fake/entity/state", "on", 1, True)
        assert mock_pub.called

        mock_pub.reset_mock()
        # Set a state of an entity that shouldn't be included
        mock_state_change_event(self.hass, State("fake.entity2", "on"))
        self.hass.block_till_done()

        assert not mock_pub.called

    @patch("homeassistant.components.mqtt.async_publish")
    @patch("homeassistant.core.dt_util.utcnow")
    def test_state_changed_event_include_domain_exclude_entity(
        self, mock_utcnow, mock_pub
    ):
        """Test filtering with included domain and excluded entity."""
        base_topic = "pub"

        incl = {"domains": ["fake"]}
        excl = {"entities": ["fake.entity2"]}

        # Add the statestream component for publishing state updates
        # Set the filter to allow fake.* items
        assert self.add_statestream(
            base_topic=base_topic, publish_include=incl, publish_exclude=excl
        )
        self.hass.block_till_done()

        # Reset the mock because it will have already gotten calls for the
        # mqtt_statestream state change on initialization, etc.
        mock_pub.reset_mock()

        # Set a state of an entity
        mock_state_change_event(self.hass, State("fake.entity", "on"))
        self.hass.block_till_done()

        # Make sure 'on' was published to pub/fake/entity/state
        mock_pub.assert_called_with(self.hass, "pub/fake/entity/state", "on", 1, True)
        assert mock_pub.called

        mock_pub.reset_mock()
        # Set a state of an entity that shouldn't be included
        mock_state_change_event(self.hass, State("fake.entity2", "on"))
        self.hass.block_till_done()

        assert not mock_pub.called