from __future__ import annotations

import json
from decimal import Decimal
from io import BytesIO
from urllib.error import HTTPError

import pytest

from jarvis_finance.cli.main import main
from jarvis_finance.fx.providers import FrankfurterFxProvider, TwelveDataFxProvider
from jarvis_finance.market_data.catalog import (
    MassiveLookupProvider,
    OpenFigiLookupProvider,
    TwelveDataLookupProvider,
    _runtime_secret_value,
    default_instrument_lookup_providers,
    instrument_provider_statuses,
)
from jarvis_finance.market_data.candidates import LookupUnavailable
from jarvis_finance.market_data.mappings import confirm_instrument_price_mapping
from jarvis_finance.market_data.prices import (
    EodhdEquityPriceProvider,
    CompositeEquityPriceProvider,
    FinnhubEquityPriceProvider,
    MassiveEquityPriceProvider,
    TwelveDataEquityPriceProvider,
    equity_price_provider_by_name,
    refresh_market_prices,
)
from jarvis_finance.storage.database import connect_memory
from jarvis_finance.storage.migrations import apply_migrations


class FakeResponse:
    def __init__(self, payload: object, headers: dict[str, str] | None = None) -> None:
        self.payload = payload
        self.headers = headers or {}

    def __enter__(self):
        return self

    def __exit__(self, *args):
        return False

    def read(self) -> bytes:
        return json.dumps(self.payload).encode("utf-8")


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','Broker','broker','CHF','now')")
    conn.execute("INSERT INTO accounts(account_id,platform_id,account_name,account_type,currency,created_at) VALUES('a1','p1','Account','brokerage','CHF','now')")
    conn.execute("INSERT INTO instruments(instrument_id,asset_class,name,ticker,isin,exchange,currency,created_at) VALUES('usd1','ETF','USD ETF','VTI','US9229087690','NYSE','USD','now')")
    return conn


def test_runtime_secret_aliases_include_new_provider_key_names(monkeypatch, tmp_path) -> None:
    monkeypatch.delenv("OPENFIGI_API_KEY", raising=False)
    monkeypatch.setenv("JARVIS_FINANCE_RUNTIME_DIR", str(tmp_path))
    secrets = tmp_path / "secrets"
    secrets.mkdir()
    env_file = secrets / ".env"
    env_file.write_text("TWELVEDATA_API_KEY" + "=td\n" + "MASSIVE_API_KEY" + "=mass\n" + "JARVIS_OPENFIGI_API_KEY" + "=figi\n", encoding="utf-8")
    env_file.chmod(0o600)

    assert _runtime_secret_value(("TWELVEDATA_API_KEY", "TWELVE_DATA_API_KEY")) == "td"
    assert _runtime_secret_value(("MASSIVE_API_KEY", "JARVIS_MASSIVE_API_KEY")) == "mass"
    assert _runtime_secret_value(("OPENFIGI_API_KEY", "JARVIS_OPENFIGI_API_KEY")) == "figi"


def test_openfigi_401_is_auth_failed_without_crash(monkeypatch) -> None:
    def fail(*args, **kwargs):
        raise HTTPError("https://api.openfigi.com/v3/mapping", 401, "Unauthorized", {}, BytesIO())

    monkeypatch.setattr("jarvis_finance.market_data.catalog.request.urlopen", fail)
    provider = OpenFigiLookupProvider(api_key="synthetic", min_interval_seconds=0)

    with pytest.raises(LookupUnavailable, match="openfigi_auth_failed"):
        provider.search(query="US9229087690", asset_class="etf")


def test_openfigi_isin_mapping_mock_returns_candidate(monkeypatch) -> None:
    provider = OpenFigiLookupProvider(api_key="synthetic", min_interval_seconds=0)
    monkeypatch.setattr(provider, "_post_json", lambda *a, **k: [{"data": [{"figi": "BBG000", "ticker": "VTI", "name": "Vanguard Total Stock Market ETF", "exchCode": "US", "currency": "USD"}]}])

    results = provider.search(query="US9229087690", asset_class="etf")

    assert results
    assert results[0].provider == "openfigi"
    assert results[0].ticker == "VTI"


