"""
Emerging classification and Emerging Score (final master pass, §20, §52).

The rule under test is §20's awkward pair: a young company must not be *penalised* for being
young, but its confidence must still be lower. These tests check that a missing long-horizon input
is EXCLUDED from the score rather than scored as zero — which is the difference between "we cannot
measure this yet" and "this is bad".

Pure, dependency-free — genuinely executed. Dual-mode: pytest, or
`PYTHONPATH=. python3 tests/test_emerging.py`.
"""
from __future__ import annotations

from datetime import date

from app.engines.discovery.emerging import (
    ESTABLISHED_MIN_YEARS,
    MIN_SIGNALS_FOR_EMERGING_SCORE,
    MIN_YEARS_FOR_ANY_CLASSIFICATION,
    STATUS_EMERGING,
    STATUS_ESTABLISHED,
    STATUS_INSUFFICIENT_HISTORY,
    assess_emerging,
    classify_history_depth,
)

AS_OF = date(2025, 12, 31)


def _strong(**over):
    """A young company doing everything right: accelerating growth, expanding margins, FCF just
    turned positive, ROIC rising, little debt, a sane multiple."""
    kw = dict(
        years_of_history=4,
        revenue_growth_series=[0.35, 0.22],
        operating_margin_series=[0.14, 0.09],
        fcf_series=[25.0, -10.0],
        roic_series=[0.16, 0.09],
        net_debt_to_ebitda=0.4,
        pe=22.0,
        as_of=AS_OF,
    )
    kw.update(over)
    return assess_emerging(**kw)


# --- classification is about the DATA, not about quality ---


def test_classification_boundaries():
    assert classify_history_depth(0) == STATUS_INSUFFICIENT_HISTORY
    assert classify_history_depth(MIN_YEARS_FOR_ANY_CLASSIFICATION - 1) == STATUS_INSUFFICIENT_HISTORY
    assert classify_history_depth(MIN_YEARS_FOR_ANY_CLASSIFICATION) == STATUS_EMERGING
    assert classify_history_depth(ESTABLISHED_MIN_YEARS - 1) == STATUS_EMERGING
    assert classify_history_depth(ESTABLISHED_MIN_YEARS) == STATUS_ESTABLISHED
    assert classify_history_depth(25) == STATUS_ESTABLISHED


def test_insufficient_history_is_a_statement_about_data_not_a_low_score():
    """Two annual periods give exactly one year-over-year change, which cannot distinguish a trend
    from a single event. The status says so; it does not become a bad score."""
    a = assess_emerging(years_of_history=2, revenue_growth_series=[0.5, 0.1],
                        operating_margin_series=[0.2, 0.1], fcf_series=[10.0, -5.0],
                        roic_series=[0.2, 0.1], net_debt_to_ebitda=0.2, pe=15.0)
    assert a.status == STATUS_INSUFFICIENT_HISTORY
    assert a.emerging_score is not None      # what IS measurable is still measured
    assert a.emerging_score > 50


# --- the core rule: absent inputs are excluded, never zeroed ---


def test_a_young_company_is_not_penalised_for_missing_long_horizon_inputs():
    """The whole point of §20. A company with no P/E (loss-making) and no leverage figure must be
    scored on the four signals it DOES have, not given 0 for the two it cannot."""
    full = _strong()
    partial = _strong(pe=None, net_debt_to_ebitda=None)
    assert partial.emerging_score is not None
    assert "valuation_sanity" in partial.signals_unavailable
    assert "balance_sheet_room" in partial.signals_unavailable
    # the four remaining signals are strong, so the score must stay strong
    assert partial.emerging_score > 60
    # and it must not simply equal the full score — different evidence, different number
    assert partial.emerging_score != full.emerging_score


def test_a_negative_pe_is_unavailable_not_expensive():
    """"We cannot value it on earnings" is not "it is expensive". A loss-making young company must
    not be scored 0 for valuation."""
    a = _strong(pe=-12.0)
    valuation = next(s for s in a.signals if s.key == "valuation_sanity")
    assert valuation.score is None
    assert "not meaningful" in valuation.reason


def test_too_few_computable_signals_yields_no_score_rather_than_a_guess():
    a = assess_emerging(years_of_history=4, revenue_growth_series=[0.3])  # one point, no deltas
    assert a.emerging_score is None
    assert len(a.signals_used) < MIN_SIGNALS_FOR_EMERGING_SCORE


