"""Tests for generated Django models — BaseDevice, BaseReading, BaseMessage."""

import pytest
from django.db import models as django_models
from django.utils import timezone

from django_devicehub.models import (
    BaseDevice,
    BaseReading,
    DeviceStatus,
    ProcessingStatus,
)

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


# ---------------------------------------------------------------------------
# Helper to get field names from a model
# ---------------------------------------------------------------------------


def _field_names(model):
    return {f.name for f in model._meta.get_fields() if hasattr(f, "column")}


# ---------------------------------------------------------------------------
# WeatherStationDevice — BaseDevice fields only (no state fields)
# ---------------------------------------------------------------------------


class TestWeatherStationDeviceFields:
    """WeatherStationDevice should have all BaseDevice fields and no extras."""

    BASE_DEVICE_FIELDS = {
        "id",
        "device_id",
        "name",
        "status",
        "battery_level",
        "last_seen",
        "firmware_version",
        "is_active",
        "mqtt_username",
        "mqtt_password_hash",
        "api_key",
        "metadata",
        "created_at",
        "updated_at",
    }

    def test_inherits_base_device(self):
        assert issubclass(WeatherStationDevice, BaseDevice)

    def test_has_all_base_fields(self):
        fields = _field_names(WeatherStationDevice)
        for f in self.BASE_DEVICE_FIELDS:
            assert f in fields, f"Missing BaseDevice field: {f}"

    def test_no_extra_column_fields(self):
        """WeatherStation has no StateFields, so no extra columns."""
        fields = _field_names(WeatherStationDevice)
        extra = fields - self.BASE_DEVICE_FIELDS
        assert extra == set(), f"Unexpected fields: {extra}"

    def test_device_type_cls_back_reference(self):
        assert WeatherStationDevice._device_type_cls is WeatherStation


# ---------------------------------------------------------------------------
# SmartCameraDevice — BaseDevice + state fields
# ---------------------------------------------------------------------------


class TestSmartCameraDeviceFields:
    def test_inherits_base_device(self):
        assert issubclass(SmartCameraDevice, BaseDevice)

    def test_has_recording_mode_field(self):
        field = SmartCameraDevice._meta.get_field("recording_mode")
        assert isinstance(field, django_models.CharField)
        assert field.default == "continuous"
        assert field.choices == [
            ("continuous", "continuous"),
            ("motion", "motion"),
            ("scheduled", "scheduled"),
        ]

    def test_has_night_vision_field(self):
        field = SmartCameraDevice._meta.get_field("night_vision")
        assert isinstance(field, django_models.BooleanField)
        assert field.default is True

    def test_device_type_cls_back_reference(self):
        assert SmartCameraDevice._device_type_cls is SmartCamera


# ---------------------------------------------------------------------------
# WeatherStationReading — device FK + timestamp + reading columns
# ---------------------------------------------------------------------------


class TestWeatherStationReadingFields:
    def test_inherits_base_reading(self):
        assert issubclass(WeatherStationReading, BaseReading)

    def test_has_device_fk(self):
        field = WeatherStationReading._meta.get_field("device")
        assert isinstance(field, django_models.ForeignKey)
        assert field.related_model is WeatherStationDevice

    def test_has_timestamp(self):
        field = WeatherStationReading._meta.get_field("timestamp")
        assert isinstance(field, django_models.DateTimeField)

    def test_has_temperature(self):
        field = WeatherStationReading._meta.get_field("temperature")
        assert isinstance(field, django_models.FloatField)
        assert field.null is True

    def test_has_humidity(self):
        field = WeatherStationReading._meta.get_field("humidity")
        assert isinstance(field, django_models.FloatField)
        assert field.null is True

    def test_has_rainfall(self):
        field = WeatherStationReading._meta.get_field("rainfall")
        assert isinstance(field, django_models.FloatField)
        assert field.null is True

    def test_has_wind_speed(self):
        field = WeatherStationReading._meta.get_field("wind_speed")
        assert isinstance(field, django_models.FloatField)
        assert field.null is True


# ---------------------------------------------------------------------------
# SoilSensorReading
# ---------------------------------------------------------------------------


class TestSoilSensorReadingFields:
    def test_has_device_fk(self):
        field = SoilSensorReading._meta.get_field("device")
        assert isinstance(field, django_models.ForeignKey)
        assert field.related_model is SoilSensorDevice

    def test_has_timestamp(self):
        assert SoilSensorReading._meta.get_field("timestamp")

    def test_has_moisture(self):
        field = SoilSensorReading._meta.get_field("moisture")
        assert isinstance(field, django_models.FloatField)

    def test_has_ph(self):
        field = SoilSensorReading._meta.get_field("ph")
        assert isinstance(field, django_models.FloatField)

    def test_has_conductivity(self):
        field = SoilSensorReading._meta.get_field("conductivity")
        assert isinstance(field, django_models.FloatField)


