"""Tests for trigger domain entities."""

from __future__ import annotations

import pytest

from noteflow.domain.triggers import TriggerAction, TriggerDecision, TriggerSignal, TriggerSource


def test_trigger_signal_weight_bounds() -> None:
    """TriggerSignal enforces weight bounds."""
    with pytest.raises(ValueError, match=r"Weight must be 0\.0-1\.0"):
        TriggerSignal(source=TriggerSource.AUDIO_ACTIVITY, weight=-0.1)

    with pytest.raises(ValueError, match=r"Weight must be 0\.0-1\.0"):
        TriggerSignal(source=TriggerSource.AUDIO_ACTIVITY, weight=1.1)

    signal = TriggerSignal(source=TriggerSource.AUDIO_ACTIVITY, weight=0.5)
    assert signal.weight == 0.5, f"expected weight 0.5, got {signal.weight}"


@pytest.mark.parametrize(
    "attr,expected",
    [("action", TriggerAction.NOTIFY), ("confidence", 0.6), ("detected_app", "Zoom Meeting")],
)
def test_trigger_decision_attributes(attr: str, expected: object) -> None:
    """TriggerDecision exposes expected attributes."""
    audio = TriggerSignal(source=TriggerSource.AUDIO_ACTIVITY, weight=0.2)
    foreground = TriggerSignal(
        source=TriggerSource.FOREGROUND_APP,
        weight=0.4,
        app_name="Zoom Meeting",
    )
    decision = TriggerDecision(
        action=TriggerAction.NOTIFY,
        confidence=0.6,
        signals=(audio, foreground),
    )
    actual = getattr(decision, attr)
    assert actual == expected, f"expected {attr}={expected!r}, got {actual!r}"


def test_trigger_decision_primary_signal() -> None:
    """TriggerDecision primary_signal returns highest weight signal."""
    audio = TriggerSignal(source=TriggerSource.AUDIO_ACTIVITY, weight=0.2)
    foreground = TriggerSignal(
        source=TriggerSource.FOREGROUND_APP,
        weight=0.4,
        app_name="Zoom Meeting",
    )
    decision = TriggerDecision(
        action=TriggerAction.NOTIFY,
        confidence=0.6,
        signals=(audio, foreground),
    )
    assert decision.primary_signal == foreground, (
        f"expected foreground signal as primary, got {decision.primary_signal}"
    )


@pytest.mark.parametrize("attr", ["primary_signal", "detected_app"])
def test_trigger_decision_empty_signals_returns_none(attr: str) -> None:
    """Empty signals returns None for primary_signal and detected_app."""
    empty = TriggerDecision(action=TriggerAction.IGNORE, confidence=0.0, signals=())
    actual = getattr(empty, attr)
    assert actual is None, f"expected {attr} to be None for empty signals, got {actual!r}"


