from __future__ import annotations

from collections.abc import Iterator
import sqlite3
from decimal import Decimal
from sqlite3 import Connection

from fastapi.testclient import TestClient

from jarvis_finance.api.dependencies import get_db
from jarvis_finance.api.main import create_app
from jarvis_finance.storage.migrations import apply_migrations


def _connect() -> Connection:
    conn = sqlite3.connect(":memory:", check_same_thread=False)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA foreign_keys = ON")
    return conn


def _client(conn: Connection) -> TestClient:
    app = create_app()

    def override_db() -> Iterator[Connection]:
        yield conn

    app.dependency_overrides[get_db] = override_db
    return TestClient(app)


def _seed(conn: Connection) -> None:
    now = "2026-05-16T12:00:00Z"
    conn.execute("INSERT INTO platforms(platform_id, name, platform_type, country, default_currency, created_at) VALUES ('p1', 'Raiffeisen', 'bank', 'CH', 'CHF', ?)", (now,))
    conn.execute("INSERT INTO platforms(platform_id, name, platform_type, country, default_currency, created_at) VALUES ('p2', 'PostFinance', 'broker', 'CH', 'CHF', ?)", (now,))
    conn.execute("INSERT INTO platforms(platform_id, name, platform_type, country, default_currency, created_at) VALUES ('p3', 'True Wealth', 'broker', 'CH', 'CHF', ?)", (now,))
    conn.execute("INSERT INTO accounts(account_id, platform_id, account_name, account_type, currency, created_at) VALUES ('cash1', 'p1', 'Privatkonto', 'cash', 'CHF', ?)", (now,))
    conn.execute("INSERT INTO accounts(account_id, platform_id, account_name, account_type, currency, created_at) VALUES ('broker1', 'p2', 'Depot', 'brokerage', 'CHF', ?)", (now,))
    conn.execute("INSERT INTO accounts(account_id, platform_id, account_name, account_type, currency, created_at) VALUES ('tw1', 'p3', 'Gesamtwert', 'managed_portfolio', 'CHF', ?)", (now,))
    conn.execute("INSERT INTO instruments(instrument_id, asset_class, name, ticker, isin, currency, created_at) VALUES ('usd1', 'etf', 'Vanguard Total Stock Market ETF', 'VTI', 'US9229087690', 'USD', ?)", (now,))
    conn.execute("INSERT INTO instruments(instrument_id, asset_class, name, ticker, isin, currency, created_at) VALUES ('chf1', 'stock', 'Swiss Demo', 'SWD', 'CH0000000000', 'CHF', ?)", (now,))
    conn.execute("INSERT INTO fx_rates(fx_rate_id, base_currency, quote_currency, rate_date, rate, provider, rate_type, quality_status, created_at) VALUES ('fx_usd', 'USD', 'CHF', '2026-05-16', '0.91', 'frankfurter', 'close', 'fresh', ?)", (now,))
    conn.execute("INSERT INTO fx_rates(fx_rate_id, base_currency, quote_currency, rate_date, rate, provider, rate_type, quality_status, created_at) VALUES ('fx_eur', 'EUR', 'CHF', '2026-05-16', '0.98', 'frankfurter', 'close', 'fresh', ?)", (now,))
    conn.execute("INSERT INTO crypto_wallets(wallet_id, wallet_name, wallet_type, created_at) VALUES ('w1', 'Cold Wallet', 'hardware', ?)", (now,))
    conn.execute("INSERT INTO crypto_assets(asset_id, symbol, coin_name, coingecko_id, created_at) VALUES ('btc', 'BTC', 'Bitcoin', 'bitcoin', ?)", (now,))
    conn.execute("INSERT INTO crypto_holdings(crypto_holding_id, wallet_id, asset_id, quantity, verification_status, created_at) VALUES ('h1', 'w1', 'btc', '0.25', 'ok', ?)", (now,))
    conn.commit()