# ---------------------------------------------------------------------------
# SmartCameraReading
# ---------------------------------------------------------------------------


class TestSmartCameraReadingFields:
    def test_has_device_fk(self):
        field = SmartCameraReading._meta.get_field("device")
        assert isinstance(field, django_models.ForeignKey)
        assert field.related_model is SmartCameraDevice

    def test_has_motion_detected(self):
        field = SmartCameraReading._meta.get_field("motion_detected")
        assert isinstance(field, django_models.BooleanField)

    def test_has_frame_rate(self):
        field = SmartCameraReading._meta.get_field("frame_rate")
        assert isinstance(field, django_models.IntegerField)


# ---------------------------------------------------------------------------
# BaseDevice.generate_credentials()
# ---------------------------------------------------------------------------


class TestGenerateCredentials:
    @pytest.mark.django_db
    def test_generate_credentials_sets_mqtt_username(self, weather_station_device):
        weather_station_device.generate_credentials()
        assert weather_station_device.mqtt_username is not None
        assert weather_station_device.device_id in weather_station_device.mqtt_username

    @pytest.mark.django_db
    def test_generate_credentials_returns_plaintext_password(
        self, weather_station_device
    ):
        creds = weather_station_device.generate_credentials()
        assert "mqtt_password" in creds
        assert len(creds["mqtt_password"]) > 0

    @pytest.mark.django_db
    def test_generate_credentials_hashes_password(self, weather_station_device):
        creds = weather_station_device.generate_credentials()
        # The hash should not equal the plaintext
        assert weather_station_device.mqtt_password_hash != creds["mqtt_password"]
        # The hash should start with a Django hasher prefix
        assert weather_station_device.mqtt_password_hash.startswith("pbkdf2_sha256$")

    @pytest.mark.django_db
    def test_generate_credentials_creates_api_key(self, weather_station_device):
        creds = weather_station_device.generate_credentials()
        assert creds["api_key"] is not None
        assert len(creds["api_key"]) == 64  # secrets.token_hex(32) = 64 chars

    @pytest.mark.django_db
    def test_generate_credentials_returns_all_keys(self, weather_station_device):
        creds = weather_station_device.generate_credentials()
        assert set(creds.keys()) == {"mqtt_username", "mqtt_password", "api_key"}


# ---------------------------------------------------------------------------
# BaseDevice.check_mqtt_password()
# ---------------------------------------------------------------------------


class TestCheckMqttPassword:
    @pytest.mark.django_db
    def test_correct_password_returns_true(self, provisioned_device):
        device, creds = provisioned_device
        assert device.check_mqtt_password(creds["mqtt_password"]) is True

    @pytest.mark.django_db
    def test_wrong_password_returns_false(self, provisioned_device):
        device, _ = provisioned_device
        assert device.check_mqtt_password("wrong-password") is False

    @pytest.mark.django_db
    def test_empty_password_returns_false(self, provisioned_device):
        device, _ = provisioned_device
        assert device.check_mqtt_password("") is False


# ---------------------------------------------------------------------------
# BaseDevice.mark_online() / mark_offline()
# ---------------------------------------------------------------------------


class TestMarkOnlineOffline:
    @pytest.mark.django_db
    def test_mark_online_sets_status(self, weather_station_device):
        assert weather_station_device.status == DeviceStatus.OFFLINE
        weather_station_device.mark_online()
        weather_station_device.refresh_from_db()
        assert weather_station_device.status == DeviceStatus.ONLINE

    @pytest.mark.django_db
    def test_mark_online_sets_last_seen(self, weather_station_device):
        assert weather_station_device.last_seen is None
        weather_station_device.mark_online()
        weather_station_device.refresh_from_db()
        assert weather_station_device.last_seen is not None
        # Should be very recent
        diff = timezone.now() - weather_station_device.last_seen
        assert diff.total_seconds() < 5

    @pytest.mark.django_db
    def test_mark_offline_sets_status(self, weather_station_device):
        weather_station_device.mark_online()
        weather_station_device.mark_offline()
        weather_station_device.refresh_from_db()
        assert weather_station_device.status == DeviceStatus.OFFLINE

    @pytest.mark.django_db
    def test_mark_offline_preserves_last_seen(self, weather_station_device):
        weather_station_device.mark_online()
        last_seen_before = weather_station_device.last_seen
        weather_station_device.mark_offline()
        weather_station_device.refresh_from_db()
        assert weather_station_device.last_seen == last_seen_before


