"""Regime-conditional execution: only specialists whose regime is live today are activated and netted."""
from __future__ import annotations

import datetime as dt

import numpy as np
import pytest

from fxhnt.adapters.persistence import DuckDbAnalyticalStore, SqlStrategyStore
from fxhnt.adapters.persistence.factory_store import FactoryStore
from fxhnt.application import RegimeConditionalExecutor
from fxhnt.config import Settings
from fxhnt.domain.factory import RegimeFit, StrategyRecord, StrategyStatus, strategy_id
from fxhnt.domain.models import AssetClass, Market, PriceSeries, StrategySpec

_NOW = dt.datetime(2026, 6, 9, tzinfo=dt.timezone.utc)


class FakeData:
    name = "fake"

    def fetch(self, market: Market, start=None, end=None) -> PriceSeries:
        close = 100.0 * np.cumprod(1.0 + np.full(600, 0.0015))  # steady uptrend -> today's regime is trend_up
        return PriceSeries(market=market, dates=tuple(f"2026-{i}" for i in range(600)), close=close)


def _spec(kind: str) -> StrategySpec:
    return StrategySpec(kind=kind, params={"window": 100.0} if kind == "trend" else {"window": 20.0, "z": 1.0})


def test_only_live_regime_specialists_are_activated(tmp_path) -> None:
    settings = Settings(analytical_path=str(tmp_path / "an.duckdb"), operational_dsn=f"sqlite:///{tmp_path / 's.db'}")
    store = SqlStrategyStore(settings.operational_dsn)
    t, m = _spec("trend"), _spec("mean_reversion")
    # trend specialist for trend-up regimes (matches today); mean-rev specialist for a panic regime (does not)
    store.upsert(StrategyRecord(strategy_id=strategy_id(["AAA"], AssetClass.FUTURE, t), markets=["AAA"],
                                asset_class=AssetClass.FUTURE, spec=t, discovered_at=_NOW, updated_at=_NOW,
                                regime_fits=[RegimeFit(regime="trend_up|low_vol", dsr=0.97, is_sharpe=1.3, oos_sharpe=0.9, passed=True),
                                             RegimeFit(regime="trend_up|high_vol", dsr=0.96, is_sharpe=1.2, oos_sharpe=0.8, passed=True)]))
    store.upsert(StrategyRecord(strategy_id=strategy_id(["AAA"], AssetClass.FUTURE, m), markets=["AAA"],
                                asset_class=AssetClass.FUTURE, spec=m, discovered_at=_NOW, updated_at=_NOW,
                                regime_fits=[RegimeFit(regime="trend_down|high_vol", dsr=0.96, is_sharpe=1.2, oos_sharpe=0.8, passed=True)]))

    ex = RegimeConditionalExecutor(FakeData(), DuckDbAnalyticalStore(settings.analytical_path), store, settings)
    rb = ex.assemble(status=StrategyStatus.DISCOVERED)

    assert any("trend" in a.spec and a.market == "AAA" for a in rb.active)        # trend is live
    assert any(d.strategy_id.startswith("mean_reversion") for d in rb.dormant)    # mean-rev is dormant
    assert rb.target_weights.get("AAA", 0.0) > 0                                  # long in the uptrend


# ── HRP weight sizing tests ───────────────────────────────────────────────────


def _make_two_deployed_specialists(store: SqlStrategyStore, market_a: str, market_b: str) -> tuple[str, str]:
    """Insert two DEPLOYED trend specialists for market_a and market_b; return their strategy IDs."""
    t = _spec("trend")
    sid_a = strategy_id([market_a], AssetClass.FUTURE, t)
    sid_b = strategy_id([market_b], AssetClass.FUTURE, t)
    for sid, mkt in [(sid_a, market_a), (sid_b, market_b)]:
        store.upsert(StrategyRecord(
            strategy_id=sid, markets=[mkt], asset_class=AssetClass.FUTURE, spec=t,
            status=StrategyStatus.DEPLOYED, discovered_at=_NOW, updated_at=_NOW,
            regime_fits=[RegimeFit(regime="trend_up|low_vol", dsr=0.97, is_sharpe=1.3, oos_sharpe=0.9, passed=True),
                         RegimeFit(regime="trend_up|high_vol", dsr=0.96, is_sharpe=1.2, oos_sharpe=0.8, passed=True)],
        ))
    return sid_a, sid_b


