from __future__ import annotations

from decimal import Decimal
import json
from typing import Any, cast

import pytest
from fastapi.testclient import TestClient

from jarvis_finance.api.dependencies import get_db
from jarvis_finance.api.main import create_app
from jarvis_finance.ledger.cost_basis import fifo_v1
from jarvis_finance.ledger.performance import Activity, effective_activities, twr_v1, xirr_v1
from jarvis_finance.services.portfolio_performance import (
    build_performance_coverage,
    build_portfolio_performance,
)
from jarvis_finance.services.performance_scope import (
    set_performance_cashflow_coverage,
    set_performance_scope_classification,
)
from jarvis_finance.storage.database import connect_memory
from jarvis_finance.storage.migrations import MIGRATION_VERSION, apply_migrations

NOW = "2026-07-23T12:00:00+00:00"


def db():
    conn = connect_memory()
    apply_migrations(conn)
    conn.execute(
        "INSERT INTO platforms(platform_id,name,platform_type,created_at) VALUES ('p','Synthetic','broker',?)",
        (NOW,),
    )
    conn.execute(
        "INSERT INTO accounts(account_id,platform_id,account_name,account_type,currency,created_at) VALUES ('a','p','Control','brokerage','CHF',?)",
        (NOW,),
    )
    set_performance_scope_classification(
        conn,
        account_id="a",
        included=True,
        classification_role="crypto_portfolio",
        source="synthetic_test_fixture",
        note="Control portfolio",
        classified_at=NOW,
    )
    set_performance_cashflow_coverage(
        conn,
        account_id="a",
        coverage_from="1900-01-01",
        coverage_to="2100-12-31",
        status="complete",
        source="synthetic_test_fixture",
        note="Complete synthetic cashflow history",
        recorded_at=NOW,
    )
    conn.execute(
        "INSERT INTO instruments(instrument_id,asset_class,name,currency,created_at) VALUES ('i','equity','Synthetic ETF','CHF',?)",
        (NOW,),
    )
    return conn


def add_valuation(
    conn,
    snapshot_id: str,
    at: str,
    value: str,
    *,
    currency: str = "CHF",
    fx: str | None = None,
    scope_kind: str = "account",
    scope_id: str = "a",
    account_id: str = "a",
    version: int = 1,
    captured_at: str = NOW,
    supersedes: str | None = None,
    quality_status: str = "complete",
    reasons: list[str] | None = None,
):
    conn.execute(
        """
        INSERT INTO portfolio_valuation_snapshots(
          snapshot_id,scope_kind,scope_id,account_id,value_original,currency,base_currency,
          fx_rate_to_base,fx_direction,valuation_at,source,captured_at,snapshot_version,
          supersedes_snapshot_id,source_reference,quality_status,reason_codes_json
        ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
        """,
        (
            snapshot_id,
            scope_kind,
            scope_id,
            account_id,
            value,
            currency,
            "CHF",
            fx,
            "original_to_base",
            at,
            "synthetic_fixture",
            captured_at,
            version,
            supersedes,
            f"ref-{snapshot_id}",
            quality_status,
            json.dumps(reasons or []),
        ),
    )


def add_activity(
    conn,
    activity_id: str,
    kind: str,
    at: str,
    *,
    instrument_id: str | None = None,
    quantity: str | None = None,
    gross: str | None = None,
    fee: str = "0",
    tax: str = "0",
    net: str | None = None,
    currency: str = "CHF",
    fx: str | None = "1",
    reversal_of: str | None = None,
    transfer_group: str | None = None,
):
    conn.execute(
        """
        INSERT INTO transactions(
          transaction_id,transaction_type,activity_kind,account_id,instrument_id,trade_date,
          booking_date,event_timestamp,quantity,gross_amount_original,fee_original,tax_original,
          net_amount_original,currency_original,fx_rate_to_chf,fx_status,source_type,source_id,
          external_transaction_id,row_hash,is_confirmed,quality_status,created_at,
          reversal_of_transaction_id,internal_transfer_group_id,source_reference
        ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
        """,
        (
            activity_id,
            kind,
            kind,
            "a",
            instrument_id,
            at[:10],
            at[:10],
            at,
            quantity,
            gross,
            fee,
            tax,
            net,
            currency,
            fx,
            "ok" if fx is not None else "missing",
            "synthetic_import",
            "fixture.csv",
            f"external-{activity_id}",
            f"hash-{activity_id}",
            1,
            "ok" if fx is not None else "incomplete",
            NOW,
            reversal_of,
            transfer_group,
            f"line-{activity_id}",
        ),
    )