def test_every_signal_reports_a_reason_even_when_unavailable():
    a = assess_emerging(years_of_history=3)
    assert len(a.signals) == 6
    for s in a.signals:
        assert s.reason, s.key


# --- the signals themselves ---


def test_revenue_acceleration_rewards_rising_growth_not_merely_high_growth():
    accelerating = _strong(revenue_growth_series=[0.30, 0.10])
    decelerating = _strong(revenue_growth_series=[0.30, 0.50])
    acc = next(s for s in accelerating.signals if s.key == "revenue_acceleration").score
    dec = next(s for s in decelerating.signals if s.key == "revenue_acceleration").score
    assert acc > dec
    # both have the SAME current growth of 30% — only the direction of change differs
    assert dec == 0.0


def test_fcf_inflection_scores_the_crossing_highest():
    crossing = _strong(fcf_series=[5.0, -20.0])
    always_positive = _strong(fcf_series=[30.0, 25.0])
    improving_but_negative = _strong(fcf_series=[-5.0, -20.0])
    deteriorating = _strong(fcf_series=[-30.0, -10.0])

    def fcf(a):
        return next(s for s in a.signals if s.key == "fcf_inflection").score

    assert fcf(crossing) == 100.0
    assert fcf(always_positive) == 70.0
    assert fcf(improving_but_negative) == 40.0
    assert fcf(deteriorating) == 0.0


def test_leverage_scores_inversely():
    low = _strong(net_debt_to_ebitda=0.0)
    high = _strong(net_debt_to_ebitda=4.0)

    def lev(a):
        return next(s for s in a.signals if s.key == "balance_sheet_room").score

    assert lev(low) == 100.0
    assert lev(high) == 0.0


def test_scores_are_clamped_to_the_zero_hundred_range():
    extreme = _strong(revenue_growth_series=[5.0, -0.5], operating_margin_series=[0.9, -0.5],
                      roic_series=[2.0, -1.0], net_debt_to_ebitda=-20.0, pe=1.0)
    for s in extreme.signals:
        if s.score is not None:
            assert 0.0 <= s.score <= 100.0, (s.key, s.score)
    assert 0.0 <= extreme.emerging_score <= 100.0


def test_a_deteriorating_company_scores_below_an_improving_one():
    improving = _strong()
    deteriorating = _strong(
        revenue_growth_series=[0.05, 0.30], operating_margin_series=[0.05, 0.12],
        fcf_series=[-30.0, -5.0], roic_series=[0.04, 0.14], net_debt_to_ebitda=4.5, pe=55.0,
    )
    assert deteriorating.emerging_score < improving.emerging_score


# --- discovery reasons are evidence, not decoration ---


def test_discovery_reasons_name_the_specific_signals_that_fired():
    a = _strong()
    joined = " | ".join(a.discovery_reasons)
    assert "revenue growth accelerating" in joined
    assert "operating margin expanding" in joined
    assert "free cash flow inflected positive" in joined
    assert "ROIC improving" in joined
    assert "low leverage" in joined


def test_a_flat_company_produces_no_discovery_reasons():
    flat = _strong(revenue_growth_series=[0.10, 0.10], operating_margin_series=[0.10, 0.10],
                   fcf_series=[10.0, 10.0], roic_series=[0.10, 0.10], net_debt_to_ebitda=2.5)
    assert flat.discovery_reasons == []


def test_the_serialised_payload_carries_everything_the_ui_needs():
    payload = _strong().as_dict()
    for key in ("status", "years_of_history", "emerging_score", "signals", "signals_used",
                "signals_unavailable", "discovery_reasons", "discovered_on"):
        assert key in payload, key
    assert payload["discovered_on"] == AS_OF.isoformat()


ALL_TESTS = [v for k, v in sorted(globals().items()) if k.startswith("test_")]

if __name__ == "__main__":
    passed = failed = 0
    for t in ALL_TESTS:
        try:
            t()
            print(f"PASS  {t.__name__}")
            passed += 1
        except Exception as exc:  # noqa: BLE001
            print(f"FAIL  {t.__name__}: {exc}")
            failed += 1
    print(f"\n{passed}/{passed + failed} passed")
    raise SystemExit(1 if failed else 0)
