"""Fixtures for the nexia integration tests."""

from dataclasses import dataclass
from unittest.mock import NonCallableMock, patch

from nexia.automation import NexiaAutomation
from nexia.home import NexiaHome
from nexia.sensor import NexiaSensor
from nexia.thermostat import NexiaThermostat
from nexia.zone import NexiaThermostatZone
import pytest

from homeassistant.components.nexia.const import DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant

from tests.common import MockConfigEntry


def create_mock_sensor(
    sensor_id: int,
    name: str,
    weight: float = 1.0,
    temperature: int = 72,
    humidity: int = 45,
) -> NonCallableMock[NexiaSensor]:
    """Create a mock NexiaSensor."""
    sensor = NonCallableMock(NexiaSensor)
    sensor.id = sensor_id
    sensor.name = name
    sensor.weight = weight
    sensor.temperature = temperature
    sensor.humidity = humidity
    sensor.has_online = False
    sensor.has_battery = False

    return sensor


def create_mock_zone(
    zone_id: int,
    name: str,
    thermostat: NexiaThermostat | None = None,
    *,
    cooling_setpoint: int = 79,
    current_mode: str = "AUTO",
    heating_setpoint: int = 63,
    is_calling: bool = False,
    is_in_permanent_hold: bool = True,
    preset: str = "None",
    presets: list[str] | None = None,
    requested_mode: str = "AUTO",
    sensors: list[NexiaSensor] | None = None,
    setpoint_status: str = "Permanent Hold",
    status: str = "Idle",
    temperature: int = 72,
) -> NonCallableMock[NexiaThermostatZone]:
    """Create a mock NexiaThermostatZone."""
    zone = NonCallableMock(NexiaThermostatZone)
    zone.zone_id = zone_id
    zone.get_name.return_value = name
    zone.thermostat = thermostat
    zone.get_active_sensor_ids.return_value = set()
    zone.get_cooling_setpoint.return_value = cooling_setpoint
    zone.get_current_mode.return_value = current_mode
    zone.get_heating_setpoint.return_value = heating_setpoint
    zone.get_preset.return_value = preset
    zone.get_presets.return_value = presets or ["None", "Home", "Away", "Sleep"]
    zone.get_requested_mode.return_value = requested_mode
    zone.get_sensors.return_value = sensors or []
    zone.get_setpoint_status.return_value = setpoint_status
    zone.get_status.return_value = status
    zone.get_temperature.return_value = temperature
    zone.is_calling.return_value = is_calling
    zone.is_in_permanent_hold.return_value = is_in_permanent_hold
    zone.is_native_zone.return_value = True
    zone.load_current_sensor_state.return_value = True
    zone.select_room_iq_sensors.return_value = True

    return zone


@dataclass
class _ThermostatStateData:
    """Mutable class so thermostat getters and setters can share state."""

    air_cleaner_mode: str
    dehumidify_setpoint: float
    emergency_heat_on: bool
    fan_mode: str
    fan_setpoint: float
    humidify_setpoint: float

    async def async_set_air_cleaner_mode(self, mode: str) -> None:
        """Set the air cleaner mode asynchronously."""
        self.air_cleaner_mode = mode

    async def async_set_dehumidify_setpoint(self, setpoint: float) -> None:
        """Set dehumidify setpoint asynchronously."""
        self.dehumidify_setpoint = setpoint

    async def async_set_emergency_heat_on(self, on: bool) -> None:
        """Set the emergency heat on asynchronously."""
        self.emergency_heat_on = on

    async def async_set_fan_mode(self, mode: str) -> None:
        """Set the fan mode asynchronously."""
        self.fan_mode = mode

    async def async_set_fan_setpoint(self, setpoint: float) -> None:
        """Set the fan setpoint asynchronously."""
        self.fan_setpoint = setpoint

    async def async_set_humidify_setpoint(self, setpoint: float) -> None:
        """Set humidify setpoint asynchronously."""
        self.humidify_setpoint = setpoint