def test_provider_priority_and_dashboard_status_without_key_leak(monkeypatch, tmp_path) -> None:
    monkeypatch.setenv("JARVIS_FINANCE_RUNTIME_DIR", str(tmp_path))
    providers = [p.name for p in default_instrument_lookup_providers()]
    assert providers == ["openfigi", "fmp", "finnhub", "twelvedata", "massive"]

    rows = instrument_provider_statuses(probe=False)
    labels = {row["Provider"]: row for row in rows}
    assert labels["EODHD"]["Hinweis"].startswith("EODHD_API_KEY; Low-volume")
    assert labels["Frankfurter FX"]["Status"] == "aktiv"
    assert labels["Stooq"]["Status"] == "verfügbar"
    rendered = json.dumps(rows)
    assert "apikey" not in rendered.lower()
    assert "synthetic" not in rendered.lower()


def test_finnhub_lookup_mock_enriches_profile_and_quote(monkeypatch) -> None:
    calls: list[str] = []

    def fake_get_json(self, url: str, **kwargs):
        calls.append(url)
        if "/search?" in url:
            return {"result": [{"symbol": "VTI", "description": "Vanguard Total Stock Market ETF"}]}
        if "/stock/profile2?" in url:
            return {"currency": "USD", "exchange": "NYSE ARCA", "country": "US"}
        if "/quote?" in url:
            return {"c": 250.12}
        return {}

    monkeypatch.setattr("jarvis_finance.market_data.catalog.FinnhubSymbolLookupProvider._get_json", fake_get_json)
    from jarvis_finance.market_data.catalog import FinnhubSymbolLookupProvider

    result = FinnhubSymbolLookupProvider(api_key="synthetic", min_interval_seconds=0).search(query="VTI", asset_class="etf")[0]

    assert result.provider == "finnhub"
    assert result.exchange == "NYSE ARCA"
    assert result.last_price == "250.12"
    assert len(calls) == 3


def test_twelvedata_symbol_lookup_and_key_alias(monkeypatch) -> None:
    monkeypatch.setenv("TWELVEDATA_API_KEY", "synthetic")
    monkeypatch.setattr("jarvis_finance.market_data.catalog.TwelveDataLookupProvider._get_json", lambda self, url, **kwargs: {"data": [{"symbol": "VTI", "instrument_name": "Vanguard Total Stock", "exchange": "NYSE", "currency": "USD", "country": "United States", "instrument_type": "ETF"}]})

    result = TwelveDataLookupProvider(min_interval_seconds=0).search(query="VTI", asset_class="etf")[0]

    assert result.provider == "twelvedata"
    assert result.ticker == "VTI"
    assert result.trading_currency == "USD"


def test_massive_reference_lookup_mock(monkeypatch) -> None:
    def fake_massive_json(self, path: str, params=None):
        if path == "/v3/reference/tickers":
            return {"results": [{"ticker": "VTI", "name": "Vanguard Total Stock Market ETF", "primary_exchange": "ARCX", "type": "ETF"}]}
        return {"results": {"ticker": "VTI", "currency_name": "USD", "locale": "us"}}

    monkeypatch.setattr(MassiveLookupProvider, "_massive_json", fake_massive_json)
    result = MassiveLookupProvider(api_key="synthetic", min_interval_seconds=0).search(query="VTI", asset_class="etf")[0]

    assert result.provider == "massive"
    assert result.ticker == "VTI"
    assert result.exchange == "ARCX"


