"""The tests for the Group components."""
# pylint: disable=protected-access
from collections import OrderedDict

import homeassistant.components.group as group
from homeassistant.const import (
    ATTR_ASSUMED_STATE,
    ATTR_FRIENDLY_NAME,
    ATTR_ICON,
    EVENT_HOMEASSISTANT_START,
    SERVICE_RELOAD,
    STATE_HOME,
    STATE_NOT_HOME,
    STATE_OFF,
    STATE_ON,
    STATE_UNKNOWN,
)
from homeassistant.core import CoreState
from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS
from homeassistant.setup import async_setup_component

from tests.async_mock import patch
from tests.common import assert_setup_component
from tests.components.group import common


async def test_setup_group_with_mixed_groupable_states(hass):
    """Try to set up a group with mixed groupable states."""

    hass.states.async_set("light.Bowl", STATE_ON)
    hass.states.async_set("device_tracker.Paulus", STATE_HOME)

    assert await async_setup_component(hass, "group", {})

    await group.Group.async_create_group(
        hass, "person_and_light", ["light.Bowl", "device_tracker.Paulus"]
    )

    await hass.async_block_till_done()

    assert STATE_ON == hass.states.get(f"{group.DOMAIN}.person_and_light").state


async def test_setup_group_with_a_non_existing_state(hass):
    """Try to set up a group with a non existing state."""
    hass.states.async_set("light.Bowl", STATE_ON)

    assert await async_setup_component(hass, "group", {})

    grp = await group.Group.async_create_group(
        hass, "light_and_nothing", ["light.Bowl", "non.existing"]
    )

    assert STATE_ON == grp.state


async def test_setup_group_with_non_groupable_states(hass):
    """Test setup with groups which are not groupable."""
    hass.states.async_set("cast.living_room", "Plex")
    hass.states.async_set("cast.bedroom", "Netflix")

    assert await async_setup_component(hass, "group", {})

    grp = await group.Group.async_create_group(
        hass, "chromecasts", ["cast.living_room", "cast.bedroom"]
    )

    assert grp.state is None


async def test_setup_empty_group(hass):
    """Try to set up an empty group."""
    grp = await group.Group.async_create_group(hass, "nothing", [])

    assert grp.state is None


async def test_monitor_group(hass):
    """Test if the group keeps track of states."""
    hass.states.async_set("light.Bowl", STATE_ON)
    hass.states.async_set("light.Ceiling", STATE_OFF)

    assert await async_setup_component(hass, "group", {})

    test_group = await group.Group.async_create_group(
        hass, "init_group", ["light.Bowl", "light.Ceiling"], False
    )

    # Test if group setup in our init mode is ok
    assert test_group.entity_id in hass.states.async_entity_ids()

    group_state = hass.states.get(test_group.entity_id)
    assert STATE_ON == group_state.state
    assert group_state.attributes.get(group.ATTR_AUTO)


async def test_group_turns_off_if_all_off(hass):
    """Test if turn off if the last device that was on turns off."""
    hass.states.async_set("light.Bowl", STATE_OFF)
    hass.states.async_set("light.Ceiling", STATE_OFF)

    assert await async_setup_component(hass, "group", {})

    test_group = await group.Group.async_create_group(
        hass, "init_group", ["light.Bowl", "light.Ceiling"], False
    )

    await hass.async_block_till_done()

    group_state = hass.states.get(test_group.entity_id)
    assert STATE_OFF == group_state.state


async def test_group_turns_on_if_all_are_off_and_one_turns_on(hass):
    """Test if turn on if all devices were turned off and one turns on."""
    hass.states.async_set("light.Bowl", STATE_OFF)
    hass.states.async_set("light.Ceiling", STATE_OFF)

    assert await async_setup_component(hass, "group", {})

    test_group = await group.Group.async_create_group(
        hass, "init_group", ["light.Bowl", "light.Ceiling"], False
    )

    # Turn one on
    hass.states.async_set("light.Ceiling", STATE_ON)
    await hass.async_block_till_done()

    group_state = hass.states.get(test_group.entity_id)
    assert STATE_ON == group_state.state


async def test_allgroup_stays_off_if_all_are_off_and_one_turns_on(hass):
    """Group with all: true, stay off if one device turns on."""
    hass.states.async_set("light.Bowl", STATE_OFF)
    hass.states.async_set("light.Ceiling", STATE_OFF)

    assert await async_setup_component(hass, "group", {})

    test_group = await group.Group.async_create_group(
        hass, "init_group", ["light.Bowl", "light.Ceiling"], False, mode=True
    )

    # Turn one on
    hass.states.async_set("light.Ceiling", STATE_ON)
    await hass.async_block_till_done()

    group_state = hass.states.get(test_group.entity_id)
    assert STATE_OFF == group_state.state


