from fxhnt.application.multistrat_exec_record import build_pos_state, compute_exec_return, scaled_weights


def test_scaled_weights_bridge_and_flatten():
    w = {"SPY": 0.6, "IEF": 0.4}
    # envelope 10k in a 100k account → scale 0.1 → weights sum to envelope/NLV
    s = scaled_weights(w, envelope=10_000.0, nlv=100_000.0)
    assert abs(s["SPY"] - 0.06) < 1e-12 and abs(s["IEF"] - 0.04) < 1e-12
    assert scaled_weights(w, 0.0, 100_000.0) == {"SPY": 0.0, "IEF": 0.0}      # $0 envelope → flatten
    assert scaled_weights(w, 10_000.0, 0.0) == {"SPY": 0.0, "IEF": 0.0}       # no NLV → flatten (safe)


def test_compute_exec_return_first_run_is_none():
    assert compute_exec_return({}, {"SPY": 500.0}) is None                    # no prior book → inception


def test_exec_return_is_envelope_basis_not_full_nlv():
    # prior book: $10k envelope fully deployed in 20 SPY @ 500 (=$10k), in a (hypothetical) large account.
    prior = build_pos_state({"SPY": 20.0}, {"SPY": 500.0}, envelope=10_000.0)
    # SPY +10% today → book +$1000 on the $10k envelope → 10% return (NOT diluted by any idle account cash)
    assert abs(compute_exec_return(prior, {"SPY": 550.0}) - 0.10) < 1e-9


def test_reallocation_capital_flow_is_not_return():
    # flat prices; the envelope will grow at the NEXT rebalance (a flow) — the RETURN of the prior book is ~0
    prior = build_pos_state({"SPY": 20.0}, {"SPY": 500.0}, envelope=10_000.0)
    assert abs(compute_exec_return(prior, {"SPY": 500.0})) < 1e-12            # flat prices → 0%, not a jump


def test_partial_deployment_reserve_earns_zero():
    # $10k envelope, only $6k deployed (12 SPY @ 500), $4k reserve. SPY +10% → +$600 on $10k = 6%.
    prior = build_pos_state({"SPY": 12.0}, {"SPY": 500.0}, envelope=10_000.0)
    assert abs(compute_exec_return(prior, {"SPY": 550.0}) - 0.06) < 1e-9


def test_none_prior_returns_none():
    # None prior → inception, no AttributeError on .get() call
    assert compute_exec_return(None, {"SPY": 500.0}) is None


def test_missing_price_degrades_not_raises():
    # Position absent from today's prices uses prior price; no KeyError raised
    prior = build_pos_state({"SPY": 20.0}, {"SPY": 500.0}, envelope=10_000.0)
    assert compute_exec_return(prior, {}) == 0.0


def test_build_pos_state_persists_venue():
    from fxhnt.application.multistrat_exec_record import build_pos_state
    st = build_pos_state({"SPY": 10.0}, {"SPY": 5.0}, 1000.0, venue="ibkr-equity")
    assert st["venue"] == "ibkr-equity" and st["envelope"] == 1000.0


def test_build_pos_state_venue_defaults_empty():
    from fxhnt.application.multistrat_exec_record import build_pos_state
    assert build_pos_state({}, {}, 0.0)["venue"] == ""


def test_exec_return_venue_switch_is_inception():
    """F3: the FIRST run on a new venue (prior book keyed by the OTHER venue's symbols) books NO return —
    comparing US SPY-keyed positions against UCITS CSPX-keyed prices would be garbage."""
    from fxhnt.application.multistrat_exec_record import exec_return_for_run
    us_prior = build_pos_state({"SPY": 20.0}, {"SPY": 500.0}, envelope=10_000.0, venue="ibkr-paper")
    # switching to the UCITS venue with UCITS-keyed prices -> inception (None), NOT a spurious return
    assert exec_return_for_run(us_prior, {"CSPX": 620.0}, venue="ibkr-ucits") is None


def test_exec_return_same_venue_books_normal_return():
    """After the inception run, subsequent SAME-venue runs book the real return as usual."""
    from fxhnt.application.multistrat_exec_record import exec_return_for_run
    ucits_prior = build_pos_state({"CSPX": 16.0}, {"CSPX": 620.0}, envelope=10_000.0, venue="ibkr-ucits")
    # $10k envelope, ~$9,920 deployed (16 CSPX @ 620); CSPX +10% -> +$992 on $10k ≈ 9.92%
    r = exec_return_for_run(ucits_prior, {"CSPX": 682.0}, venue="ibkr-ucits")
    assert r is not None and abs(r - 0.0992) < 1e-4


def test_exec_return_no_prior_is_inception():
    from fxhnt.application.multistrat_exec_record import exec_return_for_run
    assert exec_return_for_run({}, {"CSPX": 620.0}, venue="ibkr-ucits") is None
    assert exec_return_for_run(None, {"CSPX": 620.0}, venue="ibkr-ucits") is None