# ---------------------------------------------------------------------------
# BaseMessage.mark_completed() / mark_failed()
# ---------------------------------------------------------------------------


class TestMessageStatusMethods:
    @pytest.mark.django_db
    def test_mark_completed(self, weather_station_device):
        msg = WeatherStationMessage.objects.create(
            device=weather_station_device,
            topic="test/weatherstation/ws-001/data",
            payload_json={"temperature": 25},
        )
        assert msg.processing_status == ProcessingStatus.PENDING
        msg.mark_completed()
        msg.refresh_from_db()
        assert msg.processing_status == ProcessingStatus.COMPLETED
        assert msg.processed_at is not None

    @pytest.mark.django_db
    def test_mark_failed_with_error(self, weather_station_device):
        msg = WeatherStationMessage.objects.create(
            device=weather_station_device,
            topic="test/weatherstation/ws-001/data",
            payload_json={"bad": "data"},
        )
        msg.mark_failed("Parse error")
        msg.refresh_from_db()
        assert msg.processing_status == ProcessingStatus.FAILED
        assert msg.processing_error == "Parse error"
        assert msg.processed_at is not None

    @pytest.mark.django_db
    def test_mark_failed_without_error(self, weather_station_device):
        msg = WeatherStationMessage.objects.create(
            device=weather_station_device,
            topic="test/weatherstation/ws-001/data",
        )
        msg.mark_failed()
        msg.refresh_from_db()
        assert msg.processing_status == ProcessingStatus.FAILED
        assert msg.processing_error == ""


# ---------------------------------------------------------------------------
# Device.send_command() — validates payload
# ---------------------------------------------------------------------------


class TestSendCommandValidation:
    @pytest.mark.django_db
    def test_unknown_command_raises(self, weather_station_device):
        with pytest.raises(ValueError, match="Unknown command"):
            weather_station_device.send_command("fly_away")

    @pytest.mark.django_db
    def test_invalid_payload_raises_type_error(self, weather_station_device):
        with pytest.raises(TypeError, match="expected int"):
            weather_station_device.send_command(
                "set_interval", payload={"interval": "ten"}
            )

    @pytest.mark.django_db
    def test_missing_payload_field_raises(self, weather_station_device):
        with pytest.raises(ValueError, match="Missing required payload field"):
            weather_station_device.send_command(
                "set_interval", payload={"wrong_key": 5}
            )


# ---------------------------------------------------------------------------
# Model __str__
# ---------------------------------------------------------------------------


class TestModelStr:
    @pytest.mark.django_db
    def test_device_str_with_name(self, weather_station_device):
        s = str(weather_station_device)
        assert "Field Station Alpha" in s
        assert "offline" in s

    @pytest.mark.django_db
    def test_device_str_without_name(self, db):
        dev = WeatherStationDevice.objects.create(device_id="ws-noname")
        s = str(dev)
        assert "ws-noname" in s

    @pytest.mark.django_db
    def test_reading_str(self, weather_station_device):
        ts = timezone.now()
        reading = WeatherStationReading.objects.create(
            device=weather_station_device,
            timestamp=ts,
            temperature=22.5,
        )
        s = str(reading)
        assert "Reading at" in s

    @pytest.mark.django_db
    def test_message_str(self, weather_station_device):
        msg = WeatherStationMessage.objects.create(
            device=weather_station_device,
            topic="test/weatherstation/ws-001/data",
        )
        s = str(msg)
        assert "test/weatherstation/ws-001/data" in s
        assert "pending" in s


# ---------------------------------------------------------------------------
# SmartCameraDevice state field defaults
# ---------------------------------------------------------------------------


class TestSmartCameraStateDefaults:
    @pytest.mark.django_db
    def test_recording_mode_default(self, smart_camera_device):
        assert smart_camera_device.recording_mode == "continuous"

    @pytest.mark.django_db
    def test_night_vision_default(self, smart_camera_device):
        assert smart_camera_device.night_vision is True

    @pytest.mark.django_db
    def test_recording_mode_can_be_changed(self, smart_camera_device):
        smart_camera_device.recording_mode = "motion"
        smart_camera_device.save()
        smart_camera_device.refresh_from_db()
        assert smart_camera_device.recording_mode == "motion"
