"""Factory loop: a forward-survivor uncorrelated to the deployed book promotes (within governance) and the
deployed book is HRP-allocated; a tail-correlated survivor does NOT promote; kill-switch blocks promotion."""
from __future__ import annotations

import numpy as np

from fxhnt.application.factory.factory_loop import FactoryLoop, LoopConfig


class FakeStore:
    """Minimal StrategyStore stand-in keyed by strategy_id with status + forward_returns."""
    def __init__(self, records): self._r = {r["id"]: r for r in records}
    def deployed(self): return [r for r in self._r.values() if r["status"] == "DEPLOYED"]
    def forward(self): return [r for r in self._r.values() if r["status"] == "FORWARD"]
    def set_status(self, sid, status): self._r[sid]["status"] = status
    def returns(self, sid): return np.array(self._r[sid]["returns"])


def _cfg(**kw):
    base = dict(max_tail_corr=0.5, min_marginal=0.0, tail_frac=0.1, min_forward_days=20,
                max_deployed=20, max_new_per_cycle=2, kill_switch=False)
    base.update(kw); return LoopConfig(**base)


def _rec(id, status, returns):
    return {"id": id, "status": status, "returns": returns}


def test_uncorrelated_survivor_promotes_and_allocates() -> None:
    rng = np.random.default_rng(0)
    book = list(rng.normal(0.0005, 0.01, 300))
    cand = list(rng.normal(0.0005, 0.01, 300))                # independent
    store = FakeStore([_rec("DEP", "DEPLOYED", book), _rec("CAND", "FORWARD", cand)])
    loop = FactoryLoop(store, _cfg())
    res = loop.allocate_and_promote()
    assert "CAND" in res.promoted
    assert store._r["CAND"]["status"] == "DEPLOYED"
    assert abs(sum(res.weights.values()) - 1.0) < 1e-9 and set(res.weights) == {"DEP", "CAND"}


def test_tail_correlated_survivor_blocked() -> None:
    rng = np.random.default_rng(1)
    book = list(rng.normal(0.0005, 0.01, 300))
    store = FakeStore([_rec("DEP", "DEPLOYED", book), _rec("CAND", "FORWARD", list(book))])  # identical
    loop = FactoryLoop(store, _cfg())
    res = loop.allocate_and_promote()
    assert "CAND" not in res.promoted and store._r["CAND"]["status"] == "FORWARD"


def test_kill_switch_blocks_promotion() -> None:
    rng = np.random.default_rng(2)
    book = list(rng.normal(0.0005, 0.01, 300)); cand = list(rng.normal(0.0005, 0.01, 300))
    store = FakeStore([_rec("DEP", "DEPLOYED", book), _rec("CAND", "FORWARD", cand)])
    loop = FactoryLoop(store, _cfg(kill_switch=True))
    res = loop.allocate_and_promote()
    assert res.promoted == [] and store._r["CAND"]["status"] == "FORWARD"


# Fix 8 — new tests -----------------------------------------------------------

def test_below_min_forward_days_not_promoted() -> None:
    """A forward survivor with fewer than min_forward_days returns must be silently skipped."""
    rng = np.random.default_rng(42)
    book = list(rng.normal(0.0005, 0.01, 300))
    # Only 10 days of history — below the min_forward_days=20 threshold
    short = list(rng.normal(0.0005, 0.01, 10))
    store = FakeStore([_rec("DEP", "DEPLOYED", book), _rec("SHORT", "FORWARD", short)])
    loop = FactoryLoop(store, _cfg(min_forward_days=20))
    res = loop.allocate_and_promote()
    assert "SHORT" not in res.promoted
    assert store._r["SHORT"]["status"] == "FORWARD"
    assert res.forward_count == 0  # it never reached the gate


def test_empty_book_admits_first_then_gates_rest() -> None:
    """Empty deployed book: first uncorrelated survivor gets a founding admit, but a duplicate of it
    must be blocked by the running-blend re-gate on the second candidate."""
    rng = np.random.default_rng(7)
    a_returns = list(rng.normal(0.001, 0.01, 100))
    b_returns = list(a_returns)  # identical to A → corr ≈ 1.0 → must be blocked after A is admitted
    store = FakeStore([
        _rec("A", "FORWARD", a_returns),
        _rec("B", "FORWARD", b_returns),
    ])
    loop = FactoryLoop(store, _cfg(max_new_per_cycle=2))
    res = loop.allocate_and_promote()
    # Exactly one should be admitted; B is a duplicate of A so it must be blocked
    assert len(res.promoted) == 1
    promoted_id = res.promoted[0]
    other_id = "B" if promoted_id == "A" else "A"
    assert store._r[promoted_id]["status"] == "DEPLOYED"
    assert store._r[other_id]["status"] == "FORWARD"


def test_mutually_correlated_same_cycle_admits_blocked() -> None:
    """Two forward survivors uncorrelated to the book but nearly identical to each other:
    only ONE should be admitted per cycle even though max_new_per_cycle=2."""
    rng = np.random.default_rng(99)
    book = list(rng.normal(0.0005, 0.01, 300))
    # Candidates are independent of the book but almost perfectly correlated with each other
    base = rng.normal(0.001, 0.01, 300)
    noise = rng.normal(0, 1e-6, 300)
    cand_a = list(base)
    cand_b = list(base + noise)  # corr(cand_a, cand_b) ≈ 1.0
    store = FakeStore([
        _rec("DEP", "DEPLOYED", book),
        _rec("CA",  "FORWARD",  cand_a),
        _rec("CB",  "FORWARD",  cand_b),
    ])
    loop = FactoryLoop(store, _cfg(max_new_per_cycle=2))
    res = loop.allocate_and_promote()
    # Both pass the gate vs the book, but the second must fail the running-blend re-gate
    assert len(res.promoted) == 1
