"""Tests for django_devicehub.fields — Reading, Command, StateField."""

import pytest
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models

from django_devicehub.fields import (
    Command,
    Reading,
    ReadingType,
    StateField,
    reading_types,
)


# ---------------------------------------------------------------------------
# Reading.to_django_field()
# ---------------------------------------------------------------------------


class TestReadingToDjangoField:
    """Reading.to_django_field() must produce the correct Django model field."""

    def test_float_produces_float_field(self):
        r = Reading(type=ReadingType.FLOAT, unit="C")
        field = r.to_django_field()
        assert isinstance(field, models.FloatField)
        assert field.null is True
        assert field.blank is True

    def test_integer_produces_integer_field(self):
        r = Reading(type=ReadingType.INTEGER)
        field = r.to_django_field()
        assert isinstance(field, models.IntegerField)

    def test_boolean_produces_boolean_field(self):
        r = Reading(type=ReadingType.BOOLEAN)
        field = r.to_django_field()
        assert isinstance(field, models.BooleanField)

    def test_string_produces_char_field(self):
        r = Reading(type=ReadingType.STRING)
        field = r.to_django_field()
        assert isinstance(field, models.CharField)
        assert field.max_length == 255

    def test_json_produces_json_field(self):
        r = Reading(type=ReadingType.JSON)
        field = r.to_django_field()
        assert isinstance(field, models.JSONField)

    def test_binary_produces_binary_field(self):
        r = Reading(type=ReadingType.BINARY)
        field = r.to_django_field()
        assert isinstance(field, models.BinaryField)

    def test_unit_stored_as_help_text(self):
        r = Reading(type=ReadingType.FLOAT, unit="kg")
        field = r.to_django_field()
        assert field.help_text == "kg"

    def test_no_unit_no_help_text(self):
        r = Reading(type=ReadingType.FLOAT)
        field = r.to_django_field()
        assert field.help_text == ""

    def test_verbose_name_passed_through(self):
        r = Reading(type=ReadingType.FLOAT, verbose_name="Temperature (Celsius)")
        field = r.to_django_field()
        assert field.verbose_name == "Temperature (Celsius)"

    def test_unknown_type_falls_back_to_char_field(self):
        r = Reading(type="nonexistent")
        field = r.to_django_field()
        assert isinstance(field, models.CharField)
        assert field.max_length == 255


# ---------------------------------------------------------------------------
# Reading — range validators
# ---------------------------------------------------------------------------


class TestReadingRangeValidators:
    def test_range_adds_min_and_max_validators(self):
        r = Reading(type=ReadingType.FLOAT, range=(-40, 80))
        field = r.to_django_field()
        validator_types = [type(v) for v in field.validators]
        assert MinValueValidator in validator_types
        assert MaxValueValidator in validator_types

    def test_range_min_value(self):
        r = Reading(type=ReadingType.FLOAT, range=(-40, 80))
        field = r.to_django_field()
        min_val = [v for v in field.validators if isinstance(v, MinValueValidator)]
        assert len(min_val) == 1
        assert min_val[0].limit_value == -40

    def test_range_max_value(self):
        r = Reading(type=ReadingType.FLOAT, range=(-40, 80))
        field = r.to_django_field()
        max_val = [v for v in field.validators if isinstance(v, MaxValueValidator)]
        assert len(max_val) == 1
        assert max_val[0].limit_value == 80

    def test_range_none_min_skips_min_validator(self):
        r = Reading(type=ReadingType.FLOAT, range=(None, 100))
        field = r.to_django_field()
        validator_types = [type(v) for v in field.validators]
        assert MinValueValidator not in validator_types
        assert MaxValueValidator in validator_types

    def test_range_none_max_skips_max_validator(self):
        r = Reading(type=ReadingType.FLOAT, range=(0, None))
        field = r.to_django_field()
        validator_types = [type(v) for v in field.validators]
        assert MinValueValidator in validator_types
        assert MaxValueValidator not in validator_types

    def test_no_range_no_validators(self):
        r = Reading(type=ReadingType.FLOAT)
        field = r.to_django_field()
        assert field.validators == []

    def test_integer_range(self):
        r = Reading(type=ReadingType.INTEGER, range=(0, 1023))
        field = r.to_django_field()
        assert isinstance(field, models.IntegerField)
        # IntegerField may have built-in validators; verify our range validators are present
        min_vals = [v for v in field.validators if isinstance(v, MinValueValidator)]
        max_vals = [v for v in field.validators if isinstance(v, MaxValueValidator)]
        assert any(v.limit_value == 0 for v in min_vals)
        assert any(v.limit_value == 1023 for v in max_vals)


# ---------------------------------------------------------------------------
# Reading — repr / convenience alias
# ---------------------------------------------------------------------------


