"""Unit tests for the pure calendar<->EPS-history alignment.

Anchored on the research script's exact constants (SKIP costs = 45, typical lag
= 30, session penalty = 3, windows 90/14) so a silently changed constant fails
here rather than quietly corrupting surprise-history pairing.
"""

from __future__ import annotations

from datetime import date

from app.services import earnings_alignment as ea


def test_normalise_symbol():
    assert ea.normalise_symbol("bf.b ") == "BF-B"
    assert ea.normalise_symbol(" aapl") == "AAPL"
    assert ea.normalise_symbol(None) == ""


def test_normalise_session():
    assert ea.normalise_session("Before market open") == "bmo"
    assert ea.normalise_session("After market close") == "amc"
    assert ea.normalise_session("During market hours") == "unknown"
    assert ea.normalise_session(None) == "unknown"
    assert ea.normalise_session("") == "unknown"


def test_safe_number():
    assert ea.safe_number("1.5") == 1.5
    assert ea.safe_number("") is None
    assert ea.safe_number("not-a-number") is None
    assert ea.safe_number("nan") is None  # non-finite rejected


def test_constants_pinned():
    assert ea.SKIP_EVENT_COST == 45.0
    assert ea.SKIP_PERIOD_COST == 45.0
    assert ea._TYPICAL_ANNOUNCE_LAG_DAYS == 30
    assert ea._MISSING_SESSION_PENALTY == 3.0


def test_match_cost_uses_pinned_lag_and_penalty():
    period = {"period_end_date": date(2026, 3, 31)}
    # delta == 30 (typical lag) → base cost 0; known session → no penalty
    e_known = {"announce_date": date(2026, 4, 30), "session": "amc"}
    assert ea.match_cost(e_known, period) == 0.0
    # unknown session adds the penalty
    e_unknown = {"announce_date": date(2026, 4, 30), "session": "unknown"}
    assert ea.match_cost(e_unknown, period) == 3.0
    # delta 45 → |45-30| == 15
    e_far = {"announce_date": date(2026, 5, 15), "session": "amc"}
    assert ea.match_cost(e_far, period) == 15.0


def test_dedup_calendar_prefers_known_session():
    rows = [
        {"symbol": "AAPL", "announce_date": date(2026, 5, 1), "session": "unknown"},
        {"symbol": "AAPL", "announce_date": date(2026, 5, 1), "session": "amc"},
    ]
    grouped, stats = ea.dedup_calendar(rows)
    assert stats["duplicate_rows"] == 1
    assert grouped["AAPL"][0]["session"] == "amc"


def test_dedup_history_prefers_fuller_row():
    rows = [
        {"symbol": "AAPL", "period_end_date": date(2026, 3, 31), "eps_actual": 2.0, "eps_estimate": None},
        {"symbol": "AAPL", "period_end_date": date(2026, 3, 31), "eps_actual": 2.0, "eps_estimate": 1.9},
    ]
    grouped, stats = ea.dedup_history(rows)
    assert stats["duplicate_rows"] == 1
    kept = grouped["AAPL"][0]
    assert kept["eps_actual"] == 2.0 and kept["eps_estimate"] == 1.9


def _events(*days):
    return [{"announce_date": d, "session": "amc"} for d in days]


def _periods(*days):
    return [{"period_end_date": d, "eps_actual": 1.0, "eps_estimate": 0.9} for d in days]


def test_align_matches_monotonic_pairs():
    # two announcements ~30d after two quarter ends
    events = _events(date(2026, 4, 30), date(2026, 7, 30))
    periods = _periods(date(2026, 3, 31), date(2026, 6, 30))
    matches, um_events, um_periods = ea.align_symbol(
        events, periods, max_lag_days=90, max_lead_days=14
    )
    assert matches == [(0, 0), (1, 1)]
    assert um_events == [] and um_periods == []


def test_align_leaves_out_of_window_unmatched():
    # announcement 200 days after the only period end → outside the 90d window
    events = _events(date(2026, 10, 17))
    periods = _periods(date(2026, 3, 31))
    matches, um_events, um_periods = ea.align_symbol(
        events, periods, max_lag_days=90, max_lead_days=14
    )
    assert matches == []
    assert um_events == [0] and um_periods == [0]
