"""Tests for the FAK no-match re-quote retry (2026-07-20): a first-attempt
OPEN whose FAK dies unmatched is handed to Copybot.fak_requote_retry instead
of recording a miss; the retry re-enters _handle_their_buy(retry=True) with
their_size=0 once, and only a SECOND rejection records the miss (tagged).
Run: python3 tests/test_fak_retry.py — needs no network, no config.
"""
import os
import sys
import tempfile
import time

sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
import copybot    # noqa: E402
import copytrade  # noqa: E402

_TMP = tempfile.mkdtemp(prefix="copybot_test_")
FAK_RESP = "exception: no orders found to match with FAK order. FAK orders..."
WALLET = "0xabc"
TOK = "T1"

fails = []


def case(name):
    def deco(fn):
        cp0, bd0 = copytrade.clob_price, copybot.book_depth
        try:
            fn()
            print(f"PASS  {name}")
        except AssertionError as e:
            print(f"FAIL  {name}: {e}")
            fails.append(name)
        finally:
            copytrade.clob_price, copybot.book_depth = cp0, bd0
    return deco


class ScriptedEx:
    """Returns the scripted buy responses in order; repeats the last one."""
    live = False

    def __init__(self, *resps):
        self.resps = list(resps)
        self.fills = []
        self.calls = 0

    def buy(self, token_id, shares, price, meta):
        self.calls += 1
        r = self.resps.pop(0) if len(self.resps) > 1 else self.resps[0]
        if r == "fak":
            return {"ok": False, "filled_shares": 0.0, "price": price,
                    "resp": FAK_RESP, "paper": True}
        if r == "other":
            return {"ok": False, "filled_shares": 0.0, "price": price,
                    "resp": "some other rejection", "paper": True}
        self.fills.append({"side": "BUY", "token": token_id,
                           "shares": shares, "price": price})
        return {"ok": True, "filled_shares": shares, "price": price,
                "paper": True}


def mkengine(*resps):
    cfg = {"bankroll_usd": 100.0, "bankroll_pct": 0.04,
           "watch": [{"wallet": WALLET, "name": "TestSharp"}],
           "feed_path": os.path.join(_TMP, "feed.json"),
           "fill_log": os.path.join(_TMP, "fills.jsonl"),
           "risk": {"min_price": 0.02, "max_price": 0.99,
                    "max_open_positions": 50, "min_order_usd": 1.0,
                    "max_trade_usd": 100.0, "daily_spend_cap_usd": 1000.0,
                    "max_total_exposure_usd": 1000.0}}
    state = copytrade.new_state()
    state["cash"] = 100.0
    eng = copytrade.CopyTrader(cfg, state, ScriptedEx(*resps),
                               os.path.join(_TMP, "state.json"))
    copytrade.clob_price = lambda t, s: 0.50
    return eng


def their_buy(eng, retry=False, their_size=100.0):
    eng.state["their_pos"].setdefault(WALLET, {})
    if retry:                       # host passes 0 — their_pos already counts it
        eng.state["their_pos"][WALLET][TOK] = their_size
        their_size = 0.0
    eng._handle_their_buy(WALLET, TOK, their_size, 0.50, "Yes · Test market",
                          "Test market", "Yes", event="ev-1", cond="0xc0nd",
                          their_ts=123, retry=retry)


@case("first FAK reject on an OPEN -> hook fires with the copy ctx, NO miss")
def t1():
    eng = mkengine("fak")
    calls = []
    eng.on_fak_reject = calls.append
    their_buy(eng)
    assert len(calls) == 1, f"hook calls: {len(calls)}"
    ctx = calls[0]
    assert ctx["token"] == TOK and ctx["wallet"] == WALLET
    assert ctx["their_price"] == 0.50 and ctx["cond"] == "0xc0nd"
    assert not eng.state.get("missed"), eng.state.get("missed")
    assert TOK not in eng.state["my_pos"]


@case("retry=True rejection -> miss recorded 'twice (re-quote retry)', no re-schedule")
def t2():
    eng = mkengine("fak")
    calls = []
    eng.on_fak_reject = calls.append
    their_buy(eng, retry=True)
    assert not calls, "retry must never re-schedule"
    m = eng.state["missed"]
    assert len(m) == 1 and "twice (re-quote retry)" in m[0]["reason"], m


@case("no hook installed -> miss recorded at once (pre-change behavior)")
def t3():
    eng = mkengine("fak")
    their_buy(eng)
    m = eng.state["missed"]
    assert len(m) == 1 and m[0]["reason"].startswith("order rejected: "), m


@case("non-FAK rejection -> no hook, miss recorded at once")
def t4():
    eng = mkengine("other")
    calls = []
    eng.on_fak_reject = calls.append
    their_buy(eng)
    assert not calls
    assert len(eng.state["missed"]) == 1


@case("FAK reject on an ADD -> no hook, no miss (adds were never miss-tracked)")
def t5():
    eng = mkengine("fak")
    calls = []
    eng.on_fak_reject = calls.append
    eng.state["my_pos"][TOK] = {"shares": 2.0, "cost": 1.0,
                                "title": "Test market", "outcome": "Yes"}
    eng.state["their_pos"][WALLET] = {TOK: 100.0}
    eng._handle_their_buy(WALLET, TOK, 100.0, 0.50, "Yes · Test market",
                          "Test market", "Yes")
    assert not calls and not eng.state.get("missed")


@case("retry fill sizes off their FULL stake (their_size=0, no double count)")
def t6():
    eng = mkengine("ok")
    their_buy(eng, retry=True, their_size=5.0)   # their whole bet: 5 shares
    pos = eng.state["my_pos"].get(TOK)
    assert pos, "retry fill should open the position"
    # stake = min(4% × $100 equity, their 5) = $4, not capped by a doubled 10
    assert abs(pos["cost"] - 4.0) < 1e-6, pos


@case("end-to-end: Copybot retry thread re-buys, books bet + lag, no miss")
def t7():
    eng = mkengine("fak", "ok")                  # first buy dies, retry fills
    bot = copybot.Copybot(eng.cfg, eng, filt=None)
    bot.here = ""
    bot.check_book = lambda: None                # network-touching, not under test
    copybot.book_depth = lambda t: None          # _record_lag's book snapshot
    bot.fak_retry_s = 0.05
    eng.on_fak_reject = bot.fak_requote_retry
    their_buy(eng)                               # reject -> schedules the retry
    assert TOK not in eng.state["my_pos"]
    time.sleep(1.0)                              # let the daemon thread run
    pos = eng.state["my_pos"].get(TOK)
    assert pos and pos["shares"] > 0, "retry did not fill"
    assert eng.ex.calls == 2, f"buy attempts: {eng.ex.calls}"
    b = eng.state["bets"].get(TOK)
    assert b and b["status"] == "open" and b["their_price"] == 0.50, b
    assert not eng.ex.fills, "fill left undrained"
    assert not eng.state.get("missed"), eng.state["missed"]


print()
print("FAILURES:", fails or "none")
sys.exit(1 if fails else 0)
