"""
API contract tests (final master pass, §65, §66, §95, §104.20).

## What these can and cannot do

`fastapi` is **not installed** in this environment and PyPI is unreachable, so `TestClient` route
tests — the ones that would exercise auth, status codes, pagination and error responses — are
**BLOCKED**. That gap is real and is reported as such, not worked around.

What *is* executable: `pydantic` **is** installed, so the response and request **schemas** are
real, instantiable objects. That makes the contract itself testable — which is where this pass
found an actual defect (`ScoreOut.recommendation_confidence` declared and always serialised as
`None`, because nothing computed it). §95 asks that DB model, API schema and frontend expectations
agree; §104.20 makes a mismatch an acceptance failure. These tests hold that line.

They also pin the boundaries `docs/AUDIT_PERFORMANCE.md` established (`ScreenRequest.limit`'s
upper bound), because an unbounded limit is how a screener request turns into a denial of service.

Dual-mode: pytest, or `PYTHONPATH=. python3 tests/test_api_contract.py`.
"""
from __future__ import annotations

from app.schemas.common import (
    CompanyPageOut,
    CompanySummaryOut,
    MetricOut,
    ScoreOut,
    ScreenFilter,
    ScreenRequest,
    ScreenResultRow,
    ScreenUniverse,
    ValuationOut,
)


def _score(**over) -> ScoreOut:
    kw = dict(quality_score=80.0, financial_health_score=70.0, growth_score=60.0,
              competitive_advantage_score=85.0, valuation_score=55.0, overall_score=72.0,
              risk_score=70.0, recommendation="BUY")
    kw.update(over)
    return ScoreOut(**kw)


# --- the defect this pass fixed ---


def test_score_out_no_longer_declares_a_field_nothing_can_populate():
    """REGRESSION. `recommendation_confidence` was declared on ScoreOut and serialised as a literal
    None on every response, because no engine computes it. A permanently-null field is a false
    contract: a client cannot tell "not available for this security" from "never available for any
    security". It was removed from the schema; the DB column is kept and documented as unused."""
    assert "recommendation_confidence" not in ScoreOut.model_fields


def test_every_optional_score_field_can_actually_be_null():
    """The opposite failure: a field a real security genuinely may not have must not be required.
    A company with no peers has no pillar scores at all."""
    empty = ScoreOut(
        quality_score=None, financial_health_score=None, growth_score=None,
        competitive_advantage_score=None, valuation_score=None, overall_score=None,
        risk_score=None, recommendation=None,
    )
    assert empty.overall_score is None
    assert empty.confidence_score is None


# --- §9: the explainability payload must be part of the contract ---


def test_score_out_exposes_the_explainability_payload():
    """§9 requires renormalisation not to hide missing information. `subscore_detail` is where
    `missing_weight` reaches a client; before this pass the column existed and was never written
    and never exposed."""
    assert "subscore_detail" in ScoreOut.model_fields
    detail = {
        "missing_weight": 0.15,
        "subscores_included_in_overall": ["quality", "financial_health", "growth", "valuation"],
        "max_missing_weight_allowed": 0.35,
        "pillars": {"quality": {"value": 80.0, "metrics_excluded": ["peg"], "weight": 0.25}},
        "model_version": "2.0.0",
    }
    s = _score(subscore_detail=detail)
    assert s.model_dump()["subscore_detail"]["missing_weight"] == 0.15


def test_confidence_and_data_quality_are_separate_fields_and_never_blended():
    """They answer different questions — "how much of the analysis could be done" versus "how good
    were the inputs" — and the code comments say never to conflate them. The contract must keep
    them apart."""
    s = _score(confidence_score=62.0, data_quality_score=88.0)
    dumped = s.model_dump()
    assert dumped["confidence_score"] == 62.0
    assert dumped["data_quality_score"] == 88.0
    assert dumped["confidence_score"] != dumped["data_quality_score"]


# --- §95: schema and frontend expectations agree ---


def test_screen_result_row_carries_what_the_screener_table_renders():
    """`frontend/app/screener/page.tsx` renders overall_score, confidence_score,
    weighted_fair_value and margin_of_safety. Each must exist on the row schema or the column is
    permanently blank."""
    for field in ("overall_score", "recommendation", "weighted_fair_value", "margin_of_safety",
                  "confidence_score", "data_quality_score", "company"):
        assert field in ScreenResultRow.model_fields, field


def test_company_summary_carries_the_identity_fields_the_ui_shows():
    for field in ("security_id", "ticker", "company_name", "country", "sector", "industry",
                  "is_demo"):
        assert field in CompanySummaryOut.model_fields, field


