"""Acceptance test for poly_us_maker. Proves the SAFETY properties, live.

Not a unit test. Every check below runs against the real venue, because every
bug this module has had so far was invisible offline: a post-only kill returns
HTTP 200 with an order id, so "placed 20 orders" and "placed nothing" look
identical from the response alone.

All orders are priced DEEP and unfillable by construction -- we offer YES far
ABOVE the ask, which is buying NO far BELOW its bid -- and carry a short GTD, so
the venue clears them even if this script dies mid-run.

What is asserted:
  1. module starts disarmed; unarmed place does no network I/O
  2. an armed order RESTS, at the YES-side price we asked for
  3. MAX_CONCURRENT_ORDERS binds
  4. MAX_RESTING_NOTIONAL binds
  5. MAX_CONTRACTS binds against the live position
  6. reads that fail leave the caps CLOSED, not open
  7. cancel_all clears our own AND unknown orders
  8. the GTD dead-man's switch actually fires
  9. the account ends flat, with the LIVE flag back off

Usage:  python research\\pus_maker_acceptance.py
"""
import sys
import time

sys.path.insert(0, r'E:\soccer\main')
import poly_us_prices as prices
import poly_us_maker as pm
import poly_us_trader as put
import poly_us_auth as pa
import live_monitor as LM

PASS, FAIL = [], []


def check(name, ok, detail=''):
    (PASS if ok else FAIL).append(name)
    print(f'  [{"PASS" if ok else "FAIL"}] {name}{"  -- " + detail if detail else ""}')


def bal():
    return float(pa.get_account_balances()['balances'][0]['currentBalance'])


def find_leg():
    _, matches = LM.load_watchlist_file()
    for m in matches:
        div = (m.get('lc') or '').upper()
        if div not in prices.LEAGUE_SLUG:
            continue
        for side in ('home', 'draw', 'away'):
            mk = prices.find_market(div, m.get('home'), m.get('away'), side)
            if not mk:
                continue
            b = prices.get_bbo(mk['slug'])
            if b and b.get('bestAsk') is not None:
                a = float(b['bestAsk'])
                if 0.10 < a < 0.70:
                    return mk['slug'], a
    return None, None


def main():
    slug, ask = find_leg()
    if not slug:
        print('no quotable market right now -- rerun nearer kickoffs')
        return 1
    # deep: offering YES well above the ask cannot be lifted
    deep_yes = min(0.95, round(ask + 0.25, 2))
    print(f'market {slug}  ask={ask}  testing at YES {deep_yes} (unfillable)\n')

    b_start = bal()

    print('1. disarmed by default')
    check('starts disarmed', pm.is_live() is False)
    r = pm.place_maker_ask(slug, 1.0, deep_yes)
    check('unarmed place is a dry run', r.get('dry_run') is True)
    check('unarmed place placed nothing', len(put.open_orders() or []) == 0)

    print('\n2. armed order rests at the YES-side price')
    pm.set_live(True)
    try:
        pm.place_maker_ask(slug, 1.0, deep_yes)
        time.sleep(4)
        mine = [o for o in put.open_orders() if o.get('marketSlug') == slug]
        px = None
        if mine:
            p = mine[0].get('price')
            px = p.get('value') if isinstance(p, dict) else p
        check('order rests', bool(mine), f'{len(mine)} resting')
        check('rests at the requested YES price', px is not None and
              abs(float(px) - deep_yes) < 1e-6, f'book shows {px}')

        print('\n3. caps bind')
        real_orders = pm.MAX_CONCURRENT_ORDERS
        pm.MAX_CONCURRENT_ORDERS = 1          # already 1 resting
        try:
            pm.place_maker_ask(slug, 1.0, deep_yes)
            check('MAX_CONCURRENT_ORDERS binds', False, 'no exception raised')
        except RuntimeError as e:
            check('MAX_CONCURRENT_ORDERS binds', 'resting orders' in str(e))
        finally:
            pm.MAX_CONCURRENT_ORDERS = real_orders

        real_not = pm.MAX_RESTING_NOTIONAL
        pm.MAX_RESTING_NOTIONAL = 0.01
        try:
            pm.place_maker_ask(slug, 1.0, deep_yes)
            check('MAX_RESTING_NOTIONAL binds', False, 'no exception raised')
        except RuntimeError as e:
            check('MAX_RESTING_NOTIONAL binds', 'notional' in str(e))
        finally:
            pm.MAX_RESTING_NOTIONAL = real_not

        real_ct = pm.MAX_CONTRACTS
        pm.MAX_CONTRACTS = 0.5
        try:
            pm.place_maker_ask(slug, 1.0, deep_yes)
            check('MAX_CONTRACTS binds', False, 'no exception raised')
        except (RuntimeError, ValueError) as e:
            check('MAX_CONTRACTS binds', 'MAX_CONTRACTS' in str(e))
        finally:
            pm.MAX_CONTRACTS = real_ct

        print('\n4. failed reads fail CLOSED')
        orig = pm._orders
        pm._orders = lambda: (_ for _ in ()).throw(RuntimeError('venue down'))
        try:
            check('resting_notional -> inf when unreadable',
                  pm.resting_notional() == float('inf'))
            check('open_order_count -> cap when unreadable',
                  pm.open_order_count() == pm.MAX_CONCURRENT_ORDERS)
        finally:
            pm._orders = orig

        origp = pa.get_positions
        pa.get_positions = lambda: (_ for _ in ()).throw(RuntimeError('down'))
        try:
            v = pm.current_position(slug)
            check('current_position -> NaN when unreadable', v != v)
            try:
                pm.place_maker_ask(slug, 1.0, deep_yes)
                check('refuses to post on unreadable positions', False)
            except RuntimeError as e:
                check('refuses to post on unreadable positions',
                      'cannot read positions' in str(e))
        finally:
            pa.get_positions = origp

        print('\n5. cancel_all clears everything')
        n = pm.cancel_all('acceptance')
        time.sleep(3)
        check('cancel_all reports work', n >= 1, f'{n} cancelled')
        check('nothing left resting', len(put.open_orders() or []) == 0)

        print('\n6. GTD dead-man switch fires (short TTL)')
        pm.place_maker_ask(slug, 1.0, deep_yes, ttl_s=45)
        time.sleep(4)
        before = len(put.open_orders() or [])
        gone = False
        for i in range(8):
            time.sleep(15)
            n_now = len(put.open_orders() or [])
            print(f'     t+{15*(i+1):3}s resting={n_now}')
            if n_now == 0:
                gone = True
                break
        check('order self-expired without us cancelling', gone,
              f'was {before} at t+4s')
    finally:
        pm.cancel_all('acceptance cleanup')
        pm.set_live(False)

    print('\n7. final state')
    time.sleep(2)
    left = put.open_orders() or []
    pos = (pa.get_positions().get('positions') or {})
    b_end = bal()
    check('no orders left', len(left) == 0, f'{len(left)}')
    check('no positions opened', len(pos) == 0, f'{len(pos)}')
    check('balance unchanged', abs(b_end - b_start) < 1e-6,
          f'${b_start:.4f} -> ${b_end:.4f}')
    check('LIVE flag back off', pm.is_live() is False)

    print(f'\n{len(PASS)} passed, {len(FAIL)} failed')
    for f in FAIL:
        print(f'   FAILED: {f}')
    return 1 if FAIL else 0


if __name__ == '__main__':
    sys.exit(main())