def add_control_portfolio(conn):
    add_valuation(conn, "v1", "2025-01-01T00:00:00", "10000")
    add_valuation(conn, "v2", "2025-02-01T00:00:00", "11000")
    add_valuation(conn, "v3", "2025-03-01T00:00:00", "17600")
    add_valuation(conn, "vi", "2025-03-01T00:00:00", "720", scope_kind="instrument", scope_id="i")
    add_activity(conn, "deposit", "external_deposit", "2025-02-01T00:00:00", net="5000")
    add_activity(conn, "buy", "buy", "2025-01-05T00:00:00", instrument_id="i", quantity="10", gross="1000", fee="5")
    add_activity(conn, "sell", "sell", "2025-02-10T00:00:00", instrument_id="i", quantity="4", gross="480", fee="2", tax="3")
    # An unpaired transfer is intentionally not part of this complete control
    # portfolio; Sprint 20C treats it as unsupported rather than silently neutral.


def test_twr_control_is_exactly_twenty_one_percent():
    result = twr_v1(
        [("2025-01-01", Decimal("10000")), ("2025-02-01", Decimal("11000")), ("2025-03-01", Decimal("17600"))],
        [("2025-02-01", Decimal("5000"))],
    )
    assert result.value == Decimal("0.21")
    assert result.quality.status == "complete"


def test_twr_refuses_cashflow_without_boundary_valuation():
    result = twr_v1(
        [("2025-01-01", Decimal("10000")), ("2025-03-01", Decimal("17600"))],
        [("2025-02-01", Decimal("5000"))],
    )
    assert result.value is None
    assert result.quality.reasons == ("missing_cashflow_valuation",)


def test_twr_normalizes_timezone_offsets_before_matching_boundaries():
    result = twr_v1(
        [("2025-01-01T10:00:00+02:00", Decimal("100")), ("2025-02-01T08:00:00Z", Decimal("165"))],
        [("2025-01-01T08:00:00Z", Decimal("50"))],
    )
    assert result.value == Decimal("0.1")


def test_performance_reader_canonicalizes_multiple_times_on_one_business_day():
    conn = db()
    add_valuation(conn, "open", "2026-01-01T09:00:00+01:00", "100", version=1)
    add_valuation(conn, "close-early", "2026-01-02T10:00:00+01:00", "105", version=1)
    add_valuation(conn, "close-canonical", "2026-01-02T23:30:00+01:00", "110", version=2)

    result = build_portfolio_performance(
        conn,
        from_date="2026-01-01",
        to_date="2026-01-02",
        data_cutoff=NOW,
    )

    assert result["summary"]["closing_value"] == "110"
    assert result["summary"]["ttwror_cumulative"] == "0.1"
    assert [point["at"][:10] for point in result["time_series"]] == ["2026-01-01", "2026-01-02"]


def test_xirr_one_year_control_and_insufficient_structure():
    result = xirr_v1([("2025-01-01", Decimal("-1000")), ("2026-01-01", Decimal("1100"))])
    assert result.value == Decimal("0.100000000000")
    assert xirr_v1([("2025-01-01", Decimal("-1000"))]).quality.reasons == ("insufficient_cashflows",)


def test_fifo_v1_independent_control_case():
    result = fifo_v1(
        [
            {"activity_id": "buy", "kind": "buy", "quantity": Decimal("10"), "gross": Decimal("1000"), "fee": Decimal("5"), "tax": Decimal("0")},
            {"activity_id": "sell", "kind": "sell", "quantity": Decimal("4"), "gross": Decimal("480"), "fee": Decimal("2"), "tax": Decimal("3")},
        ]
    )
    assert result.remaining_quantity == Decimal("6")
    assert result.remaining_cost_basis == Decimal("603")
    assert result.realized_pnl == Decimal("73")
    assert result.disposals[0].allocated_cost_basis == Decimal("402")


