from __future__ import annotations

import jarvis_finance.services.budget_overview as budget_overview_service
from jarvis_finance.services.budget_accounts import confirm_create_budget_account
from jarvis_finance.services.budget_analytics_v2 import (
    get_budget_dashboard_cockpit,
    get_budget_vs_actual_analysis,
)
from jarvis_finance.services.budget_categories import confirm_create_category
from jarvis_finance.services.budget_overview import (
    get_budget_charts_2026,
    get_budget_status_by_category,
    get_category_month_detail,
    get_category_summary,
)
from jarvis_finance.services.budget_plans import confirm_budget_plan_item
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


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


def seed_budget(conn):
    account = confirm_create_budget_account(conn, {"name": "Haushalt", "account_type": "checking", "currency": "CHF"})["entity_id"]
    food = confirm_create_category(conn, {"name": "Essen & Haushalt", "category_type": "expense", "color": "#22c55e", "icon": "basket", "sort_order": 10})["entity_id"]
    health = confirm_create_category(conn, {"name": "Gesundheit / Medizin", "category_type": "expense", "color": "#ef4444", "icon": "heart", "sort_order": 20})["entity_id"]
    salary = confirm_create_category(conn, {"name": "Lohn Marcel", "category_type": "income", "color": "#38bdf8", "icon": "wallet", "sort_order": 1})["entity_id"]
    confirm_budget_plan_item(conn, {"name": "Food Plan", "category_id": food, "plan_month": "2026-01", "monthly_amount_chf": "100.00", "cadence": "monthly", "source_type": "manual"})
    confirm_budget_plan_item(conn, {"name": "Health Plan", "category_id": health, "plan_month": "2026-01", "monthly_amount_chf": "50.00", "cadence": "monthly", "source_type": "manual"})
    confirm_budget_plan_item(conn, {"name": "Salary Plan", "category_id": salary, "plan_month": "2026-01", "monthly_amount_chf": "300.00", "cadence": "monthly", "source_type": "manual"})
    return account, food, health, salary


def seed_transactions(conn, account: str, food: str, health: str, salary: str) -> None:
    confirm_budget_transaction(conn, {"account_id": account, "transaction_type": "income", "transaction_date": "2026-05-25", "description": "Lohn", "amount_original": "300.00", "currency_original": "CHF", "category_id": salary})
    confirm_budget_transaction(conn, {"account_id": account, "transaction_type": "expense", "transaction_date": "2026-05-03", "description": "Migros", "payee": "Migros", "amount_original": "120.00", "currency_original": "CHF", "category_id": food})
    confirm_budget_transaction(conn, {"account_id": account, "transaction_type": "expense", "transaction_date": "2026-05-10", "description": "Apotheke", "payee": "Apotheke", "amount_original": "40.00", "currency_original": "CHF", "category_id": health})
    confirm_budget_transaction(conn, {"account_id": account, "transaction_type": "expense", "transaction_date": "2026-02-10", "description": "Migros Feb", "amount_original": "80.00", "currency_original": "CHF", "category_id": food})
    confirm_budget_transaction(conn, {"account_id": account, "transaction_type": "expense", "transaction_date": "2025-05-10", "description": "Altjahr", "amount_original": "999.00", "currency_original": "CHF", "category_id": food})
    # Transfers and open candidates must not count as income/expense actuals.
    conn.execute("INSERT INTO budget_transactions(budget_transaction_id, account_id, category_id, transaction_date, transaction_type, description, amount_original, currency_original, amount_chf, fx_status, source_type, status, created_at, updated_at) VALUES ('tx_transfer', ?, NULL, '2026-05-11', 'transfer', 'Umbuchung', '777.00', 'CHF', '777.00', 'not_needed', 'manual', 'confirmed', '2026', '2026')", (account,))
    conn.execute("INSERT INTO budget_transaction_candidates(transaction_candidate_id, source_type, source_file_label, transaction_date, description, merchant, amount_original, currency_original, proposed_category_id, status, created_at, updated_at) VALUES ('cand_open','csv','x','2026-05-12','Candidate','Migros','888.00','CHF',?,'pending','2026','2026')", (food,))
    conn.execute("INSERT INTO budget_transaction_candidates(transaction_candidate_id, source_type, source_file_label, transaction_date, description, merchant, amount_original, currency_original, proposed_category_id, status, created_at, updated_at) VALUES ('cand_covered','csv','x','2026-05-13','Covered Migros','Migros','999.00','CHF',?,'covered_by_migros','2026','2026')", (food,))
    conn.commit()


