"""The tests for Valve."""
from collections.abc import Generator

import pytest

from homeassistant.components.valve import (
    DOMAIN,
    ValveDeviceClass,
    ValveEntity,
    ValveEntityDescription,
    ValveEntityFeature,
)
from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigFlow
from homeassistant.const import (
    ATTR_ENTITY_ID,
    SERVICE_SET_VALVE_POSITION,
    SERVICE_TOGGLE,
    STATE_CLOSED,
    STATE_CLOSING,
    STATE_OPEN,
    STATE_OPENING,
    STATE_UNAVAILABLE,
    Platform,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from tests.common import (
    MockConfigEntry,
    MockModule,
    MockPlatform,
    mock_config_flow,
    mock_integration,
    mock_platform,
)

TEST_DOMAIN = "test"


class MockFlow(ConfigFlow):
    """Test flow."""


class MockValveEntity(ValveEntity):
    """Mock valve device to use in tests."""

    _attr_should_poll = False
    _target_valve_position: int

    def __init__(
        self,
        unique_id: str = "mock_valve",
        name: str = "Valve",
        features: ValveEntityFeature = ValveEntityFeature(0),
        current_position: int = None,
        device_class: ValveDeviceClass = None,
        reports_position: bool = True,
    ) -> None:
        """Initialize the valve."""
        self._attr_name = name
        self._attr_unique_id = unique_id
        self._attr_supported_features = features
        self._attr_current_valve_position = current_position
        if reports_position is not None:
            self._attr_reports_position = reports_position
        if device_class is not None:
            self._attr_device_class = device_class

    def set_valve_position(self, position: int) -> None:
        """Set the valve to opening or closing towards a target percentage."""
        if position > self._attr_current_valve_position:
            self._attr_is_closing = False
            self._attr_is_opening = True
        else:
            self._attr_is_closing = True
            self._attr_is_opening = False
        self._target_valve_position = position
        self.schedule_update_ha_state()

    def stop_valve(self) -> None:
        """Stop the valve."""
        self._attr_is_closing = False
        self._attr_is_opening = False
        self._target_valve_position = None
        self._attr_is_closed = self._attr_current_valve_position == 0
        self.schedule_update_ha_state()

    @callback
    def finish_movement(self):
        """Set the value to the saved target and removes intermediate states."""
        self._attr_current_valve_position = self._target_valve_position
        self._attr_is_closing = False
        self._attr_is_opening = False
        self.async_write_ha_state()


class MockBinaryValveEntity(ValveEntity):
    """Mock valve device to use in tests."""

    def __init__(
        self,
        unique_id: str = "mock_valve_2",
        name: str = "Valve",
        features: ValveEntityFeature = ValveEntityFeature(0),
        is_closed: bool = None,
    ) -> None:
        """Initialize the valve."""
        self._attr_name = name
        self._attr_unique_id = unique_id
        self._attr_supported_features = features
        self._attr_is_closed = is_closed
        self._attr_reports_position = False

    def open_valve(self) -> None:
        """Open the valve."""
        self._attr_is_closed = False

    def close_valve(self) -> None:
        """Mock implementantion for sync close function."""
        self._attr_is_closed = True


@pytest.fixture(autouse=True)
def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]:
    """Mock config flow."""
    mock_platform(hass, f"{TEST_DOMAIN}.config_flow")

    with mock_config_flow(TEST_DOMAIN, MockFlow):
        yield


@pytest.fixture
def mock_config_entry(hass) -> tuple[MockConfigEntry, list[ValveEntity]]:
    """Mock a config entry which sets up a couple of valve entities."""
    entities = [
        MockBinaryValveEntity(
            is_closed=False,
            features=ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE,
        ),
        MockValveEntity(
            current_position=50,
            features=ValveEntityFeature.OPEN
            | ValveEntityFeature.CLOSE
            | ValveEntityFeature.STOP
            | ValveEntityFeature.SET_POSITION,
        ),
    ]

    async def async_setup_entry_init(
        hass: HomeAssistant, config_entry: ConfigEntry
    ) -> bool:
        """Set up test config entry."""
        await hass.config_entries.async_forward_entry_setup(
            config_entry, Platform.VALVE
        )
        return True

    async def async_unload_entry_init(
        hass: HomeAssistant, config_entry: ConfigEntry
    ) -> bool:
        """Unload up test config entry."""
        await hass.config_entries.async_unload_platforms(config_entry, [Platform.VALVE])
        return True

    mock_platform(hass, f"{TEST_DOMAIN}.config_flow")
    mock_integration(
        hass,
        MockModule(
            TEST_DOMAIN,
            async_setup_entry=async_setup_entry_init,
            async_unload_entry=async_unload_entry_init,
        ),
    )

    async def async_setup_entry_platform(
        hass: HomeAssistant,
        config_entry: ConfigEntry,
        async_add_entities: AddEntitiesCallback,
    ) -> None:
        """Set up test platform via config entry."""
        async_add_entities(entities)

    mock_platform(
        hass,
        f"{TEST_DOMAIN}.{DOMAIN}",
        MockPlatform(async_setup_entry=async_setup_entry_platform),
    )

    config_entry = MockConfigEntry(domain=TEST_DOMAIN)
    config_entry.add_to_hass(hass)

    return (config_entry, entities)