def create_mock_thermostat(
    thermostat_id: int,
    name: str,
    zones: list[NexiaThermostatZone] | None = None,
    *,
    air_cleaner_mode: str = "auto",
    current_compressor_speed: float = 0.0,
    deadband: int = 3,
    dehumidify_setpoint: float = 0.50,
    dehumidify_setpoint_limits: tuple[float, float] = (0.35, 0.65),
    fan_mode: str = "Auto",
    fan_modes: list[str] | None = None,
    fan_speed_setpoint: float = 0.35,
    firmware: str = "5.9.1",
    has_air_cleaner: bool = False,
    has_dehumidify_support: bool = False,
    has_emergency_heat: bool = False,
    has_humidify_support: bool = False,
    has_outdoor_temperature: bool = False,
    has_relative_humidity: bool = False,
    has_variable_fan_speed: bool = False,
    has_variable_speed_compressor: bool = False,
    has_zones: bool = False,
    humidify_setpoint: float = 0.36,
    humidify_setpoint_limits: tuple[float, float] = (0.10, 0.45),
    is_blower_active: bool = False,
    is_emergency_heat_active: bool = False,
    is_online: bool = True,
    model: str = "XL1050",
    outdoor_temperature: float | None = 30.0,
    relative_humidity: float | None = 0.52,
    requested_compressor_speed: float = 0.0,
    setpoint_limits: tuple[int, int] = (55, 99),
    system_status: str = "Idle",
    unit: str = "F",
    variable_fan_speed_limits: tuple[float, float] = (0.35, 1.0),
) -> NonCallableMock[NexiaThermostat]:
    """Create a mock NexiaThermostat."""
    # Mutable class so get_... and set_... share state
    state_data = _ThermostatStateData(
        air_cleaner_mode=air_cleaner_mode,
        dehumidify_setpoint=dehumidify_setpoint,
        emergency_heat_on=is_emergency_heat_active,
        fan_mode=fan_mode,
        fan_setpoint=fan_speed_setpoint,
        humidify_setpoint=humidify_setpoint,
    )

    thermostat = NonCallableMock(NexiaThermostat)
    thermostat.thermostat_id = thermostat_id
    thermostat.get_name.return_value = name

    if zones is not None:
        zone_ids = [z.zone_id for z in zones]
        thermostat.get_zone_ids.return_value = zone_ids
        thermostat.get_zone_by_id.side_effect = lambda zid: next(
            z for z in zones if z.zone_id == zid
        )
    else:
        thermostat.get_zone_ids.return_value = []
    thermostat.get_air_cleaner_mode.side_effect = lambda: state_data.air_cleaner_mode
    thermostat.get_current_compressor_speed.return_value = current_compressor_speed
    thermostat.get_deadband.return_value = deadband
    thermostat.get_dehumidify_setpoint.side_effect = lambda: (
        state_data.dehumidify_setpoint
    )
    thermostat.get_dehumidify_setpoint_limits.return_value = dehumidify_setpoint_limits
    thermostat.get_fan_mode.side_effect = lambda: state_data.fan_mode
    thermostat.get_fan_modes.return_value = fan_modes or ["Auto", "On", "Circulate"]
    thermostat.get_fan_speed_setpoint.side_effect = lambda: state_data.fan_setpoint
    thermostat.get_firmware.return_value = firmware
    thermostat.get_humidify_setpoint.side_effect = lambda: state_data.humidify_setpoint
    thermostat.get_humidify_setpoint_limits.return_value = humidify_setpoint_limits
    thermostat.get_humidity_setpoint_limits.return_value = (
        (humidify_setpoint_limits[0], dehumidify_setpoint_limits[1])
        if has_humidify_support and has_dehumidify_support
        else humidify_setpoint_limits
        if has_humidify_support
        else dehumidify_setpoint_limits
    )
    thermostat.get_model.return_value = model
    thermostat.get_outdoor_temperature.return_value = outdoor_temperature
    thermostat.get_relative_humidity.return_value = relative_humidity
    thermostat.get_requested_compressor_speed.return_value = requested_compressor_speed
    thermostat.get_setpoint_limits.return_value = setpoint_limits
    thermostat.get_system_status.return_value = system_status
    thermostat.get_unit.return_value = unit
    thermostat.get_variable_fan_speed_limits.return_value = variable_fan_speed_limits
    thermostat.has_air_cleaner.return_value = has_air_cleaner
    thermostat.has_dehumidify_support.return_value = has_dehumidify_support
    thermostat.has_emergency_heat.return_value = has_emergency_heat
    thermostat.has_humidify_support.return_value = has_humidify_support
    thermostat.has_outdoor_temperature.return_value = has_outdoor_temperature
    thermostat.has_relative_humidity.return_value = has_relative_humidity
    thermostat.has_variable_fan_speed.return_value = has_variable_fan_speed
    thermostat.has_variable_speed_compressor.return_value = (
        has_variable_speed_compressor
    )
    thermostat.has_zones.return_value = has_zones
    thermostat.is_blower_active.return_value = is_blower_active
    thermostat.is_emergency_heat_active.side_effect = lambda: (
        state_data.emergency_heat_on
    )
    thermostat.is_online = is_online
    thermostat.set_air_cleaner.side_effect = state_data.async_set_air_cleaner_mode
    thermostat.set_dehumidify_setpoint.side_effect = (
        state_data.async_set_dehumidify_setpoint
    )
    thermostat.set_emergency_heat.side_effect = state_data.async_set_emergency_heat_on
    thermostat.set_fan_mode.side_effect = state_data.async_set_fan_mode
    thermostat.set_fan_setpoint.side_effect = state_data.async_set_fan_setpoint
    thermostat.set_humidify_setpoint.side_effect = (
        state_data.async_set_humidify_setpoint
    )

    return thermostat


