from __future__ import annotations

from decimal import Decimal

import pytest

import jarvis_finance.services.portfolio_performance as performance_module
from jarvis_finance.api.main import READ_ONLY_POST_PATHS, create_app
from jarvis_finance.api.schemas.performance_activation import PerformanceReclassificationPreviewResponse
from jarvis_finance.services.performance_activation import preview_performance_source_activation
from jarvis_finance.services.performance_hardening import (
    build_activation_setup_overview,
    build_postfinance_component_preview,
    confirm_truewealth_cashflow_period,
    preview_performance_reclassification,
    preview_truewealth_cashflow_period,
)
from jarvis_finance.services.performance_scope import set_performance_scope_classification
from jarvis_finance.services.portfolio_analytics import store_valuation_snapshot
from jarvis_finance.services.postfinance_service import store_postfinance_component_valuations
from jarvis_finance.services.portfolio_performance import (
    _load_valuations,
    build_portfolio_performance,
    load_scope_activities,
)
from jarvis_finance.storage.database import connect_memory
from jarvis_finance.storage.migrations import MIGRATION_VERSION, apply_migrations

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


def database():
    conn = connect_memory()
    apply_migrations(conn)
    conn.execute(
        "INSERT INTO platforms(platform_id,name,platform_type,created_at) VALUES('pf','PostFinance','broker',?)",
        (NOW,),
    )
    conn.execute(
        "INSERT INTO platforms(platform_id,name,platform_type,created_at) VALUES('tw','True Wealth','broker',?)",
        (NOW,),
    )
    for account_id, platform_id, name, account_type, role in (
        ("pf-depot", "pf", "Depot", "brokerage", "postfinance_etrading_depot"),
        ("pf-cash", "pf", "Settlement Cash", "cash", "postfinance_etrading_cash"),
        ("pf-control", "pf", "E-Finance", "cash", None),
        ("tw-total", "tw", "True Wealth", "brokerage", "canonical_truewealth_total_value"),
    ):
        conn.execute(
            """INSERT INTO accounts(account_id,platform_id,account_name,account_type,currency,
                       performance_included,is_active,created_at)
               VALUES(?,?,?,?, 'CHF',0,1,?)""",
            (account_id, platform_id, name, account_type, NOW),
        )
        if role:
            set_performance_scope_classification(
                conn,
                account_id=account_id,
                included=True,
                classification_role=role,
                source="synthetic",
                note="synthetic scope",
                classified_at=NOW,
            )
    for role, account_id in (
        ("etrading_depot", "pf-depot"),
        ("etrading_cash", "pf-cash"),
        ("efinance", "pf-control"),
    ):
        conn.execute(
            "INSERT INTO postfinance_account_roles(role,account_id,created_at) VALUES(?,?,?)",
            (role, account_id, NOW),
        )
    conn.execute(
        """INSERT INTO audit_log(audit_id,timestamp,source,action,entity_type,entity_id,
                   old_values_json,new_values_json,created_by,created_at)
           VALUES('pf-audit',?,'synthetic','confirm','batch','pf-batch','{}','{}','user',?)""",
        (NOW, NOW),
    )
    conn.execute(
        """INSERT INTO postfinance_import_batches(batch_id,bundle_sha256,zip_sha256,overview_sha256,
                   parser_id,parser_version,db_revision,archive_reference,snapshot_at,status,audit_id,
                   confirmed_at,confirmed_by)
           VALUES('pf-batch',?,?,?,?,?,?,?,'2026-07-27T09:14:30','confirmed','pf-audit',?,'user')""",
        ("a" * 64, "b" * 64, "c" * 64, "synthetic", "1", "rev", "private", NOW),
    )
    conn.execute(
        """INSERT INTO postfinance_snapshots(snapshot_id,batch_id,account_id,snapshot_at,total_chf,
                   securities_chf,cash_chf,stocks_chf,etfs_chf,component_total_chf,difference_chf,
                   tolerance_chf,position_count,cash_count,open_orders,reconciliation_status,created_at)
           VALUES('pf-source','pf-batch','pf-depot','2026-07-27T09:14:30','321149.23',
                  '293465.16','27684.07','100000','193465.16','321149.23','0','0.05',22,2,0,'matched',?)""",
        (NOW,),
    )
    # Historical combined daily value on the depot account. It must not satisfy the two-account scope.
    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,source_reference,quality_status,reason_codes_json)
           VALUES('bad-combined','account','pf-depot','pf-depot','322299.2170831154','CHF','CHF','1',
                  'original_to_base','2026-07-27','daily_market_fx_v4','2026-07-27T21:36:21Z',1,
                  'daily-run','complete','[]')"""
    )
    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,source_reference,quality_status,reason_codes_json)
           VALUES('daily-instruments','instrument','all','pf-depot','295445.7570831154','CHF','CHF','1',
                  'original_to_base','2026-07-27','daily_market_fx_v4','2026-07-27T21:36:21Z',1,
                  'daily-run','complete','[]')"""
    )
    for snapshot_id, day, value, source in (
        ("tw-open", "2026-06-30", "150000", "manual_total_value"),
        ("tw-close", "2026-07-27", "152845", "truewealth_official_import"),
    ):
        conn.execute(
            """INSERT INTO account_value_snapshots(snapshot_id,account_id,valuation_date,total_value_chf,
                       currency,source_type,quality_status,created_at,is_active)
               VALUES(?, 'tw-total', ?, ?, 'CHF', ?, 'ok', ?, 1)""",
            (snapshot_id, day, value, source, NOW),
        )
    conn.commit()
    return conn


