"""
Cross-metric validation (final master pass, §7).

The 30 metrics are not 30 independent calculations. They share a small number of normalized input
fields, so ONE wrong value in ONE field propagates into a whole family of metrics at once. A test
suite that only checks each metric in isolation cannot see that; this file exists to make the
blast radius of a bad input explicit and to pin the identities that must hold between metrics.

Two kinds of test here, and they answer different questions:

1. **Identity tests** — relationships that are true by construction and must never drift apart
   (`earnings_yield == 1/pe`, `p_fcf == 1/fcf_yield`, `shareholder_yield == dividend + buyback`,
   `roic_minus_wacc == roic - wacc`, `revenue_cagr_5y == revenue_growth_cagr_5y`). If two metrics
   that are algebraically the same thing ever disagree, one of them has a bug, and no isolated
   test would catch it.

2. **Contamination tests** — deliberately corrupt one normalized field and assert exactly which
   metrics change. These are documentation-as-test: they record the real dependency graph, so a
   future reader can answer "if the provider mis-maps `revenue`, what is wrong on the page?"
   without re-deriving it. They also fail loudly if someone adds a hidden dependency.

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

from datetime import date

from app.engines.metrics.core import compute_all_metrics
from app.engines.types import FinancialSnapshot, LineItems, MarketCapBucket


def _li(period_end: str, **kwargs) -> LineItems:
    defaults = dict(
        security_id="TEST", period_end=date.fromisoformat(period_end), period_type="FY",
        filing_date=date.fromisoformat(period_end), currency="USD",
    )
    defaults.update(kwargs)
    return LineItems(**defaults)


#: A deliberately complete, internally consistent company. Every metric that can be computed at
#: all is computable from this, which is what makes the contamination tests below meaningful:
#: any metric that becomes None or changes value did so because of the field we corrupted.
_BASE_CURRENT = dict(
    revenue=1000.0, cogs=600.0, gross_profit=400.0, operating_income=250.0, ebit=250.0,
    ebitda=300.0, net_income=150.0, eps_diluted=1.5, tax_expense=50.0, pretax_income=200.0,
    interest_expense=20.0,
    cash_and_equivalents=100.0, short_term_investments=50.0, total_debt=400.0,
    total_assets=2000.0, current_assets=600.0, current_liabilities=300.0,
    shareholders_equity=900.0, minority_interest=0.0, preferred_equity=0.0,
    goodwill=100.0, intangible_assets=50.0, receivables=120.0, inventory=80.0,
    shares_outstanding=100.0, diluted_shares=100.0,
    operating_cash_flow=280.0, capital_expenditure=80.0,
    dividends_paid=30.0, buybacks=40.0, stock_issuance=10.0,
    depreciation_and_amortization=50.0,
    price=25.0, market_cap=2500.0, beta=1.1,
)

_BASE_PRIOR = dict(
    revenue=900.0, cogs=550.0, gross_profit=350.0, operating_income=210.0, ebit=210.0,
    ebitda=260.0, net_income=130.0, eps_diluted=1.28, tax_expense=45.0, pretax_income=175.0,
    interest_expense=18.0, cash_and_equivalents=90.0, short_term_investments=40.0,
    total_debt=380.0, total_assets=1850.0, current_assets=550.0, current_liabilities=280.0,
    shareholders_equity=820.0, shares_outstanding=102.0, diluted_shares=102.0,
    operating_cash_flow=250.0, capital_expenditure=70.0, dividends_paid=28.0,
    buybacks=35.0, stock_issuance=8.0, depreciation_and_amortization=48.0,
)


def _snapshot(current_overrides=None, prior_overrides=None, years=6) -> FinancialSnapshot:
    cur = dict(_BASE_CURRENT)
    cur.update(current_overrides or {})
    prior = dict(_BASE_PRIOR)
    prior.update(prior_overrides or {})

    history = []
    for n in range(1, years):
        row = dict(prior)
        # Deflate each older year by 10% so growth/CAGR metrics have a real, monotone series.
        for key in ("revenue", "net_income", "operating_income", "ebit", "ebitda", "gross_profit",
                    "operating_cash_flow", "eps_diluted", "dividends_paid"):
            if row.get(key) is not None:
                row[key] = row[key] * (0.9 ** (n - 1))
        history.append(_li(f"{2025 - n}-12-31", **row))

    return FinancialSnapshot(
        security_id="TEST", industry_id="TEST_IND", sector_id="DEFAULT",
        market_cap_bucket=MarketCapBucket.LARGE, calculation_date=date(2025, 12, 31),
        current=_li("2025-12-31", **cur), history=history,
    )


def _values(metrics) -> dict:
    return {k: v.value for k, v in metrics.items()}


def _changed(baseline: dict, mutated: dict) -> set:
    """Metric keys whose value differs between two runs (None-vs-number counts as a change)."""
    keys = set(baseline) | set(mutated)
    out = set()
    for k in keys:
        a, b = baseline.get(k), mutated.get(k)
        if a is None and b is None:
            continue
        if a is None or b is None:
            out.add(k)
        elif abs(a - b) > 1e-12:
            out.add(k)
    return out


# =====================================================================================
# 1. Identity tests — algebraic relationships that must never drift
# =====================================================================================


def test_earnings_yield_is_the_reciprocal_of_pe():
    """Both are computed from `price` and `eps_diluted` but by different functions. If one ever
    starts using a different price or a different EPS definition, this is what notices."""
    r = _values(compute_all_metrics(_snapshot()))
    assert r["pe"] is not None and r["earnings_yield"] is not None
    assert abs(r["earnings_yield"] - 1.0 / r["pe"]) < 1e-12


def test_p_fcf_is_the_reciprocal_of_fcf_yield():
    r = _values(compute_all_metrics(_snapshot()))
    assert r["p_fcf"] is not None and r["fcf_yield"] is not None
    assert abs(r["p_fcf"] - 1.0 / r["fcf_yield"]) < 1e-9


def test_shareholder_yield_equals_dividend_plus_buyback_yield():
    r = _values(compute_all_metrics(_snapshot()))
    assert abs(r["shareholder_yield"] - (r["dividend_yield"] + r["buyback_yield"])) < 1e-12


def test_fcf_equals_operating_cash_flow_minus_capex():
    r = _values(compute_all_metrics(_snapshot()))
    assert abs(r["fcf"] - (280.0 - 80.0)) < 1e-12


def test_fcf_margin_and_fcf_yield_share_one_fcf():
    """fcf_margin = fcf/revenue and fcf_yield = fcf/market_cap must be consistent with the same
    numerator — if they diverge, one of them is computing FCF differently."""
    r = _values(compute_all_metrics(_snapshot()))
    implied_from_margin = r["fcf_margin"] * 1000.0
    implied_from_yield = r["fcf_yield"] * 2500.0
    assert abs(implied_from_margin - implied_from_yield) < 1e-9
    assert abs(implied_from_margin - r["fcf"]) < 1e-9


def test_fcf_per_share_times_share_count_equals_fcf():
    r = _values(compute_all_metrics(_snapshot()))
    assert abs(r["fcf_per_share"] * 100.0 - r["fcf"]) < 1e-9


def test_revenue_cagr_5y_duplicates_revenue_growth_cagr_5y():
    """Metric #30 is the same calculation as metric #1's 5-year horizon. They are computed by two
    different functions; they must agree exactly or one of them is wrong."""
    r = _values(compute_all_metrics(_snapshot()))
    assert r["revenue_cagr_5y"] is not None
    assert abs(r["revenue_cagr_5y"] - r["revenue_growth_cagr_5y"]) < 1e-12


def test_roic_minus_wacc_is_exactly_roic_minus_the_supplied_wacc():
    m = compute_all_metrics(_snapshot(), wacc=0.08)
    r = _values(m)
    assert abs(r["roic_minus_wacc"] - (r["roic"] - 0.08)) < 1e-12


def test_net_debt_to_ebitda_is_below_gross_debt_to_ebitda_when_cash_is_positive():
    r = _values(compute_all_metrics(_snapshot()))
    assert r["net_debt_to_ebitda"] < r["debt_to_ebitda"]


def test_margin_ordering_holds_for_a_consistent_income_statement():
    """gross >= operating >= net, given a consistent set of income-statement inputs. Not a
    universal law of companies, but it IS a law of THESE inputs, so a violation means an
    implementation bug rather than an unusual company."""
    r = _values(compute_all_metrics(_snapshot()))
    assert r["gross_margin"] > r["operating_margin"] > r["net_margin"]


def test_ev_based_multiples_share_one_enterprise_value():
    """ev_to_ebitda and ev_to_fcf must imply the same EV."""
    r = _values(compute_all_metrics(_snapshot()))
    ev_from_ebitda = r["ev_to_ebitda"] * 300.0
    ev_from_fcf = r["ev_to_fcf"] * 200.0
    assert abs(ev_from_ebitda - ev_from_fcf) < 1e-9


def test_peg_is_pe_over_growth_expressed_in_percent():
    """The classic units trap: PEG takes growth as a PERCENTAGE (12.0), while every growth metric
    in this engine is a DECIMAL (0.12). compute_all_metrics multiplies by 100 on the way in. If
    that ever changes, PEG silently becomes 100x wrong and still looks like a number."""
    m = compute_all_metrics(_snapshot())
    r = _values(m)
    assert r["eps_growth_cagr_3y"] is not None and r["eps_growth_cagr_3y"] > 0
    assert abs(r["peg"] - r["pe"] / (r["eps_growth_cagr_3y"] * 100.0)) < 1e-9


# =====================================================================================
# 2. Contamination tests — the real dependency graph, recorded as executable fact
# =====================================================================================


def test_a_wrong_revenue_contaminates_every_revenue_denominated_metric():
    base = _values(compute_all_metrics(_snapshot()))
    bad = _values(compute_all_metrics(_snapshot({"revenue": 500.0})))
    changed = _changed(base, bad)
    for key in ("gross_margin", "operating_margin", "net_margin", "fcf_margin",
                "capex_to_revenue", "revenue_growth_yoy", "revenue_cagr_5y"):
        assert key in changed, f"{key} should depend on revenue but did not change"
    # ...and must NOT touch anything priced off the market or the balance sheet.
    for key in ("pe", "earnings_yield", "debt_to_equity", "current_ratio", "fcf_yield"):
        assert key not in changed, f"{key} unexpectedly depends on revenue"


def test_a_wrong_market_cap_contaminates_every_market_priced_metric():
    base = _values(compute_all_metrics(_snapshot()))
    bad = _values(compute_all_metrics(_snapshot({"market_cap": 5000.0})))
    changed = _changed(base, bad)
    for key in ("fcf_yield", "dividend_yield", "buyback_yield", "shareholder_yield",
                "ev_to_ebitda", "ev_to_fcf", "p_fcf"):
        assert key in changed, f"{key} should depend on market_cap but did not change"
    # P/E and earnings yield are computed from `price`, NOT from market_cap — a real and easily
    # forgotten distinction, since the two are normally consistent with each other.
    for key in ("pe", "earnings_yield"):
        assert key not in changed, f"{key} should use price, not market_cap"


def test_a_wrong_share_count_contaminates_per_share_and_dilution_metrics():
    base = _values(compute_all_metrics(_snapshot()))
    bad = _values(compute_all_metrics(_snapshot({"diluted_shares": 200.0})))
    changed = _changed(base, bad)
    for key in ("fcf_per_share", "share_count_growth_yoy"):
        assert key in changed, f"{key} should depend on diluted_shares but did not change"
    # EPS is supplied by the provider, not derived from net_income/shares here, so P/E is
    # unaffected by a bad share count. That is a REAL exposure, documented by this assertion:
    # market cap and EPS can silently disagree about the share count.
    assert "pe" not in changed


def test_a_wrong_ebitda_contaminates_leverage_and_ebitda_multiples_only():
    base = _values(compute_all_metrics(_snapshot()))
    bad = _values(compute_all_metrics(_snapshot({"ebitda": 150.0})))
    changed = _changed(base, bad)
    for key in ("debt_to_ebitda", "net_debt_to_ebitda", "ev_to_ebitda"):
        assert key in changed
    for key in ("roic", "operating_margin", "fcf", "pe"):
        assert key not in changed


def test_a_wrong_operating_cash_flow_contaminates_the_whole_fcf_family():
    base = _values(compute_all_metrics(_snapshot()))
    bad = _values(compute_all_metrics(_snapshot({"operating_cash_flow": 100.0})))
    changed = _changed(base, bad)
    for key in ("fcf", "fcf_per_share", "fcf_margin", "fcf_yield", "p_fcf", "ev_to_fcf",
                "fcf_payout_ratio", "fcf_growth_yoy"):
        assert key in changed, f"{key} should depend on operating_cash_flow but did not change"


def test_a_wrong_shareholders_equity_contaminates_roe_roic_and_leverage():
    base = _values(compute_all_metrics(_snapshot()))
    bad = _values(compute_all_metrics(_snapshot({"shareholders_equity": 300.0})))
    changed = _changed(base, bad)
    for key in ("roe", "roic", "debt_to_equity"):
        assert key in changed, f"{key} should depend on shareholders_equity but did not change"


def test_a_missing_field_degrades_only_its_own_dependents_not_the_whole_snapshot():
    """The point of the N/M and INSUFFICIENT_DATA discipline: one absent field must not cascade
    into a page of empty metrics."""
    m = compute_all_metrics(_snapshot({"inventory": None}))
    computed = [k for k, v in m.items() if v.value is not None]
    assert len(computed) > 25, "one missing field wiped out most of the metric set"


# =====================================================================================
# 3. shareholder_yield partial-data behaviour (final master pass, §0 NO SILENT FALLBACKS)
# =====================================================================================


def test_shareholder_yield_flags_a_missing_component_instead_of_treating_it_as_zero():
    """It used to return `(dy or 0) + (by or 0)` as an ordinary CALCULATED metric, so an UNKNOWN
    dividend was indistinguishable from a zero dividend."""
    m = compute_all_metrics(_snapshot({"dividends_paid": None}))
    sy = m["shareholder_yield"]
    assert sy.value is not None                       # the known component is still reported
    assert sy.status.value == "ESTIMATED"             # ...but no longer as a full calculation
    assert "dividend_yield" in sy.inputs_used["components_missing"]
    assert "at least this value" in (sy.note or "")


def test_shareholder_yield_is_a_plain_calculation_when_both_components_are_known():
    sy = compute_all_metrics(_snapshot())["shareholder_yield"]
    assert sy.status.value == "CALCULATED"
    assert sy.inputs_used["components_missing"] == []


def test_a_real_zero_dividend_is_not_treated_as_missing():
    """A company that pays no dividend reports 0, not null. That must stay a full calculation."""
    sy = compute_all_metrics(_snapshot({"dividends_paid": 0.0}))["shareholder_yield"]
    assert sy.status.value == "CALCULATED"
    assert sy.inputs_used["dividend_yield"] == 0.0


def test_shareholder_yield_is_none_when_neither_component_is_known():
    m = compute_all_metrics(_snapshot({"dividends_paid": None, "buybacks": None}))
    assert m["shareholder_yield"].value is None


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)