def create_mock_automation(
    automation_id: int,
    name: str,
    description: str = "",
    enabled: bool = True,
) -> NonCallableMock[NexiaAutomation]:
    """Create a mock NexiaAutomation."""
    automation = NonCallableMock(NexiaAutomation)
    automation.automation_id = automation_id
    automation.name = name
    automation.description = description
    automation.enabled = enabled

    return automation


def create_mock_nexia_home(
    house_id: int = 123456,
    thermostats: list[NexiaThermostat] | None = None,
    automations: list[NexiaAutomation] | None = None,
    root_url: str = "https://www.mynexia.com",
) -> NonCallableMock[NexiaHome]:
    """Create a mock NexiaHome."""
    nexia_home = NonCallableMock(NexiaHome)
    nexia_home.house_id = house_id
    nexia_home.root_url = root_url
    nexia_home.automations_json = [
        {"name": "automation1", "data": 1},
        {"name": "automation2", "data": 2},
    ]
    nexia_home.devices_json = [
        {"name": "device1", "data": 3},
        {"name": "device2", "data": 4},
    ]
    nexia_home.update.return_value = {}

    _thermostats = thermostats or []
    thermostat_ids = [t.thermostat_id for t in _thermostats]
    nexia_home.get_thermostat_ids.return_value = thermostat_ids
    nexia_home.get_thermostat_by_id.side_effect = lambda tid: next(
        t for t in _thermostats if t.thermostat_id == tid
    )

    _automations = automations or []
    automation_ids = [a.automation_id for a in _automations]
    nexia_home.get_automation_ids.return_value = automation_ids
    nexia_home.get_automation_by_id.side_effect = lambda aid: next(
        a for a in _automations if a.automation_id == aid
    )

    return nexia_home