def no_flow_request(conn, *, confirmation_id: str = "tw-no-flow") -> dict:
    attestation = "Für den Zeitraum gab es keine externen Ein- oder Auszahlungen."
    preview = preview_truewealth_cashflow_period(
        conn,
        mode="no_external_flows",
        coverage_from="2026-06-30",
        coverage_to="2026-07-27",
        entries=[],
        csv_text=None,
        attestation=attestation,
    )
    return {
        "mode": "no_external_flows",
        "coverage_from": "2026-06-30",
        "coverage_to": "2026-07-27",
        "entries": [],
        "csv_text": None,
        "attestation": attestation,
        "preview_id": preview["preview_id"],
        "input_fingerprint": preview["input_fingerprint"],
        "confirmation_id": confirmation_id,
        "confirm": True,
    }


def add_transaction(
    conn,
    transaction_id: str,
    account_id: str,
    transaction_type: str,
    activity_kind: str,
    amount: str,
    *,
    group_id: str | None = None,
):
    conn.execute(
        """INSERT INTO transactions(transaction_id,transaction_type,activity_kind,account_id,trade_date,
                   event_timestamp,gross_amount_original,net_amount_original,currency_original,source_type,
                   source_id,is_confirmed,quality_status,created_at,internal_transfer_group_id)
           VALUES(?,?,?,?, '2026-07-01','2026-07-01T10:00:00+02:00',?,?,'CHF','synthetic',?,1,'ok',?,?)""",
        (transaction_id, transaction_type, activity_kind, account_id, amount, amount, transaction_id, NOW, group_id),
    )


def test_postfinance_components_reconcile_once_and_diagnose_both_difference_drivers():
    conn = database()
    before = conn.total_changes
    preview = build_postfinance_component_preview(conn, day="2026-07-27")
    assert conn.total_changes == before
    assert preview["components"] == [
        {"role": "postfinance_etrading_depot", "value_chf": "293465.16"},
        {"role": "postfinance_etrading_cash", "value_chf": "27684.07"},
    ]
    assert sum(Decimal(row["value_chf"]) for row in preview["components"]) == Decimal("321149.23")
    assert preview["source_total_chf"] == "321149.23"
    assert preview["source_total_is_control_only"] is True
    assert preview["complete_account_coverage"] is True
    assert preview["daily_market_total_chf"] == "322299.22"
    assert preview["difference_chf"] == "1149.99"
    assert preview["securities_difference_chf"] == "1980.60"
    assert preview["cash_difference_chf"] == "-830.61"
    assert preview["diagnosis"] == "different_market_close_prices_fx_and_stale_depot_cash"
    assert preview["canonical_selection"]["official_import"] == "signed_source_reconciliation_and_activation_anchor"
    assert preview["canonical_selection"]["daily_market"] == "performance_day_close_after_complete_component_materialization"


