from __future__ import annotations

from pathlib import Path

from jarvis_finance.dashboard import data
from jarvis_finance.dashboard.pages import page_13_equity_manage
from jarvis_finance.market_data.candidates import (
    ProviderCandidate,
    export_mapping_decision_pack,
    generate_mapping_candidates,
    rank_candidate,
    recalculate_candidate_rankings,
    reject_mapping_candidate,
    select_mapping_candidate,
)
from jarvis_finance.storage.database import connect_memory
from jarvis_finance.storage.migrations import apply_migrations


class StaticLookupProvider:
    name = "static"

    def __init__(self, candidates_by_isin=None):
        self.candidates_by_isin = candidates_by_isin or {}

    def lookup(self, *, isin, name, ticker, exchange, currency):
        return list(self.candidates_by_isin.get(isin, []))


class FakeTab:
    def __enter__(self):
        return self

    def __exit__(self, *args):
        return False


class FakeSt:
    def title(self, *args, **kwargs): pass
    def caption(self, *args, **kwargs): pass
    def warning(self, *args, **kwargs): pass
    def subheader(self, *args, **kwargs): pass
    def write(self, *args, **kwargs): pass
    def success(self, *args, **kwargs): pass
    def error(self, *args, **kwargs): pass
    def info(self, *args, **kwargs): pass
    def divider(self, *args, **kwargs): pass
    def dataframe(self, *args, **kwargs): pass
    def table(self, *args, **kwargs): pass
    def tabs(self, labels): return [FakeTab() for _ in labels]
    def selectbox(self, label, options, **kwargs): return options[0] if options else None
    def text_input(self, *args, **kwargs): return kwargs.get("value", "")
    def text_area(self, *args, **kwargs): return ""
    def date_input(self, *args, **kwargs): return "2025-12-31"
    def checkbox(self, *args, **kwargs): return False
    def expander(self, *args, **kwargs): return FakeTab()
    def button(self, *args, **kwargs): return False


def setup_conn():
    conn = connect_memory()
    apply_migrations(conn)
    conn.execute("INSERT INTO platforms(platform_id,name,platform_type,default_currency,created_at) VALUES('p1','True Wealth','broker','CHF','now')")
    conn.execute("INSERT INTO accounts(account_id,platform_id,account_name,account_type,currency,created_at) VALUES('a1','p1','TW','brokerage','CHF','now')")
    conn.execute("INSERT INTO instruments(instrument_id,asset_class,name,ticker,isin,exchange,currency,created_at) VALUES('inst1','ETF','Global Equity ETF','GETF','CH0000000001',NULL,'USD','now')")
    conn.execute("INSERT INTO transactions(transaction_id,transaction_type,account_id,instrument_id,trade_date,quantity,currency_original,fx_status,source_type,is_confirmed,quality_status,created_at) VALUES('tx1','initial_position_snapshot','a1','inst1','2025-12-31','1','USD','missing','broker_import_reviewed_snapshot',1,'warning','now')")
    return conn


def test_candidate_ranking_prioritizes_isin_currency_exchange() -> None:
    inst = {"isin": "CH0000000001", "asset_class": "ETF", "name": "Global Equity ETF", "ticker": "GETF", "exchange": "SWX", "currency": "CHF", "trading_currency": "CHF"}
    good = ProviderCandidate(provider="openfigi", provider_symbol="GETF.SW", exchange="SWX", currency="CHF", name="Global Equity ETF", isin="CH0000000001", asset_class="etf", instrument_status="active", evidence_source="openfigi_isin_mapping")
    bad = ProviderCandidate(provider="yahoo", provider_symbol="GETF", exchange="NYSE", currency="USD", name="Global Equity ETF", isin="CH0000000001", asset_class="stock", evidence_source="yahoo_finance_search")
    good_rank = rank_candidate(inst, good, candidate_count=2)
    bad_rank = rank_candidate(inst, bad, candidate_count=2)
    assert good_rank.ranking_score > bad_rank.ranking_score
    assert "ISIN passt" in good_rank.ranking_reason
    assert "Währung passt" in good_rank.ranking_reason
    assert "currency_mismatch" in bad_rank.risk_flags


def test_same_isin_multiple_listings_remain_review_required_and_sorted() -> None:
    conn = setup_conn()
    conn.execute("UPDATE instruments SET currency='CHF', trading_currency='CHF' WHERE instrument_id='inst1'")
    provider = StaticLookupProvider({"CH0000000001": [
        ProviderCandidate(provider="openfigi", provider_symbol="GETF.US", exchange="NYSE", currency="USD", name="Global Equity ETF", isin="CH0000000001", asset_class="etf", instrument_status="active", evidence_source="openfigi_isin_mapping"),
        ProviderCandidate(provider="openfigi", provider_symbol="GETF.SW", exchange="SWX", currency="CHF", name="Global Equity ETF", isin="CH0000000001", asset_class="etf", instrument_status="active", evidence_source="openfigi_isin_mapping"),
    ]})
    generate_mapping_candidates(conn, provider=provider)
    summary = recalculate_candidate_rankings(conn)
    rows = data.get_instrument_mapping_candidates(conn)
    assert summary["multiple_listings_same_isin"] == 1
    assert {r["review_status"] for r in rows} == {"needs_manual_review"}
    assert int(rows[0]["ranking_score"]) >= int(rows[1]["ranking_score"])
    assert rows[0]["ranking_reason"]
    assert rows[0]["candidate_provider_symbol"] == "GETF.SW"


