"""Tests for the regime / level-shift primitive.

The headline case encodes the real Polleo whey-454g pattern: shelf price moves
through regimes (15.5 -> 21.6 -> 24.6) with transient promo dips in between. The
regime-local normal must (a) treat a promo dip as a discount, and (b) NOT flag a
permanent reprice as a discount — the bug a global / last-12-week normal causes.
"""
import numpy as np

from forecast_v4.regime import (
    local_baseline, classify_weeks, regime_normal_price, discount_pct,
)


def _whey_price():
    """74-ish weeks: base 21.6, two promo dips (W18-22 ->15.5, W30-31 ->17.5),
    then a permanent reprice to 24.6 from W41."""
    p = np.full(60, 21.6)
    p[18:23] = 15.5      # 5-week promo (transient)
    p[30:32] = 17.5      # 2-week promo (transient)
    p[41:] = 24.6        # permanent reprice (sustained shift)
    return p


def test_reprice_not_flagged_as_promo():
    p = _whey_price()
    normal = regime_normal_price(p, window=13)
    # During the 21.6 regime the normal is ~21.6 (promo dips excluded)...
    assert abs(normal[10] - 21.6) < 1.0
    # ...and after the reprice the normal FOLLOWS up to ~24.6 (not stuck at 21.6).
    assert normal[55] > 23.5, f"normal didn't track reprice: {normal[55]:.2f}"
    d = discount_pct(p, normal)
    # The reprice weeks are NOT a discount (this is the trap we're killing).
    assert abs(d[55]) < 0.05, f"reprice mislabeled as discount: {d[55]:.2%}"


def test_promo_dips_are_discounts():
    p = _whey_price()
    d = discount_pct(p, regime_normal_price(p, window=13))
    # The W18-22 dip to 15.5 vs ~21.6 normal ~= 28% off.
    assert d[20] > 0.20, f"promo dip not detected: {d[20]:.2%}"
    assert d[31] > 0.10, f"short promo not detected: {d[31]:.2%}"


def test_global_median_would_mislabel():
    """Contrast: a single global-median 'normal' flags the reprice as anti-promo
    and/or the early regime as promo. Regime-local must do better."""
    p = _whey_price()
    glob = np.full_like(p, np.median(p))
    d_glob = 1.0 - p / glob
    d_reg = discount_pct(p, regime_normal_price(p, window=13))
    # Global median says the 24.6 reprice is a big negative 'discount'; regime ~0.
    assert d_glob[55] < -0.05
    assert abs(d_reg[55]) < abs(d_glob[55])


def test_classify_transient_vs_shift():
    # Lumpy wholesale-like series: steady ~100, one 1-week spike, then a
    # sustained step up to ~300 for the tail.
    y = np.full(40, 100.0)
    y[15] = 500.0        # isolated one-off (transient)
    y[25:] = 300.0       # sustained level shift
    labels = classify_weeks(y, window=9, rel_thresh=0.5, min_run=3)
    # The actionable contract: the isolated spike is a one-off to STRIP...
    assert labels[15] == "event", f"one-off should be event, got {labels[15]}"
    assert labels[5] == "normal"
    # ...and the settled new level is NOT a one-off (don't strip it) — it has
    # become the new normal once the baseline adapts.
    assert labels[35] != "event", f"settled new level wrongly stripped: {labels[35]}"
    # With a trailing baseline (the live forecast edge), the baseline lags, so
    # the transition INTO the sustained level reads as 'shift', not a one-off.
    trailing = classify_weeks(
        y, baseline=local_baseline(y, window=9, center=False),
        rel_thresh=0.5, min_run=3,
    )
    assert (trailing[25:33] == "shift").any(), "sustained rise not detected as shift"


def test_zero_weeks_are_normal_for_wholesale():
    # A buyer ordering ~monthly: zeros between orders must NOT be flagged as
    # deviations to 'fix' (wholesale zeros = didn't order, handled elsewhere).
    y = np.zeros(40)
    y[::4] = 120.0       # order every 4th week
    labels = classify_weeks(y, window=13, rel_thresh=0.5, min_run=3)
    # The regular orders are the buyer's rhythm, not isolated one-offs to strip.
    assert (labels == "shift").sum() == 0 or (labels == "normal").sum() > 0


def test_no_nans_and_aligned():
    for n in (0, 1, 5, 60):
        p = np.full(n, 10.0) if n else np.array([])
        nb = local_baseline(p, window=13)
        assert len(nb) == n
        assert not np.isnan(nb).any()
