"""
tests.components.device_tracker.test_owntracks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Tests Owntracks device tracker.
"""
import json
import os
import unittest

from collections import defaultdict

from homeassistant.components import device_tracker

from homeassistant.const import (STATE_NOT_HOME, CONF_PLATFORM)

import homeassistant.components.device_tracker.owntracks as owntracks

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

USER = 'greg'
DEVICE = 'phone'

LOCATION_TOPIC = "owntracks/{}/{}".format(USER, DEVICE)
EVENT_TOPIC = "owntracks/{}/{}/event".format(USER, DEVICE)

DEVICE_TRACKER_STATE = "device_tracker.{}_{}".format(USER, DEVICE)

IBEACON_DEVICE = 'keys'
REGION_TRACKER_STATE = "device_tracker.beacon_{}".format(IBEACON_DEVICE)

LOCATION_MESSAGE = {
    'batt': 92,
    'cog': 248,
    'tid': 'user',
    'lon': 1.0,
    't': 'u',
    'alt': 27,
    'acc': 60,
    'p': 101.3977584838867,
    'vac': 4,
    'lat': 2.0,
    '_type': 'location',
    'tst': 1,
    'vel': 0}

REGION_ENTER_MESSAGE = {
    'lon': 1.0,
    'event': 'enter',
    'tid': 'user',
    'desc': 'inner',
    'wtst': 1,
    't': 'b',
    'acc': 60,
    'tst': 2,
    'lat': 2.0,
    '_type': 'transition'}


REGION_LEAVE_MESSAGE = {
    'lon': 1.0,
    'event': 'leave',
    'tid': 'user',
    'desc': 'inner',
    'wtst': 1,
    't': 'b',
    'acc': 60,
    'tst': 2,
    'lat': 2.0,
    '_type': 'transition'}