def test_decision_pack_runtime_only_contains_no_amount_fields(tmp_path) -> None:
    conn = setup_conn()
    provider = StaticLookupProvider({"CH0000000001": [
        ProviderCandidate(provider="openfigi", provider_symbol="GETF.SW", exchange="SWX", currency="CHF", name="Global Equity ETF", isin="CH0000000001", asset_class="etf", instrument_status="active", evidence_source="openfigi_isin_mapping"),
    ]})
    generate_mapping_candidates(conn, provider=provider)
    recalculate_candidate_rankings(conn)
    out = tmp_path / "runtime_exports" / "decision_pack.md"
    summary = export_mapping_decision_pack(conn, output_path=out, top_n=5)
    text = out.read_text(encoding="utf-8")
    assert summary["instrument_count"] == 1
    assert summary["candidate_rows"] == 1
    assert summary["top_candidates_present"] is True
    assert "Ranking Reason" in text
    forbidden = ["quantity", "amount", "market_value", "kontostand", "portfolioanteil", "price:"]
    assert not any(token.lower() in text.lower() for token in forbidden)


def test_candidate_confirmation_resolves_mapping_alerts_and_writes_catalog() -> None:
    conn = setup_conn()
    for rule in ["missing_provider_symbol", "ambiguous_provider_mapping", "candidate_review_required"]:
        conn.execute("INSERT INTO alerts(alert_id,priority,category,entity_type,entity_id,rule_id,message,status,created_at) VALUES(?,?,?,?,?,?,?,?,?)", (rule, "warnung", "market_data", "instrument", "inst1", rule, rule, "active", "now"))
    provider = StaticLookupProvider({"CH0000000001": [ProviderCandidate(provider="openfigi", provider_symbol="GETF.SW", exchange="SWX", currency="CHF", name="Global Equity ETF", isin="CH0000000001", asset_class="etf", instrument_status="active", evidence_source="openfigi_isin_mapping")]})
    generate_mapping_candidates(conn, provider=provider)
    candidate_id = conn.execute("SELECT candidate_id FROM instrument_price_mapping_candidates").fetchone()["candidate_id"]
    select_mapping_candidate(conn, candidate_id=candidate_id, note="manual dashboard confirmation")
    assert conn.execute("SELECT COUNT(*) AS c FROM audit_log WHERE action='select_mapping_candidate'").fetchone()["c"] == 1
    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id IN ('missing_provider_symbol','ambiguous_provider_mapping','candidate_review_required')").fetchone()["c"] == 0
    assert conn.execute("SELECT provider_symbol, exchange, trading_currency FROM instrument_price_mappings WHERE instrument_id='inst1'").fetchone()["provider_symbol"] == "GETF.SW"
    assert conn.execute("SELECT COUNT(*) AS c FROM instrument_catalog_entries WHERE provider_symbol='GETF.SW'").fetchone()["c"] == 1


def test_candidate_rejection_writes_audit() -> None:
    conn = setup_conn()
    provider = StaticLookupProvider({"CH0000000001": [ProviderCandidate(provider="openfigi", provider_symbol="GETF.SW", exchange="SWX", currency="CHF", isin="CH0000000001")]})
    generate_mapping_candidates(conn, provider=provider)
    candidate_id = conn.execute("SELECT candidate_id FROM instrument_price_mapping_candidates").fetchone()["candidate_id"]
    reject_mapping_candidate(conn, candidate_id=candidate_id, note="wrong listing")
    assert conn.execute("SELECT COUNT(*) AS c FROM audit_log WHERE action='reject_mapping_candidate'").fetchone()["c"] == 1
    assert conn.execute("SELECT review_status FROM instrument_price_mapping_candidates WHERE candidate_id=?", (candidate_id,)).fetchone()["review_status"] == "rejected"


def test_dashboard_review_ui_renders_ranked_candidates_without_live_api() -> None:
    conn = setup_conn()
    provider = StaticLookupProvider({"CH0000000001": [ProviderCandidate(provider="openfigi", provider_symbol="GETF.SW", exchange="SWX", currency="CHF", isin="CH0000000001")]})
    generate_mapping_candidates(conn, provider=provider)
    recalculate_candidate_rankings(conn)
    page_13_equity_manage.render(FakeSt(), conn)


def test_personal_finance_backlog_document_exists_without_implementation() -> None:
    doc = Path("docs/personal-finance-cashflow-budgeting-backlog.md")
    assert doc.exists()
    text = doc.read_text(encoding="utf-8")
    assert "Backlog only" in text
    assert "Do not implement" in text
    assert not Path("src/jarvis_finance/personal_finance").exists()