def test_equity_price_providers_mock_quotes(monkeypatch) -> None:
    monkeypatch.setattr(FinnhubEquityPriceProvider, "_json_url", lambda self, url, **kwargs: {"c": 250.12} if "/quote?" in url else {"currency": "USD"})
    monkeypatch.setattr(TwelveDataEquityPriceProvider, "_json_url", lambda self, url, **kwargs: {"close": "250.13", "currency": "USD", "datetime": "2026-05-16"})
    monkeypatch.setattr(MassiveEquityPriceProvider, "_massive_json", lambda self, path: {"results": [{"c": 250.14}]})

    assert FinnhubEquityPriceProvider(api_key="synthetic").get_price("VTI").close == Decimal("250.12")
    assert TwelveDataEquityPriceProvider(api_key="synthetic").get_price("VTI").close == Decimal("250.13")
    assert MassiveEquityPriceProvider(api_key="synthetic").get_price("VTI").close == Decimal("250.14")
    assert isinstance(equity_price_provider_by_name("twelvedata"), TwelveDataEquityPriceProvider)
    assert isinstance(equity_price_provider_by_name("auto"), CompositeEquityPriceProvider)


def test_composite_equity_price_provider_uses_priority_order() -> None:
    from jarvis_finance.market_data.prices import EquityPriceQuote

    class MissingProvider:
        name = "fmp"
        def get_price(self, provider_symbol: str, *, price_date: str | None = None):
            return EquityPriceQuote(provider_symbol=provider_symbol, currency="", close=None, provider=self.name, quality_status="missing", error_message="restricted_endpoint")

    class FreshProvider:
        name = "twelvedata"
        def get_price(self, provider_symbol: str, *, price_date: str | None = None):
            return EquityPriceQuote(provider_symbol=provider_symbol, currency="USD", close=Decimal("250.15"), provider=self.name, quality_status="fresh")

    quote = CompositeEquityPriceProvider([MissingProvider(), FreshProvider()]).get_price("VTI")

    assert quote.provider == "twelvedata"
    assert quote.close == Decimal("250.15")


def test_eodhd_is_explicit_low_volume_provider_not_default(monkeypatch) -> None:
    assert "eodhd" not in [p.name for p in default_instrument_lookup_providers()]
    assert isinstance(equity_price_provider_by_name("eodhd"), EodhdEquityPriceProvider)
    with pytest.raises(SystemExit):
        main(["update-equity-prices", "--provider", "eodhd", "--dry-run", "--limit", "0"])


def test_refresh_market_prices_uses_explicit_provider_only() -> None:
    conn = setup_conn()
    confirm_instrument_price_mapping(conn, instrument_id="usd1", provider="twelvedata", provider_symbol="VTI", provider_market="NYSE", confidence="manual", note="test mapping")

    class Provider:
        name = "twelvedata"
        calls = 0

        def get_price(self, provider_symbol: str, *, price_date: str | None = None):
            Provider.calls += 1
            from jarvis_finance.market_data.prices import EquityPriceQuote
            return EquityPriceQuote(provider_symbol=provider_symbol, currency="USD", close=Decimal("250.13"), provider="twelvedata", quality_status="fresh")

    result = refresh_market_prices(conn, provider=Provider(), asset_class="etf", price_date="2026-05-16")

    assert result.updated_count == 1
    assert Provider.calls == 1
    assert conn.execute("SELECT provider, close FROM market_prices WHERE instrument_id='usd1'").fetchone()["provider"] == "twelvedata"


def test_frankfurter_and_twelvedata_fx_mocks(monkeypatch) -> None:
    seen_urls: list[str] = []

    def fake_frankfurter(req, *args, **kwargs):
        seen_urls.append(req.full_url)
        return FakeResponse([{"date": "2026-05-20", "base": "USD", "quote": "CHF", "rate": 0.89}])

    monkeypatch.setattr("jarvis_finance.fx.providers.request.urlopen", fake_frankfurter)
    assert FrankfurterFxProvider().get_rate("USD", "CHF", "latest") == Decimal("0.89")
    assert "api.frankfurter.dev/v2/rates" in seen_urls[0]
    assert "base=USD" in seen_urls[0] and "quotes=CHF" in seen_urls[0]

    monkeypatch.setattr("jarvis_finance.fx.providers.request.urlopen", lambda *a, **k: FakeResponse({"rate": "0.90"}))
    assert TwelveDataFxProvider(api_key="synthetic").get_rate("USD", "CHF", "latest") == Decimal("0.90")
    assert TwelveDataFxProvider(api_key="synthetic").get_rate("CHF", "CHF", "latest") == Decimal("1")