def test_position_confirm_resolves_cached_fx_and_chf_not_needed() -> None:
    conn = _connect(); apply_migrations(conn); _seed(conn)
    client = _client(conn)
    req = {"asset_class": "etf", "account_id": "broker1", "instrument_id": "usd1", "quantity": "2", "currency": "USD", "trade_date": "2026-05-16", "transaction_type": "initial_snapshot", "cost_basis_original": "100", "note": "manual usd"}

    preview = client.post("/api/positions/preview", json=req).json()
    assert preview["fx_status"] == "ok"
    assert preview["amount_chf"] == "91.00"
    confirmed = client.post("/api/positions/confirm", json={**req, "preview_id": preview["preview_id"], "confirm": True}).json()

    tx = conn.execute("SELECT fx_rate_to_chf, fx_status, gross_amount_chf, net_amount_chf FROM transactions WHERE transaction_id=?", (confirmed["entity_id"],)).fetchone()
    assert tx["fx_rate_to_chf"] == "0.91"
    assert tx["fx_status"] == "ok"
    assert tx["gross_amount_chf"] == "91.00"

    detail = client.get("/api/portfolio/positions/broker1:usd1").json()
    assert detail["fx_status"] == "ok"
    assert detail["status"] != "FX fehlt"

    chf = {"asset_class": "stock", "account_id": "broker1", "instrument_id": "chf1", "quantity": "1", "currency": "CHF", "trade_date": "2026-05-16", "transaction_type": "initial_snapshot", "cost_basis_original": "10", "note": "manual chf"}
    preview_chf = client.post("/api/positions/preview", json=chf).json()
    assert preview_chf["fx_status"] == "not_needed"
    confirmed_chf = client.post("/api/positions/confirm", json={**chf, "preview_id": preview_chf["preview_id"], "confirm": True}).json()
    tx_chf = conn.execute("SELECT fx_rate_to_chf, fx_status FROM transactions WHERE transaction_id=?", (confirmed_chf["entity_id"],)).fetchone()
    assert tx_chf["fx_rate_to_chf"] == "1"
    assert tx_chf["fx_status"] == "not_needed"
    detail_chf = client.get("/api/portfolio/positions/broker1:chf1").json()
    assert detail_chf["fx_status"] == "not_needed"


def test_truewealth_is_managed_total_value_account_not_equity_target() -> None:
    conn = _connect(); apply_migrations(conn); _seed(conn)
    client = _client(conn)

    accounts = client.get("/api/reference/accounts").json()
    tw = next(a for a in accounts if "True Wealth" in a["label"])
    assert tw["valuation_mode"] == "manual_total_value"
    assert tw["portfolio_bucket"] == "truewealth"
    assert tw["allow_position_adds"] is False

    blocked_req = {"asset_class": "etf", "account_id": tw["account_id"], "instrument_id": "usd1", "quantity": "1", "currency": "USD", "trade_date": "2026-05-16", "transaction_type": "initial_snapshot", "cost_basis_original": "10", "note": "must be blocked"}
    blocked = client.post("/api/positions/preview", json=blocked_req)
    assert blocked.status_code == 422
    assert "gesamtwert" in blocked.text.lower() or "manual total" in blocked.text.lower()

    preview = client.post(f"/api/accounts/{tw['account_id']}/value/preview", json={"valuation_date": "2026-05-16", "total_value_chf": "12345.67", "note": "manual TW value"}).json()
    assert preview["fx_status"] == "not_needed"
    confirmed = client.post(f"/api/accounts/{tw['account_id']}/value/confirm", json={"valuation_date": "2026-05-16", "total_value_chf": "12345.67", "note": "manual TW value", "preview_id": preview["preview_id"], "confirm": True}).json()
    assert confirmed["status"] == "confirmed"
    assert conn.execute("SELECT COUNT(*) FROM account_value_snapshots WHERE account_id=?", (tw["account_id"],)).fetchone()[0] == 1
    assert conn.execute("SELECT COUNT(*) FROM audit_log WHERE action='account_value_confirm' AND entity_id=?", (tw["account_id"],)).fetchone()[0] == 1

    overview = client.get("/api/overview").json()
    assert overview["truewealth_value_chf"] == "12345.67"
    assert "postfinance_equity_value_chf" in overview


