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) == 41
    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 "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 FROM budget_recurring_payments WHERE recurring_id=?", (cand["candidate_id"],)).fetchone()
    assert row["name"] == "Netflix Premium"
    assert row["expected_amount_text"] == "21.90"

    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_missing_amount_changed_and_duplicate_subscription() -> 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")
    preview2 = preview_manual_recurring_payment(conn, {"name": "Net flix", "category_id": media, "expected_amount_text": "19.90", "frequency": "monthly", "recurring_type": "subscription", "expected_day_of_month": 5})
    confirm_manual_recurring_payment(conn, preview2["payload"])
    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")
    statuses = {w["status"] for w in warnings}
    assert "missing" in statuses
    assert "amount_changed" in statuses
    assert "new_candidate" in statuses
    assert "possible_duplicate_subscription" in statuses
    assert next_expected_date(date(2026, 3, 5), "monthly", 5) == "2026-04-05"


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"})
