"""Code to support homekit_controller tests."""
from datetime import timedelta
import json
import os
from unittest import mock

from homekit.exceptions import AccessoryNotFoundError
from homekit.model import Accessory, get_id
from homekit.model.characteristics import (
    AbstractCharacteristic,
    CharacteristicPermissions,
    CharacteristicsTypes,
)
from homekit.model.services import AbstractService, ServicesTypes

from homeassistant import config_entries
from homeassistant.components.homekit_controller import config_flow
from homeassistant.components.homekit_controller.const import (
    CONTROLLER,
    DOMAIN,
    HOMEKIT_ACCESSORY_DISPATCH,
)
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util

from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture


class FakePairing:
    """
    A test fake that pretends to be a paired HomeKit accessory.

    This only contains methods and values that exist on the upstream Pairing
    class.
    """

    def __init__(self, accessories):
        """Create a fake pairing from an accessory model."""
        self.accessories = accessories
        self.pairing_data = {}
        self.available = True

    def list_accessories_and_characteristics(self):
        """Fake implementation of list_accessories_and_characteristics."""
        accessories = [a.to_accessory_and_service_list() for a in self.accessories]
        # replicate what happens upstream right now
        self.pairing_data["accessories"] = accessories
        return accessories

    def get_characteristics(self, characteristics):
        """Fake implementation of get_characteristics."""
        if not self.available:
            raise AccessoryNotFoundError("Accessory not found")

        results = {}
        for aid, cid in characteristics:
            for accessory in self.accessories:
                if aid != accessory.aid:
                    continue
                for service in accessory.services:
                    for char in service.characteristics:
                        if char.iid != cid:
                            continue
                        results[(aid, cid)] = {"value": char.get_value()}
        return results

    def put_characteristics(self, characteristics):
        """Fake implementation of put_characteristics."""
        for aid, cid, new_val in characteristics:
            for accessory in self.accessories:
                if aid != accessory.aid:
                    continue
                for service in accessory.services:
                    for char in service.characteristics:
                        if char.iid != cid:
                            continue
                        char.set_value(new_val)
        return {}


class FakeController:
    """
    A test fake that pretends to be a paired HomeKit accessory.

    This only contains methods and values that exist on the upstream Controller
    class.
    """

    def __init__(self):
        """Create a Fake controller with no pairings."""
        self.pairings = {}

    def add(self, accessories):
        """Create and register a fake pairing for a simulated accessory."""
        pairing = FakePairing(accessories)
        self.pairings["00:00:00:00:00:00"] = pairing
        return pairing


class Helper:
    """Helper methods for interacting with HomeKit fakes."""

    def __init__(self, hass, entity_id, pairing, accessory, config_entry):
        """Create a helper for a given accessory/entity."""
        self.hass = hass
        self.entity_id = entity_id
        self.pairing = pairing
        self.accessory = accessory
        self.config_entry = config_entry

        self.characteristics = {}
        for service in self.accessory.services:
            service_name = ServicesTypes.get_short(service.type)
            for char in service.characteristics:
                char_name = CharacteristicsTypes.get_short(char.type)
                self.characteristics[(service_name, char_name)] = char

    async def poll_and_get_state(self):
        """Trigger a time based poll and return the current entity state."""
        await time_changed(self.hass, 60)

        state = self.hass.states.get(self.entity_id)
        assert state is not None
        return state


class FakeCharacteristic(AbstractCharacteristic):
    """
    A model of a generic HomeKit characteristic.

    Base is abstract and can't be instanced directly so this subclass is
    needed even though it doesn't add any methods.
    """

    def to_accessory_and_service_list(self):
        """Serialize the characteristic."""
        # Upstream doesn't correctly serialize valid_values
        # This fix will be upstreamed and this function removed when it
        # is fixed.
        record = super().to_accessory_and_service_list()
        if self.valid_values:
            record["valid-values"] = self.valid_values
        return record


class FakeService(AbstractService):
    """A model of a generic HomeKit service."""

    def __init__(self, service_name):
        """Create a fake service by its short form HAP spec name."""
        char_type = ServicesTypes.get_uuid(service_name)
        super().__init__(char_type, get_id())

    def add_characteristic(self, name):
        """Add a characteristic to this service by name."""
        full_name = "public.hap.characteristic." + name
        char = FakeCharacteristic(get_id(), full_name, None)
        char.perms = [
            CharacteristicPermissions.paired_read,
            CharacteristicPermissions.paired_write,
        ]
        self.characteristics.append(char)
        return char


async def time_changed(hass, seconds):
    """Trigger time changed."""
    next_update = dt_util.utcnow() + timedelta(seconds)
    async_fire_time_changed(hass, next_update)
    await hass.async_block_till_done()