def test_provider_search_endpoint_returns_candidates_and_no_provider_call_on_render(monkeypatch) -> None:
    conn = _connect(); apply_migrations(conn); _seed(conn)
    called = {"count": 0}

    class FakeProvider:
        name = "fake_provider"
        def search(self, *, query: str, asset_class: str | None = None):
            called["count"] += 1
            from jarvis_finance.market_data.catalog import CatalogSearchResult
            return [CatalogSearchResult(None, asset_class or "etf", "Vanguard Total Stock", isin="US9229087690", ticker="VTI", exchange="NYSEARCA", trading_currency="USD", provider="fake_provider", confidence="high", source="fake")]

    import jarvis_finance.services.manual_entry_service as svc
    monkeypatch.setattr(svc, "_provider_search_providers", lambda: [FakeProvider()])
    client = _client(conn)

    client.get("/api/overview")
    assert called["count"] == 0
    payload = client.post("/api/instruments/provider-search", json={"asset_class": "etf", "query": "VTI", "currency": "USD"}).json()
    assert called["count"] == 1
    assert payload["candidates"][0]["ticker"] == "VTI"
    assert payload["selection_required"] is True
    assert "api_key" not in str(payload).lower()


def test_cleanup_preview_and_confirm_archives_review_artifacts_preserving_manual_positions() -> None:
    conn = _connect(); apply_migrations(conn); _seed(conn)
    now = "2026-05-16T12:00:00Z"
    conn.execute("INSERT INTO accounts(account_id, platform_id, account_name, account_type, currency, created_at) VALUES ('review_acc', 'p2', 'True Wealth Review Brokerage', 'brokerage', 'CHF', ?)", (now,))
    conn.execute("INSERT INTO transactions(transaction_id, transaction_type, account_id, instrument_id, trade_date, quantity, gross_amount_original, net_amount_original, currency_original, source_type, is_confirmed, quality_status, notes, created_at) VALUES ('review_tx', 'initial_position_snapshot', 'review_acc', 'usd1', '2026-05-16', '1', '1', '1', 'USD', 'broker_import_reviewed_snapshot', 1, 'ok', 'review artifact', ?)", (now,))
    conn.execute("INSERT INTO transactions(transaction_id, transaction_type, account_id, instrument_id, trade_date, quantity, gross_amount_original, net_amount_original, currency_original, source_type, is_confirmed, quality_status, notes, created_at) VALUES ('manual_tx', 'initial_position_snapshot', 'broker1', 'chf1', '2026-05-16', '1', '1', '1', 'CHF', 'vue_manual_position', 1, 'ok', 'real manual', ?)", (now,))
    conn.commit()
    client = _client(conn)

    preview = client.post("/api/cleanup/review-artifacts/preview", json={}).json()
    assert preview["accounts"] == 1
    assert preview["transactions"] == 1
    assert preview["manual_positions_preserved"] == 1
    assert conn.execute("SELECT is_active FROM accounts WHERE account_id='review_acc'").fetchone()[0] == 1

    confirmed = client.post("/api/cleanup/review-artifacts/confirm", json={"confirm": True, "note": "acceptance cleanup"}).json()
    assert confirmed["status"] == "confirmed"
    assert conn.execute("SELECT is_active FROM accounts WHERE account_id='review_acc'").fetchone()[0] == 0
    assert conn.execute("SELECT is_voided FROM transactions WHERE transaction_id='review_tx'").fetchone()[0] == 1
    assert conn.execute("SELECT is_voided FROM transactions WHERE transaction_id='manual_tx'").fetchone()[0] == 0


