"""Known-case tests for the Buy/Sell recommendation engine."""
from __future__ import annotations

from app.engines.recommendation import (
    Recommendation, SellTriggerInputs, build_sell_trigger_inputs, compute_recommendation,
    evaluate_sell_triggers, recommendation_from_table,
)


def test_strong_buy_requires_full_combination():
    rec = recommendation_from_table(overall_score=85, margin_of_safety=0.25, risk_score_0_100=80, expected_cagr_base=0.15)
    assert rec == Recommendation.STRONG_BUY


def test_high_score_alone_is_not_strong_buy_without_margin_of_safety():
    # Great score but overvalued (negative MoS) must NOT be STRONG BUY — spec §27 prohibition.
    rec = recommendation_from_table(overall_score=95, margin_of_safety=-0.15, risk_score_0_100=90, expected_cagr_base=0.20)
    assert rec != Recommendation.STRONG_BUY
    assert rec == Recommendation.WAIT


def test_low_pe_alone_never_implies_buy():
    # A "cheap" P/E with a poor overall score and no real margin of safety signal must not buy.
    rec = recommendation_from_table(overall_score=35, margin_of_safety=0.05, risk_score_0_100=50, expected_cagr_base=0.03)
    assert rec not in (Recommendation.BUY, Recommendation.STRONG_BUY)


def test_dividend_cut_forces_reduce_even_with_good_table_score():
    result = compute_recommendation(
        overall_score=85, margin_of_safety=0.25, risk_score_0_100=80, expected_cagr_base=0.15,
        sell_trigger_inputs=SellTriggerInputs(dividend_cut=True, roic_declining_3q=True),
    )
    assert result.table_recommendation == Recommendation.STRONG_BUY
    assert result.recommendation <= Recommendation.REDUCE
    assert "DIVIDEND_CUT" in result.risks


def test_no_triggers_leaves_table_recommendation_untouched():
    result = compute_recommendation(
        overall_score=70, margin_of_safety=0.12, risk_score_0_100=60, expected_cagr_base=0.09,
        sell_trigger_inputs=SellTriggerInputs(),
    )
    assert result.recommendation == result.table_recommendation == Recommendation.BUY
    assert result.risks == []


def test_never_sell_purely_on_price_increase():
    # No deterioration triggers fired, price simply not above the overvalued band -> no override.
    result = compute_recommendation(
        overall_score=90, margin_of_safety=0.30, risk_score_0_100=85, expected_cagr_base=0.18,
        sell_trigger_inputs=SellTriggerInputs(price_above_overvalued_band=False),
    )
    assert result.recommendation == Recommendation.STRONG_BUY


def test_severe_multi_trigger_combination_forces_strong_sell():
    result = compute_recommendation(
        overall_score=30, margin_of_safety=-0.30, risk_score_0_100=20, expected_cagr_base=-0.05,
        sell_trigger_inputs=SellTriggerInputs(
            price_above_overvalued_band=True, roic_declining_3q=True, fcf_declining_3q=True,
            dividend_cut=True,
        ),
    )
    assert result.recommendation == Recommendation.STRONG_SELL


# ---------------------------------------------------------------------------
# StockLab overhaul audit (Part 17): build_sell_trigger_inputs — the real historical-input
# pipeline that replaces the previously-empty SellTriggerInputs() call in recompute.py.
# ---------------------------------------------------------------------------
def test_build_sell_trigger_inputs_detects_real_deterioration():
    inputs = build_sell_trigger_inputs(
        price=120, overvalued_price=100,  # price above the overvalued band
        roic_history=[0.08, 0.10, 0.12, 0.15],       # 3 consecutive annual declines: .08<.10<.12
        operating_margin_history=[0.18, 0.18, 0.18],  # flat -> not declining
        fcf_history=[50, 60, 70],                     # declining
        revenue_history=[900, 1000],                  # declining (2-period check)
        eps_history=[1.5, 2.0],                        # declining
        net_debt_to_ebitda_history=[3.0, 2.0, 1.0],    # rising (reading forward in time)
        interest_coverage_history=[4, 5, 6],           # declining
        diluted_shares_history=[106, 100],             # +6% YoY -> over 3% dilution threshold
        dividend_event_history=["DIVIDEND_CUT"],
    )
    assert inputs.price_above_overvalued_band is True
    assert inputs.roic_declining_3q is True
    assert inputs.operating_margin_declining_3q is False
    assert inputs.fcf_declining_3q is True
    assert inputs.revenue_declining_2q is True
    assert inputs.eps_declining_2q is True
    assert inputs.net_debt_to_ebitda_rising_3q is True
    assert inputs.interest_coverage_declining_3q is True
    assert inputs.diluted_shares_up_over_3pct_yoy is True
    assert inputs.dividend_cut is True
    # Not computed by this builder — no guidance/estimate-revision data ingested (documented).
    assert inputs.guidance_reduced is False
    assert inputs.consensus_estimate_down_over_5pct_90d is False


def test_build_sell_trigger_inputs_healthy_company_fires_nothing():
    inputs = build_sell_trigger_inputs(
        price=90, overvalued_price=100,
        roic_history=[0.15, 0.14, 0.13],
        operating_margin_history=[0.22, 0.21, 0.20],
        fcf_history=[80, 75, 70],
        revenue_history=[1100, 1000],
        eps_history=[2.2, 2.0],
        net_debt_to_ebitda_history=[1.0, 1.2, 1.4],
        interest_coverage_history=[8, 7, 6],
        diluted_shares_history=[100, 100],
        dividend_event_history=[None],
    )
    fired, severity = evaluate_sell_triggers(inputs)
    assert fired == []
    assert severity == 0


def test_build_sell_trigger_inputs_missing_history_defaults_to_false_not_crash():
    inputs = build_sell_trigger_inputs()  # no history supplied at all
    fired, severity = evaluate_sell_triggers(inputs)
    assert fired == []
    assert severity == 0


ALL_TESTS = [obj for name, obj in list(globals().items()) if name.startswith("test_") and callable(obj)]

if __name__ == "__main__":
    passed, failed = 0, []
    for fn in ALL_TESTS:
        try:
            fn()
            passed += 1
            print(f"PASS  {fn.__name__}")
        except AssertionError as e:
            failed.append(fn.__name__)
            print(f"FAIL  {fn.__name__}: {e}")
    print(f"\n{passed}/{len(ALL_TESTS)} passed")
    if failed:
        raise SystemExit(1)
