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.budget_accounts import confirm_create_budget_account
from jarvis_finance.services.budget_imports import seed_bank_transfer_candidates_from_rows
from jarvis_finance.services.budget_transactions import list_budget_transactions, list_budget_transfers
from jarvis_finance.services.transfer_pairing import (
    confirm_transfer_pair,
    list_transfer_pairs,
    propose_transfer_pairs,
    reject_transfer_pair,
)
from jarvis_finance.storage.database import connect_memory
from jarvis_finance.storage.migrations import apply_migrations


def db() -> Connection:
    conn = connect_memory()
    apply_migrations(conn)
    return conn


def accounts(conn: Connection) -> tuple[str, str, str]:
    raiffeisen = confirm_create_budget_account(
        conn,
        {"name": "Raiffeisen", "account_type": "cash", "currency": "CHF"},
    )["entity_id"]
    akb = confirm_create_budget_account(
        conn,
        {"name": "AKB Haushaltskonto", "account_type": "cash", "currency": "CHF"},
    )["entity_id"]
    reserve = confirm_create_budget_account(
        conn,
        {"name": "Reservekonto", "account_type": "cash", "currency": "CHF"},
    )["entity_id"]
    return raiffeisen, akb, reserve


def seed_reference_outflow(conn: Connection, *, amount: str = "-100.00", date: str = "2026-05-12") -> str:
    seed_bank_transfer_candidates_from_rows(
        conn,
        [
            {
                "date": date,
                "description": "Einkauf TWINT SWISSLOS E-COMMERCE",
                "amount": amount,
                "account": "OWN-RAIFFEISEN",
            }
        ],
        source_file_label="synthetic-run-raiffeisen",
        candidate_source_type="raiffeisen_bank",
    )
    return str(
        conn.execute(
            "SELECT transaction_candidate_id FROM budget_transaction_candidates WHERE source_type='raiffeisen_bank'"
        ).fetchone()[0]
    )


def seed_reference_inflow(
    conn: Connection,
    *,
    amount: str = "100.00",
    date: str = "2026-05-13",
    description: str = "Eigener Kontoübertrag",
    source_label: str = "synthetic-run-akb",
) -> str:
    seed_bank_transfer_candidates_from_rows(
        conn,
        [
            {
                "date": date,
                "description": description,
                "amount": amount,
                "account": "OWN-AKB",
            }
        ],
        source_file_label=source_label,
        candidate_source_type="akb_bank",
    )
    return str(
        conn.execute(
            "SELECT transaction_candidate_id FROM budget_transaction_candidates WHERE source_file_label=?",
            (source_label,),
        ).fetchone()[0]
    )


def seed_pair(conn: Connection) -> tuple[str, str, dict]:
    accounts(conn)
    source_id = seed_reference_outflow(conn)
    target_id = seed_reference_inflow(conn)
    propose_transfer_pairs(conn)
    pair = next(item for item in list_transfer_pairs(conn) if item["status"] == "proposed")
    return source_id, target_id, pair


def insert_candidate(
    conn: Connection,
    *,
    candidate_id: str,
    source_type: str,
    account_source: str,
    signed_amount: str,
    currency: str = "CHF",
    date: str = "2026-05-12",
) -> None:
    conn.execute(
        """
        INSERT INTO budget_transaction_candidates(
            transaction_candidate_id, source_file_label, source_row_or_range, source_type,
            transaction_date, value_date, description, amount_original, signed_amount_original,
            currency_original, confidence, requires_review, status, notes, classification,
            review_reason, account_source, raw_fingerprint, created_at, updated_at
        ) VALUES (?, ?, 'R1', ?, ?, ?, ?, ?, ?, ?, '0.50', 1, 'needs_review', ?,
                  'bank_review', 'bank_payment_unclear', ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
        """,
        (
            candidate_id,
            f"synthetic-{candidate_id}",
            source_type,
            date,
            date,
            "Synthetic booking",
            str(abs(__import__("decimal").Decimal(signed_amount))),
            signed_amount,
            currency,
            json.dumps({"transaction_type": "income" if signed_amount.startswith("+") or not signed_amount.startswith("-") else "expense"}),
            account_source,
            f"fingerprint-{candidate_id}",
        ),
    )
    conn.commit()


def test_reference_case_matches_across_import_runs_despite_misleading_merchant_text() -> None:
    conn = db()
    accounts(conn)
    source_id = seed_reference_outflow(conn)
    target_id = seed_reference_inflow(conn)

    result = propose_transfer_pairs(conn)
    pairs = list_transfer_pairs(conn)
    proposed = next(item for item in pairs if item["status"] == "proposed")

    assert result["proposed_count"] >= 1
    assert proposed["source_candidate_id"] == source_id
    assert proposed["target_candidate_id"] == target_id
    assert proposed["source_signed_amount"] == "-100.00"
    assert proposed["target_signed_amount"] == "100.00"
    assert proposed["currency"] == "CHF"
    assert proposed["source_account_name"] == "Raiffeisen"
    assert proposed["target_account_name"] == "AKB Haushaltskonto"
    assert proposed["budget_effect_chf"] == "0"
    assert "Interner Transfer Raiffeisen → AKB Haushaltskonto" in proposed["summary"]
    assert "Budgeteffekt CHF 0" in proposed["summary"]
    assert proposed["confirmation_notice"] == (
        "Als interner Transfer erkannt. Die endgültige Verbuchung erfolgt erst nach Bestätigung."
    )
    assert "merchant_text_not_decisive" in proposed["reason_codes"]


