import contextlib
from decimal import Decimal
import io
from pathlib import Path
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import patch
import json
import threading
import sys
import time
from datetime import datetime,timedelta,timezone
import urllib.request
import gauntlet as g
from point_in_time import available_observations


def report(year,income):
    return {'data_origin':'HISTORICAL_REPLAY','calendar_year':year,'starting_capital_usd':'500',
      'net_income_usd':str(income),'income_definition':'REALIZED_NET_AFTER_COSTS',
      'controls':dict.fromkeys(('intraday_3pct_breaker','capital_floor_ratchet','no_live_orders','point_in_time_inputs'),True),
      'strategy_sha256':'a'*64,'dataset_sha256':'b'*64,'ledger_sha256':'c'*64,
      'sources':[{'url':'https://example.test/TEST_FIXTURE_ONLY','point_in_time_validated':True}]}

def run_year(context):
    if context.get('hang'):time.sleep(10)
    return report(context['calendar_year'],'0')

class Tests(unittest.TestCase):
 def setUp(self):
    self.tmp=tempfile.TemporaryDirectory();self.path=Path(self.tmp.name);self.store=g.Store(self.path/'test.db');g.STOP.clear()
    self.out=io.StringIO();self.redir=contextlib.redirect_stdout(self.out);self.redir.__enter__()
 def tearDown(self):self.redir.__exit__(None,None,None);self.tmp.cleanup();g.STOP.clear()
 def test_exact_targets(self):
    self.assertEqual(g.target(1,100),Decimal('85896253455335221839410188294270212117017920333500000'))
    self.assertEqual(g.target(2,1),Decimal(600000));self.assertEqual(g.target(1,2),Decimal(1500000))
 def test_boundary(self):
    self.assertEqual(g.deviation(Decimal('500000.5'),Decimal(500000)),Decimal('.0001'))
    self.assertGreater(g.deviation(Decimal('500000.50001'),Decimal(500000)),Decimal('.0001'))
 def test_invalid_income_and_control(self):
    r=report(1926,500000);r['net_income_usd']='NaN'
    with self.assertRaises(ValueError):g.validate_report(r,1926)
    r=report(1926,500000);r['controls']['capital_floor_ratchet']=False
    with self.assertRaises(g.Blocked):g.validate_report(r,1926)
 def test_no_synthetic_claim_as_historical(self):
    r=report(1926,0);r['data_origin']='SYNTHETIC'
    with self.assertRaises(g.Blocked):g.validate_report(r,1926)
 def test_one_lesson_for_each_outside_tolerance_year(self):
    with patch.object(g,'execute_year',side_effect=lambda adapter,ctx,deadline:report(ctx['calendar_year'],'0')):
     self.assertFalse(g.run_attempt(self.store,SimpleNamespace(),1926))
    self.assertEqual(len(self.store.lessons()),100)
    self.assertEqual(self.store.snapshot()['attempts'][0]['status'],'FAILED')
 def test_full_century_requires_all_years(self):
    with patch.object(g,'execute_year',side_effect=lambda adapter,ctx,deadline:report(ctx['calendar_year'],ctx['income_target_usd'])):
     self.assertTrue(g.run_attempt(self.store,SimpleNamespace(),1926))
    self.assertEqual(self.store.snapshot()['counts']['YEAR_RESULT'],100)
    self.assertEqual(self.store.lessons(),[])
 def test_blocked_does_not_create_income(self):
    with patch.object(g,'execute_year',side_effect=g.Blocked('MISSING_DATA')):
     self.assertFalse(g.run_attempt(self.store,SimpleNamespace(),1926))
    self.assertNotIn('YEAR_RESULT',self.store.snapshot()['counts']);self.assertEqual(len(self.store.lessons()),1)
 def test_restart_preserves_history_and_attempt_number(self):
    a=self.store.start();self.store.finish(a,'FAILED');self.store.lesson('fixture',{},'fixture')
    fresh=g.Store(self.path/'test.db');self.assertEqual(len(fresh.lessons()),1);self.assertEqual(fresh.start(),2)
 def test_future_news_excluded(self):
    p=self.path/'news.jsonl';rows=[{'url':'https://example.test/TEST_ONLY','available_at':t,'text':'fixture'} for t in ['1926-01-01T00:00:00Z','2026-01-01T00:00:00Z']]
    p.write_text('\n'.join(json.dumps(r) for r in rows))
    self.assertEqual(len(available_observations(p,'1926-12-31T00:00:00Z')),1)
 def test_worker_report(self):
    r=g.execute_year(sys.modules[__name__],{'calendar_year':1926},datetime.now(timezone.utc)+timedelta(seconds=5))
    self.assertEqual(r['net_income_usd'],'0')
 def test_worker_deadline_terminates_hang(self):
    with self.assertRaisesRegex(g.Blocked,'RUN_DEADLINE_OR_STOP'):
     g.execute_year(sys.modules[__name__],{'calendar_year':1926,'hang':True},datetime.now(timezone.utc)+timedelta(seconds=.3))
 def test_monitor_read_only(self):
    server=g.make_server(self.store,0);thread=threading.Thread(target=server.serve_forever);thread.start()
    try:
     with urllib.request.urlopen(f'http://127.0.0.1:{server.server_address[1]}/api/status') as response:self.assertEqual(json.load(response)['tolerance_pct'],'0.0001')
    finally:server.shutdown();server.server_close();thread.join()

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