"""Tests for django_devicehub.device_type — DeviceTypeMeta and DeviceType."""

from django.apps import apps

from django_devicehub.device_type import DeviceType
from django_devicehub.fields import Command, Reading, StateField, reading_types

from tests.testapp.devices import SmartCamera, SoilSensor, WeatherStation
from tests.testapp.models import (
    SmartCameraDevice,
    SmartCameraMessage,
    SmartCameraReading,
    SoilSensorDevice,
    WeatherStationDevice,
    WeatherStationMessage,
    WeatherStationReading,
)


# ---------------------------------------------------------------------------
# Metaclass: collects descriptors from class namespace
# ---------------------------------------------------------------------------


class TestDeviceTypeMetaCollection:
    """DeviceTypeMeta must collect Reading, Command, StateField from the class body."""

    def test_readings_collected(self):
        assert "temperature" in WeatherStation._readings
        assert "humidity" in WeatherStation._readings
        assert "rainfall" in WeatherStation._readings
        assert "wind_speed" in WeatherStation._readings

    def test_commands_collected(self):
        assert "reboot" in WeatherStation._commands
        assert "set_interval" in WeatherStation._commands

    def test_state_fields_collected(self):
        assert "recording_mode" in SmartCamera._state_fields
        assert "night_vision" in SmartCamera._state_fields

    def test_no_state_fields_on_weather_station(self):
        assert len(WeatherStation._state_fields) == 0

    def test_descriptor_names_are_set(self):
        assert WeatherStation._readings["temperature"].name == "temperature"
        assert WeatherStation._commands["reboot"].name == "reboot"
        assert SmartCamera._state_fields["recording_mode"].name == "recording_mode"

    def test_descriptors_removed_from_class_namespace(self):
        # The metaclass deletes descriptors from the class dict; they should
        # not be accessible as class attributes.
        assert not isinstance(WeatherStation.__dict__.get("temperature"), Reading)
        assert not isinstance(WeatherStation.__dict__.get("reboot"), Command)

    def test_soil_sensor_readings(self):
        assert "moisture" in SoilSensor._readings
        assert "ph" in SoilSensor._readings
        assert "conductivity" in SoilSensor._readings

    def test_smart_camera_readings(self):
        assert "motion_detected" in SmartCamera._readings
        assert "frame_rate" in SmartCamera._readings

    def test_smart_camera_commands(self):
        assert "start_recording" in SmartCamera._commands
        assert "stop_recording" in SmartCamera._commands
        assert "set_resolution" in SmartCamera._commands


# ---------------------------------------------------------------------------
# Inheritance
# ---------------------------------------------------------------------------


class TestDeviceTypeInheritance:
    """Child classes inherit parent's readings, commands, and state fields."""

    def test_child_inherits_parent_readings(self):
        class BaseWeather(DeviceType):
            _abstract = True
            temperature = Reading(type=reading_types.FLOAT, unit="C")

        class ExtendedWeather(BaseWeather):
            _abstract = True
            pressure = Reading(type=reading_types.FLOAT, unit="hPa")

        assert "temperature" in ExtendedWeather._readings
        assert "pressure" in ExtendedWeather._readings

    def test_child_inherits_parent_commands(self):
        class BaseBot(DeviceType):
            _abstract = True
            reboot = Command()

        class SmartBot(BaseBot):
            _abstract = True
            calibrate = Command()

        assert "reboot" in SmartBot._commands
        assert "calibrate" in SmartBot._commands

    def test_child_can_override_parent_reading(self):
        class Parent(DeviceType):
            _abstract = True
            temperature = Reading(type=reading_types.FLOAT, unit="C")

        class Child(Parent):
            _abstract = True
            temperature = Reading(type=reading_types.FLOAT, unit="F", range=(-40, 176))

        assert Child._readings["temperature"].unit == "F"
        assert Child._readings["temperature"].range == (-40, 176)

    def test_child_inherits_state_fields(self):
        class BaseCam(DeviceType):
            _abstract = True
            night_mode = StateField(type="boolean", default=False)

        class ProCam(BaseCam):
            _abstract = True
            zoom_level = StateField(type="integer", default=1)

        assert "night_mode" in ProCam._state_fields
        assert "zoom_level" in ProCam._state_fields


# ---------------------------------------------------------------------------
# create_models()
# ---------------------------------------------------------------------------


class TestCreateModels:
    """create_models() returns three Django model classes."""

    def test_returns_three_models(self):
        # Already called in tests/testapp/models.py — just verify the types
        assert WeatherStationDevice is not None
        assert WeatherStationReading is not None
        assert WeatherStationMessage is not None

    def test_model_back_references_set(self):
        assert WeatherStation._device_model is WeatherStationDevice
        assert WeatherStation._reading_model is WeatherStationReading
        assert WeatherStation._message_model is WeatherStationMessage

    def test_smart_camera_models(self):
        assert SmartCamera._device_model is SmartCameraDevice
        assert SmartCamera._reading_model is SmartCameraReading
        assert SmartCamera._message_model is SmartCameraMessage