def test_postfinance_confirm_writer_materializes_exactly_two_idempotent_components():
    conn = database()
    assert store_valuation_snapshot(
        conn,
        id_prefix="daily",
        run_id="daily-equal-cash",
        scope_kind="account",
        scope_id="pf-cash",
        account_id="pf-cash",
        value_original="27684.07",
        currency="CHF",
        fx_rate_to_base="1",
        valuation_at="2026-07-27T21:00:00Z",
        source="daily_market_fx_v4",
        captured_at="2026-07-27T21:00:00Z",
        quality_status="complete",
        reason_codes=[],
    ) is True
    stored = store_postfinance_component_valuations(
        conn,
        depot_account_id="pf-depot",
        cash_account_id="pf-cash",
        securities_value_chf=Decimal("293465.16"),
        cash_value_chf=Decimal("27684.07"),
        valuation_at="2026-07-27T09:14:30",
        batch_id="pf-batch",
        captured_at=NOW,
    )
    rows = conn.execute(
        """SELECT account_id,value_original,supersedes_snapshot_id FROM portfolio_valuation_snapshots
           WHERE source='postfinance_official_import_component_v1' ORDER BY account_id"""
    ).fetchall()
    assert stored == 2
    assert len(rows) == 2
    assert {str(row["account_id"]) for row in rows} == {"pf-depot", "pf-cash"}
    cash_component = next(row for row in rows if row["account_id"] == "pf-cash")
    assert cash_component["supersedes_snapshot_id"] is not None
    assert sum((Decimal(str(row["value_original"])) for row in rows), Decimal("0")) == Decimal("321149.23")
    assert store_valuation_snapshot(
        conn,
        id_prefix="daily",
        run_id="daily-competing-observation",
        scope_kind="account",
        scope_id="pf-cash",
        account_id="pf-cash",
        value_original="28000",
        currency="CHF",
        fx_rate_to_base="1",
        valuation_at="2026-07-27T22:00:00Z",
        source="daily_market_fx_v4",
        captured_at="2026-07-27T22:00:00Z",
        quality_status="complete",
        reason_codes=[],
    ) is True
    before_replay = conn.execute("SELECT COUNT(*) FROM portfolio_valuation_snapshots").fetchone()[0]
    assert store_postfinance_component_valuations(
        conn,
        depot_account_id="pf-depot",
        cash_account_id="pf-cash",
        securities_value_chf=Decimal("293465.16"),
        cash_value_chf=Decimal("27684.07"),
        valuation_at="2026-07-27T09:14:30",
        batch_id="pf-batch",
        captured_at=NOW,
    ) == 0
    assert conn.execute("SELECT COUNT(*) FROM portfolio_valuation_snapshots").fetchone()[0] == before_replay
    with pytest.raises(ValueError, match="conflicts"):
        store_valuation_snapshot(
            conn,
            id_prefix="daily",
            run_id="daily-competing-observation",
            scope_kind="account",
            scope_id="pf-cash",
            account_id="pf-cash",
            value_original="28001",
            currency="CHF",
            fx_rate_to_base="1",
            valuation_at="2026-07-27T22:00:00Z",
            source="daily_market_fx_v4",
            captured_at="2026-07-27T22:00:00Z",
            quality_status="complete",
            reason_codes=[],
        )


def test_combined_legacy_postfinance_snapshot_is_not_complete_but_official_components_form_one_day():
    conn = database()
    result = build_portfolio_performance(
        conn,
        from_date="2026-07-27",
        to_date="2026-07-28",
        selected_account_ids=["pf-depot", "pf-cash"],
        data_cutoff="2099-01-01T00:00:00+00:00",
    )
    assert result["summary"]["opening_value"] == "321149.23"
    assert result["summary"]["closing_value"] is None
    assert "missing_closing_valuation" in result["quality"]["overall"]["reason_codes"]
    assert result["time_series"] == [{"at": "2026-07-27", "value": "321149.23"}]


