"""Unit tests for the hard filter engine.

Validates evaluate_hard_filters against requirements 4.1–4.6.
"""

from datetime import datetime, timezone

from services.signal_engine.config import HardFilterConfig
from services.signal_engine.hard_filter import HardFilterResult, evaluate_hard_filters
from services.signal_engine.models import NormalizedInput


def _make_input(**overrides) -> NormalizedInput:
    """Build a minimal NormalizedInput with sensible defaults."""
    defaults = {
        "ticker": "AAPL",
        "evaluated_at": datetime(2024, 1, 15, tzinfo=timezone.utc),
        "bars": {},
        "macro_bias": 0.5,
        "valuation_score": 0.8,
        "earnings_proximity_days": 30,
    }
    defaults.update(overrides)
    return NormalizedInput(**defaults)


DEFAULT_CONFIG = HardFilterConfig()


class TestMacroBiasFilter:
    """Requirement 4.1: macro_bias == -1.0 → SKIP with reason 'macro_bias_negative'."""

    def test_macro_bias_negative_triggers_filter(self):
        inp = _make_input(macro_bias=-1.0)
        result = evaluate_hard_filters(inp, DEFAULT_CONFIG)
        assert result.filtered is True
        assert "macro_bias_negative" in result.reasons

    def test_macro_bias_zero_does_not_trigger(self):
        inp = _make_input(macro_bias=0.0)
        result = evaluate_hard_filters(inp, DEFAULT_CONFIG)
        assert "macro_bias_negative" not in result.reasons

    def test_macro_bias_positive_does_not_trigger(self):
        inp = _make_input(macro_bias=0.5)
        result = evaluate_hard_filters(inp, DEFAULT_CONFIG)
        assert "macro_bias_negative" not in result.reasons

    def test_macro_bias_slightly_above_negative_one(self):
        inp = _make_input(macro_bias=-0.99)
        result = evaluate_hard_filters(inp, DEFAULT_CONFIG)
        assert "macro_bias_negative" not in result.reasons


class TestValuationFilter:
    """Requirement 4.2: valuation_score < 0.3 → SKIP with reason 'valuation_below_threshold'."""

    def test_valuation_below_threshold_triggers(self):
        inp = _make_input(valuation_score=0.1)
        result = evaluate_hard_filters(inp, DEFAULT_CONFIG)
        assert result.filtered is True
        assert "valuation_below_threshold" in result.reasons

    def test_valuation_at_threshold_does_not_trigger(self):
        inp = _make_input(valuation_score=0.3)
        result = evaluate_hard_filters(inp, DEFAULT_CONFIG)
        assert "valuation_below_threshold" not in result.reasons

    def test_valuation_above_threshold_does_not_trigger(self):
        inp = _make_input(valuation_score=0.5)
        result = evaluate_hard_filters(inp, DEFAULT_CONFIG)
        assert "valuation_below_threshold" not in result.reasons

    def test_valuation_none_does_not_trigger(self):
        """Missing valuation_score should NOT trigger the filter."""
        inp = _make_input(valuation_score=None)
        result = evaluate_hard_filters(inp, DEFAULT_CONFIG)
        assert "valuation_below_threshold" not in result.reasons


class TestEarningsFilter:
    """Requirement 4.3: earnings_proximity_days <= 5 → SKIP with reason 'earnings_block'."""

    def test_earnings_within_block_triggers(self):
        inp = _make_input(earnings_proximity_days=3)
        result = evaluate_hard_filters(inp, DEFAULT_CONFIG)
        assert result.filtered is True
        assert "earnings_block" in result.reasons

    def test_earnings_at_boundary_triggers(self):
        inp = _make_input(earnings_proximity_days=5)
        result = evaluate_hard_filters(inp, DEFAULT_CONFIG)
        assert result.filtered is True
        assert "earnings_block" in result.reasons

    def test_earnings_above_boundary_does_not_trigger(self):
        inp = _make_input(earnings_proximity_days=6)
        result = evaluate_hard_filters(inp, DEFAULT_CONFIG)
        assert "earnings_block" not in result.reasons

    def test_earnings_none_does_not_trigger(self):
        """Missing earnings_proximity_days should NOT trigger the filter."""
        inp = _make_input(earnings_proximity_days=None)
        result = evaluate_hard_filters(inp, DEFAULT_CONFIG)
        assert "earnings_block" not in result.reasons


class TestMultipleFilters:
    """Requirement 4.4: all triggered filter reasons are recorded."""

    def test_all_three_filters_trigger(self):
        inp = _make_input(
            macro_bias=-1.0,
            valuation_score=0.1,
            earnings_proximity_days=2,
        )
        result = evaluate_hard_filters(inp, DEFAULT_CONFIG)
        assert result.filtered is True
        assert len(result.reasons) == 3
        assert "macro_bias_negative" in result.reasons
        assert "valuation_below_threshold" in result.reasons
        assert "earnings_block" in result.reasons

    def test_two_filters_trigger(self):
        inp = _make_input(
            macro_bias=-1.0,
            valuation_score=0.8,
            earnings_proximity_days=2,
        )
        result = evaluate_hard_filters(inp, DEFAULT_CONFIG)
        assert result.filtered is True
        assert len(result.reasons) == 2
        assert "macro_bias_negative" in result.reasons
        assert "earnings_block" in result.reasons


class TestNoFilters:
    """Requirement 4.5: no hard filters trigger → pass through."""

    def test_clean_input_passes(self):
        inp = _make_input(
            macro_bias=0.5,
            valuation_score=0.8,
            earnings_proximity_days=30,
        )
        result = evaluate_hard_filters(inp, DEFAULT_CONFIG)
        assert result.filtered is False
        assert result.reasons == []

    def test_all_none_optional_fields_pass(self):
        """When optional fields are None, no filters trigger."""
        inp = _make_input(
            macro_bias=0.0,
            valuation_score=None,
            earnings_proximity_days=None,
        )
        result = evaluate_hard_filters(inp, DEFAULT_CONFIG)
        assert result.filtered is False
        assert result.reasons == []


class TestCustomConfig:
    """Verify that the filter respects custom config thresholds."""

    def test_custom_valuation_min(self):
        config = HardFilterConfig(valuation_min=0.5)
        inp = _make_input(valuation_score=0.4)
        result = evaluate_hard_filters(inp, config)
        assert result.filtered is True
        assert "valuation_below_threshold" in result.reasons

    def test_custom_earnings_days(self):
        config = HardFilterConfig(earnings_days=10)
        inp = _make_input(earnings_proximity_days=8)
        result = evaluate_hard_filters(inp, config)
        assert result.filtered is True
        assert "earnings_block" in result.reasons

    def test_custom_macro_bias_skip(self):
        config = HardFilterConfig(macro_bias_skip=-0.5)
        inp = _make_input(macro_bias=-0.5)
        result = evaluate_hard_filters(inp, config)
        assert result.filtered is True
        assert "macro_bias_negative" in result.reasons


class TestHardFilterResultDefaults:
    """Verify HardFilterResult dataclass defaults."""

    def test_default_values(self):
        result = HardFilterResult()
        assert result.filtered is False
        assert result.reasons == []

    def test_mutable_default_isolation(self):
        """Each instance should have its own reasons list."""
        r1 = HardFilterResult()
        r2 = HardFilterResult()
        r1.reasons.append("test")
        assert r2.reasons == []
