from __future__ import annotations

from datetime import date

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_recurring import (
    archive_recurring_payment,
    confirm_manual_recurring_payment,
    confirm_recurring_candidate_action,
    detect_recurring_payment_candidates,
    get_recurring_dashboard,
    list_recurring_warnings,
    next_expected_date,
    preview_manual_recurring_payment,
    preview_recurring_candidate_action,
)
from jarvis_finance.services.budget_transactions import confirm_budget_transaction
from jarvis_finance.storage.database import connect_memory
from jarvis_finance.storage.migrations import apply_migrations, get_schema_version


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


def setup(conn):
    account_id = confirm_create_budget_account(conn, {"name": "Haushalt", "account_type": "checking", "currency": "CHF"})["entity_id"]
    media = confirm_create_category(conn, {"name": "Elektronische Medien", "category_type": "expense"})["entity_id"]
    food = confirm_create_category(conn, {"name": "Essen & Haushalt", "category_type": "expense"})["entity_id"]
    insurance = confirm_create_category(conn, {"name": "Versicherungen", "category_type": "expense"})["entity_id"]
    return account_id, media, food, insurance


def tx(conn, account_id, category_id, merchant, amount, day, desc=None, tx_type="expense"):
    return confirm_budget_transaction(conn, {"account_id": account_id, "transaction_type": tx_type, "transaction_date": day, "description": desc or merchant, "payee": merchant, "amount_original": amount, "currency_original": "CHF", "category_id": category_id})["entity_id"]


def candidate(conn, category_id, merchant, amount, day, status="pending", classification=None):
    conn.execute("""
        INSERT INTO budget_transaction_candidates(transaction_candidate_id, source_file_label, source_type, transaction_date, description, merchant, amount_original, currency_original, proposed_category_id, confidence, requires_review, status, classification, created_at)
        VALUES (?, 'test.csv', 'csv_seed', ?, ?, ?, ?, 'CHF', ?, '0.80', 1, ?, ?, '2026-01-01T00:00:00Z')
    """, (f"cand_{merchant}_{day}".replace(' ', '_'), day, merchant, merchant, amount, category_id, status, classification))


def test_schema_38_creates_decimal_text_recurring_table_with_user_facing_fields() -> None:
    conn = db()
    assert get_schema_version(conn) == 53
    cols = {r["name"]: r["type"] for r in conn.execute("PRAGMA table_info(budget_recurring_payments)").fetchall()}
    assert cols["expected_amount_text"].upper() == "TEXT"
    assert cols["amount_tolerance_pct"].upper() == "TEXT"
    assert cols["tolerance_percent"].upper() == "TEXT"
    assert cols["tolerance_amount_text"].upper() == "TEXT"
    assert cols["merchant_name"].upper() == "TEXT"
    assert cols["recurring_type"].upper() == "TEXT"
    assert cols["planning_cadence"].upper() == "TEXT"
    assert cols["due_months_json"].upper() == "TEXT"
    assert cols["data_version"].upper() == "INTEGER"
    assert "ignored" in conn.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='budget_recurring_payments'").fetchone()["sql"]


def test_detection_finds_monthly_subscription_yearly_insurance_quarterly_payment_and_excludes_migros_transfer_investment() -> None:
    conn = db(); account, media, food, insurance = setup(conn)
    for d in ["2026-01-05", "2026-02-05", "2026-03-05"]: tx(conn, account, media, "Netflix", "19.90", d)
    for d in ["2025-03-01", "2026-03-01"]: tx(conn, account, insurance, "House Insurance", "1200.00", d)
    for d in ["2026-01-15", "2026-04-15", "2026-07-15"]: tx(conn, account, media, "Music School", "300.00", d)
    for d in ["2026-01-03", "2026-02-03", "2026-03-03"]: tx(conn, account, food, "Migros", "150.00", d)
    tx(conn, account, insurance, "True Wealth", "500.00", "2026-01-10", desc="Investment Transfer")
    candidate(conn, media, "OpenAI", "20.00", "2026-04-05")

    result = detect_recurring_payment_candidates(conn, today="2026-08-01")
    names = {c["name"]: c for c in result["candidates"]}

    assert names["Netflix"]["frequency"] == "monthly"
    assert names["Netflix"]["recurring_type"] == "subscription"
    assert names["House Insurance"]["frequency"] == "yearly"
    assert names["House Insurance"]["recurring_type"] == "fixed_cost"
    assert names["Music School"]["frequency"] == "quarterly"
    assert "OpenAI" not in names
    assert "Migros" not in names
    assert "True Wealth" not in names


