from __future__ import annotations

import json
from sqlite3 import Connection

import pytest
from fastapi import HTTPException
from fastapi.testclient import TestClient

from jarvis_finance.api.dependencies import get_db
from jarvis_finance.api.main import create_app
from jarvis_finance.services.household_financials import get_household_financial_summary
from jarvis_finance.services.household_import import (
    _household_review_data_version,
    _household_transaction_data_version,
    _review_item_token,
    get_household_review,
)
from jarvis_finance.services.household_review_corrections import (
    _candidate_baseline,
    _transaction_baseline,
    confirm_household_item_action,
    confirm_household_transaction_category,
    confirm_household_transaction_duplicate,
    get_household_review_item,
    get_household_transaction_detail,
    list_closed_household_duplicates,
    list_eligible_confirmed_duplicate_originals,
    list_eligible_duplicate_originals,
    preview_household_item_action,
    preview_household_transaction_category,
    preview_household_transaction_duplicate,
    transaction_token,
)
from jarvis_finance.storage.database import connect_memory
from jarvis_finance.storage.migrations import apply_migrations, get_schema_version


def database() -> Connection:
    conn = connect_memory()
    apply_migrations(conn)
    conn.execute(
        "INSERT INTO platforms(platform_id,name,platform_type,created_at) "
        "VALUES ('platform','Synthetic','bank','2026-01-01')"
    )
    for account_id, account_type in (("bank", "checking"), ("card", "credit_card")):
        conn.execute(
            """INSERT INTO accounts(
                 account_id,platform_id,account_name,account_type,currency,portfolio_bucket,created_at)
               VALUES (?, 'platform', ?, ?, 'CHF', ?, '2026-01-01')""",
            (
                f"account-{account_id}",
                f"Synthetic {account_id}",
                "credit_card" if account_id == "card" else "cash",
                "liability" if account_id == "card" else "cash",
            ),
        )
        conn.execute(
            """INSERT INTO budget_accounts(
                 budget_account_id,linked_account_id,name,account_type,currency,created_at)
               VALUES (?, ?, ?, ?, 'CHF', '2026-01-01')""",
            (account_id, f"account-{account_id}", f"Synthetic {account_id}", account_type),
        )
    for category_id, name, category_type in (
        ("expense-old", "Old expense", "expense"),
        ("expense-new", "New expense", "expense"),
        ("income", "Income", "income"),
    ):
        conn.execute(
            """INSERT INTO budget_categories(
                 category_id,name,category_type,is_active,created_at)
               VALUES (?,?,?,1,'2026-01-01')""",
            (category_id, name, category_type),
        )
    conn.execute(
        """INSERT INTO household_account_source_mappings(
             mapping_id,contract_version,source_type,source_reference_hash,budget_account_id,
             canonical_account_id,reference_hint,is_active,created_at,updated_at)
           VALUES ('card-map','household_import_v1','visa_credit_card',?,
                   'card','account-card','•••• 0001',1,'2026-01-01','2026-01-01')""",
        ("1" * 64,),
    )
    conn.commit()
    return conn


def add_candidate(
    conn: Connection,
    candidate_id: str,
    *,
    classification: str = "possible_logical_duplicate",
    source_type: str = "akb_bank",
    account_id: str = "bank",
    amount: str = "-25.00",
    tx_date: str = "2026-07-20",
    currency: str = "CHF",
) -> None:
    conn.execute(
        """INSERT INTO budget_transaction_candidates(
             transaction_candidate_id,source_file_label,source_row_or_range,source_type,
             transaction_date,description,merchant,amount_original,signed_amount_original,
             currency_original,confidence,requires_review,status,created_at,updated_at,
             classification,review_reason,account_source,household_batch_id,review_version)
           VALUES (?,?,?,?,?,'Synthetic transaction','Synthetic merchant',?,?,?,'0.5',1,
                   'needs_review','2026-07-20T00:00:00Z','2026-07-20T00:00:00Z',
                   ?,'Synthetic review',?,'batch-1',1)""",
        (
            candidate_id,
            "synthetic.csv",
            "1",
            source_type,
            tx_date,
            amount,
            amount,
            currency,
            classification,
            account_id,
        ),
    )
    conn.commit()


