import contextlib
from datetime import datetime,timezone
import io
import json
from pathlib import Path
import tempfile
from types import SimpleNamespace
import unittest
from gauntlet import Blocked
import host_replay_adapter as a
from vendor import finbot_engine as engine
from download_data import download

class ReplayTests(unittest.TestCase):
 def setUp(self):
  self.tmp=tempfile.TemporaryDirectory();self.p=Path(self.tmp.name)
  self.macro=self.p/'macro.jsonl';self.macro.write_text('')
 def tearDown(self):self.tmp.cleanup()
 def test_historical_clock_restores_engine(self):
  old=engine.timestamp
  with a.historical_clock([datetime(2015,1,1,tzinfo=timezone.utc)]):
   self.assertEqual(engine.timestamp(),'2015-01-01T00:00:00+00:00')
   e=engine.Engine(':memory:');e.initialize('500');e.fresh('2015-01-01T00:00:00+00:00');e.db.close()
  self.assertIs(engine.timestamp,old)
 def test_future_macro_is_not_exposed(self):
  self.macro.write_text(json.dumps({'available_at':'2020-01-03T00:00:00+00:00','series':'TEST_FIXTURE','observation_date':'2019-01-01','value':'1'})+'\n')
  self.assertEqual(a.macro_at(a.macro_snapshots(self.macro),datetime(2019,1,1,tzinfo=timezone.utc)),{})
 def test_research_missing_availability_rejected(self):
  r={'quantitative':{'sources':[{'observed_at':'2015-01-01T00:00:00Z'}]}}
  with self.assertRaises(Blocked):a.validate_research(r,datetime(2015,1,1,tzinfo=timezone.utc))
 def bars(self,origin='HISTORICAL_INTRADAY_QUOTES'):
  p=self.p/'bars.jsonl'
  p.write_text(json.dumps({'at':'2015-01-01T00:00:00Z','available_at':'2015-01-01T00:00:00Z','data_origin':origin,'bar_interval_seconds':60,'quotes':{'BTC/USD':'300'},'source_url':'https://example.test/TEST_FIXTURE_ONLY'})+'\n');return p
 def strategy(self):return SimpleNamespace(__file__=__file__,decide=lambda _:[],capital_floor=lambda _: '0')
 def test_partial_window_never_claims_century_year(self):
  r=a.replay({'calendar_year':2015,'attempt':1},self.strategy(),self.bars(),self.macro,self.p/'out',require_full_year=False)
  self.assertEqual(r['data_origin'],'PARTIAL_REPLAY_NOT_CENTURY');self.assertEqual(r['net_income_usd'],'0')
 def test_actual_engine_entry_exit_fees_reconcile(self):
  bars=self.bars();row=json.loads(bars.read_text());row.update(at='2015-01-01T00:01:00Z',available_at='2015-01-01T00:01:00Z',quotes={'BTC/USD':'301'})
  with bars.open('a') as f:f.write(json.dumps(row)+'\n')
  def decide(c):
   if c['at'].startswith('2015-01-01T00:00:00'):
    research={'decision':'RESEARCH_PASSED','analyzed_at':c['at']}
    for channel in engine.CHANNELS:research[channel]={'status':'PASS','summary':'UNIT TEST FIXTURE ONLY','sources':[{'url':'https://example.test/TEST_ONLY','observed_at':c['at'],'available_at':c['at']}]}
    return [{'action':'BUY','symbol':'BTC/USD','quantity':'0.1','stop':'294','trailing_pct':'.02','research':research}]
   return [{'action':'SELL','symbol':'BTC/USD'}]
  strategy=SimpleNamespace(__file__=__file__,decide=decide,capital_floor=lambda _: '0')
  r=a.replay({'calendar_year':2015,'attempt':1},strategy,bars,self.macro,self.p/'out',require_full_year=False)
  self.assertEqual(r['net_income_usd'],'-0.050250000000');self.assertEqual(r['data_origin'],'PARTIAL_REPLAY_NOT_CENTURY')
 def test_incomplete_year_is_blocked(self):
  with self.assertRaisesRegex(Blocked,'YEAR_END_COVERAGE_MISSING'):
   a.replay({'calendar_year':2015,'attempt':1},self.strategy(),self.bars(),self.macro,self.p/'out')
 def test_synthetic_origin_not_accepted(self):
  with self.assertRaisesRegex(Blocked,'VERIFIED_INTRADAY'):
   list(a.year_bars(self.bars('SYNTHETIC_TEST_ONLY'),2015))
 def test_missing_market_inputs_explicit(self):
  from unittest.mock import patch
  config=self.p/'replay_config.json';config.write_text(json.dumps({'bars_path':'absent.jsonl','macro_path':'absent_macro.jsonl'}))
  with patch.object(a,'ROOT',self.p):
   with self.assertRaisesRegex(Blocked,'MARKET_REPLAY_BARS_NOT_INSTALLED'):a.run_year({'calendar_year':1926,'attempt':1})

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