"""
Tests for the sensor framework.
"""

import time

from engine.sensors import Sensor, SensorRegistry, SensorStage, SensorValue


class TestSensorValue:
    """Tests for SensorValue dataclass."""

    def test_create_sensor_value(self):
        """SensorValue stores sensor data correctly."""
        value = SensorValue(
            sensor_name="mic",
            value=42.5,
            timestamp=1234567890.0,
            unit="dB",
        )

        assert value.sensor_name == "mic"
        assert value.value == 42.5
        assert value.timestamp == 1234567890.0
        assert value.unit == "dB"


class DummySensor(Sensor):
    """Dummy sensor for testing."""

    def __init__(self, name: str = "dummy", value: float = 1.0):
        self.name = name
        self.unit = "units"
        self._value = value

    def start(self) -> bool:
        return True

    def stop(self) -> None:
        pass

    def read(self) -> SensorValue | None:
        return SensorValue(
            sensor_name=self.name,
            value=self._value,
            timestamp=time.time(),
            unit=self.unit,
        )


class TestSensorRegistry:
    """Tests for SensorRegistry."""

    def setup_method(self):
        """Clear registry before each test."""
        SensorRegistry._sensors.clear()
        SensorRegistry._started = False

    def test_register_sensor(self):
        """SensorRegistry registers sensors."""
        sensor = DummySensor()
        SensorRegistry.register(sensor)

        assert SensorRegistry.get("dummy") is sensor

    def test_list_sensors(self):
        """SensorRegistry lists registered sensors."""
        SensorRegistry.register(DummySensor("a"))
        SensorRegistry.register(DummySensor("b"))

        sensors = SensorRegistry.list_sensors()
        assert "a" in sensors
        assert "b" in sensors

    def test_read_all(self):
        """SensorRegistry reads all sensor values."""
        SensorRegistry.register(DummySensor("a", 1.0))
        SensorRegistry.register(DummySensor("b", 2.0))

        values = SensorRegistry.read_all()
        assert values["a"] == 1.0
        assert values["b"] == 2.0


class TestSensorStage:
    """Tests for SensorStage pipeline adapter."""

    def setup_method(self):
        SensorRegistry._sensors.clear()
        SensorRegistry._started = False

    def test_sensor_stage_capabilities(self):
        """SensorStage declares correct capabilities."""
        sensor = DummySensor("mic")
        stage = SensorStage(sensor)

        assert "sensor.mic" in stage.capabilities

    def test_sensor_stage_process(self):
        """SensorStage reads sensor and stores in context."""
        from engine.pipeline.core import PipelineContext

        sensor = DummySensor("test", 42.0)
        stage = SensorStage(sensor, "test")

        ctx = PipelineContext()
        result = stage.process(None, ctx)

        assert ctx.get_state("sensor.test") == 42.0
        assert result is None


class TestApplyParamBindings:
    """Tests for sensor param bindings."""

    def test_no_bindings_returns_original(self):
        """Effect without bindings returns original config."""
        from engine.effects.types import (
            EffectConfig,
            EffectPlugin,
            apply_param_bindings,
        )

        class TestEffect(EffectPlugin):
            name = "test"
            config = EffectConfig()

            def process(self, buf, ctx):
                return buf

            def configure(self, config):
                pass

        effect = TestEffect()
        ctx = object()

        result = apply_param_bindings(effect, ctx)
        assert result is effect.config

    def test_bindings_read_sensor_values(self):
        """Param bindings read sensor values from context."""
        from engine.effects.types import (
            EffectConfig,
            EffectPlugin,
            apply_param_bindings,
        )

        class TestEffect(EffectPlugin):
            name = "test"
            config = EffectConfig(intensity=1.0)
            param_bindings = {
                "intensity": {"sensor": "mic", "transform": "linear"},
            }

            def process(self, buf, ctx):
                return buf

            def configure(self, config):
                pass

        from engine.effects.types import EffectContext

        effect = TestEffect()
        ctx = EffectContext(
            terminal_width=80,
            terminal_height=24,
            scroll_cam=0,
            ticker_height=20,
        )
        ctx.set_state("sensor.mic", 0.8)

        result = apply_param_bindings(effect, ctx)
        assert "intensity_sensor" in result.params