def test_missing_counterbooking_is_unmatched_then_later_pair_supersedes_it() -> None:
    conn = db()
    accounts(conn)
    source_id = seed_reference_outflow(conn)

    propose_transfer_pairs(conn)
    unmatched = next(item for item in list_transfer_pairs(conn) if item["status"] == "unmatched")
    assert unmatched["source_candidate_id"] == source_id
    assert unmatched["target_candidate_id"] is None
    assert unmatched["quality_status"] == "unmatched"
    assert conn.execute("SELECT COUNT(*) FROM budget_transactions").fetchone()[0] == 0

    target_id = seed_reference_inflow(conn)
    propose_transfer_pairs(conn)
    all_pairs = list_transfer_pairs(conn)
    assert next(item for item in all_pairs if item["transfer_pair_id"] == unmatched["transfer_pair_id"])["status"] == "superseded"
    proposed = next(item for item in all_pairs if item["status"] == "proposed")
    assert proposed["target_candidate_id"] == target_id


def test_ambiguous_counterbookings_are_not_selected() -> None:
    conn = db()
    accounts(conn)
    source_id = seed_reference_outflow(conn)
    seed_reference_inflow(conn, source_label="synthetic-akb-a")
    insert_candidate(
        conn,
        candidate_id="cand-akb-b",
        source_type="akb_bank",
        account_source="OWN-AKB",
        signed_amount="100.00",
        date="2026-05-13",
    )

    propose_transfer_pairs(conn)
    pair = next(item for item in list_transfer_pairs(conn) if item["source_candidate_id"] == source_id)

    assert pair["status"] == "unmatched"
    assert pair["quality_status"] == "ambiguous"
    assert pair["alternative_count"] == 2
    assert pair["target_candidate_id"] is None
    assert "ambiguous_counterbookings" in pair["reason_codes"]


def test_invalid_direction_account_amount_and_currency_are_never_guessed() -> None:
    conn = db()
    accounts(conn)
    insert_candidate(
        conn,
        candidate_id="source",
        source_type="raiffeisen_bank",
        account_source="OWN-RAIFFEISEN",
        signed_amount="-100.00",
    )
    insert_candidate(
        conn,
        candidate_id="same-direction",
        source_type="akb_bank",
        account_source="OWN-AKB",
        signed_amount="-100.00",
    )
    insert_candidate(
        conn,
        candidate_id="same-account",
        source_type="raiffeisen_bank",
        account_source="OWN-RAIFFEISEN",
        signed_amount="100.00",
    )
    insert_candidate(
        conn,
        candidate_id="other-amount",
        source_type="akb_bank",
        account_source="OWN-AKB",
        signed_amount="99.99",
    )
    insert_candidate(
        conn,
        candidate_id="other-currency",
        source_type="akb_bank",
        account_source="OWN-AKB",
        signed_amount="100.00",
        currency="EUR",
    )

    propose_transfer_pairs(conn)
    pair = next(item for item in list_transfer_pairs(conn) if item["source_candidate_id"] == "source")
    assert pair["status"] == "unmatched"
    assert pair["alternative_count"] == 0


def test_confirm_consumes_both_candidates_once_and_budget_effect_is_zero() -> None:
    conn = db()
    source_id, target_id, pair = seed_pair(conn)

    first = confirm_transfer_pair(conn, pair["transfer_pair_id"], decision_by="synthetic-user")
    second = confirm_transfer_pair(conn, pair["transfer_pair_id"], decision_by="synthetic-user")

    assert first["status"] == "confirmed"
    assert first["budget_effect_chf"] == "0"
    assert second["status"] == "confirmed"
    assert second["entity_id"] == first["entity_id"]
    assert second["idempotent"] is True
    assert conn.execute("SELECT COUNT(*) FROM budget_transfers").fetchone()[0] == 1
    assert conn.execute("SELECT COUNT(*) FROM budget_transactions WHERE transaction_type='transfer'").fetchone()[0] == 2
    assert conn.execute("SELECT COUNT(*) FROM budget_transactions WHERE transaction_type IN ('income','expense')").fetchone()[0] == 0
    statuses = conn.execute(
        "SELECT transaction_candidate_id, status, confirmed_transaction_id FROM budget_transaction_candidates WHERE transaction_candidate_id IN (?, ?)",
        (source_id, target_id),
    ).fetchall()
    assert {row["status"] for row in statuses} == {"confirmed"}
    assert all(row["confirmed_transaction_id"] for row in statuses)
    assert len(list_budget_transfers(conn, year="2026")) == 1
    assert list_budget_transactions(conn, year="2026", transaction_type="income") == []
    assert list_budget_transactions(conn, year="2026", transaction_type="expense") == []
    audit = conn.execute(
        "SELECT new_values_json FROM audit_log WHERE action='transfer_pair_confirmed'"
    ).fetchone()
    assert audit is not None
    assert "OWN-" not in audit["new_values_json"]
    assert "source_file" not in audit["new_values_json"]