def test_hrp_weights_favour_heavier_sleeve(tmp_path) -> None:
    """When a recorded HRP allocation favours sleeve A (weight 0.7) over B (weight 0.3),
    assemble() must give A a larger absolute target weight than B.
    Both legs are active in the current uptrend regime (trend_up)."""
    settings = Settings(analytical_path=str(tmp_path / "an.duckdb"), operational_dsn=f"sqlite:///{tmp_path / 's.db'}")
    store = SqlStrategyStore(settings.operational_dsn)
    sid_a, sid_b = _make_two_deployed_specialists(store, "AAA", "BBB")

    # HRP says A is the dominant sleeve
    hrp = {sid_a: 0.7, sid_b: 0.3}

    ex = RegimeConditionalExecutor(
        FakeData(), DuckDbAnalyticalStore(settings.analytical_path), store, settings,
        hrp_weights=hrp,
    )
    rb = ex.assemble(status=StrategyStatus.DEPLOYED, max_gross_leverage=1.0)

    # Both should be active (both are trend specialists in an uptrend)
    active_markets = {leg.market for leg in rb.active}
    assert "AAA" in active_markets and "BBB" in active_markets

    w_a = abs(rb.target_weights.get("AAA", 0.0))
    w_b = abs(rb.target_weights.get("BBB", 0.0))
    assert w_a > w_b, f"Expected |w_AAA|={w_a:.4f} > |w_BBB|={w_b:.4f} with HRP 0.7/0.3"


def test_no_allocation_falls_back_to_equal_weight(tmp_path) -> None:
    """When hrp_weights=None (no allocation provided), both legs are sized equally."""
    settings = Settings(analytical_path=str(tmp_path / "an.duckdb"), operational_dsn=f"sqlite:///{tmp_path / 's.db'}")
    store = SqlStrategyStore(settings.operational_dsn)
    sid_a, sid_b = _make_two_deployed_specialists(store, "CCC", "DDD")

    ex = RegimeConditionalExecutor(
        FakeData(), DuckDbAnalyticalStore(settings.analytical_path), store, settings,
        hrp_weights=None,  # explicit equal-weight fallback
    )
    rb = ex.assemble(status=StrategyStatus.DEPLOYED, max_gross_leverage=1.0)

    w_c = abs(rb.target_weights.get("CCC", 0.0))
    w_d = abs(rb.target_weights.get("DDD", 0.0))
    # Both legs have the same signal (steady uptrend position) so weights should be equal
    assert abs(w_c - w_d) < 1e-9, f"Expected equal weights, got CCC={w_c:.4f} DDD={w_d:.4f}"


def test_gross_budget_respected_with_hrp_weights(tmp_path) -> None:
    """HRP-weighted book must never exceed max_gross_leverage in total gross exposure."""
    settings = Settings(analytical_path=str(tmp_path / "an.duckdb"), operational_dsn=f"sqlite:///{tmp_path / 's.db'}")
    store = SqlStrategyStore(settings.operational_dsn)
    sid_a, sid_b = _make_two_deployed_specialists(store, "EEE", "FFF")

    # Deliberately un-normalised HRP weights (sum > 1)
    hrp = {sid_a: 0.9, sid_b: 0.8}

    ex = RegimeConditionalExecutor(
        FakeData(), DuckDbAnalyticalStore(settings.analytical_path), store, settings,
        hrp_weights=hrp,
    )
    max_gross = 1.5
    rb = ex.assemble(status=StrategyStatus.DEPLOYED, max_gross_leverage=max_gross)
    gross = sum(abs(w) for w in rb.target_weights.values())
    assert gross <= max_gross + 1e-9, f"Gross {gross:.4f} exceeds max_gross {max_gross}"


