"""Known-case tests for the scoring engine. Plain-function style, see test_metrics_known_cases.py."""
from __future__ import annotations

from app.engines.scoring.overall import DEFAULT_WEIGHTS, compute_overall_score
from app.engines.scoring.percentile import percentile_rank, peer_values_with_fallback
from app.engines.scoring.persistence import stability_score, pricing_power_score
from app.engines.scoring.subscores import (
    ScoreResult, compute_generic_subscore, compute_competitive_advantage_score,
)
from app.engines.types import DataQualityStatus, Applicability, MetricResult


def _mr(key, value, applicability=Applicability.APPLICABLE):
    return MetricResult(key=key, value=value, status=DataQualityStatus.CALCULATED,
                         applicability=applicability, formula_version="v1")


def test_percentile_rank_basic():
    assert percentile_rank(10, [1, 2, 3, 4, 5, 6, 7, 8, 9]) == 100.0
    assert percentile_rank(0, [1, 2, 3, 4, 5, 6, 7, 8, 9]) == 0.0
    assert percentile_rank(5, [1, 2, 3, 4, 5, 6, 7, 8, 9]) == 50.0  # tie counted as half


def test_percentile_rank_lower_is_better_inverts():
    # value 1 is the LOWEST in the peer set -> best for a "lower is better" metric -> ~100
    assert percentile_rank(1, [1, 2, 3, 4, 5], lower_is_better=True) == 90.0
    assert percentile_rank(5, [1, 2, 3, 4, 5], lower_is_better=True) == 10.0


def test_peer_group_fallback_widens_when_thin():
    peers, tier = peer_values_with_fallback([1, 2], [1, 2, 3, 4, 5, 6, 7, 8, 9], [], min_size=8)
    assert tier == "industry_wide"
    assert len(peers) == 9


def test_generic_subscore_excludes_not_meaningful_metrics():
    company_metrics = {
        "roic": _mr("roic", 20.0),
        "gross_margin": _mr("gross_margin", None, Applicability.NOT_MEANINGFUL),
    }
    peer_values = {
        "roic": {"industry_bucket": list(range(1, 20))},
        "gross_margin": {"industry_bucket": list(range(1, 20))},
    }
    result = compute_generic_subscore({"roic": False, "gross_margin": False}, company_metrics, peer_values)
    assert "gross_margin" in result.metrics_excluded
    assert "roic" in result.metrics_included
    assert result.value == 100.0  # roic=20 beats every peer 1..19


def test_generic_subscore_insufficient_data_when_nothing_meaningful():
    company_metrics = {"roic": _mr("roic", None, Applicability.INSUFFICIENT_DATA)}
    result = compute_generic_subscore({"roic": False}, company_metrics, {})
    assert result.value is None
    assert result.status == "INSUFFICIENT_DATA"


def test_competitive_advantage_requires_min_proxies():
    result = compute_competitive_advantage_score({"roic_persistence": 80.0, "fcf_stability": 70.0})
    assert result.value is None
    assert result.status == "INSUFFICIENT_DATA"

    result2 = compute_competitive_advantage_score({
        "roic_persistence": 80.0, "fcf_stability": 70.0, "revenue_stability": 90.0,
    })
    assert result2.value is not None
    assert abs(result2.value - 80.0) < 1e-9


def test_stability_score_perfectly_stable_series_is_100():
    assert stability_score([10, 10, 10, 10]) == 100.0


def test_pricing_power_score_rising_margin_scores_above_flat():
    rising = pricing_power_score([30.0, 31.0, 32.5, 34.0])
    flat = pricing_power_score([30.0, 30.0, 30.0, 30.0])
    falling = pricing_power_score([34.0, 32.5, 31.0, 30.0])
    assert rising is not None and flat is not None and falling is not None
    assert rising > flat > falling
    assert flat == 50.0  # zero slope maps to the midpoint exactly


def test_pricing_power_score_insufficient_points_is_none():
    assert pricing_power_score([30.0, 31.0]) is None


def test_overall_score_weighted_blend():
    sc = compute_overall_score(
        quality=ScoreResult(value=80.0), financial_health=ScoreResult(value=60.0),
        growth=ScoreResult(value=70.0), competitive_advantage=ScoreResult(value=50.0),
        valuation=ScoreResult(value=40.0),
    )
    expected = (
        DEFAULT_WEIGHTS["quality"] * 80 + DEFAULT_WEIGHTS["financial_health"] * 60
        + DEFAULT_WEIGHTS["growth"] * 70 + DEFAULT_WEIGHTS["competitive_advantage"] * 50
        + DEFAULT_WEIGHTS["valuation"] * 40
    )
    assert abs(sc.overall - expected) < 1e-9


def test_overall_score_renormalizes_when_one_subscore_missing():
    sc = compute_overall_score(
        quality=ScoreResult(value=80.0), financial_health=ScoreResult(value=None, status="INSUFFICIENT_DATA"),
        growth=ScoreResult(value=70.0), competitive_advantage=ScoreResult(value=50.0),
        valuation=ScoreResult(value=40.0),
    )
    assert sc.overall is not None
    assert "financial_health" not in sc.subscores_included_in_overall
    assert abs(sum(sc.weights_used.values()) - 1.0) < 1e-9
    assert abs(sc.missing_weight - DEFAULT_WEIGHTS["financial_health"]) < 1e-9


