import pytest
import yaml

from fxhnt.application.ucits_map import apply_ucits_map, route_ucits
from fxhnt.config import UcitsListing

# US ticker -> UCITS listing (ticker + resolving ISIN/exchange/currency). Mirrors the shipped default.
MAP = {
    "SPY": UcitsListing(ticker="CSPX", isin="IE00B5BMR087"),
    "IEF": UcitsListing(ticker="IBTM", isin="IE00B1FZS798"),
    "GLD": UcitsListing(ticker="SGLN", isin="IE00B4ND3602"),
    "PDBC": UcitsListing(ticker="ICOM", isin="IE00BDFL4P12"),
    "DBMF": UcitsListing(ticker="DBMF", isin="LU2951555585"),
}


def test_apply_ucits_map_rewrites_symbols_and_carries_listings():
    route = apply_ucits_map({"SPY": 0.6, "GLD": 0.4}, MAP)
    assert route.weights == {"CSPX": 0.6, "SGLN": 0.4}
    assert route.skipped == {}
    assert route.listings["CSPX"].isin == "IE00B5BMR087"
    assert route.listings["SGLN"].isin == "IE00B4ND3602"


def test_unmapped_sleeve_is_skipped_not_refused():
    """A single unmapped sleeve must NOT block the PRIIPs-legal ones — it is logged + skipped, the rest route."""
    route = apply_ucits_map({"SPY": 0.5, "QQQ": 0.5}, MAP)
    assert route.weights == {"CSPX": 0.5}     # SPY routed
    assert route.skipped == {"QQQ": 0.5}      # QQQ skipped, weight NOT rescaled onto CSPX
    assert "QQQ" not in route.listings


def test_non_injective_map_raises():
    """A genuine config bug (two sleeves -> one ticker) still hard-fails — distinct from a routing gap."""
    bad = {"SPY": UcitsListing(ticker="CSPX", isin="IE00B5BMR087"),
           "VOO": UcitsListing(ticker="CSPX", isin="IE00B5BMR087")}
    with pytest.raises(ValueError, match="collision"):
        apply_ucits_map({"SPY": 0.5, "VOO": 0.5}, bad)


def test_route_ucits_sizes_from_real_ucits_price_and_builds_contract_specs():
    """route_ucits sources each sleeve's price from the broker (REAL UCITS USD price), NOT the US price,
    and emits the ISIN/exchange/currency contract spec that place_order needs to resolve the order."""
    real_prices = {"CSPX": 620.0, "SGLN": 65.0}   # UCITS prices differ from US SPY(600)/GLD(200)

    def price_fn(listing: UcitsListing) -> float | None:
        return real_prices.get(listing.ticker)

    weights, prices, specs, skipped = route_ucits({"SPY": 0.6, "GLD": 0.4}, MAP, price_fn=price_fn)
    assert weights == {"CSPX": 0.6, "SGLN": 0.4}
    assert prices == {"CSPX": 620.0, "SGLN": 65.0}         # REAL UCITS prices, not 600/200
    assert skipped == {}
    assert specs["CSPX"] == {"exchange": "LSEETF", "currency": "USD",
                             "sec_id_type": "ISIN", "sec_id": "IE00B5BMR087"}


def test_route_ucits_skips_sleeve_with_no_price():
    """An unresolvable/priceless contract is logged + skipped; survivors keep their own weight (no rescale)."""
    def price_fn(listing: UcitsListing) -> float | None:
        return 620.0 if listing.ticker == "CSPX" else None   # SGLN fails to price

    weights, prices, specs, skipped = route_ucits({"SPY": 0.6, "GLD": 0.4}, MAP, price_fn=price_fn)
    assert weights == {"CSPX": 0.6}      # SGLN dropped, CSPX weight unchanged (still 0.6, NOT rescaled to 1.0)
    assert prices == {"CSPX": 620.0}
    assert "SGLN" not in specs
    assert skipped == {"GLD": 0.4}       # GLD reported as skipped (unpriced)