def test_confirm_revalidates_both_candidates_and_rolls_back_atomically() -> None:
    conn = db()
    _source_id, target_id, pair = seed_pair(conn)
    conn.execute(
        "UPDATE budget_transaction_candidates SET status='ignored' WHERE transaction_candidate_id=?",
        (target_id,),
    )
    conn.commit()

    with pytest.raises(HTTPException) as exc:
        confirm_transfer_pair(conn, pair["transfer_pair_id"])

    assert exc.value.status_code == 409
    assert conn.execute("SELECT COUNT(*) FROM budget_transfers").fetchone()[0] == 0
    assert conn.execute("SELECT COUNT(*) FROM budget_transactions").fetchone()[0] == 0
    assert conn.execute(
        "SELECT status FROM budget_transfer_pairs WHERE transfer_pair_id=?",
        (pair["transfer_pair_id"],),
    ).fetchone()[0] == "proposed"


def test_reject_does_not_consume_candidates_and_is_idempotent() -> None:
    conn = db()
    source_id, target_id, pair = seed_pair(conn)

    first = reject_transfer_pair(conn, pair["transfer_pair_id"], decision_by="synthetic-user", note="Not this pair")
    second = reject_transfer_pair(conn, pair["transfer_pair_id"], decision_by="synthetic-user")

    assert first["status"] == "rejected"
    assert second["status"] == "rejected"
    assert second["idempotent"] is True
    candidate_statuses = conn.execute(
        "SELECT status, linked_candidate_id FROM budget_transaction_candidates WHERE transaction_candidate_id IN (?, ?)",
        (source_id, target_id),
    ).fetchall()
    assert {row["status"] for row in candidate_statuses} == {"needs_review"}
    assert all(row["linked_candidate_id"] is None for row in candidate_statuses)
    assert conn.execute("SELECT COUNT(*) FROM budget_transactions").fetchone()[0] == 0
    assert conn.execute("SELECT COUNT(*) FROM audit_log WHERE action='transfer_pair_rejected'").fetchone()[0] == 1


def test_competing_pair_cannot_create_a_second_confirmed_transfer() -> None:
    conn = db()
    source_id, _target_id, pair = seed_pair(conn)
    insert_candidate(
        conn,
        candidate_id="other-target",
        source_type="akb_bank",
        account_source="OWN-AKB",
        signed_amount="100.00",
    )
    conn.execute(
        """
        INSERT INTO budget_transfer_pairs(
            transfer_pair_id, source_candidate_id, target_candidate_id, source_account_id,
            target_account_id, source_signed_amount, target_signed_amount, currency,
            source_booking_date, target_booking_date, status, quality_status,
            evidence_json, reason_codes_json, budget_effect_chf, created_at, updated_at
        )
        SELECT 'pair_competing', source_candidate_id, 'other-target', source_account_id,
               target_account_id, source_signed_amount, '100.00', currency,
               source_booking_date, target_booking_date, 'proposed', 'strong',
               evidence_json, reason_codes_json, '0', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
        FROM budget_transfer_pairs WHERE transfer_pair_id=?
        """,
        (pair["transfer_pair_id"],),
    )
    conn.commit()

    confirm_transfer_pair(conn, pair["transfer_pair_id"])
    with pytest.raises(HTTPException) as exc:
        confirm_transfer_pair(conn, "pair_competing")

    assert exc.value.status_code == 409
    assert conn.execute("SELECT COUNT(*) FROM budget_transfers").fetchone()[0] == 1


def test_api_contract_redacts_source_data_and_write_security_remains_active() -> None:
    conn = db()
    _source_id, _target_id, pair = seed_pair(conn)

    def override_db():
        yield conn

    app = create_app(write_mode="test")
    app.dependency_overrides[get_db] = override_db
    client = TestClient(app)

    openapi = client.get("/openapi.json").json()["paths"]
    assert "/api/budget/transfer-pairs" in openapi
    assert "/api/budget/transfer-pairs/{pair_id}/confirm" in openapi
    response = client.get("/api/budget/transfer-pairs")
    assert response.status_code == 200
    body = response.json()
    serialized = json.dumps(body)
    assert "OWN-" not in serialized
    assert "synthetic-run" not in serialized
    assert "source_file_label" not in serialized
    assert body[0]["confirmation_notice"].startswith("Als interner Transfer erkannt")

    confirmed = client.post(
        f"/api/budget/transfer-pairs/{pair['transfer_pair_id']}/confirm",
        json={"confirm": True, "note": "Synthetic UAT"},
    )
    assert confirmed.status_code == 200
    assert confirmed.json()["budget_effect_chf"] == "0"

    blocked_app = create_app(write_mode="disabled")
    blocked_app.dependency_overrides[get_db] = override_db
    blocked = TestClient(blocked_app)
    assert blocked.post("/api/budget/transfer-pairs/match", json={}).status_code == 403