class TestSensorLifecycle:
    """Tests for sensor start/stop lifecycle."""

    def setup_method(self):
        SensorRegistry._sensors.clear()
        SensorRegistry._started = False

    def test_start_all(self):
        """SensorRegistry starts all sensors."""
        started = []

        class StatefulSensor(Sensor):
            name = "stateful"

            def start(self) -> bool:
                started.append("start")
                return True

            def stop(self) -> None:
                started.append("stop")

            def read(self) -> SensorValue | None:
                return SensorValue("stateful", 1.0, 0.0)

        SensorRegistry.register(StatefulSensor())
        SensorRegistry.start_all()

        assert "start" in started
        assert SensorRegistry._started is True

    def test_stop_all(self):
        """SensorRegistry stops all sensors."""
        stopped = []

        class StatefulSensor(Sensor):
            name = "stateful"

            def start(self) -> bool:
                return True

            def stop(self) -> None:
                stopped.append("stop")

            def read(self) -> SensorValue | None:
                return SensorValue("stateful", 1.0, 0.0)

        SensorRegistry.register(StatefulSensor())
        SensorRegistry.start_all()
        SensorRegistry.stop_all()

        assert "stop" in stopped
        assert SensorRegistry._started is False

    def test_unavailable_sensor(self):
        """Unavailable sensor returns None from read."""

        class UnavailableSensor(Sensor):
            name = "unavailable"

            @property
            def available(self) -> bool:
                return False

            def start(self) -> bool:
                return False

            def stop(self) -> None:
                pass

            def read(self) -> SensorValue | None:
                return None

        sensor = UnavailableSensor()
        assert sensor.available is False
        assert sensor.read() is None


class TestTransforms:
    """Tests for sensor value transforms."""

    def test_exponential_transform(self):
        """Exponential transform squares the value."""
        from engine.effects.types import (
            EffectConfig,
            EffectPlugin,
            apply_param_bindings,
        )

        class TestEffect(EffectPlugin):
            name = "test"
            config = EffectConfig(intensity=1.0)
            param_bindings = {
                "intensity": {"sensor": "mic", "transform": "exponential"},
            }

            def process(self, buf, ctx):
                return buf

            def configure(self, config):
                pass

        from engine.effects.types import EffectContext

        effect = TestEffect()
        ctx = EffectContext(80, 24, 0, 20)
        ctx.set_state("sensor.mic", 0.5)

        result = apply_param_bindings(effect, ctx)
        # 0.5^2 = 0.25, then scaled: 0.5 + 0.25*0.5 = 0.625
        assert result.intensity != effect.config.intensity

    def test_inverse_transform(self):
        """Inverse transform inverts the value."""
        from engine.effects.types import (
            EffectConfig,
            EffectPlugin,
            apply_param_bindings,
        )

        class TestEffect(EffectPlugin):
            name = "test"
            config = EffectConfig(intensity=1.0)
            param_bindings = {
                "intensity": {"sensor": "mic", "transform": "inverse"},
            }

            def process(self, buf, ctx):
                return buf

            def configure(self, config):
                pass

        from engine.effects.types import EffectContext

        effect = TestEffect()
        ctx = EffectContext(80, 24, 0, 20)
        ctx.set_state("sensor.mic", 0.8)

        result = apply_param_bindings(effect, ctx)
        # 1.0 - 0.8 = 0.2
        assert abs(result.params["intensity_sensor"] - 0.2) < 0.001

    def test_threshold_transform(self):
        """Threshold transform applies binary threshold."""
        from engine.effects.types import (
            EffectConfig,
            EffectPlugin,
            apply_param_bindings,
        )

        class TestEffect(EffectPlugin):
            name = "test"
            config = EffectConfig(intensity=1.0)
            param_bindings = {
                "intensity": {
                    "sensor": "mic",
                    "transform": "threshold",
                    "threshold": 0.5,
                },
            }

            def process(self, buf, ctx):
                return buf

            def configure(self, config):
                pass

        from engine.effects.types import EffectContext

        effect = TestEffect()
        ctx = EffectContext(80, 24, 0, 20)

        # Above threshold
        ctx.set_state("sensor.mic", 0.8)
        result = apply_param_bindings(effect, ctx)
        assert result.params["intensity_sensor"] == 1.0

        # Below threshold
        ctx.set_state("sensor.mic", 0.3)
        result = apply_param_bindings(effect, ctx)
        assert result.params["intensity_sensor"] == 0.0