def test_remove_position_voids_single_manual_snapshot_but_blocks_history() -> None:
    conn = _connect(); apply_migrations(conn); _seed(conn)
    client = _client(conn)
    req = {"asset_class": "stock", "account_id": "broker1", "instrument_id": "chf1", "quantity": "1", "currency": "CHF", "trade_date": "2026-05-16", "transaction_type": "initial_snapshot", "cost_basis_original": "10", "note": "bad input"}
    preview = client.post("/api/positions/preview", json=req).json()
    client.post("/api/positions/confirm", json={**req, "preview_id": preview["preview_id"], "confirm": True})

    remove_preview = client.post("/api/positions/broker1:chf1/remove/preview", json={}).json()
    assert remove_preview["safe_to_remove"] is True
    done = client.post("/api/positions/broker1:chf1/remove/confirm", json={"confirm": True, "confirmation_text": "ENTFERNEN", "note": "wrong position"}).json()
    assert done["status"] == "confirmed"
    assert conn.execute("SELECT COUNT(*) FROM transactions WHERE is_voided=1 AND instrument_id='chf1'").fetchone()[0] == 1
    assert conn.execute("SELECT COUNT(*) FROM audit_log WHERE action='position_remove_confirm'").fetchone()[0] == 1

    # Legacy manual snapshots with a normalized but older source_type are still safe.
    conn.execute("INSERT INTO transactions(transaction_id, transaction_type, account_id, instrument_id, trade_date, quantity, gross_amount_original, net_amount_original, currency_original, fx_rate_to_chf, fx_status, source_type, is_confirmed, quality_status, notes, created_at) VALUES ('legacy_snapshot', 'initial_snapshot', 'broker1', 'usd1', '2026-05-16', '1', '10', '10', 'USD', '0.91', 'ok', 'manual_initial_snapshot', 1, 'ok', 'legacy manual snapshot', '2026-05-16T12:00:00Z')")
    conn.commit()
    legacy_preview = client.post("/api/positions/broker1:usd1/remove/preview", json={}).json()
    assert legacy_preview["safe_to_remove"] is True
    legacy_done = client.post("/api/positions/broker1:usd1/remove/confirm", json={"confirm": True, "confirmation_text": "ENTFERNEN", "note": "legacy cleanup"}).json()
    assert legacy_done["status"] == "confirmed"

    # Legacy/manual-dashboard adjustments with exactly one row are also removable as manual snapshot-like corrections.
    conn.execute("INSERT INTO transactions(transaction_id, transaction_type, account_id, instrument_id, trade_date, quantity, gross_amount_original, net_amount_original, currency_original, fx_rate_to_chf, fx_status, source_type, is_confirmed, quality_status, notes, created_at) VALUES ('manual_adjustment_snapshot', 'manual_adjustment', 'broker1', 'usd1', '2026-05-16', '1', '10', '10', 'USD', '0.91', 'ok', 'manual_dashboard', 1, 'ok', 'manual adjustment snapshot', '2026-05-16T12:00:00Z')")
    conn.commit()
    adjustment_preview = client.post("/api/positions/broker1:usd1/remove/preview", json={}).json()
    assert adjustment_preview["safe_to_remove"] is True
    adjustment_done = client.post("/api/positions/broker1:usd1/remove/confirm", json={"confirm": True, "confirmation_text": "ENTFERNEN", "note": "manual adjustment cleanup"}).json()
    assert adjustment_done["status"] == "confirmed"

    # More than one transaction -> no hard delete/void by this simple workflow.
    preview2 = client.post("/api/positions/preview", json=req).json(); client.post("/api/positions/confirm", json={**req, "preview_id": preview2["preview_id"], "confirm": True})
    conn.execute("INSERT INTO transactions(transaction_id, transaction_type, account_id, instrument_id, trade_date, quantity, gross_amount_original, net_amount_original, currency_original, source_type, is_confirmed, quality_status, notes, created_at) VALUES ('hist_tx', 'buy', 'broker1', 'chf1', '2026-05-17', '1', '10', '10', 'CHF', 'vue_manual_position', 1, 'ok', 'history', '2026-05-17T12:00:00Z')")
    conn.commit()
    blocked = client.post("/api/positions/broker1:chf1/remove/preview", json={}).json()
    assert blocked["safe_to_remove"] is False