async def test_valve_setup(
    hass: HomeAssistant, mock_config_entry: tuple[MockConfigEntry, list[ValveEntity]]
) -> None:
    """Test setup and tear down of valve platform and entity."""
    config_entry = mock_config_entry[0]

    assert await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()
    entity_id = mock_config_entry[1][0].entity_id

    assert config_entry.state == ConfigEntryState.LOADED
    assert hass.states.get(entity_id)

    assert await hass.config_entries.async_unload(config_entry.entry_id)
    await hass.async_block_till_done()

    assert config_entry.state == ConfigEntryState.NOT_LOADED
    entity_state = hass.states.get(entity_id)

    assert entity_state
    assert entity_state.state == STATE_UNAVAILABLE


async def test_services(
    hass: HomeAssistant, mock_config_entry: tuple[MockConfigEntry, list[ValveEntity]]
) -> None:
    """Test the provided services."""
    config_entry = mock_config_entry[0]
    ent1, ent2 = mock_config_entry[1]

    assert await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    # Test init all valves should be open
    assert is_open(hass, ent1)
    assert is_open(hass, ent2)

    # call basic toggle services
    await call_service(hass, SERVICE_TOGGLE, ent1)
    await call_service(hass, SERVICE_TOGGLE, ent2)

    # entities without stop should be closed and with stop should be closing
    assert is_closed(hass, ent1)
    assert is_closing(hass, ent2)
    ent2.finish_movement()
    assert is_closed(hass, ent2)

    # call basic toggle services and set different valve position states
    await call_service(hass, SERVICE_TOGGLE, ent1)
    await call_service(hass, SERVICE_TOGGLE, ent2)
    await hass.async_block_till_done()

    # entities should be in correct state depending on the SUPPORT_STOP feature and valve position
    assert is_open(hass, ent1)
    assert is_opening(hass, ent2)

    # call basic toggle services
    await call_service(hass, SERVICE_TOGGLE, ent1)
    await call_service(hass, SERVICE_TOGGLE, ent2)

    # entities should be in correct state depending on the SUPPORT_STOP feature and valve position
    assert is_closed(hass, ent1)
    assert not is_opening(hass, ent2)
    assert not is_closing(hass, ent2)
    assert is_closed(hass, ent2)

    await call_service(hass, SERVICE_SET_VALVE_POSITION, ent2, 50)
    assert is_opening(hass, ent2)


async def test_valve_device_class(hass: HomeAssistant) -> None:
    """Test valve entity with defaults."""
    default_valve = MockValveEntity()
    default_valve.hass = hass

    assert default_valve.device_class is None

    entity_description = ValveEntityDescription(
        key="test",
        device_class=ValveDeviceClass.GAS,
    )
    default_valve.entity_description = entity_description
    assert default_valve.device_class is ValveDeviceClass.GAS

    water_valve = MockValveEntity(device_class=ValveDeviceClass.WATER)
    water_valve.hass = hass

    assert water_valve.device_class is ValveDeviceClass.WATER


async def test_valve_report_position(hass: HomeAssistant) -> None:
    """Test valve entity with defaults."""
    default_valve = MockValveEntity(reports_position=None)
    default_valve.hass = hass

    with pytest.raises(ValueError):
        default_valve.reports_position

    second_valve = MockValveEntity(reports_position=True)
    second_valve.hass = hass

    assert second_valve.reports_position is True

    entity_description = ValveEntityDescription(key="test", reports_position=True)
    third_valve = MockValveEntity(reports_position=None)
    third_valve.entity_description = entity_description
    assert third_valve.reports_position is True


async def test_none_state(hass: HomeAssistant) -> None:
    """Test different criteria for closeness."""
    binary_valve_with_none_is_closed_attr = MockBinaryValveEntity(is_closed=None)
    binary_valve_with_none_is_closed_attr.hass = hass

    assert binary_valve_with_none_is_closed_attr.state is None

    pos_valve_with_none_is_closed_attr = MockValveEntity()
    pos_valve_with_none_is_closed_attr.hass = hass

    assert pos_valve_with_none_is_closed_attr.state is None


async def test_supported_features(hass: HomeAssistant) -> None:
    """Test valve entity with defaults."""
    valve = MockValveEntity(features=None)
    valve.hass = hass

    assert valve.supported_features is None


def call_service(hass, service, ent, position=None):
    """Call any service on entity."""
    params = {ATTR_ENTITY_ID: ent.entity_id}
    if position is not None:
        params["position"] = position
    return hass.services.async_call(DOMAIN, service, params, blocking=True)


def set_valve_position(ent, position) -> None:
    """Set a position value to a valve."""
    ent._values["current_valve_position"] = position


def is_open(hass, ent):
    """Return if the valve is closed based on the statemachine."""
    return hass.states.is_state(ent.entity_id, STATE_OPEN)


def is_opening(hass, ent):
    """Return if the valve is closed based on the statemachine."""
    return hass.states.is_state(ent.entity_id, STATE_OPENING)


def is_closed(hass, ent):
    """Return if the valve is closed based on the statemachine."""
    return hass.states.is_state(ent.entity_id, STATE_CLOSED)


def is_closing(hass, ent):
    """Return if the valve is closed based on the statemachine."""
    return hass.states.is_state(ent.entity_id, STATE_CLOSING)