class TestOscillatorSensor:
    """Tests for OscillatorSensor."""

    def setup_method(self):
        SensorRegistry._sensors.clear()
        SensorRegistry._started = False

    def test_sine_waveform(self):
        """Oscillator generates sine wave."""
        from engine.sensors.oscillator import OscillatorSensor

        osc = OscillatorSensor(name="test", waveform="sine", frequency=1.0)
        osc.start()

        values = [osc.read().value for _ in range(10)]
        assert all(0 <= v <= 1 for v in values)

    def test_square_waveform(self):
        """Oscillator generates square wave."""
        from engine.sensors.oscillator import OscillatorSensor

        osc = OscillatorSensor(name="test", waveform="square", frequency=10.0)
        osc.start()

        values = [osc.read().value for _ in range(10)]
        assert all(v in (0.0, 1.0) for v in values)

    def test_waveform_types(self):
        """All waveform types work."""
        from engine.sensors.oscillator import OscillatorSensor

        for wf in ["sine", "square", "sawtooth", "triangle", "noise"]:
            osc = OscillatorSensor(name=wf, waveform=wf, frequency=1.0)
            osc.start()
            val = osc.read()
            assert val is not None
            assert 0 <= val.value <= 1

    def test_invalid_waveform_raises(self):
        """Invalid waveform returns None."""
        from engine.sensors.oscillator import OscillatorSensor

        osc = OscillatorSensor(waveform="invalid")
        osc.start()
        val = osc.read()
        assert val is None

    def test_sensor_driven_oscillator(self):
        """Oscillator can be driven by another sensor."""
        from engine.sensors.oscillator import OscillatorSensor

        class ModSensor(Sensor):
            name = "mod"

            def start(self) -> bool:
                return True

            def stop(self) -> None:
                pass

            def read(self) -> SensorValue | None:
                return SensorValue("mod", 0.5, 0.0)

        SensorRegistry.register(ModSensor())

        osc = OscillatorSensor(
            name="lfo", waveform="sine", frequency=0.1, input_sensor="mod"
        )
        osc.start()

        val = osc.read()
        assert val is not None
        assert 0 <= val.value <= 1


class TestMicSensor:
    """Tests for MicSensor."""

    def setup_method(self):
        SensorRegistry._sensors.clear()
        SensorRegistry._started = False

    def test_mic_sensor_creation(self):
        """MicSensor can be created."""
        from engine.sensors.mic import MicSensor

        sensor = MicSensor()
        assert sensor.name == "mic"
        assert sensor.unit == "dB"

    def test_mic_sensor_custom_name(self):
        """MicSensor can have custom name."""
        from engine.sensors.mic import MicSensor

        sensor = MicSensor(name="my_mic")
        assert sensor.name == "my_mic"

    def test_mic_sensor_start(self):
        """MicSensor.start returns bool."""
        from engine.sensors.mic import MicSensor

        sensor = MicSensor()
        result = sensor.start()
        assert isinstance(result, bool)

    def test_mic_sensor_read_returns_value_or_none(self):
        """MicSensor.read returns SensorValue or None."""
        from engine.sensors.mic import MicSensor

        sensor = MicSensor()
        sensor.start()
        # May be None if no mic available
        result = sensor.read()
        # Just check it doesn't raise - result depends on system
        assert result is None or isinstance(result, SensorValue)
