"""Tests for the per-type ValueSource factory registry.

Locks in the contract that ``CREATE_BUILDERS`` and ``UPDATE_APPLIERS``
stay symmetric with storage's ``_VALUE_SOURCE_MAP``, and exercises a
couple of representative builders end-to-end.
"""

from __future__ import annotations

from datetime import datetime, timezone

import pytest

from ledgrab.storage import value_source_factories as vsf
from ledgrab.storage.value_source import (
    AdaptiveValueSource,
    HAEntityValueSource,
    StaticValueSource,
    _VALUE_SOURCE_MAP,
)

# ---------------------------------------------------------------------------
# Coverage / shape
# ---------------------------------------------------------------------------


def test_create_builders_cover_every_storage_kind():
    assert set(vsf.CREATE_BUILDERS.keys()) == set(_VALUE_SOURCE_MAP.keys())


def test_update_appliers_cover_every_storage_kind():
    assert set(vsf.UPDATE_APPLIERS.keys()) == set(_VALUE_SOURCE_MAP.keys())


def test_coverage_assertion_raises_when_create_drifts(monkeypatch):
    """Removing a builder makes _assert_factory_coverage() fail loudly."""
    pruned = {k: v for k, v in vsf.CREATE_BUILDERS.items() if k != "static"}
    monkeypatch.setattr(vsf, "CREATE_BUILDERS", pruned)
    with pytest.raises(RuntimeError, match="static"):
        vsf._assert_factory_coverage()


def test_coverage_assertion_raises_when_update_drifts(monkeypatch):
    pruned = {k: v for k, v in vsf.UPDATE_APPLIERS.items() if k != "http"}
    monkeypatch.setattr(vsf, "UPDATE_APPLIERS", pruned)
    with pytest.raises(RuntimeError, match="http"):
        vsf._assert_factory_coverage()


# ---------------------------------------------------------------------------
# build_source — representative paths
# ---------------------------------------------------------------------------


def _now() -> datetime:
    return datetime.now(timezone.utc)


def test_build_source_static_uses_value_default():
    src = vsf.build_source(
        source_type="static",
        sid="vs_t",
        name="Test",
        now=_now(),
        description=None,
        tags=None,
        icon=None,
        icon_color=None,
    )
    assert isinstance(src, StaticValueSource)
    assert src.value == 1.0
    assert src.source_type == "static"


def test_build_source_static_honours_supplied_value():
    src = vsf.build_source(
        source_type="static",
        sid="vs_t",
        name="Test",
        now=_now(),
        description=None,
        tags=None,
        icon=None,
        icon_color=None,
        value=0.42,
    )
    assert src.value == 0.42


def test_build_source_adaptive_time_requires_schedule_with_two_points():
    with pytest.raises(ValueError, match="Time of day schedule"):
        vsf.build_source(
            source_type="adaptive_time",
            sid="vs_t",
            name="Test",
            now=_now(),
            description=None,
            tags=None,
            icon=None,
            icon_color=None,
            schedule=[{"time": "00:00", "value": 0.5}],  # only one point
        )


def test_build_source_adaptive_scene_routes_to_adaptive_class_with_scene_type():
    """``adaptive_scene`` and ``adaptive_time`` share AdaptiveValueSource but
    differ in source_type — the builder must set the right discriminator."""
    src = vsf.build_source(
        source_type="adaptive_scene",
        sid="vs_t",
        name="Test",
        now=_now(),
        description=None,
        tags=None,
        icon=None,
        icon_color=None,
        picture_source_id="pic_42",
    )
    assert isinstance(src, AdaptiveValueSource)
    assert src.source_type == "adaptive_scene"
    assert src.picture_source_id == "pic_42"


def test_build_source_ha_entity_requires_ha_source_id_and_entity_id():
    with pytest.raises(ValueError, match="HA source ID"):
        vsf.build_source(
            source_type="ha_entity",
            sid="vs_t",
            name="Test",
            now=_now(),
            description=None,
            tags=None,
            icon=None,
            icon_color=None,
            entity_id="light.foo",  # missing ha_source_id
        )

    with pytest.raises(ValueError, match="Entity ID"):
        vsf.build_source(
            source_type="ha_entity",
            sid="vs_t",
            name="Test",
            now=_now(),
            description=None,
            tags=None,
            icon=None,
            icon_color=None,
            ha_source_id="ha_main",  # missing entity_id
        )


def test_build_source_http_validates_interval():
    with pytest.raises(ValueError, match="interval_s"):
        vsf.build_source(
            source_type="http",
            sid="vs_t",
            name="Test",
            now=_now(),
            description=None,
            tags=None,
            icon=None,
            icon_color=None,
            http_endpoint_id="ep_1",
            interval_s=0,
        )


def test_build_source_unknown_type_raises():
    with pytest.raises(ValueError, match="Invalid source type"):
        vsf.build_source(
            source_type="totally_unregistered",
            sid="vs_t",
            name="Test",
            now=_now(),
            description=None,
            tags=None,
            icon=None,
            icon_color=None,
        )


def test_build_source_game_event_is_not_creatable_via_store():
    """game_event sources were never creatable through the store API.

    The builder exists for coverage-assertion symmetry but raises so the
    historical contract holds (game-event value sources are wired up by
    the game-integration setup path, not user CRUD).
    """
    with pytest.raises(NotImplementedError, match="game_event"):
        vsf.build_source(
            source_type="game_event",
            sid="vs_t",
            name="Test",
            now=_now(),
            description=None,
            tags=None,
            icon=None,
            icon_color=None,
        )


# ---------------------------------------------------------------------------
# apply_update — representative paths
# ---------------------------------------------------------------------------


def test_apply_update_static_mutates_value():
    src = vsf.build_source(
        source_type="static",
        sid="vs_t",
        name="Test",
        now=_now(),
        description=None,
        tags=None,
        icon=None,
        icon_color=None,
    )
    vsf.apply_update(src, value=0.75)
    assert src.value == 0.75


def test_apply_update_ignores_unknown_kwargs():
    """Stray kwargs are silently dropped by each applier's ``**_``."""
    src = vsf.build_source(
        source_type="static",
        sid="vs_t",
        name="Test",
        now=_now(),
        description=None,
        tags=None,
        icon=None,
        icon_color=None,
    )
    vsf.apply_update(src, value=0.5, totally_made_up_field="ignored")
    assert src.value == 0.5


def test_apply_update_ha_entity_validates_smoothing_path():
    src = vsf.build_source(
        source_type="ha_entity",
        sid="vs_t",
        name="Test",
        now=_now(),
        description=None,
        tags=None,
        icon=None,
        icon_color=None,
        ha_source_id="ha_main",
        entity_id="light.foo",
    )
    assert isinstance(src, HAEntityValueSource)
    vsf.apply_update(src, smoothing=0.5, attribute="brightness")
    assert src.smoothing == 0.5
    assert src.attribute == "brightness"


def test_apply_update_http_rejects_zero_interval():
    src = vsf.build_source(
        source_type="http",
        sid="vs_t",
        name="Test",
        now=_now(),
        description=None,
        tags=None,
        icon=None,
        icon_color=None,
        http_endpoint_id="ep_1",
    )
    with pytest.raises(ValueError, match="interval_s"):
        vsf.apply_update(src, interval_s=0)