class TestTriggerSignalEdgeCases:
    """Edge case tests for TriggerSignal."""

    def test_weight_exactly_zero(self) -> None:
        """Test weight at exact lower boundary (0.0) is valid."""
        signal = TriggerSignal(source=TriggerSource.AUDIO_ACTIVITY, weight=0.0)
        assert signal.weight == 0.0, f"expected weight 0.0 at lower boundary, got {signal.weight}"

    def test_weight_exactly_one(self) -> None:
        """Test weight at exact upper boundary (1.0) is valid."""
        signal = TriggerSignal(source=TriggerSource.AUDIO_ACTIVITY, weight=1.0)
        assert signal.weight == 1.0, f"expected weight 1.0 at upper boundary, got {signal.weight}"

    def test_weight_very_small_positive(self) -> None:
        """Test very small positive weight."""
        signal = TriggerSignal(source=TriggerSource.AUDIO_ACTIVITY, weight=0.0001)
        assert signal.weight == 0.0001, f"expected small positive weight 0.0001, got {signal.weight}"

    def test_weight_just_below_one(self) -> None:
        """Test weight just below upper boundary."""
        signal = TriggerSignal(source=TriggerSource.AUDIO_ACTIVITY, weight=0.9999)
        assert signal.weight == 0.9999, f"expected weight 0.9999 just below boundary, got {signal.weight}"

    @pytest.mark.parametrize("source", list(TriggerSource))
    def test_all_trigger_sources(self, source: TriggerSource) -> None:
        """Test all trigger sources can create signals."""
        signal = TriggerSignal(source=source, weight=0.5)
        assert signal.source == source, f"expected source {source}, got {signal.source}"

    def test_foreground_app_signal_with_app_name(self) -> None:
        """Test foreground app signal stores app name."""
        signal = TriggerSignal(
            source=TriggerSource.FOREGROUND_APP,
            weight=0.8,
            app_name="Microsoft Teams",
        )
        assert signal.app_name == "Microsoft Teams", (
            f"expected app_name 'Microsoft Teams', got {signal.app_name!r}"
        )

    def test_calendar_signal_basic(self) -> None:
        """Test calendar trigger source is valid."""
        signal = TriggerSignal(
            source=TriggerSource.CALENDAR,
            weight=0.9,
        )
        assert signal.source == TriggerSource.CALENDAR, (
            f"expected source CALENDAR, got {signal.source}"
        )
        assert signal.weight == 0.9, f"expected weight 0.9, got {signal.weight}"


class TestTriggerDecisionEdgeCases:
    """Edge case tests for TriggerDecision."""

    def test_single_signal_is_primary(self) -> None:
        """Test single signal is always primary."""
        signal = TriggerSignal(source=TriggerSource.AUDIO_ACTIVITY, weight=0.5)
        decision = TriggerDecision(
            action=TriggerAction.NOTIFY,
            confidence=0.5,
            signals=(signal,),
        )
        assert decision.primary_signal == signal, (
            f"expected single signal to be primary, got {decision.primary_signal}"
        )

    @pytest.mark.parametrize("action", list(TriggerAction))
    def test_all_trigger_actions(self, action: TriggerAction) -> None:
        """Test all trigger actions are valid."""
        decision = TriggerDecision(
            action=action,
            confidence=0.5,
            signals=(),
        )
        assert decision.action == action, f"expected action {action}, got {decision.action}"

    def test_confidence_zero(self) -> None:
        """Test zero confidence decision."""
        decision = TriggerDecision(
            action=TriggerAction.IGNORE,
            confidence=0.0,
            signals=(),
        )
        assert decision.confidence == 0.0, f"expected zero confidence, got {decision.confidence}"

    def test_confidence_one(self) -> None:
        """Test full confidence decision."""
        decision = TriggerDecision(
            action=TriggerAction.AUTO_START,
            confidence=1.0,
            signals=(),
        )
        assert decision.confidence == 1.0, f"expected full confidence 1.0, got {decision.confidence}"

    def test_many_signals_finds_highest_weight(self) -> None:
        """Test primary_signal with many signals finds highest."""
        signals = tuple(
            TriggerSignal(source=TriggerSource.AUDIO_ACTIVITY, weight=i * 0.1)
            for i in range(1, 10)
        )
        decision = TriggerDecision(
            action=TriggerAction.NOTIFY,
            confidence=0.9,
            signals=signals,
        )
        assert decision.primary_signal is not None, "expected primary_signal to exist for non-empty signals"
        assert decision.primary_signal.weight == 0.9, (
            f"expected highest weight 0.9 as primary, got {decision.primary_signal.weight}"
        )

    def test_equal_weights_returns_first(self) -> None:
        """Test when signals have equal weights, first is returned."""
        signal1 = TriggerSignal(source=TriggerSource.AUDIO_ACTIVITY, weight=0.5)
        signal2 = TriggerSignal(source=TriggerSource.FOREGROUND_APP, weight=0.5)
        decision = TriggerDecision(
            action=TriggerAction.NOTIFY,
            confidence=0.5,
            signals=(signal1, signal2),
        )
        # max() returns first element when weights are equal
        assert decision.primary_signal in (signal1, signal2), (
            f"expected primary_signal to be one of the equal-weight signals, got {decision.primary_signal}"
        )