def test_wallet_and_account_create_deactivate_delete_rules_are_audited() -> None:
    conn = _connect(); apply_migrations(conn); _seed(conn)
    client = _client(conn)

    wallet_preview = client.post("/api/wallets/preview", json={"name": "Empty Wallet", "wallet_type": "exchange", "note": "new"}).json()
    wallet = client.post("/api/wallets/confirm", json={"name": "Empty Wallet", "wallet_type": "exchange", "note": "new", "preview_id": wallet_preview["preview_id"], "confirm": True}).json()
    wid = wallet["entity_id"]
    deleted = client.post(f"/api/wallets/{wid}/delete/confirm", json={"confirm": True, "note": "empty"}).json()
    assert deleted["message"].lower().startswith("wallet gelöscht")

    nonempty = client.post("/api/wallets/w1/delete/confirm", json={"confirm": True, "note": "archive"}).json()
    assert "deaktiviert" in nonempty["message"].lower()
    assert conn.execute("SELECT is_active FROM crypto_wallets WHERE wallet_id='w1'").fetchone()[0] == 0

    account_preview = client.post("/api/accounts/preview", json={"platform_name": "Anderes Konto", "account_name": "Manual Broker", "account_type": "brokerage", "currency": "CHF", "note": "new"}).json()
    account = client.post("/api/accounts/confirm", json={"platform_name": "Anderes Konto", "account_name": "Manual Broker", "account_type": "brokerage", "currency": "CHF", "note": "new", "preview_id": account_preview["preview_id"], "confirm": True}).json()
    aid = account["entity_id"]
    deactivated = client.post(f"/api/accounts/{aid}/deactivate/confirm", json={"confirm": True, "note": "not needed"}).json()
    assert deactivated["status"] == "confirmed"
    assert conn.execute("SELECT COUNT(*) FROM audit_log WHERE action IN ('wallet_create_confirm','wallet_delete_confirm','wallet_deactivate_confirm','account_create_confirm','account_deactivate_confirm')").fetchone()[0] >= 5



def test_provider_search_deduplicates_enriches_filters_and_ranks_goog(monkeypatch) -> None:
    conn = _connect(); apply_migrations(conn); _seed(conn)

    class FakeProviderA:
        name = "openfigi"
        def search(self, *, query: str, asset_class: str | None = None):
            from jarvis_finance.market_data.catalog import CatalogSearchResult
            return [
                CatalogSearchResult(None, "stock", "ALPHABET INC-CL C", isin="US02079K1079", ticker="GOOG", exchange="NASDAQ", trading_currency="USD", provider="openfigi", provider_symbol="BBG009S3NB30", provider_market="NASDAQ", country="US", confidence="medium", source="openfigi", evidence_note="primary listing", last_price="175.12", price_currency="USD", price_date="2026-05-16", security_type="Common Stock", exchange_name="Nasdaq Global Select", mic="XNAS"),
                CatalogSearchResult(None, "stock", "ALPHABET INC - CDR", isin="CA02079K3055", ticker="GOOG", exchange="NEO", trading_currency="CAD", provider="openfigi", provider_symbol="BBGCDR", provider_market="NEO", country="CA", confidence="medium", source="openfigi", evidence_note="CDR", security_type="CDR"),
            ]

    class FakeProviderB:
        name = "finnhub"
        def search(self, *, query: str, asset_class: str | None = None):
            from jarvis_finance.market_data.catalog import CatalogSearchResult
            return [
                CatalogSearchResult(None, "stock", "Alphabet Inc Class C", isin="US02079K1079", ticker="GOOG", exchange="NASDAQ", trading_currency="USD", provider="finnhub", provider_symbol="GOOG", provider_market="NASDAQ", country="US", confidence="medium", source="finnhub", evidence_note="symbol lookup", last_price="175.10", price_currency="USD", price_date="2026-05-16", security_type="Common Stock", exchange_name="Nasdaq", mic="XNAS"),
                CatalogSearchResult(None, "stock", "Googol Technology Co Ltd", ticker="8118", exchange="HKEX", trading_currency="HKD", provider="finnhub", country="HK", confidence="low", source="finnhub", evidence_note="weak name match"),
            ]

    import jarvis_finance.services.manual_entry_service as svc
    monkeypatch.setattr(svc, "_provider_search_providers", lambda: [FakeProviderA(), FakeProviderB()])
    client = _client(conn)

    payload = client.post("/api/instruments/provider-search", json={"asset_class": "stock", "query": "GOOG", "currency": "USD", "exchange": "NASDAQ", "country": "US", "main_listing_only": True, "hide_cdr_adr_derivatives": True}).json()

    assert len(payload["candidates"]) == 1
    goog = payload["candidates"][0]
    assert goog["ticker"] == "GOOG"
    assert goog["isin"] == "US02079K1079"
    assert goog["exchange"] == "NASDAQ"
    assert goog["exchange_name"]
    assert goog["mic"] == "XNAS"
    assert goog["currency"] == "USD"
    assert goog["country"] == "US"
    assert goog["security_type"] == "Common Stock"
    assert goog["last_price"]
    assert goog["price_currency"] == "USD"
    assert goog["price_date"] == "2026-05-16"
    assert goog["provider_symbol"] == "BBG009S3NB30"
    assert "openfigi" in goog["provider"] and "finnhub" in goog["provider"]
    assert goog["confidence_label"] in {"Eindeutig", "Wahrscheinlich"}