def test_fifo_sale_consumes_multiple_lots_in_order():
    result = fifo_v1([
        {"activity_id": "b1", "kind": "buy", "quantity": Decimal("2"), "gross": Decimal("200"), "fee": Decimal("0"), "tax": Decimal("0")},
        {"activity_id": "b2", "kind": "buy", "quantity": Decimal("3"), "gross": Decimal("360"), "fee": Decimal("0"), "tax": Decimal("0")},
        {"activity_id": "s", "kind": "sell", "quantity": Decimal("4"), "gross": Decimal("600"), "fee": Decimal("4"), "tax": Decimal("0")},
    ])
    assert [(item.lot_activity_id, item.quantity) for item in result.disposals] == [("b1", Decimal("2")), ("b2", Decimal("2"))]
    assert result.remaining_quantity == Decimal("1")
    assert result.remaining_cost_basis == Decimal("120")
    assert result.realized_pnl == Decimal("156")


def test_reversal_neutralizes_original_without_overwrite():
    original = Activity("x", "buy", "a", "2025-01-01", "2025-01-01", "CHF", quantity=Decimal("1"), gross=Decimal("100"))
    reversal = Activity("r", "reversal", "a", "2025-01-02", "2025-01-02", "CHF", reversal_of="x")
    effective, reasons = effective_activities([original, reversal])
    assert effective == []
    assert reasons == ()


def test_unsupported_activity_is_excluded_and_explained():
    unsupported = Activity("x", "unsupported", "a", "2025-01-01", "2025-01-01", "CHF", gross=Decimal("100"), supported=False)
    effective, reasons = effective_activities([unsupported])
    assert effective == []
    assert reasons == ("unsupported_activity",)


def test_full_control_portfolio_contract_and_reproducibility():
    conn = db()
    add_control_portfolio(conn)
    first = build_portfolio_performance(
        conn,
        from_date="2025-01-01",
        to_date="2025-03-01",
        data_cutoff=NOW,
    )
    second = build_portfolio_performance(
        conn,
        from_date="2025-01-01",
        to_date="2025-03-01",
        data_cutoff=NOW,
    )
    assert first == second
    assert first["summary"] == {
        "opening_value": "10000",
        "closing_value": "17600",
        "net_external_cashflows": "5000",
        "fees": "7",
        "taxes": "3",
        "ttwror_cumulative": "0.21",
        "ttwror_annualized": None,
        "xirr_annualized": first["summary"]["xirr_annualized"],
        "investment_result": "2600",
        "twr": "0.21",
        "mwr": first["summary"]["mwr"],
        "realized_pnl": "73",
        "unrealized_pnl": "117",
        "remaining_cost_basis": "603",
    }
    assert first["summary"]["mwr"] is not None
    assert first["quality"]["twr"]["status"] == "complete"
    assert first["quality"]["cost_basis"]["status"] == "complete"
    assert first["external_cashflows"] == [{"at": "2025-02-01T00:00:00", "kind": "external_deposit", "amount": "5000"}]
    assert len(first["input_fingerprint"]) == 64


def test_total_value_account_has_performance_without_invented_lots():
    conn = db()
    add_valuation(conn, "v1", "2025-01-01", "10000")
    add_valuation(conn, "v2", "2025-12-31", "11000")
    result = build_portfolio_performance(conn, from_date="2025-01-01", to_date="2025-12-31", data_cutoff=NOW)
    assert result["summary"]["twr"] == "0.1"
    assert result["summary"]["remaining_cost_basis"] is None
    assert result["quality"]["cost_basis"]["status"] == "unavailable"


def test_opening_day_cashflow_is_already_in_day_end_anchor():
    conn = db()
    add_valuation(conn, "v1", "2026-06-30", "150000")
    add_valuation(conn, "v2", "2026-07-27", "152845")
    add_activity(conn, "opening-deposit", "external_deposit", "2026-06-30", net="900")

    result = build_portfolio_performance(
        conn,
        from_date="2026-06-30",
        to_date="2026-07-27",
        method="both",
        data_cutoff=NOW,
    )

    summary = cast(dict[str, Any], result["summary"])
    assert result["external_cashflows"] == []
    assert summary["net_external_cashflows"] == "0"
    assert summary["investment_result"] == "2845"
    assert summary["ttwror_cumulative"] == "0.018966666666666666666666667"
    assert summary["xirr_annualized"] is not None