def test_detection_finds_irregular_recurring_candidate_but_does_not_auto_activate() -> None:
    conn = db(); account, media, _food, _insurance = setup(conn)
    for d in ["2026-01-03", "2026-02-20", "2026-04-11"]:
        tx(conn, account, media, "OpenAI ChatGPT", "20.00", d)

    result = detect_recurring_payment_candidates(conn, today="2026-05-01")
    names = {c["name"]: c for c in result["candidates"]}

    assert names["OpenAI ChatGPT"]["frequency"] == "irregular"
    assert names["OpenAI ChatGPT"]["recurring_type"] == "subscription"
    row = conn.execute("SELECT status FROM budget_recurring_payments WHERE name='OpenAI ChatGPT'").fetchone()
    assert row["status"] == "candidate"
    assert conn.execute("SELECT COUNT(*) FROM budget_transactions WHERE source_type='recurring_payment'").fetchone()[0] == 0


def test_dashboard_read_does_not_silently_create_detected_candidates() -> None:
    conn = db(); account, media, _food, _insurance = setup(conn)
    for d in ["2026-01-05", "2026-02-05", "2026-03-05"]:
        tx(conn, account, media, "Netflix", "19.90", d)

    dashboard = get_recurring_dashboard(conn, today="2026-04-01")

    assert dashboard["candidates"] == []
    assert conn.execute("SELECT COUNT(*) FROM budget_recurring_payments").fetchone()[0] == 0


def test_review_activate_ignore_manual_archive_and_audit() -> None:
    conn = db(); account, media, _food, _insurance = setup(conn)
    for d in ["2026-01-05", "2026-02-05", "2026-03-05"]: tx(conn, account, media, "Netflix", "19.90", d)
    cand = detect_recurring_payment_candidates(conn, today="2026-04-01")["candidates"][0]

    preview = preview_recurring_candidate_action(conn, cand["candidate_id"], {"action": "activate", "recurring_type": "subscription"})
    assert preview["requires_explicit_confirm"] is True
    result = confirm_recurring_candidate_action(conn, preview["payload"])
    assert result["status"] == "confirmed"
    active = get_recurring_dashboard(conn, today="2026-04-10")["active"]
    assert active[0]["status"] == "active"
    assert active[0]["budget_month_chf"] == "19.90"
    assert active[0]["budget_year_chf"] == "238.80"
    assert conn.execute("SELECT COUNT(*) FROM audit_log WHERE action LIKE 'recurring_%'").fetchone()[0] >= 1

    manual_preview = preview_manual_recurring_payment(conn, {"name": "Manual Abo", "category_id": media, "expected_amount_text": "10.00", "frequency": "monthly", "recurring_type": "subscription", "expected_day_of_month": 7})
    manual = confirm_manual_recurring_payment(conn, manual_preview["payload"])
    assert manual["status"] == "confirmed"
    assert archive_recurring_payment(conn, manual["entity_id"], {"notes": "not needed"})["status"] == "archived"

    ignored_preview = preview_recurring_candidate_action(conn, cand["candidate_id"], {"action": "ignore"})
    ignored = confirm_recurring_candidate_action(conn, ignored_preview["payload"])
    assert ignored["status"] == "ignored"