def add_transaction(
    conn: Connection,
    transaction_id: str,
    *,
    account_id: str = "bank",
    transaction_type: str = "expense",
    amount: str = "-25.00",
    category_id: str | None = "expense-old",
    tx_date: str = "2026-07-20",
    source_candidate_id: str | None = None,
    reversal_of_transaction_id: str | None = None,
    currency: str = "CHF",
) -> None:
    conn.execute(
        """INSERT INTO budget_transactions(
             budget_transaction_id,account_id,transaction_type,transaction_date,booking_date,
             description,payee,amount_original,currency_original,fx_rate_to_chf,amount_chf,
             fx_status,category_id,status,source_type,source_candidate_id,notes,created_at,updated_at,
             reversal_of_transaction_id)
           VALUES (?,?,?,?,?,'Synthetic transaction','Synthetic merchant',?,?,'1',?,
                   'not_needed',?,'confirmed','manual',?,'{}','2026-07-20T00:00:00Z',
                   '2026-07-20T00:00:00Z',?)""",
        (
            transaction_id,
            account_id,
            transaction_type,
            tx_date,
            tx_date,
            amount,
            currency,
            amount,
            category_id,
            source_candidate_id,
            reversal_of_transaction_id,
        ),
    )
    conn.commit()


def item_request(conn: Connection, candidate_id: str, action: str, **extra: object) -> dict[str, object]:
    row = conn.execute(
        "SELECT * FROM budget_transaction_candidates WHERE transaction_candidate_id=?", (candidate_id,)
    ).fetchone()
    assert row is not None
    return {
        "item_token": _review_item_token(candidate_id),
        "data_version": _household_review_data_version(conn),
        "action": action,
        "candidate_version": int(row["review_version"]),
        "candidate_baseline": _candidate_baseline(row),
        **extra,
    }


def category_request(conn: Connection, transaction_id: str, new_category_id: str) -> dict[str, object]:
    row = conn.execute(
        "SELECT * FROM budget_transactions WHERE budget_transaction_id=?", (transaction_id,)
    ).fetchone()
    assert row is not None
    return {
        "transaction_token": transaction_token(transaction_id),
        "data_version": _household_transaction_data_version(conn),
        "transaction_baseline": _transaction_baseline(row),
        "new_category_id": new_category_id,
    }


def assert_http_error(status: int, callable_: object, *args: object) -> None:
    with pytest.raises(HTTPException) as exc:
        callable_(*args)  # type: ignore[operator]
    assert exc.value.status_code == status