def test_cashflow_after_opening_day_remains_in_period():
    conn = db()
    add_valuation(conn, "v1", "2026-06-30", "150000")
    add_valuation(conn, "flow-boundary", "2026-07-01", "150900")
    add_valuation(conn, "v2", "2026-07-27", "152845")
    add_activity(conn, "period-deposit", "external_deposit", "2026-07-01", net="900")

    result = build_portfolio_performance(
        conn,
        from_date="2026-06-30",
        to_date="2026-07-27",
        method="both",
        data_cutoff=NOW,
    )

    summary = cast(dict[str, Any], result["summary"])
    assert result["external_cashflows"] == [
        {"at": "2026-07-01", "kind": "external_deposit", "amount": "900"}
    ]
    assert summary["net_external_cashflows"] == "900"


def test_unclassified_period_activity_blocks_both_ttwror_and_personal_xirr():
    conn = db()
    add_valuation(conn, "v1", "2025-01-01", "10000")
    add_valuation(conn, "v2", "2025-12-31", "11000")
    add_activity(conn, "unknown-flow", "unclassified_cash_movement", "2025-06-01", net="500")

    result = build_portfolio_performance(
        conn,
        from_date="2025-01-01",
        to_date="2025-12-31",
        method="both",
        data_cutoff=NOW,
    )

    assert result["summary"]["ttwror_cumulative"] is None
    assert result["summary"]["xirr_annualized"] is None
    assert "cashflow_classification_incomplete" in result["quality"]["ttwror"]["reason_codes"]
    assert "cashflow_classification_incomplete" in result["quality"]["xirr"]["reason_codes"]


def test_missing_cashflow_history_evidence_blocks_returns_even_without_recorded_flows():
    conn = db()
    set_performance_cashflow_coverage(
        conn,
        account_id="a",
        coverage_from="2025-01-01",
        coverage_to="2025-12-31",
        status="unavailable",
        source="synthetic_missing_history",
        note="No complete external cashflow history",
        recorded_at="2026-07-23T12:01:00+00:00",
    )
    add_valuation(conn, "v1", "2025-01-01", "10000")
    add_valuation(conn, "v2", "2025-12-31", "11000")

    result = build_portfolio_performance(
        conn,
        from_date="2025-01-01",
        to_date="2025-12-31",
        method="both",
        data_cutoff=NOW,
    )

    assert result["summary"]["ttwror_cumulative"] is None
    assert result["summary"]["xirr_annualized"] is None
    assert result["quality"]["ttwror"]["reason_codes"] == [
        "external_cashflow_history_missing"
    ]
    assert "external_cashflow_history_missing" in result["quality"]["xirr"]["reason_codes"]


def test_historical_unsupported_activity_does_not_poison_later_period_cashflows():
    conn = db()
    add_valuation(conn, "v1", "2025-01-01", "10000")
    add_valuation(conn, "v2", "2025-12-31", "11000")
    add_activity(conn, "old-unknown", "unclassified_cash_movement", "2024-06-01", net="500")
    add_activity(conn, "period-dividend", "dividend", "2025-06-01", gross="100", net="100")

    result = build_portfolio_performance(
        conn,
        from_date="2025-01-01",
        to_date="2025-12-31",
        method="both",
        data_cutoff=NOW,
    )

    assert result["summary"]["ttwror_cumulative"] == "0.1"
    assert result["quality"]["ttwror"]["status"] == "complete"
    assert result["quality"]["xirr"]["status"] == "complete"