def test_confirmed_candidate_can_be_edited_paused_and_archived_with_audit() -> None:
    conn = db(); account, media, _food, _insurance = setup(conn)
    for d in ["2026-01-05", "2026-02-05", "2026-03-05"]: tx(conn, account, media, "Netflix", "19.90", d)
    cand = detect_recurring_payment_candidates(conn, today="2026-04-01")["candidates"][0]
    preview = preview_recurring_candidate_action(conn, cand["candidate_id"], {"action": "activate", "recurring_type": "subscription"})
    confirm_recurring_candidate_action(conn, preview["payload"])

    edit_preview = preview_recurring_candidate_action(conn, cand["candidate_id"], {"action": "edit", "name": "Netflix Premium", "expected_amount_text": "21.90", "frequency": "monthly", "recurring_type": "subscription"})
    edited = confirm_recurring_candidate_action(conn, edit_preview["payload"])
    assert edited["status"] == "confirmed"
    row = conn.execute("SELECT name, expected_amount_text, periodicity_status FROM budget_recurring_payments WHERE recurring_id=?", (cand["candidate_id"],)).fetchone()
    assert row["name"] == "Netflix Premium"
    assert row["expected_amount_text"] == "21.90"
    assert row["periodicity_status"] == "confirmed"

    pause_preview = preview_recurring_candidate_action(conn, cand["candidate_id"], {"action": "pause"})
    paused = confirm_recurring_candidate_action(conn, pause_preview["payload"])
    assert paused["status"] == "paused"
    archived = archive_recurring_payment(conn, cand["candidate_id"], {"notes": "done"})
    assert archived["status"] == "archived"
    assert conn.execute("SELECT COUNT(*) FROM audit_log WHERE action LIKE 'recurring_%'").fetchone()[0] >= 4


def test_warnings_are_one_actionable_card_per_contract_and_bundle_reasons() -> None:
    conn = db(); account, media, _food, _insurance = setup(conn)
    preview = preview_manual_recurring_payment(conn, {"name": "Netflix", "category_id": media, "expected_amount_text": "19.90", "frequency": "monthly", "recurring_type": "subscription", "expected_day_of_month": 5, "amount_tolerance_pct": "5", "date_tolerance_days": 3})
    recurring_id = confirm_manual_recurring_payment(conn, preview["payload"])["entity_id"]
    tx(conn, account, media, "Netflix", "25.00", "2026-03-05")
    for d in ["2026-01-10", "2026-02-10", "2026-03-10"]: tx(conn, account, media, "Spotify", "12.00", d)
    detect_recurring_payment_candidates(conn, today="2026-04-01")

    warnings = list_recurring_warnings(conn, today="2026-05-20")
    netflix_cards = [w for w in warnings if w["recurring_id"] == recurring_id]
    assert len(netflix_cards) == 1
    assert {reason["code"] for reason in netflix_cards[0]["reasons"]} == {"missing", "amount_changed"}
    assert netflix_cards[0]["data_through"] == "2026-03-10"
    assert netflix_cards[0]["actions"] == ["contract_review", "adjust_period", "mark_paused", "accept_new_amount", "ignore_exception", "open_transactions"]
    assert "priority" not in netflix_cards[0]
    assert any(w["status"] == "new_candidate" for w in warnings)
    assert len({w["recurring_id"] for w in warnings}) == len(warnings)
    assert next_expected_date(date(2026, 3, 5), "monthly", 5) == "2026-04-05"


def test_single_autobahnvignette_is_never_monthly_or_twelve_budgeted_payments() -> None:
    conn = db(); account, media, _food, _insurance = setup(conn)
    tx(conn, account, media, "Autobahnvignette 2026", "12.00", "2026-05-12")

    candidate_item = detect_recurring_payment_candidates(conn, today="2026-08-01")["candidates"][0]

    assert candidate_item["frequency"] == "yearly"
    assert candidate_item["periodicity_status"] == "semantic_proposal"
    assert candidate_item["next_expected_date"] is None
    assert "muss bestätigt" in candidate_item["periodicity_rationale"]
    stored = conn.execute("SELECT frequency, planning_cadence, periodicity_status, next_expected_date FROM budget_recurring_payments").fetchone()
    assert stored["frequency"] == "yearly"
    assert stored["planning_cadence"] == "yearly"
    assert stored["periodicity_status"] == "semantic_proposal"
    assert stored["next_expected_date"] is None