async def test_allgroup_turn_on_if_last_turns_on(hass):
    """Group with all: true, turn on if all devices are on."""
    hass.states.async_set("light.Bowl", STATE_ON)
    hass.states.async_set("light.Ceiling", STATE_OFF)

    assert await async_setup_component(hass, "group", {})

    test_group = await group.Group.async_create_group(
        hass, "init_group", ["light.Bowl", "light.Ceiling"], False, mode=True
    )

    # Turn one on
    hass.states.async_set("light.Ceiling", STATE_ON)
    await hass.async_block_till_done()

    group_state = hass.states.get(test_group.entity_id)
    assert STATE_ON == group_state.state


async def test_expand_entity_ids(hass):
    """Test expand_entity_ids method."""
    hass.states.async_set("light.Bowl", STATE_ON)
    hass.states.async_set("light.Ceiling", STATE_OFF)

    assert await async_setup_component(hass, "group", {})

    test_group = await group.Group.async_create_group(
        hass, "init_group", ["light.Bowl", "light.Ceiling"], False
    )

    assert sorted(["light.ceiling", "light.bowl"]) == sorted(
        group.expand_entity_ids(hass, [test_group.entity_id])
    )


async def test_expand_entity_ids_does_not_return_duplicates(hass):
    """Test that expand_entity_ids does not return duplicates."""
    hass.states.async_set("light.Bowl", STATE_ON)
    hass.states.async_set("light.Ceiling", STATE_OFF)

    assert await async_setup_component(hass, "group", {})

    test_group = await group.Group.async_create_group(
        hass, "init_group", ["light.Bowl", "light.Ceiling"], False
    )

    assert ["light.bowl", "light.ceiling"] == sorted(
        group.expand_entity_ids(hass, [test_group.entity_id, "light.Ceiling"])
    )

    assert ["light.bowl", "light.ceiling"] == sorted(
        group.expand_entity_ids(hass, ["light.bowl", test_group.entity_id])
    )


async def test_expand_entity_ids_recursive(hass):
    """Test expand_entity_ids method with a group that contains itself."""
    hass.states.async_set("light.Bowl", STATE_ON)
    hass.states.async_set("light.Ceiling", STATE_OFF)

    assert await async_setup_component(hass, "group", {})

    test_group = await group.Group.async_create_group(
        hass,
        "init_group",
        ["light.Bowl", "light.Ceiling", "group.init_group"],
        False,
    )

    assert sorted(["light.ceiling", "light.bowl"]) == sorted(
        group.expand_entity_ids(hass, [test_group.entity_id])
    )


async def test_expand_entity_ids_ignores_non_strings(hass):
    """Test that non string elements in lists are ignored."""
    assert [] == group.expand_entity_ids(hass, [5, True])


async def test_get_entity_ids(hass):
    """Test get_entity_ids method."""
    hass.states.async_set("light.Bowl", STATE_ON)
    hass.states.async_set("light.Ceiling", STATE_OFF)

    assert await async_setup_component(hass, "group", {})

    test_group = await group.Group.async_create_group(
        hass, "init_group", ["light.Bowl", "light.Ceiling"], False
    )

    assert ["light.bowl", "light.ceiling"] == sorted(
        group.get_entity_ids(hass, test_group.entity_id)
    )


async def test_get_entity_ids_with_domain_filter(hass):
    """Test if get_entity_ids works with a domain_filter."""
    hass.states.async_set("switch.AC", STATE_OFF)

    assert await async_setup_component(hass, "group", {})

    mixed_group = await group.Group.async_create_group(
        hass, "mixed_group", ["light.Bowl", "switch.AC"], False
    )

    assert ["switch.ac"] == group.get_entity_ids(
        hass, mixed_group.entity_id, domain_filter="switch"
    )


async def test_get_entity_ids_with_non_existing_group_name(hass):
    """Test get_entity_ids with a non existing group."""
    assert [] == group.get_entity_ids(hass, "non_existing")


async def test_get_entity_ids_with_non_group_state(hass):
    """Test get_entity_ids with a non group state."""
    assert [] == group.get_entity_ids(hass, "switch.AC")