def test_reclassification_derives_income_expense_trade_and_scope_boundary_without_writes():
    conn = database()
    add_transaction(conn, "dividend", "pf-depot", "dividend", "external_cashflow", "100")
    add_transaction(conn, "interest", "pf-cash", "interest", "external_cashflow", "5")
    add_transaction(conn, "fee", "pf-depot", "fee", "external_cashflow", "-7")
    add_transaction(conn, "buy", "pf-depot", "buy", "trade", "-1000")
    add_transaction(conn, "split", "pf-depot", "split", "corporate_action", "0")
    add_transaction(conn, "inside-a", "pf-depot", "transfer", "internal_transfer", "-50", group_id="inside")
    add_transaction(conn, "inside-b", "pf-cash", "transfer", "internal_transfer", "50", group_id="inside")
    add_transaction(conn, "outside-a", "pf-control", "transfer", "internal_transfer", "-500", group_id="outside")
    add_transaction(conn, "outside-b", "pf-cash", "transfer", "internal_transfer", "500", group_id="outside")
    conn.commit()
    before = conn.total_changes
    preview = preview_performance_reclassification(
        conn, source="postfinance", period_from="2026-01-01", period_to="2026-12-31"
    )
    assert conn.total_changes == before
    classes = {(row["transaction_type"], row["performance_class"], row["scope_boundary"]) for row in preview["rows"]}
    assert ("dividend", "internal_income", "none") in classes
    assert ("interest", "internal_income", "none") in classes
    assert ("fee", "internal_expense", "none") in classes
    assert ("buy", "portfolio_trade", "none") in classes
    assert ("split", "corporate_action", "none") in classes
    assert ("transfer", "internal_transfer", "inside_scope") in classes
    assert ("transfer", "external_deposit", "crosses_scope") in classes
    assert preview["unclear_count"] == 0
    assert preview["can_activate"] is True
    assert len(preview["input_fingerprint"]) == 64


def test_unpaired_transfer_is_unclear_and_blocks_activation():
    conn = database()
    add_transaction(conn, "unpaired", "pf-cash", "transfer", "internal_transfer", "10", group_id="missing-leg")
    conn.commit()
    preview = preview_performance_reclassification(
        conn, source="postfinance", period_from="2026-01-01", period_to="2026-12-31"
    )
    assert preview["unclear_count"] == 1
    assert preview["can_activate"] is False
    activities = load_scope_activities(
        conn,
        account_ids=["pf-depot", "pf-cash"],
        to_date="2026-12-31",
        data_cutoff="2099-01-01T00:00:00+00:00",
    )
    unpaired = next(item for item in activities if item.activity_id == "unpaired")
    assert unpaired.kind == "internal_transfer"
    assert unpaired.supported is False


def test_truewealth_nullflow_preview_confirm_is_period_bound_idempotent_and_unlocks_returns():
    conn = database()
    before = conn.total_changes
    preview = preview_truewealth_cashflow_period(
        conn,
        mode="no_external_flows",
        coverage_from="2026-06-30",
        coverage_to="2026-07-27",
        entries=[],
        csv_text=None,
        attestation="Für den Zeitraum gab es keine externen Ein- oder Auszahlungen.",
    )
    assert conn.total_changes == before
    request = {
        "mode": "no_external_flows",
        "coverage_from": "2026-06-30",
        "coverage_to": "2026-07-27",
        "entries": [],
        "csv_text": None,
        "attestation": "Für den Zeitraum gab es keine externen Ein- oder Auszahlungen.",
        "preview_id": preview["preview_id"],
        "input_fingerprint": preview["input_fingerprint"],
        "confirmation_id": "tw-nullflow-2026-06-30-2026-07-27",
        "confirm": True,
    }
    first = confirm_truewealth_cashflow_period(conn, request)
    second = confirm_truewealth_cashflow_period(conn, request)
    assert first["idempotent"] is False and second["idempotent"] is True
    assert conn.execute("SELECT COUNT(*) FROM transactions WHERE account_id='tw-total'").fetchone()[0] == 0
    assert conn.execute("SELECT COUNT(*) FROM account_value_snapshots WHERE account_id='tw-total'").fetchone()[0] == 2
    assert conn.execute("SELECT COUNT(*) FROM portfolio_valuation_snapshots WHERE account_id='tw-total'").fetchone()[0] == 0
    assert conn.execute("SELECT COUNT(*) FROM audit_log WHERE action='truewealth_cashflow_period_confirmed'").fetchone()[0] == 1
    result = build_portfolio_performance(
        conn,
        from_date="2026-06-30",
        to_date="2026-07-27",
        selected_account_ids=["tw-total"],
        data_cutoff="2099-01-01T00:00:00+00:00",
    )
    assert result["quality"]["ttwror"]["status"] == "complete"
    assert result["quality"]["xirr"]["status"] == "complete"