def test_is_demo_defaults_to_false_and_is_never_optional():
    """A missing demo flag rendering as "not demo" is the one default that must never be silent —
    §63 requires DEMO to be clearly marked, and §0 forbids presenting demo data as production."""
    row = CompanySummaryOut(security_id="s", ticker="T", company_name="Test", country=None,
                            sector=None, industry=None)
    assert row.is_demo is False
    assert CompanySummaryOut.model_fields["is_demo"].is_required() is False


def test_valuation_out_exposes_the_full_price_ladder_and_its_confidence():
    for field in ("wacc", "bear_fair_value", "base_fair_value", "bull_fair_value",
                  "weighted_fair_value", "fair_value_confidence", "margin_of_safety",
                  "strong_buy_price", "buy_price", "overvalued_price", "expected_return_5y",
                  "business_profile"):
        assert field in ValuationOut.model_fields, field


def test_metric_out_carries_status_and_applicability_not_just_a_number():
    """A metric value without its status is unreadable: the client cannot distinguish "not
    meaningful for this industry" from "we failed to compute it"."""
    for field in ("key", "value", "display", "status", "applicability", "formula_version", "note"):
        assert field in MetricOut.model_fields, field
    m = MetricOut(key="pe", value=None, display="N/M", status="MISSING",
                  applicability="NOT_MEANINGFUL", formula_version="v1")
    assert m.display == "N/M"


def test_company_page_composes_the_pieces_without_duplicating_them():
    page = CompanyPageOut(
        company=CompanySummaryOut(security_id="s", ticker="T", company_name="Test",
                                  country=None, sector=None, industry=None),
        score=_score(), valuation=None,
    )
    assert page.score.overall_score == 72.0
    assert page.valuation is None      # a company with no valuation is representable
    assert page.metrics == [] and page.why == [] and page.risks == []


# --- §49/§104.9: bounded requests ---


def test_screen_request_limit_is_capped():
    """docs/AUDIT_PERFORMANCE.md finding #1: an uncapped limit lets one request force tens of
    thousands of per-row lookups. The cap is part of the contract, not a runtime check."""
    ScreenRequest(limit=200)
    try:
        ScreenRequest(limit=100000)
    except Exception:
        pass
    else:
        raise AssertionError("ScreenRequest accepted limit=100000")


def test_screen_request_offset_cannot_be_negative():
    ScreenRequest(offset=0)
    try:
        ScreenRequest(offset=-1)
    except Exception:
        return
    raise AssertionError("ScreenRequest accepted a negative offset")


def test_screen_request_defaults_are_bounded_and_sensible():
    r = ScreenRequest()
    assert r.limit <= 200 and r.offset == 0
    assert r.universe.include_demo is False, "demo data must not leak into a default screen"
    assert r.logic == "AND"


def test_screen_filter_supports_the_documented_relative_modes():
    """`relative` defaults to absolute. The other modes are declared in the contract; whether they
    are IMPLEMENTED in the query executor is a separate question, recorded in the final report."""
    assert ScreenFilter(metric="roic", op="gt", value=0.15).relative == "absolute"
    for mode in ("industry_percentile", "historical_percentile", "peer_percentile"):
        assert ScreenFilter(metric="roic", op="gt", value=0.15, relative=mode).relative == mode


def test_screen_universe_defaults_exclude_demo_data():
    assert ScreenUniverse().include_demo is False


# --- serialisation round-trip: what a client actually receives ---


def test_a_fully_populated_score_serialises_every_field():
    s = _score(confidence_score=70.0, data_quality_score=80.0,
               weights_used={"quality": 0.25}, triggers_fired=["ROIC_DETERIORATION"],
               subscore_detail={"missing_weight": 0.0})
    dumped = s.model_dump()
    assert set(dumped) == set(ScoreOut.model_fields)
    assert dumped["triggers_fired"] == ["ROIC_DETERIORATION"]


def test_null_scores_serialise_as_null_not_as_zero():
    """The single most important serialisation property in this platform: a missing score must
    never reach a client as 0, which reads as "worst possible" rather than "unknown"."""
    dumped = ScoreOut(
        quality_score=None, financial_health_score=None, growth_score=None,
        competitive_advantage_score=None, valuation_score=None, overall_score=None,
        risk_score=None, recommendation=None,
    ).model_dump()
    for key, value in dumped.items():
        assert value is None or key in ("weights_used", "triggers_fired", "subscore_detail"), key


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)