def test_provider_search_suppresses_raw_provider_errors_when_candidate_exists(monkeypatch) -> None:
    conn = _connect(); apply_migrations(conn); _seed(conn)

    class WorkingProvider:
        name = "openfigi"
        def search(self, *, query: str, asset_class: str | None = None):
            from jarvis_finance.market_data.catalog import CatalogSearchResult
            return [CatalogSearchResult(None, asset_class or "stock", "Alphabet Inc Class C", isin="US02079K1079", ticker="GOOG", exchange="NASDAQ", trading_currency="USD", provider="openfigi", provider_symbol="GOOG", country="US", confidence="high", source="openfigi")]

    class BrokenProvider:
        name = "broken_provider"
        def search(self, *, query: str, asset_class: str | None = None):
            raise RuntimeError("upstream HTTP 500 secret-ish raw provider failure")

    import jarvis_finance.services.manual_entry_service as svc
    monkeypatch.setattr(svc, "_provider_search_providers", lambda: [WorkingProvider(), BrokenProvider()])
    client = _client(conn)

    payload = client.post("/api/instruments/provider-search", json={"asset_class": "stock", "query": "GOOG", "currency": "USD"}).json()

    assert payload["candidates"]
    assert payload["warnings"] == ["Datenquelle teilweise eingeschränkt: Ein Provider lieferte keine Daten; Treffer stammt aus openfigi."]
    assert "HTTP 500" not in str(payload)
    assert "raw provider failure" not in str(payload)


def test_provider_search_no_candidates_has_clear_user_warning(monkeypatch) -> None:
    conn = _connect(); apply_migrations(conn); _seed(conn)

    class EmptyProvider:
        name = "empty_provider"
        def search(self, *, query: str, asset_class: str | None = None):
            return []

    import jarvis_finance.services.manual_entry_service as svc
    monkeypatch.setattr(svc, "_provider_search_providers", lambda: [EmptyProvider()])
    client = _client(conn)

    payload = client.post("/api/instruments/provider-search", json={"asset_class": "stock", "query": "DOESNOTEXIST"}).json()

    assert payload["candidates"] == []
    assert payload["warnings"] == ["Keine verlässlichen Providerdaten gefunden."]