class TestDeviceTrackerOwnTracks(unittest.TestCase):
    """ Test the Template sensor. """

    def setup_method(self, method):
        """ Init needed objects. """
        self.hass = get_test_home_assistant()
        mock_mqtt_component(self.hass)
        self.assertTrue(device_tracker.setup(self.hass, {
            device_tracker.DOMAIN: {
                CONF_PLATFORM: 'owntracks'
            }}))

        self.hass.states.set(
            'zone.inner', 'zoning',
            {
                'name': 'zone',
                'latitude': 2.1,
                'longitude': 1.1,
                'radius': 10
            })

        self.hass.states.set(
            'zone.inner_2', 'zoning',
            {
                'name': 'zone',
                'latitude': 2.1,
                'longitude': 1.1,
                'radius': 10
            })

        self.hass.states.set(
            'zone.outer', 'zoning',
            {
                'name': 'zone',
                'latitude': 2.0,
                'longitude': 1.0,
                'radius': 100000
            })

        self.hass.states.set(
            'zone.passive', 'zoning',
            {
                'name': 'zone',
                'latitude': 3.0,
                'longitude': 1.0,
                'radius': 10,
                'passive': True
            })
        # Clear state between teste
        self.hass.states.set(DEVICE_TRACKER_STATE, None)
        owntracks.REGIONS_ENTERED = defaultdict(list)
        owntracks.MOBILE_BEACONS_ACTIVE = defaultdict(list)

    def teardown_method(self, method):
        """ Stop down stuff we started. """
        self.hass.stop()

        try:
            os.remove(self.hass.config.path(device_tracker.YAML_DEVICES))
        except FileNotFoundError:
            pass

    def send_message(self, topic, message):
        fire_mqtt_message(
            self.hass, topic, json.dumps(message))
        self.hass.pool.block_till_done()

    def assert_location_state(self, location):
        state = self.hass.states.get(DEVICE_TRACKER_STATE)
        self.assertEqual(state.state, location)

    def assert_location_latitude(self, latitude):
        state = self.hass.states.get(DEVICE_TRACKER_STATE)
        self.assertEqual(state.attributes.get('latitude'), latitude)

    def assert_location_accuracy(self, accuracy):
        state = self.hass.states.get(DEVICE_TRACKER_STATE)
        self.assertEqual(state.attributes.get('gps_accuracy'), accuracy)

    def assert_tracker_state(self, location):
        state = self.hass.states.get(REGION_TRACKER_STATE)
        self.assertEqual(state.state, location)

    def assert_tracker_latitude(self, latitude):
        state = self.hass.states.get(REGION_TRACKER_STATE)
        self.assertEqual(state.attributes.get('latitude'), latitude)

    def assert_tracker_accuracy(self, accuracy):
        state = self.hass.states.get(REGION_TRACKER_STATE)
        self.assertEqual(state.attributes.get('gps_accuracy'), accuracy)

    def test_location_update(self):
        self.send_message(LOCATION_TOPIC, LOCATION_MESSAGE)

        self.assert_location_latitude(2.0)
        self.assert_location_accuracy(60.0)
        self.assert_location_state('outer')

    def test_event_entry_exit(self):
        self.send_message(EVENT_TOPIC, REGION_ENTER_MESSAGE)

        # Enter uses the zone's gps co-ords
        self.assert_location_latitude(2.1)
        self.assert_location_accuracy(10.0)
        self.assert_location_state('inner')

        self.send_message(LOCATION_TOPIC, LOCATION_MESSAGE)

        #  Updates ignored when in a zone
        self.assert_location_latitude(2.1)
        self.assert_location_accuracy(10.0)
        self.assert_location_state('inner')

        self.send_message(EVENT_TOPIC, REGION_LEAVE_MESSAGE)

        # Exit switches back to GPS
        self.assert_location_latitude(2.0)
        self.assert_location_accuracy(60.0)
        self.assert_location_state('outer')

        # Left clean zone state
        self.assertFalse(owntracks.REGIONS_ENTERED[USER])

    def test_event_exit_outside_zone_sets_away(self):
        self.send_message(EVENT_TOPIC, REGION_ENTER_MESSAGE)
        self.assert_location_state('inner')

        # Exit message far away GPS location
        message = REGION_LEAVE_MESSAGE.copy()
        message['lon'] = 90.1
        message['lat'] = 90.1
        self.send_message(EVENT_TOPIC, message)

        # Exit forces zone change to away
        self.assert_location_state(STATE_NOT_HOME)

    def test_event_entry_exit_right_order(self):
        # Enter inner zone
        self.send_message(EVENT_TOPIC, REGION_ENTER_MESSAGE)

        self.assert_location_state('inner')
        self.assert_location_latitude(2.1)
        self.assert_location_accuracy(10.0)

        # Enter inner2 zone
        message = REGION_ENTER_MESSAGE.copy()
        message['desc'] = "inner_2"
        self.send_message(EVENT_TOPIC, message)
        self.assert_location_state('inner_2')
        self.assert_location_latitude(2.1)
        self.assert_location_accuracy(10.0)

        # Exit inner_2 - should be in 'inner'
        message = REGION_LEAVE_MESSAGE.copy()
        message['desc'] = "inner_2"
        self.send_message(EVENT_TOPIC, message)
        self.assert_location_state('inner')
        self.assert_location_latitude(2.1)
        self.assert_location_accuracy(10.0)

        # Exit inner - should be in 'outer'
        self.send_message(EVENT_TOPIC, REGION_LEAVE_MESSAGE)
        self.assert_location_state('outer')
        self.assert_location_latitude(2.0)
        self.assert_location_accuracy(60.0)

    def test_event_entry_exit_wrong_order(self):
        # Enter inner zone
        self.send_message(EVENT_TOPIC, REGION_ENTER_MESSAGE)
        self.assert_location_state('inner')

        # Enter inner2 zone
        message = REGION_ENTER_MESSAGE.copy()
        message['desc'] = "inner_2"
        self.send_message(EVENT_TOPIC, message)
        self.assert_location_state('inner_2')

        # Exit inner - should still be in 'inner_2'
        self.send_message(EVENT_TOPIC, REGION_LEAVE_MESSAGE)
        self.assert_location_state('inner_2')

        # Exit inner_2 - should be in 'outer'
        message = REGION_LEAVE_MESSAGE.copy()
        message['desc'] = "inner_2"
        self.send_message(EVENT_TOPIC, message)
        self.assert_location_state('outer')

    def test_event_entry_exit_passive_zone(self):
        # Enter passive zone
        message = REGION_ENTER_MESSAGE.copy()
        message['desc'] = "passive"
        self.send_message(EVENT_TOPIC, message)

        # Should pick up gps put not zone
        self.assert_location_state('not_home')
        self.assert_location_latitude(3.0)
        self.assert_location_accuracy(10.0)

        # Enter inner2 zone
        message = REGION_ENTER_MESSAGE.copy()
        message['desc'] = "inner_2"
        self.send_message(EVENT_TOPIC, message)
        self.assert_location_state('inner_2')
        self.assert_location_latitude(2.1)
        self.assert_location_accuracy(10.0)

        # Exit inner_2 - should be in 'passive'
        # ie gps co-ords - but not zone
        message = REGION_LEAVE_MESSAGE.copy()
        message['desc'] = "inner_2"
        self.send_message(EVENT_TOPIC, message)
        self.assert_location_state('not_home')
        self.assert_location_latitude(3.0)
        self.assert_location_accuracy(10.0)

        # Exit passive - should be in 'outer'
        message = REGION_LEAVE_MESSAGE.copy()
        message['desc'] = "passive"
        self.send_message(EVENT_TOPIC, message)
        self.assert_location_state('outer')
        self.assert_location_latitude(2.0)
        self.assert_location_accuracy(60.0)

    def test_event_entry_unknown_zone(self):
        # Just treat as location update
        message = REGION_ENTER_MESSAGE.copy()
        message['desc'] = "unknown"
        self.send_message(EVENT_TOPIC, message)
        self.assert_location_latitude(2.0)
        self.assert_location_state('outer')

    def test_event_exit_unknown_zone(self):
        # Just treat as location update
        message = REGION_LEAVE_MESSAGE.copy()
        message['desc'] = "unknown"
        self.send_message(EVENT_TOPIC, message)
        self.assert_location_latitude(2.0)
        self.assert_location_state('outer')

    def test_event_entry_zone_loading_dash(self):
        # Make sure the leading - is ignored
        # Ownracks uses this to switch on hold
        message = REGION_ENTER_MESSAGE.copy()
        message['desc'] = "-inner"
        self.send_message(EVENT_TOPIC, REGION_ENTER_MESSAGE)

        self.assert_location_state('inner')

    def test_mobile_enter_move_beacon(self):
        # Enter mobile beacon, should set location
        message = REGION_ENTER_MESSAGE.copy()
        message['desc'] = IBEACON_DEVICE
        self.send_message(EVENT_TOPIC, message)

        self.assert_tracker_latitude(2.0)
        self.assert_tracker_state('outer')

        # Move should move beacon
        message = LOCATION_MESSAGE.copy()
        message['lat'] = "3.0"
        self.send_message(LOCATION_TOPIC, message)

        self.assert_tracker_latitude(3.0)
        self.assert_tracker_state(STATE_NOT_HOME)

    def test_mobile_enter_exit_region_beacon(self):
        # Start tracking beacon
        message = REGION_ENTER_MESSAGE.copy()
        message['desc'] = IBEACON_DEVICE
        self.send_message(EVENT_TOPIC, message)
        self.assert_tracker_latitude(2.0)
        self.assert_tracker_state('outer')

        # Enter location should move beacon
        message = REGION_ENTER_MESSAGE.copy()
        message['desc'] = "inner_2"
        self.send_message(EVENT_TOPIC, message)

        self.assert_tracker_latitude(2.1)
        self.assert_tracker_state('inner_2')

        # Exit location should switch to gps
        message = REGION_LEAVE_MESSAGE.copy()
        message['desc'] = "inner_2"
        self.send_message(EVENT_TOPIC, message)
        self.assert_tracker_latitude(2.0)

    def test_mobile_exit_move_beacon(self):
        # Start tracking beacon
        message = REGION_ENTER_MESSAGE.copy()
        message['desc'] = IBEACON_DEVICE
        self.send_message(EVENT_TOPIC, message)
        self.assert_tracker_latitude(2.0)
        self.assert_tracker_state('outer')

        # Exit mobile beacon, should set location
        message = REGION_LEAVE_MESSAGE.copy()
        message['desc'] = IBEACON_DEVICE
        message['lat'] = "3.0"
        self.send_message(EVENT_TOPIC, message)

        self.assert_tracker_latitude(3.0)

        # Move after exit should do nothing
        message = LOCATION_MESSAGE.copy()
        message['lat'] = "4.0"
        self.send_message(LOCATION_TOPIC, LOCATION_MESSAGE)
        self.assert_tracker_latitude(3.0)

    def test_mobile_multiple_async_enter_exit(self):
        # Test race condition
        enter_message = REGION_ENTER_MESSAGE.copy()
        enter_message['desc'] = IBEACON_DEVICE
        exit_message = REGION_LEAVE_MESSAGE.copy()
        exit_message['desc'] = IBEACON_DEVICE

        for i in range(0, 20):
            fire_mqtt_message(
                self.hass, EVENT_TOPIC, json.dumps(enter_message))
            fire_mqtt_message(
                self.hass, EVENT_TOPIC, json.dumps(exit_message))

        fire_mqtt_message(
            self.hass, EVENT_TOPIC, json.dumps(enter_message))

        self.hass.pool.block_till_done()
        self.send_message(EVENT_TOPIC, exit_message)
        self.assertEqual(owntracks.MOBILE_BEACONS_ACTIVE['greg_phone'], [])

    def test_mobile_multiple_enter_exit(self):
        # Should only happen if the iphone dies
        enter_message = REGION_ENTER_MESSAGE.copy()
        enter_message['desc'] = IBEACON_DEVICE
        exit_message = REGION_LEAVE_MESSAGE.copy()
        exit_message['desc'] = IBEACON_DEVICE

        self.send_message(EVENT_TOPIC, enter_message)
        self.send_message(EVENT_TOPIC, enter_message)
        self.send_message(EVENT_TOPIC, exit_message)

        self.assertEqual(owntracks.MOBILE_BEACONS_ACTIVE['greg_phone'], [])