import importlib
import importlib.util
import json
from pathlib import Path
import sys
import tempfile
import unittest
from unittest.mock import patch
from repair_strategy_mapping import repair,RepairError,ENTRY

FIXTURE='''# UNIT TEST ONLY; not a supplied trading strategy.
def decide(context):return [{'action':'HOLD'}]
def capital_floor(context):return '0'
'''
class Tests(unittest.TestCase):
 def setUp(self):
  self.tmp=tempfile.TemporaryDirectory();self.parent=Path(self.tmp.name);self.root=self.parent/'project';self.root.mkdir()
  (self.root/'gauntlet.py').write_text('# fixture')
  self.config={'strategy_module':None,'bars_path':'data/bars.jsonl','macro_path':'data/macro.jsonl','output_path':'runs/replay_ledgers','private_setting':'keep'}
  (self.root/'replay_config.json').write_text(json.dumps(self.config));(self.root/'runs').mkdir();(self.root/'runs/gauntlet.db').write_bytes(b'KEEP ATTEMPT 179')
 def tearDown(self):
  sys.modules.pop(ENTRY,None);sys.modules.pop('_finbot_selected_host_strategy',None);self.tmp.cleanup()
 def file(self,name='host_strategy.py',folder=None,content=FIXTURE):
  p=(folder or self.root)/name;p.write_text(content);return p
 def test_null_maps_real_host_file_without_source_execution(self):
  source=self.file(content="raise RuntimeError('must not execute during repair')\n"+FIXTURE)
  before=source.read_bytes();r=repair(self.root);c=json.loads((self.root/'replay_config.json').read_text())
  self.assertEqual(r['status'],'MAPPING_REPAIRED');self.assertEqual(c['strategy_module'],ENTRY);self.assertEqual(c['private_setting'],'keep');self.assertEqual(source.read_bytes(),before)
  self.assertEqual((self.root/'runs/gauntlet.db').read_bytes(),b'KEEP ATTEMPT 179')
 def test_strategy_filename_and_idempotent(self):
  self.file('strategy.py');repair(self.root);config=(self.root/'replay_config.json').read_bytes();self.assertEqual(repair(self.root)['status'],'ALREADY_MAPPED');self.assertEqual((self.root/'replay_config.json').read_bytes(),config)
 def test_parent_file_and_bridge_import_from_other_working_directory(self):
  source=self.file('strategy.py',self.parent);repair(self.root)
  path=self.root/(ENTRY+'.py');spec=importlib.util.spec_from_file_location(ENTRY,path);module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module)
  self.assertEqual(module.decide({})[0]['action'],'HOLD');self.assertEqual(module.capital_floor({}),'0');self.assertEqual(module.__file__,str(source))
 def test_missing_source_does_not_modify_config(self):
  before=(self.root/'replay_config.json').read_bytes()
  with self.assertRaisesRegex(RepairError,'NO_STRATEGY_SOURCE_FOUND'):repair(self.root)
  self.assertEqual((self.root/'replay_config.json').read_bytes(),before)
 def test_class_only_or_wrong_signature_rejected(self):
  source=self.file(content='class Strategy:\n def decide(self,c):pass\n def capital_floor(self,c):pass\n')
  with self.assertRaisesRegex(RepairError,'STRATEGY_INTERFACE_MISSING'):repair(self.root)
  source.write_text('def decide(a,b):pass\ndef capital_floor(c):pass\n')
  with self.assertRaisesRegex(RepairError,'CALLBACK_MUST_ACCEPT_ONE_CONTEXT'):repair(self.root)
 def test_ambiguous_sources_not_chosen_arbitrarily(self):
  self.file();self.file('strategy.py',content=FIXTURE+'\n# different real strategy\n')
  with self.assertRaisesRegex(RepairError,'AMBIGUOUS_STRATEGY'):repair(self.root)
 def test_isolation_pin_preserved(self):
  source=self.file();(self.root/'replay_isolation.json').write_text(json.dumps({'image_id':'sha256:'+'a'*64,'strategy_file':'old.py','custom':'keep'}));repair(self.root)
  c=json.loads((self.root/'replay_isolation.json').read_text());self.assertEqual(c['strategy_file'],str(source));self.assertEqual(c['image_id'],'sha256:'+'a'*64);self.assertEqual(c['custom'],'keep')
 def test_check_does_not_write(self):
  self.file();r=repair(self.root,check=True);self.assertEqual(r['status'],'CHECK_ONLY');self.assertFalse((self.root/'strategy_mapping.json').exists())
 def test_rollback_if_atomic_write_fails(self):
  self.file();original=(self.root/'replay_config.json').read_bytes()
  import repair_strategy_mapping as m
  write=m.atomic_write;calls=[0]
  def fail_once(path,content):
   calls[0]+=1
   if calls[0]==2:raise OSError('test failure')
   return write(path,content)
  with patch.object(m,'atomic_write',side_effect=fail_once):
   with self.assertRaises(OSError):repair(self.root)
  self.assertEqual((self.root/'replay_config.json').read_bytes(),original);self.assertFalse((self.root/'strategy_mapping.json').exists())

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