@pytest.fixture
def mock_nexia_home() -> NonCallableMock[NexiaHome]:
    """Return a default mock NexiaHome for use in tests that don't need custom setup."""
    zone = create_mock_zone(
        zone_id=100,
        name="Nick Office",
        is_calling=True,
        status="Relieving Air",
        temperature=73,
    )
    thermostat1 = create_mock_thermostat(
        thermostat_id=2000000,
        name="Nick Office",
        zones=[zone],
        has_dehumidify_support=True,
        dehumidify_setpoint=0.45,
        has_relative_humidity=True,
        system_status="Cooling",
    )
    zone.thermostat = thermostat1

    zone = create_mock_zone(zone_id=200, name="Main Zone")
    thermostat2 = create_mock_thermostat(
        thermostat_id=2000001,
        name="Master Suite",
        zones=[zone],
        has_outdoor_temperature=True,
        outdoor_temperature=87.0,  # °F -> ~30.6 °C in the fixture data
        has_relative_humidity=True,
        relative_humidity=0.52,
        has_variable_fan_speed=True,
        fan_speed_setpoint=0.35,
        variable_fan_speed_limits=(0.35, 1.0),
        has_variable_speed_compressor=True,
        current_compressor_speed=0.69,
        requested_compressor_speed=0.69,
        is_blower_active=True,
        system_status="Cooling",
    )
    zone.thermostat = thermostat2

    zone = create_mock_zone(zone_id=300, name="Zone B")
    thermostat3 = create_mock_thermostat(
        thermostat_id=2000002,
        name="Downstairs East Wing",
        zones=[zone],
        has_variable_fan_speed=True,
        fan_speed_setpoint=0.45,
    )
    zone.thermostat = thermostat3

    zone = create_mock_zone(zone_id=400, name="Kitchen", temperature=77)
    thermostat4 = create_mock_thermostat(
        thermostat_id=2000003,
        name="Kitchen",
        zones=[zone],
        has_dehumidify_support=True,
        has_relative_humidity=True,
        relative_humidity=0.36,
    )
    zone.thermostat = thermostat4

    sensor1 = create_mock_sensor(sensor_id=1, name="Center", weight=0.5)
    sensor2 = create_mock_sensor(sensor_id=2, name="Upstairs", weight=0.5)
    zone = create_mock_zone(
        zone_id=500, name="Center NativeZone", sensors=[sensor1, sensor2]
    )
    zone.get_active_sensor_ids.return_value = {1, 2}
    zone.get_sensor_by_id.side_effect = lambda sid: {1: sensor1, 2: sensor2}[sid]
    thermostat5 = create_mock_thermostat(
        thermostat_id=2000004,
        name="Center NativeZone",
        zones=[zone],
    )
    zone.thermostat = thermostat5

    automations = [
        create_mock_automation(1001, "Away Short", "Sets all zones to away temps."),
        create_mock_automation(1002, "Power Outage", "Hold zones at 55, 90 °F"),
        create_mock_automation(1003, "Power Restored", "Return to Run Schedule"),
    ]

    return create_mock_nexia_home(
        thermostats=[thermostat1, thermostat2, thermostat3, thermostat4, thermostat5],
        automations=automations,
    )


@pytest.fixture
def patch_nexia_home(
    mock_nexia_home: NonCallableMock[NexiaHome],
) -> NonCallableMock[NexiaHome]:
    """Patches NexiaHome to return a mock instance for the duration of a test."""
    with patch(
        "homeassistant.components.nexia.NexiaHome", return_value=mock_nexia_home
    ):
        yield mock_nexia_home


async def setup_integration(
    hass: HomeAssistant, patch_nexia_home: NexiaHome, unique_id: str = "123456"
) -> MockConfigEntry:
    """Set up the nexia integration with a pre-configured mock NexiaHome."""
    entry = MockConfigEntry(
        domain=DOMAIN,
        data={CONF_USERNAME: "mock", CONF_PASSWORD: "mock"},
        minor_version=2,
        unique_id=unique_id,
    )
    entry.add_to_hass(hass)

    # validate setup
    for tid in patch_nexia_home.get_thermostat_ids():
        thermostat = patch_nexia_home.get_thermostat_by_id(tid)
        for zid in thermostat.get_zone_ids():
            assert thermostat.get_zone_by_id(zid).thermostat is thermostat

    await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()

    return entry
