"""Generic Philips Hue component tests."""
import asyncio
import logging
import unittest
from unittest.mock import call, MagicMock, patch

from homeassistant.components import configurator, hue
from homeassistant.const import CONF_FILENAME, CONF_HOST
from homeassistant.setup import setup_component, async_setup_component

from tests.common import (
    assert_setup_component, get_test_home_assistant, get_test_config_dir,
    MockDependency
)

_LOGGER = logging.getLogger(__name__)


class TestSetup(unittest.TestCase):
    """Test the Hue component."""

    def setUp(self):  # pylint: disable=invalid-name
        """Setup things to be run when tests are started."""
        self.hass = get_test_home_assistant()
        self.skip_teardown_stop = False

    def tearDown(self):
        """Stop everything that was started."""
        if not self.skip_teardown_stop:
            self.hass.stop()

    @MockDependency('phue')
    def test_setup_no_domain(self, mock_phue):
        """If it's not in the config we won't even try."""
        with assert_setup_component(0):
            self.assertTrue(setup_component(
                self.hass, hue.DOMAIN, {}))
            mock_phue.Bridge.assert_not_called()
            self.assertEqual({}, self.hass.data[hue.DOMAIN])

    @MockDependency('phue')
    def test_setup_with_host(self, mock_phue):
        """Host specified in the config file."""
        mock_bridge = mock_phue.Bridge

        with assert_setup_component(1):
            with patch('homeassistant.helpers.discovery.load_platform') \
                    as mock_load:
                self.assertTrue(setup_component(
                    self.hass, hue.DOMAIN,
                    {hue.DOMAIN: {hue.CONF_BRIDGES: [
                        {CONF_HOST: 'localhost'}]}}))

                mock_bridge.assert_called_once_with(
                    'localhost',
                    config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE))
                mock_load.assert_called_once_with(
                    self.hass, 'light', hue.DOMAIN,
                    {'bridge_id': '127.0.0.1'})

                self.assertTrue(hue.DOMAIN in self.hass.data)
                self.assertEqual(1, len(self.hass.data[hue.DOMAIN]))

    @MockDependency('phue')
    def test_setup_with_phue_conf(self, mock_phue):
        """No host in the config file, but one is cached in phue.conf."""
        mock_bridge = mock_phue.Bridge

        with assert_setup_component(1):
            with patch(
                    'homeassistant.components.hue._find_host_from_config',
                    return_value='localhost'):
                with patch('homeassistant.helpers.discovery.load_platform') \
                        as mock_load:
                    self.assertTrue(setup_component(
                        self.hass, hue.DOMAIN,
                        {hue.DOMAIN: {hue.CONF_BRIDGES: [
                            {CONF_FILENAME: 'phue.conf'}]}}))

                    mock_bridge.assert_called_once_with(
                        'localhost',
                        config_file_path=get_test_config_dir(
                            hue.PHUE_CONFIG_FILE))
                    mock_load.assert_called_once_with(
                        self.hass, 'light', hue.DOMAIN,
                        {'bridge_id': '127.0.0.1'})

                    self.assertTrue(hue.DOMAIN in self.hass.data)
                    self.assertEqual(1, len(self.hass.data[hue.DOMAIN]))

    @MockDependency('phue')
    def test_setup_with_multiple_hosts(self, mock_phue):
        """Multiple hosts specified in the config file."""
        mock_bridge = mock_phue.Bridge

        with assert_setup_component(1):
            with patch('homeassistant.helpers.discovery.load_platform') \
                    as mock_load:
                self.assertTrue(setup_component(
                    self.hass, hue.DOMAIN,
                    {hue.DOMAIN: {hue.CONF_BRIDGES: [
                        {CONF_HOST: 'localhost'},
                        {CONF_HOST: '192.168.0.1'}]}}))

                mock_bridge.assert_has_calls([
                    call(
                        'localhost',
                        config_file_path=get_test_config_dir(
                            hue.PHUE_CONFIG_FILE)),
                    call(
                        '192.168.0.1',
                        config_file_path=get_test_config_dir(
                            hue.PHUE_CONFIG_FILE))])
                mock_load.mock_bridge.assert_not_called()
                mock_load.assert_has_calls([
                    call(
                        self.hass, 'light', hue.DOMAIN,
                        {'bridge_id': '127.0.0.1'}),
                    call(
                        self.hass, 'light', hue.DOMAIN,
                        {'bridge_id': '192.168.0.1'}),
                ], any_order=True)

                self.assertTrue(hue.DOMAIN in self.hass.data)
                self.assertEqual(2, len(self.hass.data[hue.DOMAIN]))

    @MockDependency('phue')
    def test_bridge_discovered(self, mock_phue):
        """Bridge discovery."""
        mock_bridge = mock_phue.Bridge
        mock_service = MagicMock()
        discovery_info = {'host': '192.168.0.10', 'serial': 'foobar'}

        with patch('homeassistant.helpers.discovery.load_platform') \
                as mock_load:
            self.assertTrue(setup_component(
                self.hass, hue.DOMAIN, {}))
            hue.bridge_discovered(self.hass, mock_service, discovery_info)

            mock_bridge.assert_called_once_with(
                '192.168.0.10',
                config_file_path=get_test_config_dir('phue-foobar.conf'))
            mock_load.assert_called_once_with(
                self.hass, 'light', hue.DOMAIN,
                {'bridge_id': '192.168.0.10'})

            self.assertTrue(hue.DOMAIN in self.hass.data)
            self.assertEqual(1, len(self.hass.data[hue.DOMAIN]))

    @MockDependency('phue')
    def test_bridge_configure_and_discovered(self, mock_phue):
        """Bridge is in the config file, then we discover it."""
        mock_bridge = mock_phue.Bridge
        mock_service = MagicMock()
        discovery_info = {'host': '192.168.1.10', 'serial': 'foobar'}

        with assert_setup_component(1):
            with patch('homeassistant.helpers.discovery.load_platform') \
                    as mock_load:
                # First we set up the component from config
                self.assertTrue(setup_component(
                    self.hass, hue.DOMAIN,
                    {hue.DOMAIN: {hue.CONF_BRIDGES: [
                        {CONF_HOST: '192.168.1.10'}]}}))

                mock_bridge.assert_called_once_with(
                    '192.168.1.10',
                    config_file_path=get_test_config_dir(
                        hue.PHUE_CONFIG_FILE))
                calls_to_mock_load = [
                    call(
                        self.hass, 'light', hue.DOMAIN,
                        {'bridge_id': '192.168.1.10'}),
                ]
                mock_load.assert_has_calls(calls_to_mock_load)

                self.assertTrue(hue.DOMAIN in self.hass.data)
                self.assertEqual(1, len(self.hass.data[hue.DOMAIN]))

                # Then we discover the same bridge
                hue.bridge_discovered(self.hass, mock_service, discovery_info)

                # No additional calls
                mock_bridge.assert_called_once_with(
                    '192.168.1.10',
                    config_file_path=get_test_config_dir(
                        hue.PHUE_CONFIG_FILE))
                mock_load.assert_has_calls(calls_to_mock_load)

                # Still only one
                self.assertTrue(hue.DOMAIN in self.hass.data)
                self.assertEqual(1, len(self.hass.data[hue.DOMAIN]))