async def test_group_being_init_before_first_tracked_state_is_set_to_on(hass):
    """Test if the groups turn on.

    If no states existed and now a state it is tracking is being added
    as ON.
    """

    assert await async_setup_component(hass, "group", {})

    test_group = await group.Group.async_create_group(
        hass, "test group", ["light.not_there_1"]
    )

    hass.states.async_set("light.not_there_1", STATE_ON)

    await hass.async_block_till_done()

    group_state = hass.states.get(test_group.entity_id)
    assert STATE_ON == group_state.state


async def test_group_being_init_before_first_tracked_state_is_set_to_off(hass):
    """Test if the group turns off.

    If no states existed and now a state it is tracking is being added
    as OFF.
    """
    assert await async_setup_component(hass, "group", {})
    test_group = await group.Group.async_create_group(
        hass, "test group", ["light.not_there_1"]
    )

    hass.states.async_set("light.not_there_1", STATE_OFF)

    await hass.async_block_till_done()

    group_state = hass.states.get(test_group.entity_id)
    assert STATE_OFF == group_state.state


async def test_groups_get_unique_names(hass):
    """Two groups with same name should both have a unique entity id."""

    assert await async_setup_component(hass, "group", {})

    grp1 = await group.Group.async_create_group(hass, "Je suis Charlie")
    grp2 = await group.Group.async_create_group(hass, "Je suis Charlie")

    assert grp1.entity_id != grp2.entity_id


async def test_expand_entity_ids_expands_nested_groups(hass):
    """Test if entity ids epands to nested groups."""

    assert await async_setup_component(hass, "group", {})

    await group.Group.async_create_group(
        hass, "light", ["light.test_1", "light.test_2"]
    )
    await group.Group.async_create_group(
        hass, "switch", ["switch.test_1", "switch.test_2"]
    )
    await group.Group.async_create_group(
        hass, "group_of_groups", ["group.light", "group.switch"]
    )

    assert [
        "light.test_1",
        "light.test_2",
        "switch.test_1",
        "switch.test_2",
    ] == sorted(group.expand_entity_ids(hass, ["group.group_of_groups"]))


async def test_set_assumed_state_based_on_tracked(hass):
    """Test assumed state."""
    hass.states.async_set("light.Bowl", STATE_ON)
    hass.states.async_set("light.Ceiling", STATE_OFF)

    assert await async_setup_component(hass, "group", {})

    test_group = await group.Group.async_create_group(
        hass, "init_group", ["light.Bowl", "light.Ceiling", "sensor.no_exist"]
    )

    state = hass.states.get(test_group.entity_id)
    assert not state.attributes.get(ATTR_ASSUMED_STATE)

    hass.states.async_set("light.Bowl", STATE_ON, {ATTR_ASSUMED_STATE: True})
    await hass.async_block_till_done()

    state = hass.states.get(test_group.entity_id)
    assert state.attributes.get(ATTR_ASSUMED_STATE)

    hass.states.async_set("light.Bowl", STATE_ON)
    await hass.async_block_till_done()

    state = hass.states.get(test_group.entity_id)
    assert not state.attributes.get(ATTR_ASSUMED_STATE)


async def test_group_updated_after_device_tracker_zone_change(hass):
    """Test group state when device tracker in group changes zone."""
    hass.states.async_set("device_tracker.Adam", STATE_HOME)
    hass.states.async_set("device_tracker.Eve", STATE_NOT_HOME)
    await hass.async_block_till_done()

    assert await async_setup_component(hass, "group", {})
    assert await async_setup_component(hass, "device_tracker", {})

    await group.Group.async_create_group(
        hass, "peeps", ["device_tracker.Adam", "device_tracker.Eve"]
    )

    hass.states.async_set("device_tracker.Adam", "cool_state_not_home")
    await hass.async_block_till_done()
    assert STATE_NOT_HOME == hass.states.get(f"{group.DOMAIN}.peeps").state


async def test_is_on(hass):
    """Test is_on method."""
    hass.states.async_set("light.Bowl", STATE_ON)
    hass.states.async_set("light.Ceiling", STATE_OFF)

    assert group.is_on(hass, "group.none") is False
    assert await async_setup_component(hass, "light", {})
    assert await async_setup_component(hass, "group", {})
    await hass.async_block_till_done()

    test_group = await group.Group.async_create_group(
        hass, "init_group", ["light.Bowl", "light.Ceiling"], False
    )
    await hass.async_block_till_done()

    assert group.is_on(hass, test_group.entity_id) is True
    hass.states.async_set("light.Bowl", STATE_OFF)
    await hass.async_block_till_done()
    assert group.is_on(hass, test_group.entity_id) is False

    # Try on non existing state
    assert not group.is_on(hass, "non.existing")