def test_existing_immutable_legacy_account_values_are_bound_read_only():
    conn = db()
    conn.executemany(
        """
        INSERT INTO account_value_snapshots(
          snapshot_id,account_id,valuation_date,total_value_chf,currency,source_type,
          quality_status,created_at,updated_at
        ) VALUES (?,?,?,?,?,?,?,?,NULL)
        """,
        [
            ("legacy-open", "a", "2025-01-01", "10000", "CHF", "manual_total_value", "ok", "2025-01-02T00:00:00Z"),
            ("legacy-close", "a", "2025-12-31", "11000", "CHF", "manual_total_value", "ok", "2026-01-01T00:00:00Z"),
        ],
    )
    result = build_portfolio_performance(conn, from_date="2025-01-01", to_date="2025-12-31", data_cutoff=NOW)
    assert result["summary"]["twr"] == "0.1"
    assert result["sources"] == ["legacy:manual_total_value"]


def test_missing_foreign_fx_never_becomes_one_or_zero_performance():
    conn = db()
    add_valuation(conn, "v1", "2025-01-01", "10000", currency="EUR", fx=None)
    add_valuation(conn, "v2", "2025-12-31", "11000", currency="EUR", fx=None)
    result = build_portfolio_performance(conn, from_date="2025-01-01", to_date="2025-12-31", data_cutoff=NOW)
    assert result["summary"]["twr"] is None
    assert "missing_fx" in result["quality"]["twr"]["reason_codes"]


def test_foreign_valuation_uses_stored_changing_fx_snapshots():
    conn = db()
    add_valuation(conn, "v1", "2025-01-01", "10000", currency="EUR", fx="0.9")
    add_valuation(conn, "v2", "2025-12-31", "11000", currency="EUR", fx="1.0")
    result = build_portfolio_performance(conn, from_date="2025-01-01", to_date="2025-12-31", data_cutoff=NOW)
    assert result["summary"]["opening_value"] == "9000.0"
    assert result["summary"]["closing_value"] == "11000.0"
    assert result["summary"]["twr"] == str(Decimal("11000") / Decimal("9000") - 1)


def test_dividend_withholding_and_net_credit_are_not_external_cashflow():
    conn = db()
    add_valuation(conn, "v1", "2025-01-01", "10000")
    add_valuation(conn, "v2", "2025-12-31", "10070")
    add_activity(conn, "div", "dividend", "2025-06-01", instrument_id="i", gross="100", tax="30", net="70")
    result = build_portfolio_performance(conn, from_date="2025-01-01", to_date="2025-12-31", data_cutoff=NOW)
    assert result["external_cashflows"] == []
    assert result["summary"]["taxes"] == "30"
    assert result["summary"]["net_external_cashflows"] == "0"


def test_missing_opening_or_closing_valuation_has_null_returns():
    conn = db()
    add_valuation(conn, "middle", "2025-06-01", "10000")
    result = build_portfolio_performance(conn, from_date="2025-01-01", to_date="2025-12-31", data_cutoff=NOW)
    assert result["summary"]["twr"] is None
    assert result["summary"]["mwr"] is None
    assert set(result["quality"]["twr"]["reason_codes"]) >= {"missing_opening_valuation", "missing_closing_valuation"}


def test_stale_snapshot_keeps_calculation_but_marks_metric_partial():
    conn = db()
    add_valuation(conn, "v1", "2025-01-01", "10000")
    add_valuation(conn, "v2", "2025-12-31", "11000", quality_status="partial", reasons=["stale_valuation"])
    result = build_portfolio_performance(conn, from_date="2025-01-01", to_date="2025-12-31", data_cutoff=NOW)
    assert result["summary"]["twr"] == "0.1"
    assert result["quality"]["twr"]["status"] == "partial"
    assert result["quality"]["twr"]["reason_codes"] == ["stale_valuation"]


def test_mwr_rejects_ambiguous_sign_pattern():
    result = xirr_v1([("2025-01-01", Decimal("-100")), ("2025-06-01", Decimal("250")), ("2026-01-01", Decimal("-160"))])
    assert result.value is None
    assert result.quality.reasons == ("mwr_multiple_solutions",)