class _FakeExecSvc:
    def __init__(self):
        self.calls = []

    def rebalance_weights(self, sid, weights, prices, *, max_gross, execute, contract_specs=None,
                          sizing_capital=None):
        self.calls.append((sid, dict(weights), execute))
        self.sizing_capital = sizing_capital   # the envelope basis for the entry-floor threshold
        class _Plan:
            nlv = 100000.0; orders = []; notes = []; blocked = False; executed = execute
        return _Plan()


class _FakeRepo:
    def __init__(self, alloc):
        self._alloc = alloc; self.book = {}

    def current_allocation(self):  # B2a — must NOT be called in paper mode
        raise AssertionError("current_allocation must not be read in paper mode")

    def get_book_state(self, k):
        return self.book.get(k, {})

    def set_book_state(self, k, extra, at):
        self.book[k] = extra


class _B2aRepo(_FakeRepo):
    def current_allocation(self):
        return self._alloc


def test_plan_and_record_paper_mode_bypasses_b2a():
    import datetime as dt
    from fxhnt.application.multistrat_exec_record import plan_and_record
    repo = _FakeRepo(alloc={"multistrat": 0.0}); svc = _FakeExecSvc()
    _plan, envelope = plan_and_record(
        repo=repo, exec_svc=svc, within_weights={"SPY": 1.0}, prices={"SPY": 10.0},
        nlv=500000.0, today="2026-07-11", at=dt.datetime(2026, 7, 11), max_gross=2.0,
        execute=False, paper_envelope=1_000_000.0, account_is_paper=True, venue="ibkr-equity")
    assert envelope == 500000.0  # min(1e6 paper, 5e5 nlv); B2a NOT consulted


def test_plan_and_record_paper_mode_refuses_live_account():
    import datetime as dt
    import pytest
    from fxhnt.application.multistrat_exec_record import plan_and_record
    repo = _FakeRepo(alloc={"multistrat": 0.0}); svc = _FakeExecSvc()
    with pytest.raises(ValueError, match="paper-envelope refused"):
        plan_and_record(
            repo=repo, exec_svc=svc, within_weights={"SPY": 1.0}, prices={"SPY": 10.0},
            nlv=500000.0, today="2026-07-11", at=dt.datetime(2026, 7, 11), max_gross=2.0,
            execute=True, paper_envelope=1_000_000.0, account_is_paper=False, venue="ibkr-equity")
    assert svc.calls == []  # NO orders on refusal


def test_plan_and_record_default_mode_reads_b2a():
    import datetime as dt
    from fxhnt.application.multistrat_exec_record import plan_and_record
    repo = _B2aRepo(alloc={"multistrat": 25000.0}); svc = _FakeExecSvc()
    _plan, envelope = plan_and_record(
        repo=repo, exec_svc=svc, within_weights={"SPY": 1.0}, prices={"SPY": 10.0},
        nlv=500000.0, today="2026-07-11", at=dt.datetime(2026, 7, 11), max_gross=2.0,
        execute=False, paper_envelope=0.0, account_is_paper=True)
    assert envelope == 25000.0  # min($25k B2a, $500k nlv)


# ---- superseded_holdings: self-cleaning of multistrat's OWN legacy lines (e.g. IDTM after IEF->CBU0) --------
import datetime as _dt
from types import SimpleNamespace

from fxhnt.application.multistrat_exec_record import superseded_holdings


def _fill(sym, price, day):
    return SimpleNamespace(symbol=sym, price=price, at=_dt.datetime(2026, 7, day, 9, 0))


def test_superseded_flags_own_legacy_line_at_last_fill_price():
    # multistrat traded IDTM (the legacy IEF line) + CSPX; it now targets CSPX/CBU0/IGLN. Held: IDTM + CSPX.
    fills = [_fill("IDTM", 95.0, 15), _fill("CSPX", 500.0, 15), _fill("IDTM", 96.0, 16)]
    out = superseded_holdings(fills=fills, current_targets={"CSPX", "CBU0", "IGLN"},
                              held={"IDTM": 98.0, "CSPX": 24.0})
    assert out == {"IDTM": 96.0}          # IDTM: held + traded + not a target -> flagged @ LAST fill price


def test_superseded_never_touches_another_strategys_position():
    # A held symbol multistrat NEVER traded (another strategy's line on the SHARED account) is never flagged.
    out = superseded_holdings(fills=[_fill("CSPX", 500.0, 15)], current_targets={"CSPX"},
                              held={"CSPX": 24.0, "SOMEOTHER": 10.0})
    assert out == {}


def test_superseded_ignores_zero_qty_and_current_targets():
    out = superseded_holdings(fills=[_fill("IDTM", 95.0, 15), _fill("CSPX", 500.0, 15)],
                              current_targets={"CSPX", "CBU0"}, held={"IDTM": 0.0, "CSPX": 24.0})
    assert out == {}                       # IDTM flat (0 qty); CSPX is a current target