async def test_reloading_groups(hass):
    """Test reloading the group config."""
    assert await async_setup_component(
        hass,
        "group",
        {
            "group": {
                "second_group": {"entities": "light.Bowl", "icon": "mdi:work"},
                "test_group": "hello.world,sensor.happy",
                "empty_group": {"name": "Empty Group", "entities": None},
            }
        },
    )
    await hass.async_block_till_done()

    await group.Group.async_create_group(
        hass, "all tests", ["test.one", "test.two"], user_defined=False
    )

    await hass.async_block_till_done()

    assert sorted(hass.states.async_entity_ids()) == [
        "group.all_tests",
        "group.empty_group",
        "group.second_group",
        "group.test_group",
    ]
    assert hass.bus.async_listeners()["state_changed"] == 1
    assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS]["hello.world"]) == 1
    assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS]["light.bowl"]) == 1
    assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.one"]) == 1
    assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.two"]) == 1

    with patch(
        "homeassistant.config.load_yaml_config_file",
        return_value={
            "group": {"hello": {"entities": "light.Bowl", "icon": "mdi:work"}}
        },
    ):
        await hass.services.async_call(group.DOMAIN, SERVICE_RELOAD)
        await hass.async_block_till_done()

    assert sorted(hass.states.async_entity_ids()) == [
        "group.all_tests",
        "group.hello",
    ]
    assert hass.bus.async_listeners()["state_changed"] == 1
    assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS]["light.bowl"]) == 1
    assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.one"]) == 1
    assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.two"]) == 1


async def test_modify_group(hass):
    """Test modifying a group."""
    group_conf = OrderedDict()
    group_conf["modify_group"] = {
        "name": "friendly_name",
        "icon": "mdi:work",
        "entities": None,
    }

    assert await async_setup_component(hass, "group", {"group": group_conf})
    await hass.async_block_till_done()
    assert hass.states.get(f"{group.DOMAIN}.modify_group")

    # The old way would create a new group modify_group1 because
    # internally it didn't know anything about those created in the config
    common.async_set_group(hass, "modify_group", icon="mdi:play")
    await hass.async_block_till_done()

    group_state = hass.states.get(f"{group.DOMAIN}.modify_group")
    assert group_state

    assert hass.states.async_entity_ids() == ["group.modify_group"]
    assert group_state.attributes.get(ATTR_ICON) == "mdi:play"
    assert group_state.attributes.get(ATTR_FRIENDLY_NAME) == "friendly_name"


async def test_setup(hass):
    """Test setup method."""
    hass.states.async_set("light.Bowl", STATE_ON)
    hass.states.async_set("light.Ceiling", STATE_OFF)

    group_conf = OrderedDict()
    group_conf["test_group"] = "hello.world,sensor.happy"
    group_conf["empty_group"] = {"name": "Empty Group", "entities": None}
    assert await async_setup_component(hass, "light", {})
    await hass.async_block_till_done()

    assert await async_setup_component(hass, "group", {"group": group_conf})
    await hass.async_block_till_done()

    test_group = await group.Group.async_create_group(
        hass, "init_group", ["light.Bowl", "light.Ceiling"], False
    )
    await group.Group.async_create_group(
        hass,
        "created_group",
        ["light.Bowl", f"{test_group.entity_id}"],
        True,
        "mdi:work",
    )
    await hass.async_block_till_done()

    group_state = hass.states.get(f"{group.DOMAIN}.created_group")
    assert STATE_ON == group_state.state
    assert {test_group.entity_id, "light.bowl"} == set(
        group_state.attributes["entity_id"]
    )
    assert group_state.attributes.get(group.ATTR_AUTO) is None
    assert "mdi:work" == group_state.attributes.get(ATTR_ICON)
    assert 3 == group_state.attributes.get(group.ATTR_ORDER)

    group_state = hass.states.get(f"{group.DOMAIN}.test_group")
    assert STATE_UNKNOWN == group_state.state
    assert {"sensor.happy", "hello.world"} == set(group_state.attributes["entity_id"])
    assert group_state.attributes.get(group.ATTR_AUTO) is None
    assert group_state.attributes.get(ATTR_ICON) is None
    assert 0 == group_state.attributes.get(group.ATTR_ORDER)


async def test_service_group_services(hass):
    """Check if service are available."""
    with assert_setup_component(0, "group"):
        await async_setup_component(hass, "group", {"group": {}})