def test_budget_dashboard_cockpit_is_monthly_and_excludes_candidates_transfers_and_2025(
    budget_reference_date,
) -> None:
    conn = db()
    account, food, health, salary = seed_budget(conn)
    seed_transactions(conn, account, food, health, salary)
    result = get_budget_dashboard_cockpit(conn, month="2026-05")
    assert result["purpose"] == "monthly_cockpit"
    assert result["month"] == "2026-05"
    assert result["kpis"]["income_current_month_chf"] == "300.00"
    assert result["kpis"]["expense_current_month_chf"] == "160.00"
    assert result["kpis"]["net_cashflow_current_month_chf"] == "140.00"
    assert result["kpis"]["budget_consumption_percent"] == "106.67"
    assert result["kpis"]["savings_rate_percent"] == "46.67"
    assert result["open_todos"]["review_candidates"] == 0
    assert all("888.00" not in str(section) and "999.00" not in str(section) and "777.00" not in str(section) for section in [result])
    assert result["top_expense_categories"][0]["category"] == "Essen & Haushalt"
    assert result["critical_categories"]
    assert result["monthly_cashflow"]["income_chf"] == "300.00"


def test_budget_status_rows_are_operational_category_control_with_monthly_detail(
    budget_reference_date,
) -> None:
    conn = db()
    account, food, health, salary = seed_budget(conn)
    seed_transactions(conn, account, food, health, salary)
    rows = get_budget_status_by_category(conn, year="2026")
    food_row = next(r for r in rows if r["category_id"] == food)
    assert food_row["purpose"] == "category_control"
    assert food_row["budget_month"] == "100.00"
    assert food_row["budget_year"] == "1200.00"
    assert food_row["actual_current_month"] == "120.00"
    assert food_row["actual_year_to_date"] == "200.00"
    assert len(food_row["monthly_analysis"]) == 12
    assert food_row["monthly_analysis"][4]["actual_chf"] == "120.00"
    assert "999.00" not in str(food_row)


def test_budget_vs_actual_analysis_is_separate_chart_viewmodel(budget_reference_date) -> None:
    conn = db()
    account, food, health, salary = seed_budget(conn)
    seed_transactions(conn, account, food, health, salary)
    result = get_budget_vs_actual_analysis(conn, year="2026")
    assert result["purpose"] == "plan_vs_reality_analysis"
    assert result["year"] == "2026"
    assert result["chart_library"] == "native-svg"
    assert result["empty_state"] is False
    assert result["budget_vs_actual_by_category"]
    assert result["top_overruns"][0]["category"] == "Essen & Haushalt"
    assert result["cashflow_by_month"][4]["month"] == "2026-05"
    assert result["cashflow_by_month"][4]["net_cashflow_chf"] == "140.00"
    assert result["category_donut"][0]["category"] == "Essen & Haushalt"
    assert result["trend_by_month"][4]["cumulative_net_chf"] == "60.00"
    assert "777.00" not in str(result)
    assert "888.00" not in str(result)
    assert "999.00" not in str(result)


def test_year_charts_reuse_one_canonical_effect_scan(monkeypatch, budget_reference_date) -> None:
    conn = db()
    account, food, health, salary = seed_budget(conn)
    seed_transactions(conn, account, food, health, salary)
    original = budget_overview_service.list_household_financial_effects
    calls = 0

    def counted(*args, **kwargs):
        nonlocal calls
        calls += 1
        return original(*args, **kwargs)

    monkeypatch.setattr(budget_overview_service, "list_household_financial_effects", counted)
    result = get_budget_charts_2026(conn, year="2026")
    assert calls == 1
    assert result["cashflow_by_month"]


def test_changed_analytics_and_category_contracts_expose_partial_financial_data(
    budget_reference_date,
) -> None:
    conn = db()
    account, food, _, _ = seed_budget(conn)
    confirm_budget_transaction(conn, {
        "account_id": account,
        "transaction_type": "expense",
        "transaction_date": "2026-05-20",
        "description": "Synthetic missing FX",
        "amount_original": "50.00",
        "currency_original": "EUR",
        "category_id": food,
    })

    cockpit = get_budget_dashboard_cockpit(conn, month="2026-05")
    assert cockpit["data_status"] == "partial"
    assert cockpit["unavailable_chf_count"] == 1
    assert cockpit["warnings"][0]["code"] == "missing_chf_conversion"

    analysis = get_budget_vs_actual_analysis(conn, year="2026")
    assert analysis["data_status"] == "partial"
    assert analysis["unavailable_chf_count"] == 1
    assert analysis["warnings"][0]["code"] == "missing_chf_conversion"
    assert analysis["totals"]["data_status"] == "partial"

    categories = get_category_summary(conn, "2026-05")
    assert categories["categories"] == []
    assert categories["data_status"] == "partial"
    assert categories["unavailable_chf_count"] == 1
    assert categories["warnings"][0]["code"] == "missing_chf_conversion"

    status_row = next(row for row in get_budget_status_by_category(conn, year="2026") if row["category_id"] == food)
    assert status_row["actual_current_month"] == "0.00"
    assert status_row["actual_year_to_date"] == "0.00"
    assert status_row["forecast_year"] == "0.00"
    assert status_row["status"] != "Grün"
    assert status_row["data_status"] == "partial"
    assert status_row["warnings"][0]["code"] == "missing_chf_conversion"

    detail = get_category_month_detail(conn, category_id=food, month="2026-05")
    assert detail["actual_chf"] == "0.00"
    assert detail["data_status"] == "partial"
    assert detail["warnings"][0]["code"] == "missing_chf_conversion"