# ---------------------------------------------------------------------------
# StockLab overhaul audit (Part 7): score-inflation guard on compute_overall_score, plus new
# Confidence and Data Quality engines. See overall.py's module docstring for the reasoning.
# ---------------------------------------------------------------------------
def test_overall_score_refuses_to_extrapolate_when_too_much_weight_missing():
    # Missing financial_health (0.20) + competitive_advantage (0.15) = 0.35 missing weight, which
    # sits exactly at the default cap (> triggers refusal, so 0.35 itself must still compute) —
    # bump to missing growth too (0.20) for a clean over-cap case: 0.20+0.20+0.15 = 0.55 > 0.35.
    sc = compute_overall_score(
        quality=ScoreResult(value=80.0),
        financial_health=ScoreResult(value=None, status="INSUFFICIENT_DATA"),
        growth=ScoreResult(value=None, status="INSUFFICIENT_DATA"),
        competitive_advantage=ScoreResult(value=None, status="INSUFFICIENT_DATA"),
        valuation=ScoreResult(value=40.0),
    )
    assert sc.overall is None
    assert sc.subscores_included_in_overall == []
    assert sc.missing_weight > 0.35


def test_overall_score_computes_right_at_the_missing_weight_boundary():
    # Only competitive_advantage (0.15) missing -> well under the 0.35 cap -> still computes.
    sc = compute_overall_score(
        quality=ScoreResult(value=80.0), financial_health=ScoreResult(value=60.0),
        growth=ScoreResult(value=70.0),
        competitive_advantage=ScoreResult(value=None, status="INSUFFICIENT_DATA"),
        valuation=ScoreResult(value=40.0),
    )
    assert sc.overall is not None
    assert abs(sc.missing_weight - DEFAULT_WEIGHTS["competitive_advantage"]) < 1e-9


def test_confidence_score_full_data_is_high():
    from app.engines.scoring.confidence import compute_confidence_score
    from app.engines.types import DataQualityStatus as DQS
    r = compute_confidence_score(
        pillars_present=5, pillars_total=5,
        metric_statuses=[DQS.CALCULATED] * 28 + [DQS.VERIFIED] * 2,
        metrics_missing_count=0,
        years_of_history=5,
        dcf_fair_value=100.0, multiples_fair_value=98.0,
    )
    assert r.value is not None
    assert r.value > 90.0
    assert abs(sum(c.weight for c in r.components) - 1.0) < 1e-6


def test_confidence_score_thin_mostly_assumed_data_is_low():
    from app.engines.scoring.confidence import compute_confidence_score
    from app.engines.types import DataQualityStatus as DQS
    r = compute_confidence_score(
        pillars_present=2, pillars_total=5,
        metric_statuses=[DQS.ASSUMPTION] * 5,
        metrics_missing_count=20,
        years_of_history=1,
    )
    assert r.value is not None
    assert r.value < 40.0


def test_confidence_score_omits_unavailable_components_rather_than_zeroing_them():
    from app.engines.scoring.confidence import compute_confidence_score
    from app.engines.types import DataQualityStatus as DQS
    r = compute_confidence_score(
        pillars_present=5, pillars_total=5, metric_statuses=[DQS.CALCULATED] * 10,
    )  # no years_of_history, no valuation figures supplied
    names = {c.name for c in r.components}
    assert "historical_depth" not in names
    assert "valuation_agreement" not in names
    assert abs(sum(c.weight for c in r.components) - 1.0) < 1e-6


def test_data_quality_score_official_filings_verified_is_high():
    from app.engines.scoring.data_quality import compute_data_quality_score
    from app.engines.types import DataQualityStatus as DQS
    r = compute_data_quality_score(
        source_tiers=["OFFICIAL_FILING"] * 8,
        field_statuses=[DQS.VERIFIED] * 6 + [DQS.CALCULATED] * 2,
    )
    assert r.value is not None
    assert r.value > 90.0


def test_data_quality_score_secondary_estimated_is_lower():
    from app.engines.scoring.data_quality import compute_data_quality_score
    from app.engines.types import DataQualityStatus as DQS
    r = compute_data_quality_score(
        source_tiers=["SECONDARY"] * 4,
        field_statuses=[DQS.ESTIMATED] * 3 + [DQS.CONFLICTING] * 1,
    )
    assert r.value is not None
    assert r.value < 60.0
    assert r.conflicting_field_count == 1


# --- app/engines/scoring/wiring.py --------------------------------------------------------------
# StockLab overhaul, Part A1: the assembly functions that turn recompute.py's in-memory data
# (metrics dict, 5 pillar scores, FinancialSnapshot.history, blended fair values) into the inputs
# compute_confidence_score()/compute_data_quality_score() take. These are the pieces that can be
# genuinely TESTED without a database -- the recompute.py -> DB -> API integration path itself
# cannot run in this sandbox (no sqlalchemy) and is DOCUMENTED / NOT TESTED, see
# docs/AUDIT_CONFIDENCE_DATA_QUALITY.md.

