import ast,io,json,logging,queue,sqlite3,threading,unittest,uuid
from datetime import datetime,timezone,timedelta
from pathlib import Path
from http.server import BaseHTTPRequestHandler,ThreadingHTTPServer
from unittest.mock import Mock
from alert_time_guard import time_guard_error,MAX_AGE_SECONDS,MAX_FUTURE_SECONDS

NOW=datetime(2026,9,16,6,tzinfo=timezone.utc)
class Rules(unittest.TestCase):
 def test_cases(self):
  cases=[({},'MISSING'),({'bar_time':NOW.isoformat()},'MISSING'),({'sent_at':'{{timenow}}'},'INVALID'),({'sent_at':'2026-09-16T06:00:00'},'INVALID'),({'sent_at':True},'INVALID'),({'sent_at':float('nan')},'INVALID'),({'sent_at':(NOW-timedelta(seconds=61)).isoformat()},'STALE'),({'sent_at':(NOW+timedelta(seconds=6)).isoformat()},'FUTURE'),({'sent_at':NOW.isoformat(),'timenow':(NOW-timedelta(seconds=10)).isoformat()},'CONFLICTING')]
  for payload,code in cases:
   with self.subTest(payload=payload):self.assertIn(code,time_guard_error(payload,NOW))
 def test_valid_boundary_and_timezone(self):
  for value in [NOW.isoformat(),(NOW-timedelta(seconds=60)).isoformat(),'2026-09-16T08:00:00+02:00',NOW.timestamp(),NOW.timestamp()*1000]:
   with self.subTest(value=value):self.assertIsNone(time_guard_error({'sent_at':value,'bar_time':'2026-09-01T00:00:00Z'},NOW))

class Receiver(unittest.TestCase):
 def setUp(self):
  tree=ast.parse(Path(__file__).with_name('webhook_receiver.py').read_text(encoding='utf-8'))
  # Execute actual receiver definitions only: no startup, network or production filesystem.
  self.ns=dict(globals(),DB_PATH=':memory:',DB_LOCK=threading.RLock(),PENDING_MAX_AGE_SECONDS=600,log=logging.getLogger('test'),ALLOWED_SECRETS={'test-only'},LOG_DIR=Path('/nonexistent-fixture'))
  definitions=[n for n in tree.body if isinstance(n,(ast.FunctionDef,ast.ClassDef))]
  exec(compile(ast.Module(body=definitions,type_ignores=[]),'receiver_test','exec'),self.ns)
  self.db=self.ns['init_db']()
  self.ns['get_bybit']=Mock(side_effect=AssertionError('Bybit must not be called'))
 def tearDown(self):self.db.close()
 def post(self,extra):
  p={'secret':'test-only','symbol':'ZECUSDT.P','strategy':'SQW','side':'sell','qty':1};p.update(extra)
  body=json.dumps(p).encode();h=object.__new__(self.ns['WebhookHandler'])
  h.path='/webhook';h.headers={'Content-Length':str(len(body))};h.rfile=io.BytesIO(body);h.db=self.db;h.in_queue=queue.Queue();h._send_json=Mock()
  h.do_POST();return h
 def test_rejected_at_ingress_never_enqueued(self):
  for extra in [{},{'sent_at':'bad'},{'sent_at':'2026-01-01T00:00:00Z'},{'sent_at':'2099-01-01T00:00:00Z'}]:
   h=self.post(extra);self.assertEqual(h._send_json.call_args.args[0],422);self.assertTrue(h.in_queue.empty())
  self.assertEqual(self.db.execute("select count(*) from queue where status='pending'").fetchone()[0],0)
  self.assertEqual(self.db.execute("select count(*) from queue where status='failed'").fetchone()[0],4)
  self.ns['get_bybit'].assert_not_called()
 def test_fresh_alert_is_queued_once(self):
  h=self.post({'sent_at':datetime.now(timezone.utc).isoformat()})
  self.assertEqual(h._send_json.call_args.args[0],200);self.assertEqual(h.in_queue.qsize(),1)
 def test_alert_expiring_in_queue_blocked_before_bybit(self):
  p={'sent_at':(datetime.now(timezone.utc)-timedelta(seconds=61)).isoformat()}
  rid=self.ns['enqueue_request'](self.db,p)
  self.ns['process_order'](self.db,rid,p)
  self.assertEqual(self.db.execute('select status from queue where request_id=?',(rid,)).fetchone()[0],'failed')
  self.ns['get_bybit'].assert_not_called()
 def test_final_order_paths_have_guard(self):
  source=Path(__file__).with_name('webhook_receiver.py').read_text(encoding='utf-8')
  self.assertIn('if not request_is_fresh_pending(conn, request_id):\n        return\n    order = bybit.create_market_order',source)
  self.assertIn('if not request_is_fresh_pending(conn, request_id):\n            return\n        try:\n            order = bybit._request("POST", "/v5/order/create"',source)

if __name__=='__main__':unittest.main()