async def setup_accessories_from_file(hass, path):
    """Load an collection of accessory defs from JSON data."""
    accessories_fixture = await hass.async_add_executor_job(
        load_fixture, os.path.join("homekit_controller", path)
    )
    accessories_json = json.loads(accessories_fixture)

    accessories = []

    for accessory_data in accessories_json:
        accessory = Accessory("Name", "Mfr", "Model", "0001", "0.1")
        accessory.services = []
        accessory.aid = accessory_data["aid"]
        for service_data in accessory_data["services"]:
            service = FakeService("public.hap.service.accessory-information")
            service.type = service_data["type"]
            service.iid = service_data["iid"]

            for char_data in service_data["characteristics"]:
                char = FakeCharacteristic(1, "23", None)
                char.type = char_data["type"]
                char.iid = char_data["iid"]
                char.perms = char_data["perms"]
                char.format = char_data["format"]
                if "description" in char_data:
                    char.description = char_data["description"]
                if "value" in char_data:
                    char.value = char_data["value"]
                if "minValue" in char_data:
                    char.minValue = char_data["minValue"]
                if "maxValue" in char_data:
                    char.maxValue = char_data["maxValue"]
                if "valid-values" in char_data:
                    char.valid_values = char_data["valid-values"]
                service.characteristics.append(char)

            accessory.services.append(service)

        accessories.append(accessory)

    return accessories


async def setup_platform(hass):
    """Load the platform but with a fake Controller API."""
    config = {"discovery": {}}

    with mock.patch("homekit.Controller") as controller:
        fake_controller = controller.return_value = FakeController()
        await async_setup_component(hass, DOMAIN, config)

    return fake_controller


async def setup_test_accessories(hass, accessories):
    """Load a fake homekit device based on captured JSON profile."""
    fake_controller = await setup_platform(hass)
    pairing = fake_controller.add(accessories)

    discovery_info = {
        "name": "TestDevice",
        "host": "127.0.0.1",
        "port": 8080,
        "properties": {"md": "TestDevice", "id": "00:00:00:00:00:00", "c#": 1},
    }

    pairing.pairing_data.update(
        {"AccessoryPairingID": discovery_info["properties"]["id"]}
    )

    config_entry = MockConfigEntry(
        version=1,
        domain="homekit_controller",
        entry_id="TestData",
        data=pairing.pairing_data,
        title="test",
        connection_class=config_entries.CONN_CLASS_LOCAL_PUSH,
    )

    config_entry.add_to_hass(hass)

    pairing_cls_loc = "homeassistant.components.homekit_controller.connection.IpPairing"
    with mock.patch(pairing_cls_loc) as pairing_cls:
        pairing_cls.return_value = pairing
        await config_entry.async_setup(hass)
        await hass.async_block_till_done()

    return config_entry, pairing


async def device_config_changed(hass, accessories):
    """Discover new devices added to HomeAssistant at runtime."""
    # Update the accessories our FakePairing knows about
    controller = hass.data[CONTROLLER]
    pairing = controller.pairings["00:00:00:00:00:00"]
    pairing.accessories = accessories

    discovery_info = {
        "name": "TestDevice",
        "host": "127.0.0.1",
        "port": 8080,
        "properties": {
            "md": "TestDevice",
            "id": "00:00:00:00:00:00",
            "c#": "2",
            "sf": "0",
        },
    }

    # Config Flow will abort and notify us if the discovery event is of
    # interest - in this case c# has incremented
    flow = config_flow.HomekitControllerFlowHandler()
    flow.hass = hass
    flow.context = {}
    result = await flow.async_step_zeroconf(discovery_info)
    assert result["type"] == "abort"
    assert result["reason"] == "already_configured"

    # Wait for services to reconfigure
    await hass.async_block_till_done()
    await hass.async_block_till_done()


async def setup_test_component(hass, services, capitalize=False, suffix=None):
    """Load a fake homekit accessory based on a homekit accessory model.

    If capitalize is True, property names will be in upper case.

    If suffix is set, entityId will include the suffix
    """
    domain = None
    for service in services:
        service_name = ServicesTypes.get_short(service.type)
        if service_name in HOMEKIT_ACCESSORY_DISPATCH:
            domain = HOMEKIT_ACCESSORY_DISPATCH[service_name]
            break

    assert domain, "Cannot map test homekit services to homeassistant domain"

    accessory = Accessory("TestDevice", "example.com", "Test", "0001", "0.1")
    accessory.services.extend(services)

    config_entry, pairing = await setup_test_accessories(hass, [accessory])
    entity = "testdevice" if suffix is None else "testdevice_{}".format(suffix)
    return Helper(hass, ".".join((domain, entity)), pairing, accessory, config_entry)