def test_edited_quarterly_user_choice_recalculates_and_is_confirmed_with_binding() -> None:
    conn = db(); account, media, _food, _insurance = setup(conn)
    tx(conn, account, media, "Autobahnvignette 2026", "12.00", "2026-05-12")
    candidate_item = detect_recurring_payment_candidates(conn, today="2026-08-01")["candidates"][0]

    preview = preview_recurring_candidate_action(conn, candidate_item["candidate_id"], {
        "action": "activate", "name": "Vignette Testvertrag", "planning_cadence": "quarterly",
        "planning_type": "fixed_cost", "expected_amount_text": "12.00", "due_months": [2, 5, 8, 11],
    })
    assert preview["review"]["annual_effect_chf"] == "48.00"
    assert preview["review"]["monthly_reserve_chf"] == "4.00"
    tampered = {**preview["payload"], "values": {**preview["payload"]["values"], "name": "Manipuliert"}}
    with pytest.raises(HTTPException) as stale_fingerprint:
        confirm_recurring_candidate_action(conn, tampered)
    assert stale_fingerprint.value.status_code == 409
    result = confirm_recurring_candidate_action(conn, preview["payload"])
    assert result["status"] == "confirmed"
    with pytest.raises(HTTPException) as stale_version:
        confirm_recurring_candidate_action(conn, preview["payload"])
    assert stale_version.value.status_code == 409
    stored = conn.execute("SELECT name, planning_cadence, due_months_json, user_override, periodicity_status FROM budget_recurring_payments").fetchone()
    assert stored["name"] == "Vignette Testvertrag"
    assert stored["planning_cadence"] == "quarterly"
    assert stored["due_months_json"] == "[2, 5, 8, 11]"
    assert stored["user_override"] == 1
    assert stored["periodicity_status"] == "confirmed"
    audit = conn.execute("SELECT new_values_json FROM audit_log WHERE action='recurring_activate_confirmed'").fetchone()
    assert preview["preview_fingerprint"] in audit["new_values_json"]


def test_integrations_dashboard_budgetplanning_and_data_explorer_fields() -> None:
    conn = db(); account, media, _food, _insurance = setup(conn)
    preview = preview_manual_recurring_payment(conn, {"name": "Netflix", "category_id": media, "expected_amount_text": "20.00", "frequency": "monthly", "recurring_type": "subscription", "expected_day_of_month": 5})
    confirm_manual_recurring_payment(conn, preview["payload"])
    tx(conn, account, media, "Netflix", "20.00", "2026-02-05")

    dash = get_recurring_dashboard(conn, today="2026-03-01")
    assert dash["analytics"]["fixed_cost_quote_percent"] is not None
    assert dash["analytics"]["subscriptions_year_chf"] == "240.00"
    assert dash["kpis"]["monthly_fixed_costs_chf"] == "20.00"
    assert dash["kpis"]["annual_fixed_costs_chf"] == "240.00"
    assert dash["kpis"]["active_recurring_count"] == 1
    assert dash["kpis"]["open_candidate_count"] == 0
    assert dash["kpis"]["largest_fixed_costs"][0]["name"] == "Netflix"
    assert dash["category_breakdown"][0]["known_recurring_month_chf"] == "20.00"

    from jarvis_finance.services.budget_planning import get_budget_planning_matrix
    row = next(r for r in get_budget_planning_matrix(conn, year="2026", current_month="2026-02")["rows"] if r["category_id"] == media)
    assert row["known_recurring_month_chf"] == "20.00"
    assert "fixed_cost_quote_percent" in row

    from jarvis_finance.services.budget_data_explorer import get_budget_data_explorer
    explorer = get_budget_data_explorer(conn, year="2026", recurring_type="subscription")
    assert explorer["filters"]["recurring_type"] == "subscription"
    assert all(r["recurring_type"] == "subscription" for r in explorer["transactions"])
    from jarvis_finance.services.budget_overview import get_budget_overview
    overview = get_budget_overview(conn, month="2026-02")
    assert overview["fixed_costs_month_chf"] == "20.00"
    assert overview["open_fixed_cost_candidate_count"] == 0
    assert "missing_fixed_cost_payment_count" in overview


def test_invalid_manual_recurring_values_blocked() -> None:
    conn = db(); _account, media, _food, _insurance = setup(conn)
    with pytest.raises(HTTPException):
        preview_manual_recurring_payment(conn, {"name": "Bad", "category_id": media, "expected_amount_text": "-1", "frequency": "monthly", "recurring_type": "subscription"})
