from __future__ import annotations

import pytest
from fastapi import HTTPException

from jarvis_finance.services.budget_accounts import confirm_create_budget_account
from jarvis_finance.services.budget_categories import confirm_create_category
from jarvis_finance.services.budget_csv_imports import import_budget_csv_text
from jarvis_finance.services.budget_imports import (
    batch_mark_candidates,
    confirm_duplicate_override,
    confirm_transaction_candidate,
    list_transaction_candidates,
    preview_batch_confirm_candidates,
)
from jarvis_finance.storage.database import connect_memory
from jarvis_finance.storage.migrations import apply_migrations


def db():
    conn = connect_memory()
    apply_migrations(conn)
    return conn


def setup_base(conn):
    account = confirm_create_budget_account(conn, {"name": "Budget Konto", "account_type": "cash", "currency": "CHF"})["entity_id"]
    food = confirm_create_category(conn, {"name": "Essen & Haushalt", "category_type": "expense"})["entity_id"]
    return account, food


def candidate(conn, cid: str, *, status: str = "pending", category_id: str | None = None, description: str = "M Windisch", source_type: str = "migros_receipts") -> 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, currency_original,
            proposed_category_id, proposed_category_name, confidence, requires_review, status,
            classification, review_reason, raw_fingerprint, created_at, updated_at
        ) VALUES (?, '2026 Migros Einkäufe.csv', ?, ?, '2026-03-20', ?, 'M Windisch', '0.40', 'CHF', ?,
            CASE WHEN ? IS NULL THEN NULL ELSE 'Essen & Haushalt' END, '0.80', 0, ?, 'migros_receipt_food_household',
            'duplicate_detection_v1', ?, '2026-01-01T00:00:00', '2026-01-01T00:00:00')
        """,
        (cid, cid, source_type, description, category_id, category_id, status, cid),
    )
    conn.commit()


def test_normal_review_and_migros_tab_exclude_duplicates_but_duplicates_tab_shows_them() -> None:
    conn = db(); _account, food = setup_base(conn)
    candidate(conn, "open1", status="pending", category_id=food)
    candidate(conn, "dup1", status="duplicate_candidate", category_id=food)
    candidate(conn, "blocked1", status="duplicate_blocked", category_id=food)
    candidate(conn, "possible1", status="possible_duplicate", category_id=food)

    normal_ids = {r["transaction_candidate_id"] for r in list_transaction_candidates(conn, budget_year="2026")}
    migros_ids = {r["transaction_candidate_id"] for r in list_transaction_candidates(conn, tab="migros", budget_year="2026")}
    duplicate_ids = {r["transaction_candidate_id"] for r in list_transaction_candidates(conn, tab="duplicates", budget_year="2026")}

    assert normal_ids == {"open1"}
    assert migros_ids == {"open1"}
    assert duplicate_ids == {"dup1", "blocked1", "possible1"}


@pytest.mark.parametrize("status", ["duplicate_candidate", "duplicate_blocked", "possible_duplicate", "superseded", "covered_by_migros", "ignored"])
def test_normal_confirm_guard_blocks_duplicate_and_covered_statuses(status: str) -> None:
    conn = db(); account, food = setup_base(conn)
    candidate(conn, f"cand_{status}", status=status, category_id=food)

    with pytest.raises(HTTPException) as exc:
        confirm_transaction_candidate(conn, f"cand_{status}", {"account_id": account, "category_id": food, "budget_year": "2026"})

    assert exc.value.status_code == 409
    assert "Kandidat ist als Duplikat/covered/ignored markiert" in str(exc.value.detail)
    assert conn.execute("SELECT COUNT(*) FROM budget_transactions").fetchone()[0] == 0


def test_duplicate_override_requires_reason_and_audits_explicit_booking() -> None:
    conn = db(); account, food = setup_base(conn)
    candidate(conn, "dup1", status="duplicate_candidate", category_id=food)

    with pytest.raises(HTTPException) as exc:
        confirm_duplicate_override(conn, "dup1", {"account_id": account, "category_id": food, "budget_year": "2026"})
    assert exc.value.status_code == 422

    result = confirm_duplicate_override(conn, "dup1", {"account_id": account, "category_id": food, "budget_year": "2026", "reason": "Bewusst separater Bon trotz Treffer"})

    assert result["status"] == "confirmed"
    assert conn.execute("SELECT COUNT(*) FROM budget_transactions").fetchone()[0] == 1
    audit_actions = {r[0] for r in conn.execute("SELECT action FROM audit_log").fetchall()}
    assert "candidate_duplicate_override_confirmed" in audit_actions


def test_batch_confirm_skips_duplicates_and_reports_per_candidate_error() -> None:
    conn = db(); account, food = setup_base(conn)
    candidate(conn, "ok1", status="pending", category_id=food, description="Coop Einkauf", source_type="visa_credit_card")
    candidate(conn, "dup1", status="duplicate_blocked", category_id=food)

    preview = preview_batch_confirm_candidates(conn, candidate_ids=["ok1", "dup1"], account_id=account, budget_year="2026")
    assert preview["count"] == 1
    assert preview["can_confirm"] is False
    assert preview["conflicts"] == [{"candidate_id": "dup1", "reason": "duplicate_candidate_blocked"}]

    result = batch_mark_candidates(conn, candidate_ids=["ok1", "dup1"], action="confirm", account_id=account, preview_id=preview["preview_id"], budget_year="2026")
    assert result["confirmed_count"] == 1
    assert result["skipped_count"] == 1
    assert result["errors"] == [{"candidate_id": "dup1", "error": "duplicate_candidate_blocked"}]
    assert conn.execute("SELECT status FROM budget_transaction_candidates WHERE transaction_candidate_id='dup1'").fetchone()[0] == "duplicate_blocked"


def test_migros_receipt_key_import_is_idempotent_across_reimports_and_article_rows_are_detail_only() -> None:
    conn = db(); _account, food = setup_base(conn)
    csv_text = "Datum;Zeit;Filiale;Kassennummer;Transaktionsnummer;Artikel;Menge;Aktion;Umsatz\n02.01.2026;10:00;Migros Test;1;99;Artikel A;1;;1.10\n02.01.2026;10:00;Migros Test;1;99;Artikel B;1;;2.20\n"

    first = import_budget_csv_text(conn, "migros_receipts", csv_text, source_file_label="2026 Migros Einkäufe.csv", dry_run=False, default_category_id=food)
    second = import_budget_csv_text(conn, "migros_receipts", csv_text, source_file_label="same receipt exported again.csv", dry_run=False, default_category_id=food)

    assert first["candidate_count"] == 1
    assert second["candidate_count"] == 0
    assert conn.execute("SELECT COUNT(*) FROM budget_transaction_candidates WHERE source_type IN ('migros_receipts','migros_receipt')").fetchone()[0] == 1
    assert conn.execute("SELECT COUNT(*) FROM budget_import_line_items").fetchone()[0] == 2