def test_schema_48_adds_versioned_immutable_settlement_contract() -> None:
    conn = database()
    assert get_schema_version(conn) == 53
    tables = {row[0] for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")}
    assert "household_credit_card_settlements" in tables
    columns = {row[1] for row in conn.execute("PRAGMA table_info(budget_transaction_candidates)")}
    assert "review_version" in columns
    triggers = {row[0] for row in conn.execute("SELECT name FROM sqlite_master WHERE type='trigger'")}
    assert {
        "household_card_settlement_no_delete",
        "household_card_settlement_identity_immutable",
        "household_correction_audit_immutable_update",
        "household_correction_audit_no_delete",
    }.issubset(triggers)


def test_keep_not_duplicate_is_single_item_version_bound_once_only_and_idempotent() -> None:
    conn = database()
    add_candidate(conn, "candidate-1")
    add_candidate(conn, "candidate-2")
    request = item_request(conn, "candidate-1", "keep_not_duplicate", category_id="expense-new")
    old_review_version = request["data_version"]
    before = get_household_financial_summary(conn)

    preview = preview_household_item_action(conn, request)
    assert preview["item_token"] == request["item_token"]
    assert preview["expected_writes"] == {
        "candidate_updates": 1,
        "audit_events": 1,
        "transactions": 1,
    }
    confirm = confirm_household_item_action(
        conn, request | {"confirm": True, "preview_fingerprint": preview["preview_fingerprint"]}
    )
    after = get_household_financial_summary(conn)

    rows = conn.execute(
        "SELECT transaction_candidate_id,status,review_version FROM budget_transaction_candidates ORDER BY transaction_candidate_id"
    ).fetchall()
    assert [tuple(row) for row in rows] == [
        ("candidate-1", "confirmed", 2),
        ("candidate-2", "needs_review", 1),
    ]
    assert conn.execute("SELECT COUNT(*) FROM budget_transactions").fetchone()[0] == 1
    kept = conn.execute(
        "SELECT classification,confirmed_transaction_id FROM budget_transaction_candidates "
        "WHERE transaction_candidate_id='candidate-1'"
    ).fetchone()
    assert kept["classification"] == "user_confirmed_not_duplicate"
    kept_detail = get_household_transaction_detail(
        conn, transaction_token(str(kept["confirmed_transaction_id"]))
    )
    assert kept_detail["special_workflow"] is None
    recategorize = category_request(conn, str(kept["confirmed_transaction_id"]), "expense-old")
    assert preview_household_transaction_category(conn, recategorize)["new_category"]["category_id"] == "expense-old"
    assert after["expense_chf"] != before["expense_chf"]
    assert confirm["data_version"] != old_review_version

    retry = confirm_household_item_action(
        conn, request | {"confirm": True, "preview_fingerprint": preview["preview_fingerprint"]}
    )
    assert retry["idempotent"] is True
    assert conn.execute(
        "SELECT COUNT(*) FROM audit_log WHERE entity_type='household_review_item'"
    ).fetchone()[0] == 1

    forged = request | {
        "item_token": _review_item_token("candidate-2"),
        "confirm": True,
        "preview_fingerprint": preview["preview_fingerprint"],
    }
    assert_http_error(409, confirm_household_item_action, conn, forged)


def test_item_preview_requires_current_candidate_version_and_baseline() -> None:
    conn = database()
    add_candidate(conn, "candidate-1")
    request = item_request(conn, "candidate-1", "keep_not_duplicate", category_id="expense-new")
    assert_http_error(422, preview_household_item_action, conn, request | {"candidate_baseline": None})
    assert_http_error(422, preview_household_item_action, conn, request | {"candidate_version": None})
    assert_http_error(409, preview_household_item_action, conn, request | {"candidate_version": 2})
    assert_http_error(409, preview_household_item_action, conn, request | {"candidate_baseline": "stale"})


def test_keep_not_duplicate_rejects_non_chf_instead_of_assuming_fx_one() -> None:
    conn = database()
    add_candidate(conn, "candidate-usd", currency="USD")
    request = item_request(
        conn, "candidate-usd", "keep_not_duplicate", category_id="expense-new"
    )
    assert_http_error(409, preview_household_item_action, conn, request)
    assert conn.execute("SELECT COUNT(*) FROM budget_transactions").fetchone()[0] == 0


def test_duplicate_exclusion_has_original_zero_effect_and_audited_reopen() -> None:
    conn = database()
    add_transaction(conn, "original")
    add_transaction(conn, "unrelated")
    conn.execute(
        "UPDATE budget_transactions SET payee='Unrelated merchant' WHERE budget_transaction_id='unrelated'"
    )
    conn.commit()
    add_candidate(conn, "candidate-1")
    before_summary = get_household_financial_summary(conn)
    before_transactions = conn.execute("SELECT COUNT(*) FROM budget_transactions").fetchone()[0]
    request = item_request(
        conn,
        "candidate-1",
        "exclude_duplicate",
        original_transaction_token=transaction_token("original"),
    )
    assert_http_error(
        409,
        preview_household_item_action,
        conn,
        request | {"original_transaction_token": transaction_token("unrelated")},
    )
    preview = preview_household_item_action(conn, request)
    assert preview["original_transaction"]["original_transaction_token"] == transaction_token("original")
    assert preview["financial_effect"]["additional_chf"] == "0.00"
    confirm_household_item_action(
        conn, request | {"confirm": True, "preview_fingerprint": preview["preview_fingerprint"]}
    )
    candidate = conn.execute(
        "SELECT * FROM budget_transaction_candidates WHERE transaction_candidate_id='candidate-1'"
    ).fetchone()
    assert candidate["status"] == "duplicate"
    assert candidate["duplicate_of_transaction_id"] == "original"
    assert conn.execute("SELECT COUNT(*) FROM budget_transactions").fetchone()[0] == before_transactions
    assert get_household_financial_summary(conn)["expense_chf"] == before_summary["expense_chf"]

    reopen = item_request(conn, "candidate-1", "reopen_duplicate")
    reopen_preview = preview_household_item_action(conn, reopen)
    confirm_household_item_action(
        conn,
        reopen | {"confirm": True, "preview_fingerprint": reopen_preview["preview_fingerprint"]},
    )
    candidate = conn.execute(
        "SELECT status,requires_review,duplicate_of_transaction_id,review_version "
        "FROM budget_transaction_candidates WHERE transaction_candidate_id='candidate-1'"
    ).fetchone()
    assert tuple(candidate) == ("needs_review", 1, None, 3)
    assert conn.execute(
        "SELECT COUNT(*) FROM audit_log WHERE entity_type='household_review_item'"
    ).fetchone()[0] == 2


def test_duplicate_original_requires_exact_signed_direction_and_semantics() -> None:
    conn = database()
    add_candidate(conn, "candidate-1", amount="-25.00")
    add_transaction(conn, "opposite", transaction_type="income", amount="25.00")
    add_transaction(conn, "fee", transaction_type="fee", amount="-25.00")
    request = item_request(conn, "candidate-1", "exclude_duplicate")
    for transaction_id in ("opposite", "fee"):
        assert_http_error(
            409,
            preview_household_item_action,
            conn,
            request
            | {"original_transaction_token": transaction_token(transaction_id)},
        )


def test_credit_card_settlement_is_neutral_and_partial_requires_explicit_acknowledgement() -> None:
    conn = database()
    add_transaction(conn, "purchase", account_id="card", amount="-100.00")
    add_candidate(
        conn,
        "payment",
        classification="credit_card_payment",
        source_type="akb_bank",
        account_id="bank",
        amount="-100.00",
    )
    add_candidate(
        conn,
        "counterpost",
        classification="credit_card_payment_counterpost",
        source_type="visa_credit_card",
        account_id="card",
        amount="100.00",
    )
    before = get_household_financial_summary(conn)
    request = item_request(conn, "payment", "credit_card_settlement")
    preview = preview_household_item_action(conn, request)
    assert preview["counterpost_status"] == "matched"
    assert preview["completeness_status"] == "partial"
    assert preview["data_status"] == "partial"
    assert preview["requires_partial_acknowledgement"] is True
    assert preview["mapped_purchase_sum"] == "100.00"
    assert preview["payment_amount"] == "100.00"
    assert preview["statement_period"] == {
        "date_from": None,
        "date_to": None,
        "status": "not_evidenced",
        "search_window_days": 45,
    }
    assert preview["found_purchase_sum"] == "100.00"
    assert preview["refund_credit_sum"] == "0.00"
    assert preview["opening_balance"] is None
    assert preview["remaining_difference"] == "0.00"
    assert preview["financial_effect"]["additional_chf"] == "0.00"
    confirm_payload = request | {
        "confirm": True,
        "preview_fingerprint": preview["preview_fingerprint"],
    }
    assert_http_error(422, confirm_household_item_action, conn, confirm_payload)
    result = confirm_household_item_action(
        conn, confirm_payload | {"acknowledge_partial": True}
    )
    assert result["settlement_id"]
    assert get_household_financial_summary(conn)["expense_chf"] == before["expense_chf"]
    assert conn.execute(
        "SELECT COUNT(*) FROM budget_transactions WHERE transaction_type='transfer'"
    ).fetchone()[0] == 2
    relation = conn.execute(
        "SELECT completeness_status,data_status,acknowledged_partial FROM household_credit_card_settlements"
    ).fetchone()
    assert tuple(relation) == ("partial", "partial", 1)

    conn2 = database()
    add_candidate(
        conn2,
        "payment",
        classification="credit_card_payment",
        source_type="akb_bank",
        account_id="bank",
        amount="-100.00",
    )
    partial_request = item_request(conn2, "payment", "credit_card_settlement")
    partial = preview_household_item_action(conn2, partial_request)
    assert partial["completeness_status"] == "partial"
    assert partial["data_status"] == "partial"
    assert partial["requires_partial_acknowledgement"] is True
    confirm_payload = partial_request | {
        "confirm": True,
        "preview_fingerprint": partial["preview_fingerprint"],
    }
    assert_http_error(422, confirm_household_item_action, conn2, confirm_payload)
    confirmed = confirm_household_item_action(conn2, confirm_payload | {"acknowledge_partial": True})
    assert confirmed["status"] == "confirmed"
    retry = confirm_household_item_action(
        conn2, confirm_payload | {"acknowledge_partial": True}
    )
    assert retry["idempotent"] is True
    assert_http_error(409, confirm_household_item_action, conn2, confirm_payload)
    relation = conn2.execute(
        "SELECT completeness_status,data_status,acknowledged_partial FROM household_credit_card_settlements"
    ).fetchone()
    assert tuple(relation) == ("partial", "partial", 1)


def test_confirmed_legacy_duplicate_preview_is_exact_audited_and_non_destructive() -> None:
    conn = database()
    add_candidate(conn, "legacy-candidate", source_type="csv_seed", account_id="bank", amount="20.00", tx_date="2026-05-12")
    add_candidate(conn, "canonical-candidate", source_type="visa_credit_card", account_id="card", amount="20.00", tx_date="2026-05-12")
    conn.execute(
        "UPDATE budget_transaction_candidates SET status='confirmed',requires_review=0,household_batch_id=NULL WHERE transaction_candidate_id='legacy-candidate'"
    )
    conn.execute(
        """UPDATE budget_transaction_candidates SET status='confirmed',requires_review=0,
           source_row_fingerprint=?,logical_fingerprint=? WHERE transaction_candidate_id='canonical-candidate'""",
        ("a" * 64, "b" * 64),
    )
    add_transaction(conn, "legacy-transaction", account_id="bank", amount="20.00", tx_date="2026-05-12", source_candidate_id="legacy-candidate")
    add_transaction(conn, "canonical-transaction", account_id="card", amount="-20.00", tx_date="2026-05-12", source_candidate_id="canonical-candidate")

    duplicate_token = transaction_token("legacy-transaction")
    original_token = transaction_token("canonical-transaction")
    conn.execute("UPDATE budget_transaction_candidates SET source_type='akb_bank' WHERE transaction_candidate_id='legacy-candidate'")
    assert list_eligible_confirmed_duplicate_originals(conn, duplicate_token)["items"] == []
    conn.execute("UPDATE budget_transaction_candidates SET source_type='csv_seed' WHERE transaction_candidate_id='legacy-candidate'")
    conn.execute("UPDATE budget_transaction_candidates SET source_type='akb_bank' WHERE transaction_candidate_id='canonical-candidate'")
    assert list_eligible_confirmed_duplicate_originals(conn, duplicate_token)["items"] == []
    conn.execute("UPDATE budget_transaction_candidates SET source_type='visa_credit_card' WHERE transaction_candidate_id='canonical-candidate'")
    conn.execute("UPDATE budget_accounts SET account_type='checking' WHERE budget_account_id='card'")
    assert list_eligible_confirmed_duplicate_originals(conn, duplicate_token)["items"] == []
    conn.execute("UPDATE budget_accounts SET account_type='credit_card' WHERE budget_account_id='card'")
    conn.execute("UPDATE budget_accounts SET is_active=0 WHERE budget_account_id='card'")
    assert list_eligible_confirmed_duplicate_originals(conn, duplicate_token)["items"] == []
    conn.execute("UPDATE budget_accounts SET is_active=1 WHERE budget_account_id='card'")
    conn.execute("UPDATE budget_transaction_candidates SET account_source='bank' WHERE transaction_candidate_id='canonical-candidate'")
    assert list_eligible_confirmed_duplicate_originals(conn, duplicate_token)["items"] == []
    conn.execute("UPDATE budget_transaction_candidates SET account_source='card' WHERE transaction_candidate_id='canonical-candidate'")
    eligible = list_eligible_confirmed_duplicate_originals(conn, duplicate_token)
    assert [item["transaction_token"] for item in eligible["items"]] == [original_token]
    request = {
        "transaction_token": duplicate_token,
        "data_version": eligible["data_version"],
        "transaction_baseline": eligible["transaction_baseline"],
        "original_transaction_token": original_token,
    }
    preview = preview_household_transaction_duplicate(conn, request)
    assert preview["financial_effect"] == {
        "expense_before_chf": "20.00",
        "expense_after_chf": "0.00",
        "expense_change_chf": "-20.00",
        "additional_income_chf": "0.00",
    }
    assert preview["deletes"] == 0
    assert preview["expected_writes"] == {"transactions": 1, "candidate_updates": 1, "audit_events": 1}

    confirm_payload = {**request, "confirm": True, "preview_fingerprint": preview["preview_fingerprint"]}
    confirmed = confirm_household_transaction_duplicate(conn, confirm_payload)
    assert confirmed["status"] == "confirmed"
    assert conn.execute("SELECT status FROM budget_transactions WHERE budget_transaction_id='legacy-transaction'").fetchone()["status"] == "archived"
    candidate = conn.execute(
        "SELECT status,duplicate_of_transaction_id FROM budget_transaction_candidates WHERE transaction_candidate_id='legacy-candidate'"
    ).fetchone()
    assert dict(candidate) == {"status": "duplicate", "duplicate_of_transaction_id": "canonical-transaction"}
    assert conn.execute("SELECT COUNT(*) AS count FROM budget_transactions").fetchone()["count"] == 2
    assert get_household_financial_summary(conn)["expense_chf"] == "20.00"
    audit_values = json.loads(
        conn.execute(
            "SELECT new_values_json FROM audit_log WHERE audit_id=?",
            (confirmed["audit_id"],),
        ).fetchone()["new_values_json"]
    )
    assert audit_values["preview_fingerprint"] == preview["preview_fingerprint"]
    assert audit_values["data_version"] == request["data_version"]
    assert audit_values["transaction_baseline"] == request["transaction_baseline"]
    assert audit_values["original_baseline"] == preview["original_baseline"]
    assert audit_values["preview_financial_effect"] == preview["financial_effect"]
    assert audit_values["verified_financial_effect"] == {
        "expense_before_chf": "40.00",
        "expense_after_chf": "20.00",
        "expense_delta_chf": "-20.00",
        "income_before_chf": "0.00",
        "income_after_chf": "0.00",
        "income_delta_chf": "0.00",
    }
    assert confirm_household_transaction_duplicate(conn, confirm_payload)["idempotent"] is True


def test_internal_transfer_is_item_bound_balanced_audited_and_financially_neutral() -> None:
    conn = database()
    add_candidate(conn, "transfer-candidate", classification="expense_candidate", amount="-75.00")
    token = _review_item_token("transfer-candidate")
    review_item = next(item for item in get_household_review(conn, limit=20)["items"] if item["item_token"] == token)
    assert "internal_transfer" in review_item["available_actions"]
    assert len(review_item["eligible_transfer_accounts"]) == 1
    counterpart_token = review_item["eligible_transfer_accounts"][0]["account_token"]
    request = item_request(
        conn,
        "transfer-candidate",
        "internal_transfer",
        counterpart_account_token=counterpart_token,
    )
    before = get_household_financial_summary(conn)
    preview = preview_household_item_action(conn, request)
    assert preview["financial_effect"]["additional_chf"] == "0.00"
    assert preview["expected_writes"] == {
        "candidate_updates": 1,
        "audit_events": 1,
        "transactions": 2,
        "transfer_relations": 1,
    }
    assert preview["from_account"]["account_name"] == "Synthetic bank"
    assert preview["to_account"]["account_name"] == "Synthetic card"
    result = confirm_household_item_action(
        conn,
        request | {"confirm": True, "preview_fingerprint": preview["preview_fingerprint"]},
    )
    assert result["transfer_id"]
    transactions = conn.execute(
        "SELECT transaction_type,amount_original,source_candidate_id FROM budget_transactions ORDER BY amount_original"
    ).fetchall()
    assert [tuple(row) for row in transactions] == [
        ("transfer", "-75.00", "transfer-candidate"),
        ("transfer", "75.00", None),
    ]
    relation = conn.execute(
        "SELECT from_account_id,to_account_id,amount_original,transfer_type FROM budget_transfers"
    ).fetchone()
    assert tuple(relation) == ("bank", "card", "75.00", "internal_transfer")
    candidate = conn.execute(
        "SELECT status,classification,review_version FROM budget_transaction_candidates WHERE transaction_candidate_id='transfer-candidate'"
    ).fetchone()
    assert tuple(candidate) == ("confirmed", "user_confirmed_internal_transfer", 2)
    after = get_household_financial_summary(conn)
    for key in ("income_chf", "expense_chf", "net_chf"):
        assert after[key] == before[key]
    assert after["neutral_transfer_volume_chf"] == "75.00"
    assert after["neutral_transfer_count"] == 2
    retry = confirm_household_item_action(
        conn,
        request | {"confirm": True, "preview_fingerprint": preview["preview_fingerprint"]},
    )
    assert retry["idempotent"] is True
    assert retry["transfer_id"] == result["transfer_id"]


@pytest.mark.parametrize("account_state", ["inactive", "wrong_currency"])
def test_transfer_and_settlement_fail_closed_for_invalid_source_account(
    account_state: str,
) -> None:
    conn = database()
    add_candidate(
        conn,
        "transfer-candidate",
        classification="expense_candidate",
        amount="-75.00",
    )
    add_candidate(
        conn,
        "settlement-candidate",
        classification="credit_card_payment",
        amount="-100.00",
    )
    if account_state == "inactive":
        conn.execute("UPDATE budget_accounts SET is_active=0 WHERE budget_account_id='bank'")
    else:
        conn.execute("UPDATE budget_accounts SET currency='USD' WHERE budget_account_id='bank'")
    conn.commit()

    review = get_household_review(conn, limit=20)
    by_token = {item["item_token"]: item for item in review["items"]}
    transfer_item = by_token[_review_item_token("transfer-candidate")]
    settlement_item = by_token[_review_item_token("settlement-candidate")]
    assert "internal_transfer" not in transfer_item["available_actions"]
    assert transfer_item["eligible_transfer_accounts"] == []
    assert "credit_card_settlement" not in settlement_item["available_actions"]

    transfer_request = item_request(
        conn,
        "transfer-candidate",
        "internal_transfer",
        counterpart_account_token="invalid",
    )
    settlement_request = item_request(
        conn,
        "settlement-candidate",
        "credit_card_settlement",
    )
    assert_http_error(409, preview_household_item_action, conn, transfer_request)
    assert_http_error(409, preview_household_item_action, conn, settlement_request)


def test_category_change_is_fingerprint_bound_type_checked_audited_and_total_neutral() -> None:
    conn = database()
    add_transaction(conn, "expense-1")
    add_transaction(conn, "expense-2", amount="-10.00")
    request = category_request(conn, "expense-1", "expense-new")
    old_data_version = request["data_version"]
    before = get_household_financial_summary(conn)
    detail = get_household_transaction_detail(conn, transaction_token("expense-1"))
    assert detail["booking_date"] == "2026-07-20"
    assert detail["import_review_status"] == "confirmed"
    assert detail["capabilities"]["change_category"] is True

    preview = preview_household_transaction_category(conn, request)
    assert preview["old_category"]["category_id"] == "expense-old"
    assert preview["new_category"]["category_id"] == "expense-new"
    assert preview["total_financial_effect"]["change_chf"] == "0.00"
    result = confirm_household_transaction_category(
        conn, request | {"confirm": True, "preview_fingerprint": preview["preview_fingerprint"]}
    )
    assert result["data_version"] != old_data_version
    assert get_household_financial_summary(conn)["expense_chf"] == before["expense_chf"]
    assert conn.execute(
        "SELECT category_id FROM budget_transactions WHERE budget_transaction_id='expense-1'"
    ).fetchone()[0] == "expense-new"
    audit = conn.execute(
        "SELECT old_values_json,new_values_json FROM audit_log "
        "WHERE entity_type='household_transaction_category'"
    ).fetchone()
    assert '"category_id": "expense-old"' in audit[0]
    assert '"new_category_id": "expense-new"' in audit[1]

    retry = confirm_household_transaction_category(
        conn, request | {"confirm": True, "preview_fingerprint": preview["preview_fingerprint"]}
    )
    assert retry["idempotent"] is True
    forged = request | {
        "transaction_token": transaction_token("expense-2"),
        "confirm": True,
        "preview_fingerprint": preview["preview_fingerprint"],
    }
    assert_http_error(409, confirm_household_transaction_category, conn, forged)

    wrong_type = category_request(conn, "expense-2", "income")
    assert_http_error(409, preview_household_transaction_category, conn, wrong_type)
    assert_http_error(
        422,
        preview_household_transaction_category,
        conn,
        category_request(conn, "expense-2", "expense-new") | {"transaction_baseline": None},
    )


def test_category_preview_reports_missing_chf_value_without_relabeling_foreign_amount() -> None:
    conn = database()
    add_transaction(conn, "expense-usd", amount="-100.00", currency="USD")
    conn.execute(
        """UPDATE budget_transactions
           SET amount_chf=NULL,fx_rate_to_chf=NULL,fx_status='missing'
           WHERE budget_transaction_id='expense-usd'"""
    )
    conn.commit()
    preview = preview_household_transaction_category(
        conn,
        category_request(conn, "expense-usd", "expense-new"),
    )
    assert preview["chf_value_status"] == "unavailable"
    assert preview["original_amount"] == "100.00"
    assert preview["original_currency"] == "USD"
    assert preview["category_deltas"] == {
        "old_category_chf": None,
        "new_category_chf": None,
    }
    assert preview["total_financial_effect"] == {
        "before_chf": None,
        "after_chf": None,
        "change_chf": "0.00",
    }


def test_refund_category_change_redirects_to_origin_and_special_transactions_fail_closed() -> None:
    conn = database()
    add_transaction(conn, "origin")
    add_transaction(
        conn,
        "refund",
        transaction_type="refund",
        amount="10.00",
        category_id="expense-old",
        reversal_of_transaction_id="origin",
    )
    request = category_request(conn, "refund", "expense-new")
    preview = preview_household_transaction_category(conn, request)
    assert preview["linked_refund_redirected"] is True
    assert preview["target_transaction_token"] == transaction_token("origin")
    confirm_household_transaction_category(
        conn, request | {"confirm": True, "preview_fingerprint": preview["preview_fingerprint"]}
    )
    categories = dict(
        conn.execute(
            "SELECT budget_transaction_id,category_id FROM budget_transactions "
            "WHERE budget_transaction_id IN ('origin','refund')"
        ).fetchall()
    )
    assert categories == {"origin": "expense-new", "refund": "expense-old"}

    add_transaction(conn, "transfer", transaction_type="transfer", amount="-20.00", category_id=None)
    transfer_detail = get_household_transaction_detail(conn, transaction_token("transfer"))
    assert transfer_detail["special_workflow"] == "transfer"
    assert transfer_detail["capabilities"]["change_category"] is False
    assert_http_error(
        409,
        preview_household_transaction_category,
        conn,
        category_request(conn, "transfer", "expense-new"),
    )


def test_read_models_expose_exact_capabilities_paginated_originals_and_reopen() -> None:
    conn = database()
    for transaction_id in ("original-a", "original-b", "original-c"):
        add_transaction(conn, transaction_id)
    add_transaction(conn, "wrong-direction", transaction_type="income", amount="25.00")
    add_candidate(conn, "duplicate")
    add_candidate(
        conn,
        "bank-payment",
        classification="credit_card_payment",
        source_type="akb_bank",
        amount="-100.00",
    )
    add_candidate(
        conn,
        "card-side-payment",
        classification="credit_card_payment_counterpost",
        source_type="visa_credit_card",
        account_id="card",
        amount="100.00",
    )

    review = get_household_review(conn, limit=20)
    by_token = {item["item_token"]: item for item in review["items"]}
    duplicate_token = _review_item_token("duplicate")
    assert by_token[duplicate_token]["available_actions"] == [
        "keep_not_duplicate",
        "exclude_duplicate",
    ]
    assert by_token[_review_item_token("bank-payment")]["capabilities"][
        "credit_card_settlement"
    ] is True
    assert "credit_card_settlement" not in by_token[
        _review_item_token("card-side-payment")
    ]["available_actions"]

    first = list_eligible_duplicate_originals(conn, duplicate_token, limit=1)
    assert first["total"] == 3
    assert first["has_more"] is True
    assert first["next_cursor"].startswith("page_")
    assert "original" not in first["next_cursor"]
    tokens = [first["items"][0]["transaction_token"]]
    cursor = first["next_cursor"]
    while cursor:
        page = list_eligible_duplicate_originals(
            conn, duplicate_token, limit=1, cursor=cursor
        )
        tokens.extend(item["transaction_token"] for item in page["items"])
        cursor = page["next_cursor"]
    assert len(tokens) == len(set(tokens)) == 3
    assert transaction_token("wrong-direction") not in tokens

    request = item_request(
        conn,
        "duplicate",
        "exclude_duplicate",
        original_transaction_token=tokens[0],
    )
    preview = preview_household_item_action(conn, request)
    confirm_household_item_action(
        conn,
        request
        | {"confirm": True, "preview_fingerprint": preview["preview_fingerprint"]},
    )
    closed = list_closed_household_duplicates(conn)
    assert closed["total"] == 1
    closed_item = closed["items"][0]
    assert closed_item["item_token"] == duplicate_token
    assert closed_item["candidate_version"] == 2
    assert closed_item["candidate_baseline"]
    assert closed_item["data_version"] == closed["data_version"]
    assert closed_item["available_actions"] == ["reopen_duplicate"]
    assert get_household_review_item(conn, duplicate_token) == closed_item


def test_api_exposes_typed_contract_and_only_previews_bypass_disabled_write_gate() -> None:
    conn = database()
    add_transaction(conn, "original")
    add_candidate(conn, "candidate")
    add_candidate(
        conn,
        "transfer-api",
        classification="expense_candidate",
        amount="-75.00",
    )

    def override_db():
        yield conn

    app = create_app(write_mode="disabled")
    app.dependency_overrides[get_db] = override_db
    client = TestClient(app)
    openapi = client.get("/openapi.json").json()
    paths = openapi["paths"]
    for path in (
        "/api/budget/household/review/items/preview",
        "/api/budget/household/review/items/confirm",
        "/api/budget/household/review/duplicates",
        "/api/budget/household/review/items/{item_token}",
        "/api/budget/household/review/items/{item_token}/eligible-duplicate-originals",
        "/api/budget/household/transactions/{transaction_token}",
        "/api/budget/household/transactions/category/preview",
        "/api/budget/household/transactions/category/confirm",
    ):
        assert path in paths

    schemas = openapi["components"]["schemas"]
    assert {"candidate_version", "candidate_baseline"}.issubset(
        schemas["HouseholdItemActionRequest"]["required"]
    )
    assert "transaction_baseline" in schemas["HouseholdCategoryChangeRequest"]["required"]
    assert "internal_transfer" in schemas["HouseholdItemActionRequest"]["properties"]["action"]["enum"]
    assert {
        "item_token",
        "candidate_version",
        "candidate_baseline",
        "data_version",
        "available_actions",
        "capabilities",
    }.issubset(schemas["HouseholdReviewItemRead"]["required"])

    item_token = _review_item_token("candidate")
    detail = client.get(f"/api/budget/household/review/items/{item_token}")
    assert detail.status_code == 200
    assert detail.json()["candidate_baseline"]
    originals = client.get(
        f"/api/budget/household/review/items/{item_token}/eligible-duplicate-originals?limit=1"
    )
    assert originals.status_code == 200
    assert originals.json()["total"] == 1
    assert client.get("/api/budget/household/review/duplicates").status_code == 200

    transfer_review = client.get("/api/budget/household/review?limit=20").json()
    transfer_item = next(
        item
        for item in transfer_review["items"]
        if item["item_token"] == _review_item_token("transfer-api")
    )
    transfer_preview = client.post(
        "/api/budget/household/review/items/preview",
        json={
            "item_token": transfer_item["item_token"],
            "data_version": transfer_review["data_version"],
            "action": "internal_transfer",
            "candidate_version": transfer_item["candidate_version"],
            "candidate_baseline": transfer_item["candidate_baseline"],
            "counterpart_account_token": transfer_item["eligible_transfer_accounts"][0][
                "account_token"
            ],
        },
    )
    assert transfer_preview.status_code == 200
    assert transfer_preview.json()["action"] == "internal_transfer"

    before = conn.total_changes
    assert client.post("/api/budget/household/review/items/preview", json={}).status_code == 422
    assert client.post(
        "/api/budget/household/transactions/category/preview", json={}
    ).status_code == 422
    assert conn.total_changes == before
    assert client.post("/api/budget/household/review/items/confirm", json={}).status_code == 403
    assert client.post(
        "/api/budget/household/transactions/category/confirm", json={}
    ).status_code == 403