# ---------------------------------------------------------------------------
# Generated models registered with Django app registry
# ---------------------------------------------------------------------------


class TestDjangoAppRegistration:
    def test_weather_station_device_in_app_registry(self):
        model = apps.get_model("testapp", "WeatherStationDevice")
        assert model is WeatherStationDevice

    def test_weather_station_reading_in_app_registry(self):
        model = apps.get_model("testapp", "WeatherStationReading")
        assert model is WeatherStationReading

    def test_weather_station_message_in_app_registry(self):
        model = apps.get_model("testapp", "WeatherStationMessage")
        assert model is WeatherStationMessage

    def test_soil_sensor_device_in_app_registry(self):
        model = apps.get_model("testapp", "SoilSensorDevice")
        assert model is SoilSensorDevice

    def test_smart_camera_device_in_app_registry(self):
        model = apps.get_model("testapp", "SmartCameraDevice")
        assert model is SmartCameraDevice


# ---------------------------------------------------------------------------
# get_meta()
# ---------------------------------------------------------------------------


class TestGetMeta:
    def test_returns_dict(self):
        meta = WeatherStation.get_meta()
        assert isinstance(meta, dict)

    def test_protocol_from_meta(self):
        assert WeatherStation.get_meta()["protocol"] == "mqtt"

    def test_broker_from_meta(self):
        assert WeatherStation.get_meta()["broker"] == "default"

    def test_heartbeat_interval_from_meta(self):
        assert WeatherStation.get_meta()["heartbeat_interval"] == 300

    def test_defaults_applied_when_not_set(self):
        meta = SmartCamera.get_meta()
        # SmartCamera.Meta does not set auth or topic_template
        assert meta["auth"] == "token"
        assert (
            meta["topic_template"] == "{prefix}/{type_name}/{device_id}/{message_type}"
        )

    def test_soil_sensor_custom_heartbeat(self):
        assert SoilSensor.get_meta()["heartbeat_interval"] == 600


# ---------------------------------------------------------------------------
# get_topic() and get_subscribe_topics()
# ---------------------------------------------------------------------------


class TestTopics:
    """Topics use TOPIC_PREFIX from settings (test settings: 'test')."""

    def test_get_topic_data(self):
        topic = WeatherStation.get_topic("ws-001", "data")
        assert topic == "test/weatherstation/ws-001/data"

    def test_get_topic_status(self):
        topic = WeatherStation.get_topic("ws-001", "status")
        assert topic == "test/weatherstation/ws-001/status"

    def test_get_topic_cmd(self):
        topic = SoilSensor.get_topic("ss-001", "cmd")
        assert topic == "test/soilsensor/ss-001/cmd"

    def test_get_topic_default_message_type_is_data(self):
        topic = WeatherStation.get_topic("ws-001")
        assert topic == "test/weatherstation/ws-001/data"

    def test_get_subscribe_topics_returns_two(self):
        topics = WeatherStation.get_subscribe_topics()
        assert len(topics) == 2

    def test_get_subscribe_topics_contain_wildcards(self):
        topics = WeatherStation.get_subscribe_topics()
        assert "test/weatherstation/+/data" in topics
        assert "test/weatherstation/+/status" in topics

    def test_smart_camera_subscribe_topics(self):
        topics = SmartCamera.get_subscribe_topics()
        assert "test/smartcamera/+/data" in topics
        assert "test/smartcamera/+/status" in topics


# ---------------------------------------------------------------------------
# Hooks are callable
# ---------------------------------------------------------------------------


class TestHooks:
    def test_on_data_is_callable(self):
        dt = WeatherStation()
        assert callable(dt.on_data)

    def test_on_status_change_is_callable(self):
        dt = WeatherStation()
        assert callable(dt.on_status_change)

    def test_on_connect_is_callable(self):
        dt = DeviceType()
        assert callable(dt.on_connect)

    def test_on_disconnect_is_callable(self):
        dt = DeviceType()
        assert callable(dt.on_disconnect)

    def test_on_command_response_is_callable(self):
        dt = DeviceType()
        assert callable(dt.on_command_response)

    def test_base_hooks_do_not_raise(self):
        dt = DeviceType()
        # These should be safe no-ops on the base class
        dt.on_data(None, {})
        dt.on_status_change(None, "offline", "online")
        dt.on_connect(None)
        dt.on_disconnect(None)
        dt.on_command_response(None, "cmd", {})