def test_pillar_completeness_counts_only_non_none():
    from app.engines.scoring.wiring import pillar_completeness
    present, total = pillar_completeness([80.0, None, 55.0, None, 90.0])
    assert (present, total) == (3, 5)


def test_pillar_completeness_all_present():
    from app.engines.scoring.wiring import pillar_completeness
    assert pillar_completeness([1.0, 2.0, 3.0, 4.0, 5.0]) == (5, 5)


def test_pillar_completeness_empty_list():
    from app.engines.scoring.wiring import pillar_completeness
    assert pillar_completeness([]) == (0, 0)


def test_metric_completeness_inputs_splits_meaningful_from_real_gaps():
    from app.engines.scoring.wiring import metric_completeness_inputs
    from app.engines.types import Applicability, DataQualityStatus, MetricResult

    metrics = {
        "roic": _mr("roic", 20.0, Applicability.APPLICABLE),
        "dividend_yield": MetricResult(
            key="dividend_yield", value=None, status=DataQualityStatus.MISSING,
            applicability=Applicability.NOT_MEANINGFUL, formula_version="v1",
        ),
        "fcf_margin": MetricResult(
            key="fcf_margin", value=None, status=DataQualityStatus.MISSING,
            applicability=Applicability.INSUFFICIENT_DATA, formula_version="v1",
        ),
    }
    statuses, real_gaps = metric_completeness_inputs(metrics)
    # only roic is meaningful (dividend_yield is a correct N/M, not a gap)
    assert statuses == [DataQualityStatus.CALCULATED]
    # only fcf_margin (INSUFFICIENT_DATA) counts as a real gap -- NOT_MEANINGFUL is excluded
    assert real_gaps == 1


def test_metric_completeness_inputs_all_not_meaningful_yields_zero_gaps():
    from app.engines.scoring.wiring import metric_completeness_inputs
    from app.engines.types import Applicability, DataQualityStatus, MetricResult

    metrics = {
        "dividend_yield": MetricResult(
            key="dividend_yield", value=None, status=DataQualityStatus.MISSING,
            applicability=Applicability.NOT_MEANINGFUL, formula_version="v1",
        ),
    }
    statuses, real_gaps = metric_completeness_inputs(metrics)
    assert statuses == []
    assert real_gaps == 0  # NOT_MEANINGFUL is never a "gap" -- it's a correct determination


def test_metric_completeness_inputs_empty_dict():
    from app.engines.scoring.wiring import metric_completeness_inputs
    assert metric_completeness_inputs({}) == ([], 0)


def test_years_of_history_adds_current_period():
    from app.engines.scoring.wiring import years_of_history
    assert years_of_history(0) == 1   # only the current period, no prior history rows
    assert years_of_history(4) == 5   # current + 4 prior FinancialSnapshot.history entries


def test_average_ignoring_none_basic():
    from app.engines.scoring.wiring import average_ignoring_none
    assert average_ignoring_none([10.0, 20.0, 30.0]) == 20.0


def test_average_ignoring_none_skips_none_values():
    from app.engines.scoring.wiring import average_ignoring_none
    assert average_ignoring_none([10.0, None, 30.0]) == 20.0


def test_average_ignoring_none_all_none_returns_none():
    from app.engines.scoring.wiring import average_ignoring_none
    assert average_ignoring_none([None, None]) is None


def test_average_ignoring_none_empty_list_returns_none():
    from app.engines.scoring.wiring import average_ignoring_none
    assert average_ignoring_none([]) is None


# --- Part B2: metrics with no peers are excluded, not scored a neutral 50 ---


def test_metric_with_no_peers_is_excluded_from_the_subscore():
    metrics = {"roic": _mr("roic", 25.0), "operating_margin": _mr("operating_margin", 0.30)}
    peers = {"roic": {"industry_bucket": [], "industry": [10.0, 20.0, 30.0], "sector": []}}
    result = compute_generic_subscore({"roic": False, "operating_margin": False}, metrics, peers)
    assert result.metrics_included == ["roic"]
    assert "operating_margin" in result.metrics_excluded


def test_subscore_with_no_peers_at_all_is_insufficient_data_not_fifty():
    """The regression this closes: with no peer universe supplied, every subscore used to come out
    at exactly 50.0 with status CALCULATED -- indistinguishable from a real, well-peered score."""
    result = compute_generic_subscore({"roic": False}, {"roic": _mr("roic", 25.0)}, {})
    assert result.value is None
    assert result.status == "INSUFFICIENT_DATA"


def test_subscore_still_computed_from_the_metrics_that_do_have_peers():
    metrics = {"roic": _mr("roic", 25.0), "operating_margin": _mr("operating_margin", 0.30)}
    peers = {"roic": {"industry_bucket": [], "industry": [10.0, 20.0, 30.0], "sector": []}}
    result = compute_generic_subscore({"roic": False, "operating_margin": False}, metrics, peers)
    assert abs(result.value - 200.0 / 3) < 1e-9  # 25 beats 2 of 10/20/30
    assert result.status != "INSUFFICIENT_DATA"


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)