def test_snapshot_versions_are_immutable_and_cutoff_reproducible():
    conn = db()
    add_valuation(conn, "old", "2025-01-01", "10000", version=1, captured_at="2025-01-02T00:00:00+00:00")
    add_valuation(conn, "new", "2025-01-01", "10100", version=2, captured_at="2025-02-01T00:00:00+00:00", supersedes="old")
    with pytest.raises(Exception, match="immutable"):
        conn.execute("UPDATE portfolio_valuation_snapshots SET value_original='1' WHERE snapshot_id='old'")
    add_valuation(conn, "close", "2025-12-31", "11000", captured_at="2025-01-03T00:00:00+00:00")
    early = build_portfolio_performance(conn, from_date="2025-01-01", to_date="2025-12-31", data_cutoff="2025-01-31T23:59:59+00:00")
    late = build_portfolio_performance(conn, from_date="2025-01-01", to_date="2025-12-31", data_cutoff=NOW)
    assert early["summary"]["opening_value"] == "10000"
    assert late["summary"]["opening_value"] == "10100"
    assert early["input_fingerprint"] != late["input_fingerprint"]


def test_realized_pnl_is_period_specific_while_remaining_basis_is_as_of_end():
    conn = db()
    add_valuation(conn, "open", "2025-01-01", "10000")
    add_valuation(conn, "close", "2025-12-31", "11000")
    add_valuation(conn, "instrument-close", "2025-12-31", "780", scope_kind="instrument", scope_id="i")
    add_activity(conn, "buy-old", "buy", "2024-01-01", instrument_id="i", quantity="10", gross="1000")
    add_activity(conn, "sell-old", "sell", "2024-06-01", instrument_id="i", quantity="2", gross="240")
    add_activity(conn, "sell-period", "sell", "2025-06-01", instrument_id="i", quantity="2", gross="260")
    result = build_portfolio_performance(conn, from_date="2025-01-01", to_date="2025-12-31", data_cutoff=NOW)
    assert result["summary"]["realized_pnl"] == "60"
    assert result["summary"]["remaining_cost_basis"] == "600"


def test_snapshot_constraints_reject_invalid_decimal_fx_and_quality_json():
    conn = db()
    with pytest.raises(Exception):
        add_valuation(conn, "negative", "2025-01-01", "-1")
    with pytest.raises(Exception):
        add_valuation(conn, "bad-fx", "2025-01-01", "1", currency="EUR", fx="0")
    with pytest.raises(Exception):
        conn.execute(
            """
            INSERT INTO portfolio_valuation_snapshots(
              snapshot_id,scope_kind,scope_id,account_id,value_original,currency,base_currency,
              fx_direction,valuation_at,source,captured_at,snapshot_version,reason_codes_json
            ) VALUES ('bad-json','account','a','a','1','CHF','CHF','original_to_base','2025-01-01','fixture',?,1,'{}')
            """,
            (NOW,),
        )