def test_unallocated_leg_gets_equal_weight_of_remainder(tmp_path) -> None:
    """A leg not in the HRP dict (newly promoted) gets an equal share of the unallocated budget;
    the allocated leg gets its HRP-proportional share."""
    settings = Settings(analytical_path=str(tmp_path / "an.duckdb"), operational_dsn=f"sqlite:///{tmp_path / 's.db'}")
    store = SqlStrategyStore(settings.operational_dsn)
    sid_a, sid_b = _make_two_deployed_specialists(store, "GGG", "HHH")

    # Only sid_a is in the HRP allocation — sid_b is newly promoted, not yet recorded.
    hrp = {sid_a: 1.0}  # sid_b absent

    ex = RegimeConditionalExecutor(
        FakeData(), DuckDbAnalyticalStore(settings.analytical_path), store, settings,
        hrp_weights=hrp,
    )
    rb = ex.assemble(status=StrategyStatus.DEPLOYED, max_gross_leverage=1.0)

    # Both should be active
    active_markets = {leg.market for leg in rb.active}
    assert "GGG" in active_markets and "HHH" in active_markets

    w_g = abs(rb.target_weights.get("GGG", 0.0))
    w_h = abs(rb.target_weights.get("HHH", 0.0))
    # With n_allocated=1, n_unallocated=1: each gets 0.5 of the budget → equal
    # (The split is n_allocated/n_total and n_unallocated/n_total)
    # They should be equal here since the HRP budget for 1 leg = ew budget for 1 leg = 0.5
    assert abs(w_g - w_h) < 1e-9, f"With one allocated one unallocated: expected equal split, got GGG={w_g:.4f} HHH={w_h:.4f}"


def test_factory_store_latest_weights_no_cycle(tmp_path) -> None:
    """latest_weights() returns None when no cycle has ever been recorded."""
    fs = FactoryStore(f"sqlite:///{tmp_path / 'fs.db'}")
    assert fs.latest_weights() is None


def test_factory_store_latest_weights_returns_allocation(tmp_path) -> None:
    """latest_weights() returns the weights dict from the most recent cycle."""
    fs = FactoryStore(f"sqlite:///{tmp_path / 'fs.db'}")
    now = dt.datetime(2026, 6, 14)
    fs.record_cycle(cycle_id="fc01", proposed=2, tested=2, survived=1, forward=1,
                    deployed=1, retired=0, global_trials=10, at=now)
    fs.record_allocation("fc01", {"strat-abc": 0.6, "strat-xyz": 0.4}, now)

    weights = fs.latest_weights()
    assert weights == {"strat-abc": 0.6, "strat-xyz": 0.4}


def test_factory_store_latest_weights_empty_allocation(tmp_path) -> None:
    """latest_weights() returns {} (not None) when a cycle ran but no sleeves were allocated."""
    fs = FactoryStore(f"sqlite:///{tmp_path / 'fs.db'}")
    now = dt.datetime(2026, 6, 14)
    fs.record_cycle(cycle_id="fc02", proposed=0, tested=0, survived=0, forward=0,
                    deployed=0, retired=0, global_trials=0, at=now)
    # No record_allocation call → allocation table is empty for this cycle

    weights = fs.latest_weights()
    assert weights == {}  # cycle exists, no allocation rows → empty dict (not None)


# ── I1: zero-sum HRP guard — must not crash, must produce equal-weight fallback ─


def test_hrp_zero_weights_all_allocated_no_crash(tmp_path) -> None:
    """When ALL active legs have HRP weight 0.0 (all-allocated path, hrp_sum=0),
    assemble() must NOT raise ZeroDivisionError; it must fall back to equal-weight
    within max_gross_leverage."""
    settings = Settings(analytical_path=str(tmp_path / "an.duckdb"), operational_dsn=f"sqlite:///{tmp_path / 's.db'}")
    store = SqlStrategyStore(settings.operational_dsn)
    sid_a, sid_b = _make_two_deployed_specialists(store, "ZZA", "ZZB")

    # Both legs have weight 0.0 → hrp_sum=0 → would crash without the guard
    hrp = {sid_a: 0.0, sid_b: 0.0}

    ex = RegimeConditionalExecutor(
        FakeData(), DuckDbAnalyticalStore(settings.analytical_path), store, settings,
        hrp_weights=hrp,
    )
    rb = ex.assemble(status=StrategyStatus.DEPLOYED, max_gross_leverage=1.0)

    # Must not crash; both legs should be active
    active_markets = {leg.market for leg in rb.active}
    assert "ZZA" in active_markets and "ZZB" in active_markets

    # Equal-weight fallback: each leg gets 0.5 of the gross budget
    w_a = abs(rb.target_weights.get("ZZA", 0.0))
    w_b = abs(rb.target_weights.get("ZZB", 0.0))
    assert abs(w_a - w_b) < 1e-9, f"Expected equal-weight fallback; got ZZA={w_a:.4f} ZZB={w_b:.4f}"
    gross = sum(abs(w) for w in rb.target_weights.values())
    assert gross <= 1.0 + 1e-9, f"Gross {gross:.4f} exceeds max_gross_leverage=1.0"


