from fxhnt.application.bybit_exec_record import (
    build_crypto_pos_state,
    compute_crypto_exec_return,
    crypto_envelope,
)


def _state(positions, marks, envelope, cursor=0):
    return build_crypto_pos_state(positions, marks, envelope, cursor)


def test_inception_and_flat_return_none():
    assert compute_crypto_exec_return({}, {"BTCUSDT": 100.0}, 0.0, 0.0) is None          # no prior book
    flat = _state({"BTCUSDT": 1.0}, {"BTCUSDT": 100.0}, 0.0)                              # envelope 0
    assert compute_crypto_exec_return(flat, {"BTCUSDT": 100.0}, 0.0, 0.0) is None


def test_envelope_basis_undiluted():
    # $10k envelope, book long 100 units entered at 100 -> mark 101 = +$100 price pnl. Return = 100/10000 = 1%,
    # regardless of any (irrelevant) larger testnet balance — the divisor is the ENVELOPE.
    prior = _state({"BTCUSDT": 100.0}, {"BTCUSDT": 100.0}, 10_000.0)
    r = compute_crypto_exec_return(prior, {"BTCUSDT": 101.0}, 0.0, 0.0)
    assert abs(r - 0.01) < 1e-9


def test_funding_is_return_not_flow():
    # flat marks, +$50 funding on a $10k envelope -> +0.5% (funding is edge return).
    prior = _state({"BTCUSDT": 100.0}, {"BTCUSDT": 100.0}, 10_000.0)
    r = compute_crypto_exec_return(prior, {"BTCUSDT": 100.0}, 50.0, 0.0)
    assert abs(r - 0.005) < 1e-9


def test_fees_reduce_return():
    # flat marks, $30 fees on a $10k envelope -> -0.3%.
    prior = _state({"BTCUSDT": 100.0}, {"BTCUSDT": 100.0}, 10_000.0)
    r = compute_crypto_exec_return(prior, {"BTCUSDT": 100.0}, 0.0, 30.0)
    assert abs(r - (-0.003)) < 1e-9


def test_missing_today_mark_falls_back_to_prior():
    prior = _state({"BTCUSDT": 100.0}, {"BTCUSDT": 100.0}, 10_000.0)
    assert compute_crypto_exec_return(prior, {}, 0.0, 0.0) == 0.0                          # no price move


def test_missing_prior_mark_is_zero_pnl_not_full_notional():
    # a held symbol absent from PRIOR marks (rotated out then back) must NOT book its full notional as gain.
    prior = _state({"BTCUSDT": 100.0}, {}, 10_000.0)                                       # no prior mark stored
    r = compute_crypto_exec_return(prior, {"BTCUSDT": 101.0}, 0.0, 0.0)
    assert r == 0.0                                                                        # NOT +101*100/10000


class _Repo:
    def __init__(self, alloc):
        self._alloc = alloc

    def current_allocation(self):
        return self._alloc


def test_crypto_envelope_caps_at_equity_and_zero_when_absent():
    assert crypto_envelope(_Repo({"bybit_4edge": 8_000.0}), 5_000.0) == 5_000.0     # capped at equity
    assert crypto_envelope(_Repo({"bybit_4edge": 8_000.0}), 20_000.0) == 8_000.0    # alloc binds
    assert crypto_envelope(_Repo({}), 20_000.0) == 0.0                              # de-funded -> flatten


def test_apply_fills_builds_post_rebalance_book():
    from fxhnt.application.bybit_exec_record import apply_fills
    current = {"BTCUSDT": 1.0, "ETHUSDT": -2.0}
    fills = [{"symbol": "BTCUSDT", "side": "BUY", "qty": 0.5},      # 1.0 + 0.5 = 1.5
             {"symbol": "ETHUSDT", "side": "BUY", "qty": 2.0},      # -2.0 + 2.0 = 0.0 -> dropped
             {"symbol": "SOLUSDT", "side": "SELL", "qty": 3.0},     # new short -3.0
             {"symbol": "BTCUSDT", "error": True, "qty": 9.0}]      # error row ignored
    assert apply_fills(current, fills) == {"BTCUSDT": 1.5, "SOLUSDT": -3.0}


def test_apply_fills_case_insensitive_side():
    """Lowercase and mixed-case sides should be normalized and classified correctly."""
    from fxhnt.application.bybit_exec_record import apply_fills

    # lowercase "sell" should result in negative signed qty
    current = {"ETHUSDT": 1.0}
    fills = [{"symbol": "ETHUSDT", "side": "sell", "qty": 1.0}]  # 1.0 + (-1.0) = 0.0 -> dropped
    assert apply_fills(current, fills) == {}
    # mixed-case "BuY" should result in positive signed qty
    current = {"BTCUSDT": 0.0}
    fills = [{"symbol": "BTCUSDT", "side": "BuY", "qty": 1.5}]  # 0.0 + 1.5 = 1.5
    assert apply_fills(current, fills) == {"BTCUSDT": 1.5}


def test_apply_fills_rejects_unknown_side():
    """Unknown side values (e.g. 'HOLD') should raise ValueError."""
    import pytest

    from fxhnt.application.bybit_exec_record import apply_fills

    current = {"SOLUSDT": 0.0}
    fills = [{"symbol": "SOLUSDT", "side": "HOLD", "qty": 2.0}]
    with pytest.raises(ValueError, match="apply_fills: unexpected fill side"):
        apply_fills(current, fills)


def test_build_crypto_pos_state_persists_venue():
    from fxhnt.application.bybit_exec_record import build_crypto_pos_state
    st = build_crypto_pos_state({"BTCUSDT": 1.0}, {"BTCUSDT": 60000.0}, 5000.0, 123, venue="bybit-testnet")
    assert st["venue"] == "bybit-testnet" and st["funding_cursor_ts"] == 123


def test_paper_or_b2a_crypto_envelope_paper_mode():
    from fxhnt.application.bybit_exec_record import paper_or_b2a_crypto_envelope
    class _Repo:
        def current_allocation(self):
            raise AssertionError("B2a must not be read in paper mode")
    assert paper_or_b2a_crypto_envelope(_Repo(), equity=8000.0, paper_envelope=10000.0, is_paper=True) == 8000.0


def test_paper_or_b2a_crypto_envelope_refuses_non_testnet():
    import pytest
    from fxhnt.application.bybit_exec_record import paper_or_b2a_crypto_envelope
    with pytest.raises(ValueError, match="paper-envelope refused"):
        paper_or_b2a_crypto_envelope(object(), equity=8000.0, paper_envelope=10000.0, is_paper=False)


def test_paper_or_b2a_crypto_envelope_default_reads_b2a():
    from fxhnt.application.bybit_exec_record import paper_or_b2a_crypto_envelope
    class _Repo:
        def current_allocation(self):
            return {"bybit_4edge": 3000.0}
    assert paper_or_b2a_crypto_envelope(_Repo(), equity=8000.0, paper_envelope=0.0, is_paper=True) == 3000.0