class TestHueBridge(unittest.TestCase):
    """Test the HueBridge class."""

    def setUp(self):  # pylint: disable=invalid-name
        """Setup things to be run when tests are started."""
        self.hass = get_test_home_assistant()
        self.hass.data[hue.DOMAIN] = {}
        self.skip_teardown_stop = False

    def tearDown(self):
        """Stop everything that was started."""
        if not self.skip_teardown_stop:
            self.hass.stop()

    @MockDependency('phue')
    def test_setup_bridge_connection_refused(self, mock_phue):
        """Test a registration failed with a connection refused exception."""
        mock_bridge = mock_phue.Bridge
        mock_bridge.side_effect = ConnectionRefusedError()

        bridge = hue.HueBridge('localhost', self.hass, hue.PHUE_CONFIG_FILE)
        bridge.setup()
        self.assertFalse(bridge.configured)
        self.assertTrue(bridge.config_request_id is None)

        mock_bridge.assert_called_once_with(
            'localhost',
            config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE))

    @MockDependency('phue')
    def test_setup_bridge_registration_exception(self, mock_phue):
        """Test a registration failed with an exception."""
        mock_bridge = mock_phue.Bridge
        mock_phue.PhueRegistrationException = Exception
        mock_bridge.side_effect = mock_phue.PhueRegistrationException(1, 2)

        bridge = hue.HueBridge('localhost', self.hass, hue.PHUE_CONFIG_FILE)
        bridge.setup()
        self.assertFalse(bridge.configured)
        self.assertFalse(bridge.config_request_id is None)
        self.assertTrue(isinstance(bridge.config_request_id, str))

        mock_bridge.assert_called_once_with(
            'localhost',
            config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE))

    @MockDependency('phue')
    def test_setup_bridge_registration_succeeds(self, mock_phue):
        """Test a registration success sequence."""
        mock_bridge = mock_phue.Bridge
        mock_phue.PhueRegistrationException = Exception
        mock_bridge.side_effect = [
            # First call, raise because not registered
            mock_phue.PhueRegistrationException(1, 2),
            # Second call, registration is done
            None,
        ]

        bridge = hue.HueBridge('localhost', self.hass, hue.PHUE_CONFIG_FILE)
        bridge.setup()
        self.assertFalse(bridge.configured)
        self.assertFalse(bridge.config_request_id is None)

        # Simulate the user confirming the registration
        self.hass.services.call(
            configurator.DOMAIN, configurator.SERVICE_CONFIGURE,
            {configurator.ATTR_CONFIGURE_ID: bridge.config_request_id})

        self.hass.block_till_done()
        self.assertTrue(bridge.configured)
        self.assertTrue(bridge.config_request_id is None)

        # We should see a total of two identical calls
        args = call(
            'localhost',
            config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE))
        mock_bridge.assert_has_calls([args, args])

        # Make sure the request is done
        self.assertEqual(1, len(self.hass.states.all()))
        self.assertEqual('configured', self.hass.states.all()[0].state)

    @MockDependency('phue')
    def test_setup_bridge_registration_fails(self, mock_phue):
        """
        Test a registration failure sequence.

        This may happen when we start the registration process, the user
        responds to the request but the bridge has become unreachable.
        """
        mock_bridge = mock_phue.Bridge
        mock_phue.PhueRegistrationException = Exception
        mock_bridge.side_effect = [
            # First call, raise because not registered
            mock_phue.PhueRegistrationException(1, 2),
            # Second call, the bridge has gone away
            ConnectionRefusedError(),
        ]

        bridge = hue.HueBridge('localhost', self.hass, hue.PHUE_CONFIG_FILE)
        bridge.setup()
        self.assertFalse(bridge.configured)
        self.assertFalse(bridge.config_request_id is None)

        # Simulate the user confirming the registration
        self.hass.services.call(
            configurator.DOMAIN, configurator.SERVICE_CONFIGURE,
            {configurator.ATTR_CONFIGURE_ID: bridge.config_request_id})

        self.hass.block_till_done()
        self.assertFalse(bridge.configured)
        self.assertFalse(bridge.config_request_id is None)

        # We should see a total of two identical calls
        args = call(
            'localhost',
            config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE))
        mock_bridge.assert_has_calls([args, args])

        # The request should still be pending
        self.assertEqual(1, len(self.hass.states.all()))
        self.assertEqual('configure', self.hass.states.all()[0].state)

    @MockDependency('phue')
    def test_setup_bridge_registration_retry(self, mock_phue):
        """
        Test a registration retry sequence.

        This may happen when we start the registration process, the user
        responds to the request but we fail to confirm it with the bridge.
        """
        mock_bridge = mock_phue.Bridge
        mock_phue.PhueRegistrationException = Exception
        mock_bridge.side_effect = [
            # First call, raise because not registered
            mock_phue.PhueRegistrationException(1, 2),
            # Second call, for whatever reason authentication fails
            mock_phue.PhueRegistrationException(1, 2),
        ]

        bridge = hue.HueBridge('localhost', self.hass, hue.PHUE_CONFIG_FILE)
        bridge.setup()
        self.assertFalse(bridge.configured)
        self.assertFalse(bridge.config_request_id is None)

        # Simulate the user confirming the registration
        self.hass.services.call(
            configurator.DOMAIN, configurator.SERVICE_CONFIGURE,
            {configurator.ATTR_CONFIGURE_ID: bridge.config_request_id})

        self.hass.block_till_done()
        self.assertFalse(bridge.configured)
        self.assertFalse(bridge.config_request_id is None)

        # We should see a total of two identical calls
        args = call(
            'localhost',
            config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE))
        mock_bridge.assert_has_calls([args, args])

        # Make sure the request is done
        self.assertEqual(1, len(self.hass.states.all()))
        self.assertEqual('configure', self.hass.states.all()[0].state)
        self.assertEqual(
            'Failed to register, please try again.',
            self.hass.states.all()[0].attributes.get(configurator.ATTR_ERRORS))

    @MockDependency('phue')
    def test_hue_activate_scene(self, mock_phue):
        """Test the hue_activate_scene service."""
        with patch('homeassistant.helpers.discovery.load_platform'):
            bridge = hue.HueBridge('localhost', self.hass,
                                   hue.PHUE_CONFIG_FILE)
            bridge.setup()

            # No args
            self.hass.services.call(hue.DOMAIN, hue.SERVICE_HUE_SCENE,
                                    blocking=True)
            bridge.bridge.run_scene.assert_not_called()

            # Only one arg
            self.hass.services.call(
                hue.DOMAIN, hue.SERVICE_HUE_SCENE,
                {hue.ATTR_GROUP_NAME: 'group'},
                blocking=True)
            bridge.bridge.run_scene.assert_not_called()

            self.hass.services.call(
                hue.DOMAIN, hue.SERVICE_HUE_SCENE,
                {hue.ATTR_SCENE_NAME: 'scene'},
                blocking=True)
            bridge.bridge.run_scene.assert_not_called()

            # Both required args
            self.hass.services.call(
                hue.DOMAIN, hue.SERVICE_HUE_SCENE,
                {hue.ATTR_GROUP_NAME: 'group', hue.ATTR_SCENE_NAME: 'scene'},
                blocking=True)
            bridge.bridge.run_scene.assert_called_once_with('group', 'scene')


@asyncio.coroutine
def test_setup_no_host(hass, requests_mock):
    """No host specified in any way."""
    requests_mock.get(hue.API_NUPNP, json=[])
    with MockDependency('phue') as mock_phue:
        result = yield from async_setup_component(
            hass, hue.DOMAIN, {hue.DOMAIN: {}})
        assert result

        mock_phue.Bridge.assert_not_called()

        assert hass.data[hue.DOMAIN] == {}