class TestReadingMisc:
    def test_repr(self):
        r = Reading(type=ReadingType.FLOAT, unit="C")
        assert "float" in repr(r)
        assert "C" in repr(r)

    def test_reading_types_alias(self):
        assert reading_types.FLOAT == ReadingType.FLOAT
        assert reading_types.INTEGER == ReadingType.INTEGER
        assert reading_types.BOOLEAN == ReadingType.BOOLEAN
        assert reading_types.STRING == ReadingType.STRING
        assert reading_types.JSON == ReadingType.JSON
        assert reading_types.BINARY == ReadingType.BINARY


# ---------------------------------------------------------------------------
# Command.validate_payload()
# ---------------------------------------------------------------------------


class TestCommandValidatePayload:
    def test_empty_payload_schema_accepts_anything(self):
        cmd = Command()
        assert cmd.validate_payload({"foo": "bar"}) is True

    def test_valid_payload_passes(self):
        cmd = Command(payload={"interval": int})
        assert cmd.validate_payload({"interval": 30}) is True

    def test_missing_field_raises_value_error(self):
        cmd = Command(payload={"interval": int})
        with pytest.raises(ValueError, match="Missing required payload field"):
            cmd.validate_payload({})

    def test_wrong_type_raises_type_error(self):
        cmd = Command(payload={"interval": int})
        with pytest.raises(TypeError, match="expected int"):
            cmd.validate_payload({"interval": "thirty"})

    def test_multi_field_payload(self):
        cmd = Command(payload={"width": int, "height": int})
        assert cmd.validate_payload({"width": 1920, "height": 1080}) is True

    def test_multi_field_payload_missing_one(self):
        cmd = Command(payload={"width": int, "height": int})
        with pytest.raises(ValueError, match="height"):
            cmd.validate_payload({"width": 1920})

    def test_extra_fields_are_ignored(self):
        cmd = Command(payload={"interval": int})
        assert cmd.validate_payload({"interval": 10, "extra": "ok"}) is True

    def test_command_repr(self):
        cmd = Command(payload={"interval": int})
        assert "interval" in repr(cmd)


# ---------------------------------------------------------------------------
# StateField.to_django_field()
# ---------------------------------------------------------------------------


class TestStateFieldToDjangoField:
    def test_string_produces_char_field(self):
        sf = StateField(type="string")
        field = sf.to_django_field()
        assert isinstance(field, models.CharField)
        assert field.max_length == 255

    def test_float_produces_float_field(self):
        sf = StateField(type="float")
        field = sf.to_django_field()
        assert isinstance(field, models.FloatField)

    def test_integer_produces_integer_field(self):
        sf = StateField(type="integer")
        field = sf.to_django_field()
        assert isinstance(field, models.IntegerField)

    def test_boolean_produces_boolean_field(self):
        sf = StateField(type="boolean")
        field = sf.to_django_field()
        assert isinstance(field, models.BooleanField)

    def test_json_produces_json_field(self):
        sf = StateField(type="json")
        field = sf.to_django_field()
        assert isinstance(field, models.JSONField)

    def test_datetime_produces_datetime_field(self):
        sf = StateField(type="datetime")
        field = sf.to_django_field()
        assert isinstance(field, models.DateTimeField)

    def test_default_sets_value_and_clears_null(self):
        sf = StateField(type="string", default="continuous")
        field = sf.to_django_field()
        assert field.default == "continuous"
        assert field.null is False

    def test_no_default_keeps_null(self):
        sf = StateField(type="string")
        field = sf.to_django_field()
        assert field.null is True

    def test_boolean_default(self):
        sf = StateField(type="boolean", default=True)
        field = sf.to_django_field()
        assert field.default is True
        assert field.null is False

    def test_choices_converted_to_tuple_pairs(self):
        sf = StateField(
            type="string",
            default="continuous",
            choices=["continuous", "motion", "scheduled"],
        )
        field = sf.to_django_field()
        assert field.choices == [
            ("continuous", "continuous"),
            ("motion", "motion"),
            ("scheduled", "scheduled"),
        ]

    def test_no_choices_means_no_choices_attr(self):
        sf = StateField(type="string")
        field = sf.to_django_field()
        # Django sets choices to an empty sequence when not specified
        assert not field.choices

    def test_verbose_name(self):
        sf = StateField(type="string", verbose_name="Recording Mode")
        field = sf.to_django_field()
        assert field.verbose_name == "Recording Mode"

    def test_unknown_type_falls_back_to_char_field(self):
        sf = StateField(type="nonexistent")
        field = sf.to_django_field()
        assert isinstance(field, models.CharField)

    def test_repr(self):
        sf = StateField(type="string")
        assert "string" in repr(sf)