def test_route_ucits_skips_sleeve_whose_price_fn_raises():
    """F1: if the price fetch RAISES for ONE sleeve (IB API blip / thin line), that sleeve is skipped and the
    OTHERS still route — one bad sleeve cannot wedge the whole (PRIIPs-legal) rebalance."""
    def price_fn(listing: UcitsListing) -> float | None:
        if listing.ticker == "SGLN":
            raise RuntimeError("IB API blew up on this thin line")
        return 620.0

    weights, prices, specs, skipped = route_ucits({"SPY": 0.6, "GLD": 0.4}, MAP, price_fn=price_fn)
    assert weights == {"CSPX": 0.6}      # SPY still routes despite GLD's price fetch raising
    assert prices == {"CSPX": 620.0}
    assert "SGLN" not in specs
    assert skipped == {"GLD": 0.4}       # the raising sleeve is downgraded to a per-sleeve skip


def test_default_map_covers_all_fund_instruments_with_verified_isins():
    """The shipped default UCITS map must cover every multistrat sleeve, each with a verified ISIN."""
    from fxhnt.config import UcitsSettings
    from fxhnt.domain.strategies.multistrat import FUND_INSTRUMENTS

    default_map = UcitsSettings().map
    route = apply_ucits_map({s: 1.0 for s in FUND_INSTRUMENTS}, default_map)
    assert route.skipped == {}                       # every fund instrument maps
    assert len(route.weights) == len(FUND_INSTRUMENTS)
    verified = {  # ISIN + LSE USD line ticker, probed live on DU9600528 2026-07-15 (LSEETF+ISIN resolves)
        "SPY": ("CSPX", "IE00B5BMR087"), "IEF": ("CBU0", "IE00B3VWN518"),  # Acc line, ~$12M ADV (not thin Dist IDTM)
        "GLD": ("IGLN", "IE00B4ND3602"), "PDBC": ("ICOM", "IE00BDFL4P12"),
        "DBMF": ("DBMF", "LU2951555585"),
    }
    for us, (ticker, isin) in verified.items():
        listing = default_map[us]
        assert listing.ticker == ticker
        assert listing.isin == isin
        assert listing.currency == "USD"


def test_ucits_rebalancer_armed_eu_hours_us_leg_suspended():
    """The EU-domiciled DU paper account can't trade US ETFs (PRIIPs/KID), so the PRIIPs-legal UCITS leg is
    the ARMED one and the US leg is SUSPENDED (2026-07-15). The UCITS CronJob must be armed on a weekday slot
    INSIDE LSE/Euronext hours (open ~07:00 UTC BST, close ~15:30) so the UCITS ETFs actually price + fill —
    a run outside market hours gets no price and skips."""
    with open("infra/k8s/jobs/fxhnt-ucits-rebalancer.yaml") as f:
        ucits = [d for d in yaml.safe_load_all(f) if d and d.get("kind") == "CronJob"]
    assert len(ucits) == 1 and ucits[0]["metadata"]["name"] == "fxhnt-ucits-rebalancer"
    assert ucits[0]["spec"]["suspend"] is False, "UCITS leg must be ARMED (the PRIIPs-legal EU leg)"
    mins, hrs, _, _, dow = ucits[0]["spec"]["schedule"].split()
    assert 7 <= int(hrs) <= 15, "UCITS run must be inside LSE/Euronext hours (07:00-15:30 UTC)"
    assert dow in ("1-5", "MON-FRI"), "weekdays only (markets closed weekends)"

    with open("infra/k8s/jobs/fxhnt-ibkr-rebalancer.yaml") as f:
        us = [d for d in yaml.safe_load_all(f) if d and d.get("kind") == "CronJob"]
    assert us and us[0]["spec"]["suspend"] is True, "US ETF leg must be SUSPENDED (PRIIPs/KID-rejected on the EU account)"