def test_truewealth_manual_and_csv_cashflows_share_validation_and_confirm_once():
    conn = database()
    csv_text = "date,direction,amount,currency,amount_chf,fx_source,evidence_reference\n2026-07-10,deposit,1000,CHF,1000,identity,statement-1\n"
    preview = preview_truewealth_cashflow_period(
        conn,
        mode="external_cashflows",
        coverage_from="2026-06-30",
        coverage_to="2026-07-27",
        entries=[],
        csv_text=csv_text,
        attestation="Alle externen Ein- und Auszahlungen sind vollständig.",
    )
    assert preview["entry_count"] == 1
    request = {
        "mode": "external_cashflows",
        "coverage_from": "2026-06-30",
        "coverage_to": "2026-07-27",
        "entries": [],
        "csv_text": csv_text,
        "attestation": "Alle externen Ein- und Auszahlungen sind vollständig.",
        "preview_id": preview["preview_id"],
        "input_fingerprint": preview["input_fingerprint"],
        "confirmation_id": "tw-flow-one",
        "confirm": True,
    }
    confirm_truewealth_cashflow_period(conn, request)
    row = conn.execute(
        "SELECT transaction_type,activity_kind,net_amount_original,net_amount_chf FROM transactions WHERE account_id='tw-total'"
    ).fetchone()
    assert tuple(row) == ("external_deposit", "external_deposit", "1000", "1000")

    replay_preview = preview_truewealth_cashflow_period(
        conn,
        mode="external_cashflows",
        coverage_from="2026-06-30",
        coverage_to="2026-07-27",
        entries=[],
        csv_text=csv_text,
        attestation="Alle externen Ein- und Auszahlungen sind vollständig.",
    )
    assert replay_preview["duplicate_count"] == 1
    assert replay_preview["expected_changes"]["cashflow_transactions"] == 0
    replay = confirm_truewealth_cashflow_period(
        conn,
        {
            **request,
            "preview_id": replay_preview["preview_id"],
            "input_fingerprint": replay_preview["input_fingerprint"],
            "confirmation_id": "tw-flow-two",
        },
    )
    assert replay["written_cashflows"] == 0
    assert conn.execute("SELECT COUNT(*) FROM transactions WHERE account_id='tw-total'").fetchone()[0] == 1

    changed = {**request, "coverage_to": "2026-07-28"}
    with pytest.raises(ValueError, match="different payload|stale"):
        confirm_truewealth_cashflow_period(conn, changed)


