"""Test the UPNP component."""
from collections import OrderedDict
from unittest.mock import patch, MagicMock

import pytest

from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.setup import async_setup_component
from homeassistant.components.upnp import IP_SERVICE, DATA_UPNP


class MockService(MagicMock):
    """Mock upnp IP service."""

    async def add_port_mapping(self, *args, **kwargs):
        """Original function."""
        self.mock_add_port_mapping(*args, **kwargs)

    async def delete_port_mapping(self, *args, **kwargs):
        """Original function."""
        self.mock_delete_port_mapping(*args, **kwargs)


class MockDevice(MagicMock):
    """Mock upnp device."""

    def find_first_service(self, *args, **kwargs):
        """Original function."""
        self._service = MockService()
        return self._service

    def peep_first_service(self):
        """Access Mock first service."""
        return self._service


class MockResp(MagicMock):
    """Mock upnp msearch response."""

    async def get_device(self, *args, **kwargs):
        """Original function."""
        device = MockDevice()
        service = {'serviceType': IP_SERVICE}
        device.services = [service]
        return device


@pytest.fixture
def mock_msearch_first(*args, **kwargs):
    """Wrap async mock msearch_first."""
    async def async_mock_msearch_first(*args, **kwargs):
        """Mock msearch_first."""
        return MockResp(*args, **kwargs)

    with patch('pyupnp_async.msearch_first', new=async_mock_msearch_first):
        yield


@pytest.fixture
def mock_async_exception(*args, **kwargs):
    """Wrap async mock exception."""
    async def async_mock_exception(*args, **kwargs):
        return Exception

    with patch('pyupnp_async.msearch_first', new=async_mock_exception):
        yield


@pytest.fixture
def mock_local_ip():
    """Mock get_local_ip."""
    with patch('homeassistant.components.upnp.get_local_ip',
               return_value='192.168.0.10'):
        yield


async def test_setup_fail_if_no_ip(hass):
    """Test setup fails if we can't find a local IP."""
    with patch('homeassistant.components.upnp.get_local_ip',
               return_value='127.0.0.1'):
        result = await async_setup_component(hass, 'upnp', {
            'upnp': {}
        })

    assert not result


async def test_setup_fail_if_cannot_select_igd(hass,
                                               mock_local_ip,
                                               mock_async_exception):
    """Test setup fails if we can't find an UPnP IGD."""
    result = await async_setup_component(hass, 'upnp', {
        'upnp': {}
    })

    assert not result


async def test_setup_succeeds_if_specify_ip(hass, mock_msearch_first):
    """Test setup succeeds if we specify IP and can't find a local IP."""
    with patch('homeassistant.components.upnp.get_local_ip',
               return_value='127.0.0.1'):
        result = await async_setup_component(hass, 'upnp', {
            'upnp': {
                'local_ip': '192.168.0.10',
                'port_mapping': 'True'
            }
        })

    assert result
    mock_service = hass.data[DATA_UPNP].peep_first_service()
    assert len(mock_service.mock_add_port_mapping.mock_calls) == 1
    mock_service.mock_add_port_mapping.assert_called_once_with(
        8123, 8123, '192.168.0.10', 'TCP', desc='Home Assistant')


async def test_no_config_maps_hass_local_to_remote_port(hass,
                                                        mock_local_ip,
                                                        mock_msearch_first):
    """Test by default we map local to remote port."""
    result = await async_setup_component(hass, 'upnp', {
        'upnp': {
                'port_mapping': 'True'
        }
    })

    assert result
    mock_service = hass.data[DATA_UPNP].peep_first_service()
    assert len(mock_service.mock_add_port_mapping.mock_calls) == 1
    mock_service.mock_add_port_mapping.assert_called_once_with(
        8123, 8123, '192.168.0.10', 'TCP', desc='Home Assistant')


async def test_map_hass_to_remote_port(hass,
                                       mock_local_ip,
                                       mock_msearch_first):
    """Test mapping hass to remote port."""
    result = await async_setup_component(hass, 'upnp', {
        'upnp': {
            'port_mapping': 'True',
            'ports': {
                'hass': 1000
            }
        }
    })

    assert result
    mock_service = hass.data[DATA_UPNP].peep_first_service()
    assert len(mock_service.mock_add_port_mapping.mock_calls) == 1
    mock_service.mock_add_port_mapping.assert_called_once_with(
        8123, 1000, '192.168.0.10', 'TCP', desc='Home Assistant')


async def test_map_internal_to_remote_ports(hass,
                                            mock_local_ip,
                                            mock_msearch_first):
    """Test mapping local to remote ports."""
    ports = OrderedDict()
    ports['hass'] = 1000
    ports[1883] = 3883

    result = await async_setup_component(hass, 'upnp', {
        'upnp': {
            'port_mapping': 'True',
            'ports': ports
        }
    })

    assert result
    mock_service = hass.data[DATA_UPNP].peep_first_service()
    assert len(mock_service.mock_add_port_mapping.mock_calls) == 2

    mock_service.mock_add_port_mapping.assert_any_call(
        8123, 1000, '192.168.0.10', 'TCP', desc='Home Assistant')
    mock_service.mock_add_port_mapping.assert_any_call(
        1883, 3883, '192.168.0.10', 'TCP', desc='Home Assistant')

    hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
    await hass.async_block_till_done()
    assert len(mock_service.mock_delete_port_mapping.mock_calls) == 2

    mock_service.mock_delete_port_mapping.assert_any_call(1000, 'TCP')
    mock_service.mock_delete_port_mapping.assert_any_call(3883, 'TCP')