def test_api_is_read_only_fail_closed_and_openapi_documents_contract():
    conn = db()
    add_control_portfolio(conn)
    app = create_app(write_mode="test")
    app.dependency_overrides[get_db] = lambda: conn
    client = TestClient(app)
    before = conn.total_changes
    response = client.get("/api/portfolio/performance?from=2025-01-01&to=2025-03-01&method=both&data_cutoff=2026-07-23T12%3A00%3A00%2B00%3A00")
    assert response.status_code == 200
    assert response.json()["summary"]["twr"] == "0.21"
    assert [point["value"] for point in response.json()["ttwror_series"]] == ["0", "0.1", "0.21"]
    assert conn.total_changes == before
    invalid = client.get("/api/portfolio/performance?from=2025-03-01&to=2025-01-01")
    assert invalid.status_code == 400
    assert invalid.json()["detail"] == "Zeitraum: 'von' muss vor 'bis' liegen"
    invalid_method = client.get("/api/portfolio/performance?from=2025-01-01&to=2025-03-01&method=estimate")
    assert invalid_method.status_code == 400
    assert invalid_method.json()["detail"] == "Methode muss twr, mwr oder both sein"
    invalid_currency = client.get("/api/portfolio/performance?from=2025-01-01&to=2025-03-01&base_currency=GBP")
    assert invalid_currency.status_code == 400
    assert invalid_currency.json()["detail"] == "Basiswährung muss CHF, EUR oder USD sein"
    path = app.openapi()["paths"]["/api/portfolio/performance"]
    assert set(path) == {"get"}
    assert "Ungültiger Performance-Zeitraum" in path["get"]["responses"]["400"]["description"]
    response_schema = path["get"]["responses"]["200"]["content"]["application/json"]["schema"]
    assert response_schema["$ref"].endswith("/PortfolioPerformanceResponse")
    period_schema = app.openapi()["components"]["schemas"]["PerformancePeriod"]
    assert set(period_schema["properties"]) == {"from", "to"}
    wealth_schema = app.openapi()["paths"]["/api/portfolio/wealth-cockpit"]["get"][
        "responses"
    ]["200"]["content"]["application/json"]["schema"]
    assert wealth_schema["$ref"].endswith("/WealthCockpitResponse")
    before_coverage = conn.total_changes
    coverage_response = client.get("/api/portfolio/performance/coverage")
    assert coverage_response.status_code == 200
    assert conn.total_changes == before_coverage
    coverage_payload = coverage_response.json()
    assert coverage_payload["expected_scopes"] == ["postfinance", "truewealth", "crypto"]
    assert {row["scope"] for row in coverage_payload["rows"]} == {"portfolio", "postfinance", "truewealth", "crypto"}
    rows = {row["scope"]: row for row in coverage_payload["rows"]}
    assert rows["crypto"]["ttwror_status"] == "complete"
    assert rows["crypto"]["xirr_status"] == "complete"
    assert rows["crypto"]["valuation_dates"] == 3
    assert rows["crypto"]["scope_classification_status"] == "complete"
    assert rows["crypto"]["cashflow_coverage_status"] == "complete"
    assert rows["portfolio"]["ttwror_status"] == "unavailable"
    assert rows["portfolio"]["reason_codes"] == ["required_scope_coverage_incomplete"]
    assert "account_id" not in coverage_response.text


def test_coverage_is_unavailable_when_every_required_scope_is_unavailable():
    conn = connect_memory()
    apply_migrations(conn)

    coverage = build_performance_coverage(
        conn,
        from_date="2026-01-01",
        to_date="2026-08-02",
    )

    assert coverage["status"] == "unavailable"
    coverage_rows = coverage["rows"]
    assert isinstance(coverage_rows, list)
    assert all(row["ttwror_status"] == "unavailable" for row in coverage_rows)
    assert all(
        row["cashflow_coverage_status"] in {"unavailable", "partial"}
        for row in coverage_rows
    )


def test_coverage_evaluates_the_requested_period_not_lifetime_bounds():
    conn = db()
    add_valuation(conn, "old-open", "2024-01-01", "10000")
    add_valuation(conn, "old-close", "2024-12-31", "11000")

    coverage = build_performance_coverage(
        conn,
        from_date="2025-01-01",
        to_date="2025-12-31",
    )
    coverage_rows = coverage["rows"]
    assert isinstance(coverage_rows, list)
    rows = {row["scope"]: row for row in coverage_rows}

    assert rows["crypto"]["valuation_from"] == "2024-01-01"
    assert rows["crypto"]["valuation_to"] == "2024-12-31"
    assert rows["crypto"]["ttwror_status"] == "unavailable"
    assert set(rows["crypto"]["reason_codes"]) >= {
        "missing_opening_valuation",
        "missing_closing_valuation",
    }


def test_migration_is_additive_and_preserves_sprint_41_rows():
    conn = db()
    conn.execute(
        """
        INSERT INTO portfolio_policies(
          policy_id,version,is_active,effective_from,base_currency,benchmarks_json,
          restrictions_json,request_fingerprint,audit_id,created_at
        ) VALUES ('policy',1,1,'2025-01-01','CHF','[]','[]','request','audit',?)
        """,
        (NOW,),
    )
    apply_migrations(conn)
    assert conn.execute("SELECT policy_id FROM portfolio_policies").fetchone()[0] == "policy"
    assert conn.execute("SELECT MAX(version) FROM schema_migrations").fetchone()[0] == MIGRATION_VERSION
    assert conn.execute("SELECT COUNT(*) FROM portfolio_valuation_snapshots").fetchone()[0] == 0