def test_postfinance_official_components_are_read_only_anchor_and_bad_combined_row_is_rejected():
    conn = database()
    values = _load_valuations(
        conn,
        account_ids=["pf-depot", "pf-cash"],
        from_date="2026-07-27",
        to_date="2026-07-27",
        data_cutoff="2099-01-01T00:00:00+00:00",
        base_currency="CHF",
    )
    account_values = [value for value in values if value.scope_kind == "account"]
    assert {value.account_id for value in account_values} == {"pf-depot", "pf-cash"}
    assert sum((value.value_base or Decimal("0") for value in account_values), Decimal("0")) == Decimal("321149.23")

    for trigger in conn.execute("SELECT name FROM sqlite_master WHERE type='trigger' AND tbl_name='postfinance_snapshots'").fetchall():
        conn.execute(f'DROP TRIGGER "{trigger[0]}"')
    conn.execute("DELETE FROM postfinance_snapshots")
    values_without_official = _load_valuations(
        conn,
        account_ids=["pf-depot", "pf-cash"],
        from_date="2026-07-27",
        to_date="2026-07-27",
        data_cutoff="2099-01-01T00:00:00+00:00",
        base_currency="CHF",
    )
    assert [value for value in values_without_official if value.scope_kind == "account"] == []


def test_partial_daily_postfinance_component_set_cannot_override_complete_official_pair():
    conn = database()
    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,
             source_reference,quality_status,reason_codes_json)
           VALUES('bad-partial-cash','account','pf-cash','pf-cash','100','CHF','CHF','1',
                  'original_to_base','2026-07-27','daily_market_fx_v4','2026-07-27T21:36:21Z',1,
                  'daily-run','partial','[\"stale_valuation\"]')"""
    )
    values = _load_valuations(
        conn,
        account_ids=["pf-depot", "pf-cash"],
        from_date="2026-07-27",
        to_date="2026-07-27",
        data_cutoff="2099-01-01T00:00:00+00:00",
        base_currency="CHF",
    )
    account_values = [value for value in values if value.scope_kind == "account"]
    assert len(account_values) == 2
    assert all(value.source.startswith("postfinance_official_import_component_v1") for value in account_values)
    assert sum((value.value_base or Decimal("0") for value in account_values), Decimal("0")) == Decimal("321149.23")


def test_latest_same_day_official_postfinance_snapshot_is_selected_as_one_pair():
    conn = database()
    assert store_postfinance_component_valuations(
        conn,
        depot_account_id="pf-depot",
        cash_account_id="pf-cash",
        securities_value_chf=Decimal("293465.16"),
        cash_value_chf=Decimal("27684.07"),
        valuation_at="2026-07-27T09:14:30",
        batch_id="pf-batch",
        captured_at=NOW,
    ) == 2
    later = "2026-07-27T13:00:00+00:00"
    conn.execute(
        """INSERT INTO audit_log(audit_id,timestamp,source,action,entity_type,entity_id,
                   old_values_json,new_values_json,created_by,created_at)
           VALUES('pf-audit-later',?,'synthetic','confirm','batch','pf-batch-later','{}','{}','user',?)""",
        (later, later),
    )
    conn.execute(
        """INSERT INTO postfinance_import_batches(batch_id,bundle_sha256,zip_sha256,overview_sha256,
                   parser_id,parser_version,db_revision,archive_reference,snapshot_at,status,audit_id,
                   confirmed_at,confirmed_by)
           VALUES('pf-batch-later',?,?,?,?,?,?,?,'2026-07-27T11:14:30','confirmed',
                  'pf-audit-later',?,'user')""",
        ("d" * 64, "e" * 64, "f" * 64, "synthetic", "1", "rev", "private", later),
    )
    conn.execute(
        """INSERT INTO postfinance_snapshots(snapshot_id,batch_id,account_id,snapshot_at,total_chf,
                   securities_chf,cash_chf,stocks_chf,etfs_chf,component_total_chf,difference_chf,
                   tolerance_chf,position_count,cash_count,open_orders,reconciliation_status,created_at)
           VALUES('pf-source-later','pf-batch-later','pf-depot','2026-07-27T11:14:30','330000',
                  '300000','30000','100000','200000','330000','0','0.05',22,2,0,'matched',?)""",
        (later,),
    )
    values = _load_valuations(
        conn,
        account_ids=["pf-depot", "pf-cash"],
        from_date="2026-07-27",
        to_date="2026-07-27",
        data_cutoff="2099-01-01T00:00:00+00:00",
        base_currency="CHF",
    )
    selected = [value for value in values if value.scope_kind == "account"]
    assert len(selected) == 2
    assert {value.source for value in selected} == {
        "postfinance_official_import_component_v1:pf-source-later"
    }
    assert sum((value.value_base or Decimal("0") for value in selected), Decimal("0")) == Decimal("330000")


def test_partial_postfinance_components_from_different_runs_are_not_combined():
    conn = database()
    assert store_postfinance_component_valuations(
        conn,
        depot_account_id="pf-depot",
        cash_account_id="pf-cash",
        securities_value_chf=Decimal("293465.16"),
        cash_value_chf=Decimal("27684.07"),
        valuation_at="2026-07-27T09:14:30",
        batch_id="pf-batch",
        captured_at=NOW,
    ) == 2
    for account_id, run_id, value, captured_at in (
        ("pf-depot", "daily-depot-only", "300000", "2026-07-27T21:00:00Z"),
        ("pf-cash", "daily-cash-only", "30000", "2026-07-27T22:00:00Z"),
    ):
        assert store_valuation_snapshot(
            conn,
            id_prefix="daily",
            run_id=run_id,
            scope_kind="account",
            scope_id=account_id,
            account_id=account_id,
            value_original=value,
            currency="CHF",
            fx_rate_to_base="1",
            valuation_at=captured_at,
            source="daily_market_fx_v4",
            captured_at=captured_at,
            quality_status="complete",
            reason_codes=[],
        ) is True
    values = _load_valuations(
        conn,
        account_ids=["pf-depot", "pf-cash"],
        from_date="2026-07-27",
        to_date="2026-07-27",
        data_cutoff="2026-08-03T23:59:59+00:00",
        base_currency="CHF",
    )
    account_values = [item for item in values if item.scope_kind == "account"]
    assert len(account_values) == 2
    assert sum((item.value_base or Decimal("0") for item in account_values), Decimal("0")) == Decimal("321149.23")
    assert len({item.source for item in account_values}) == 1
    assert account_values[0].source.startswith("postfinance_official_import_component_v1")


def test_postfinance_activation_requires_both_component_roles():
    conn = database()
    set_performance_scope_classification(
        conn,
        account_id="pf-cash",
        included=False,
        classification_role="not_in_investment_performance_scope",
        source="synthetic-review",
        note="simulate missing cash role",
        classified_at=NOW,
    )
    preview = preview_performance_source_activation(
        conn,
        source="postfinance",
        tracking_mode="transactions",
        period_from="2026-07-01",
        period_to="2026-07-27",
        evidence_reference="complete source bundle",
        attest_complete_external_flows=True,
    )
    assert preview["can_confirm"] is False
    assert "postfinance_component_scope_incomplete" in preview["blockers"]


def test_missing_scope_reclassification_preview_matches_declared_response_schema():
    preview = preview_performance_reclassification(
        database(),
        source="crypto",
        period_from="2026-07-01",
        period_to="2026-07-27",
    )
    response = PerformanceReclassificationPreviewResponse.model_validate(preview)
    assert response.activity_count == 0
    assert response.can_activate is False
    assert response.reason_codes == ["performance_scope_missing"]


def test_setup_cannot_report_ready_when_reclassification_is_unclear(monkeypatch):
    conn = database()
    add_transaction(
        conn,
        "tw-unpaired",
        "tw-total",
        "transfer",
        "internal_transfer",
        "100",
        group_id="tw-missing-leg",
    )
    conn.commit()

    rows = []
    for source in ("postfinance", "truewealth", "crypto"):
        rows.append(
            {
                "scope": source,
                "scope_classification_status": "complete",
                "cashflow_coverage_status": "complete",
                "ttwror_status": "complete",
                "xirr_status": "complete",
                "valuation_dates": 2,
                "position_dates": 2,
                "reason_codes": [],
                "reliable_from": "2026-06-30",
                "valuation_from": "2026-06-30",
            }
        )
    monkeypatch.setattr(performance_module, "build_performance_coverage", lambda _conn: {"rows": rows})
    overview = build_activation_setup_overview(conn)
    truewealth = next(item for item in overview["sources"] if item["source"] == "truewealth")
    assert truewealth["diagnostics"]["reclassification_status"] == "review_required"
    assert truewealth["status"] == "review_inputs"


def test_truewealth_coverage_extensions_preserve_prior_complete_period_and_reject_gaps():
    conn = database()
    first = no_flow_request(conn)
    confirm_truewealth_cashflow_period(conn, first)
    narrower = preview_truewealth_cashflow_period(
        conn,
        mode="no_external_flows",
        coverage_from="2026-07-20",
        coverage_to="2026-07-27",
        entries=[],
        csv_text=None,
        attestation="Für diesen Teilzeitraum gab es keine externen Ein- oder Auszahlungen.",
    )
    assert narrower["effective_coverage_from"] == "2026-06-30"
    assert narrower["effective_coverage_to"] == "2026-07-27"
    confirm_truewealth_cashflow_period(
        conn,
        {
            "mode": "no_external_flows",
            "coverage_from": "2026-07-20",
            "coverage_to": "2026-07-27",
            "entries": [],
            "csv_text": None,
            "attestation": "Für diesen Teilzeitraum gab es keine externen Ein- oder Auszahlungen.",
            "preview_id": narrower["preview_id"],
            "input_fingerprint": narrower["input_fingerprint"],
            "confirmation_id": "tw-no-flow-narrower",
            "confirm": True,
        },
    )
    coverage = conn.execute(
        "SELECT coverage_from,coverage_to FROM performance_cashflow_coverage WHERE account_id='tw-total'"
    ).fetchone()
    assert tuple(coverage) == ("2026-06-30", "2026-07-27")
    with pytest.raises(ValueError, match="gap"):
        preview_truewealth_cashflow_period(
            conn,
            mode="no_external_flows",
            coverage_from="2026-08-10",
            coverage_to="2026-08-20",
            entries=[],
            csv_text=None,
            attestation="Für den späteren Zeitraum gab es keine externen Ein- oder Auszahlungen.",
        )


def test_schema_stays_49():
    assert MIGRATION_VERSION == 51


def test_non_postfinance_source_variants_remain_single_canonical_account_value():
    conn = database()
    for snapshot_id, source, version, value in (
        ("tw-canonical-old", "source_a", 1, "151000"),
        ("tw-canonical-new", "source_b", 2, "152845"),
    ):
        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,
                 source_reference,quality_status,reason_codes_json)
               VALUES(?, 'account','tw-total','tw-total',?,'CHF','CHF','1','original_to_base',
                      '2026-07-27',?, ?, ?, ?, 'complete','[]')""",
            (snapshot_id, value, source, f"2026-07-27T{version:02d}:00:00Z", version, snapshot_id),
        )
    values = _load_valuations(
        conn,
        account_ids=["tw-total"],
        from_date="2026-07-27",
        to_date="2026-07-27",
        data_cutoff="2099-01-01T00:00:00+00:00",
        base_currency="CHF",
    )
    account_values = [value for value in values if value.scope_kind == "account"]
    assert len(account_values) == 1
    assert account_values[0].snapshot_id == "tw-canonical-new"
    assert account_values[0].value_base == Decimal("152845")


def test_sprint20c_api_contract_exposes_safe_preview_and_guarded_confirm_paths():
    paths = create_app().openapi()["paths"]
    assert "/api/portfolio/performance/setup" in paths
    assert "/api/portfolio/performance/postfinance-components" in paths
    assert "/api/portfolio/performance/reclassification/preview" in paths
    assert "/api/portfolio/performance/truewealth-cashflows/preview" in paths
    assert "/api/portfolio/performance/truewealth-cashflows/confirm" in paths
    assert "/api/portfolio/performance/reclassification/preview" in READ_ONLY_POST_PATHS
    assert "/api/portfolio/performance/truewealth-cashflows/preview" in READ_ONLY_POST_PATHS
    assert "/api/portfolio/performance/truewealth-cashflows/confirm" not in READ_ONLY_POST_PATHS