def test_provider_candidate_preview_confirm_materializes_instrument_and_audit() -> None:
    conn = _connect(); apply_migrations(conn); _seed(conn)
    now = "2026-05-16T12:00:00Z"
    conn.execute("""
        INSERT INTO instrument_catalog_entries(catalog_entry_id, asset_class, name, normalized_name, isin, ticker, exchange, trading_currency, instrument_currency, provider, provider_symbol, provider_market, country, source, source_confidence, last_price, price_currency, price_date, created_at, updated_at)
        VALUES ('cat_goog', 'stock', 'ALPHABET INC-CL C', 'alphabet inc cl c', 'US02079K1079', 'GOOG', 'NASDAQ', 'USD', 'USD', 'openfigi', 'BBG009S3NB30', 'NASDAQ', 'US', 'openfigi', 'high', '175.12', 'USD', '2026-05-16', ?, ?)
    """, (now, now))
    conn.commit()
    client = _client(conn)
    req = {"asset_class": "stock", "account_id": "broker1", "candidate_id": "cat_goog", "quantity": "2", "currency": "USD", "trade_date": "2026-05-16", "transaction_type": "initial_snapshot", "cost_basis_original": "100", "note": "provider candidate"}

    before = conn.execute("SELECT COUNT(*) FROM instruments WHERE isin='US02079K1079'").fetchone()[0]
    preview = client.post("/api/positions/preview", json=req).json()
    assert preview["summary"].startswith("ALPHABET")
    assert conn.execute("SELECT COUNT(*) FROM instruments WHERE isin='US02079K1079'").fetchone()[0] == before
    confirmed = client.post("/api/positions/confirm", json={**req, "preview_id": preview["preview_id"], "confirm": True}).json()

    tx = conn.execute("SELECT instrument_id, currency_original FROM transactions WHERE transaction_id=?", (confirmed["entity_id"],)).fetchone()
    assert tx["currency_original"] == "USD"
    inst = conn.execute("SELECT ticker, isin, exchange, currency, provider_symbol, data_provider_primary FROM instruments WHERE instrument_id=?", (tx["instrument_id"],)).fetchone()
    assert dict(inst) == {"ticker": "GOOG", "isin": "US02079K1079", "exchange": "NASDAQ", "currency": "USD", "provider_symbol": "BBG009S3NB30", "data_provider_primary": "openfigi"}
    price = conn.execute("SELECT close, currency, provider FROM market_prices WHERE instrument_id=?", (tx["instrument_id"],)).fetchone()
    assert price["close"] == "175.12"
    assert price["currency"] == "USD"
    assert price["provider"] == "openfigi"
    equity = client.get("/api/equity/positions").json()
    goog_row = next(row for row in equity if row["ticker"] == "GOOG")
    assert goog_row["currency"] == "USD"
    assert goog_row["status"] == "Bewertet"
    assert goog_row["market_value_chf"] is not None
    assert conn.execute("SELECT COUNT(*) FROM audit_log WHERE action='position_confirm'").fetchone()[0] == 1


def test_crypto_detail_exposes_cached_coingecko_info_without_provider_call() -> None:
    conn = _connect(); apply_migrations(conn); _seed(conn)
    conn.execute("UPDATE crypto_assets SET notes=? WHERE asset_id='btc'", ('{"coingecko": {"market_cap_rank": 1, "market_cap": "2000000000000", "volume_24h": "50000000000", "change_24h_pct": "2.50", "change_7d_pct": "5.00", "homepage": "https://bitcoin.org", "image_url": "https://example.test/btc.png"}}',))
    conn.execute("INSERT INTO crypto_prices(crypto_price_id, asset_id, coingecko_id, price_currency, price, provider, fetched_at, quality_status) VALUES ('cp1', 'btc', 'bitcoin', 'CHF', '80000', 'coingecko_cache', '2026-05-16T12:00:00Z', 'fresh')")
    conn.commit()
    client = _client(conn)
    before = conn.total_changes

    detail = client.get("/api/crypto/positions/btc").json()

    assert detail["coingecko_info"]["market_cap_rank"] == 1
    assert detail["coingecko_info"]["homepage"] == "https://bitcoin.org"
    assert detail["read_only_note"].startswith("Live-Modus optional")
    assert conn.total_changes == before
    assert conn.execute("SELECT COUNT(*) FROM crypto_price_points WHERE asset_id='btc'").fetchone()[0] == 0



def test_crypto_detail_sanitizes_cached_coingecko_urls() -> None:
    conn = _connect(); apply_migrations(conn); _seed(conn)
    conn.execute("UPDATE crypto_assets SET notes=? WHERE asset_id='btc'", ('{"coingecko": {"homepage": "javascript:alert(1)", "image_url": "data:image/svg+xml,evil"}}',))
    conn.commit()
    client = _client(conn)

    detail = client.get("/api/crypto/positions/btc").json()

    assert detail["coingecko_info"]["homepage"] is None
    assert detail["coingecko_info"]["image_url"] is None
