"""Unit tests for the breadth indicator and the event-study measurement."""

from __future__ import annotations

from datetime import date, timedelta

from app.services.breadth_service import _breadth_from_closes, compute_divergence_series
from app.services.event_study_service import (
    _lead,
    _percentile,
    detect_events,
    event_centered,
    signal_centered,
)


def _days(n: int, start: date = date(2021, 1, 1)) -> list[date]:
    return [start + timedelta(days=i) for i in range(n)]


# ---------------------------------------------------------------------------
# Event detection
# ---------------------------------------------------------------------------

def test_detect_events_single_drawdown():
    closes = [100.0] * 300 + [85.0] * 5    # 15% off the trailing high -> one event
    dates = _days(len(closes))
    events = detect_events(closes, dates, threshold_pct=15.0)
    assert len(events) == 1
    assert events[0]["index"] == 300


def test_detect_events_dedup_without_recovery():
    closes = [100.0] * 300 + [85.0] * 5 + [80.0] * 5   # deepens but never recovers
    events = detect_events(closes, _days(len(closes)), threshold_pct=15.0)
    assert len(events) == 1


def test_detect_events_two_after_recovery():
    closes = [100.0] * 300 + [85.0] * 10 + [100.0] * 300 + [85.0] * 10
    events = detect_events(closes, _days(len(closes)), threshold_pct=15.0)
    assert len(events) == 2


def test_detect_events_cooldown_suppresses_close_recross():
    # Dips below threshold then re-crosses only a few bars later.
    closes = [100.0] * 300 + [85.0] * 3 + [100.0] * 3 + [85.0] * 3
    dates = _days(len(closes))
    assert len(detect_events(closes, dates, threshold_pct=15.0, cooldown=40)) == 1
    assert len(detect_events(closes, dates, threshold_pct=15.0, cooldown=3)) == 2


def test_percentile_interpolation():
    vals = [float(v) for v in range(0, 101, 10)]  # 0,10,...,100
    assert _percentile(vals, 50) == 50.0
    assert _percentile(vals, 80) == 80.0
    assert _percentile([], 50) is None


def test_lead_earliest_crossing():
    dates = _days(200)
    t0 = 120
    indicator = {dates[i]: (70.0 if t0 - 30 <= i <= t0 else 10.0) for i in range(len(dates))}
    assert _lead(indicator, t0, dates, pre=60, threshold=60.0) == 30
    assert _lead(indicator, t0, dates, pre=60, threshold=80.0) is None


# ---------------------------------------------------------------------------
# Event-centered lead time
# ---------------------------------------------------------------------------

def test_event_centered_lead_time():
    dates = _days(200)
    t0 = 120
    # Indicator goes hot 30 days before t0 and stays hot through t0.
    indicator = {dates[i]: (70.0 if t0 - 30 <= i <= t0 else 10.0) for i in range(len(dates))}
    res = event_centered(indicator, [t0], dates, pre=60, post=20, threshold=60.0)
    assert res["median_lead_days"] == 30
    assert res["events_with_signal"] == 1


def test_breadth_divergence_leads_coincident():
    dates = _days(200)
    t0 = 120
    breadth_ind = {dates[i]: (70.0 if t0 - 30 <= i <= t0 else 10.0) for i in range(len(dates))}
    coincident = {dates[i]: (70.0 if t0 - 2 <= i <= t0 else 10.0) for i in range(len(dates))}
    bd = event_centered(breadth_ind, [t0], dates, threshold=60.0)
    cd = event_centered(coincident, [t0], dates, threshold=60.0)
    assert bd["median_lead_days"] > cd["median_lead_days"]


# ---------------------------------------------------------------------------
# Signal-centered precision / recall
# ---------------------------------------------------------------------------

def test_signal_centered_base_rate_and_recall():
    dates = _days(200)
    t0 = 120
    indicator = {dates[i]: (70.0 if t0 - 30 <= i <= t0 else 10.0) for i in range(len(dates))}
    res = signal_centered(indicator, [t0], dates, horizon=20)
    assert 0.0 < res["base_rate"] < 1.0
    # An aligned indicator should catch some of the pre-event window at a mid threshold.
    row60 = next(r for r in res["rows"] if r["threshold"] == 60)
    assert row60["recall"] is not None and row60["recall"] > 0


# ---------------------------------------------------------------------------
# Breadth aggregation + divergence
# ---------------------------------------------------------------------------

def test_breadth_from_closes_fraction_above_sma():
    dates = _days(5)
    closes_by_symbol = {
        "A": list(zip(dates, [1.0, 2.0, 3.0, 4.0, 5.0])),   # rising -> above its SMA
        "B": list(zip(dates, [5.0, 4.0, 3.0, 2.0, 1.0])),   # falling -> below
        "C": list(zip(dates, [3.0, 3.0, 3.0, 3.0, 3.0])),   # flat -> not strictly above
    }
    breadth = _breadth_from_closes(closes_by_symbol, window=3, min_tickers=2)
    # At d2: SMA(3) over each -> only A is strictly above -> 1/3.
    assert breadth[dates[2]] == round(1 / 3 * 100, 2)


def test_divergence_high_when_price_up_breadth_down():
    dates = _days(10)
    breadth = {dates[i]: 80.0 - i * 3 for i in range(len(dates))}      # falling breadth
    benchmark = list(zip(dates, [100.0 + i for i in range(len(dates))]))  # rising price
    div = compute_divergence_series(breadth, benchmark, lookback=3)
    last = div[dates[-1]]
    assert last > 50.0   # fragile: price up while breadth deteriorates