def test_hrp_zero_weight_single_leg_no_crash(tmp_path) -> None:
    """Single-leg book with {'a': 0.0} (all-allocated, hrp_sum=0) must not crash
    and must produce a valid equal-weight result within max_gross_leverage."""
    settings = Settings(analytical_path=str(tmp_path / "an.duckdb"), operational_dsn=f"sqlite:///{tmp_path / 's.db'}")
    store = SqlStrategyStore(settings.operational_dsn)
    t = _spec("trend")
    sid_a = strategy_id(["ZZ1"], AssetClass.FUTURE, t)
    store.upsert(StrategyRecord(
        strategy_id=sid_a, markets=["ZZ1"], asset_class=AssetClass.FUTURE, spec=t,
        status=StrategyStatus.DEPLOYED, discovered_at=_NOW, updated_at=_NOW,
        regime_fits=[RegimeFit(regime="trend_up|low_vol", dsr=0.97, is_sharpe=1.3, oos_sharpe=0.9, passed=True),
                     RegimeFit(regime="trend_up|high_vol", dsr=0.96, is_sharpe=1.2, oos_sharpe=0.8, passed=True)],
    ))

    hrp = {sid_a: 0.0}  # single leg, weight zero → hrp_sum=0

    ex = RegimeConditionalExecutor(
        FakeData(), DuckDbAnalyticalStore(settings.analytical_path), store, settings,
        hrp_weights=hrp,
    )
    rb = ex.assemble(status=StrategyStatus.DEPLOYED, max_gross_leverage=1.0)

    assert "ZZ1" in {leg.market for leg in rb.active}
    gross = sum(abs(w) for w in rb.target_weights.values())
    assert gross <= 1.0 + 1e-9, f"Gross {gross:.4f} exceeds max_gross_leverage=1.0"


def test_hrp_zero_weights_mixed_branch_no_crash(tmp_path) -> None:
    """Mixed branch: one leg has HRP weight 0.0 (hrp_sum=0) and one is unallocated.
    Must not crash and must produce equal-weight fallback."""
    settings = Settings(analytical_path=str(tmp_path / "an.duckdb"), operational_dsn=f"sqlite:///{tmp_path / 's.db'}")
    store = SqlStrategyStore(settings.operational_dsn)
    sid_a, sid_b = _make_two_deployed_specialists(store, "ZZX", "ZZY")

    # sid_a has weight 0.0; sid_b is absent (unallocated) → mixed branch, hrp_sum=0
    hrp = {sid_a: 0.0}

    ex = RegimeConditionalExecutor(
        FakeData(), DuckDbAnalyticalStore(settings.analytical_path), store, settings,
        hrp_weights=hrp,
    )
    rb = ex.assemble(status=StrategyStatus.DEPLOYED, max_gross_leverage=1.0)

    active_markets = {leg.market for leg in rb.active}
    assert "ZZX" in active_markets and "ZZY" in active_markets

    # Equal-weight fallback when hrp_sum <= 0
    w_x = abs(rb.target_weights.get("ZZX", 0.0))
    w_y = abs(rb.target_weights.get("ZZY", 0.0))
    assert abs(w_x - w_y) < 1e-9, f"Expected equal-weight fallback; got ZZX={w_x:.4f} ZZY={w_y:.4f}"
    gross = sum(abs(w) for w in rb.target_weights.values())
    assert gross <= 1.0 + 1e-9, f"Gross {gross:.4f} exceeds max_gross_leverage=1.0"
