import bootstrap
import copy
from dataclasses import replace
import json
import unittest
from data_robust import OUT,read
from refined import Refined,gates
from features import Settings
from state import Episode
from evaluate_integrated import group_states

C=read(OUT/'candidates.json');CFG=C['config'];EXTRA=C['refinement']


def feature(**kw):
    f=dict(actor=0,limb='LW',kind='hand',speed=.8,near=.05,vertical=.05,cosine=.9,
        approach=.06,elapsed=.1,extension=.04,angular=20.,motion=.08,delta=[.08,0],
        target_region='torso',hand_contact=.5,target_hand_offset=.5,shoulder_vy=0.,hip_vy=0.,
        root_vy=0.,torso_horizontal=0.,response_speed=.3,response_motion=.03,response_cosine=.5,
        own_shoulder_distance=.30,own_head_distance=.30)
    f.update(kw);return f


def row(t,fs=None,token='S1',reset=False):
    p=dict(token=token,refs=['T1','T2'],orders=[0,1],mode='STABLE_T_AND_GEOMETRY',
        candidate_state='CURRENT_NEAR',hold_age=0.,reset=reset,features=[feature()] if fs is None else fs)
    return dict(seconds=t,pairs=[p],ambiguous=0,pairs_over_cap=0)


class Tests(unittest.TestCase):
    def engine(self):return Refined(CFG,EXTRA)
    def test_one_person_no_pair(self):
        e=self.engine()
        for i in range(20):
            r=row(i/10);r['pairs']=[];self.assertFalse(e.step(r)['hit'])
    def test_near_static_not_motion(self):
        e=self.engine()
        for i in range(20):self.assertFalse(e.step(row(i/10,[feature(speed=0,motion=0,approach=0,extension=0)]))['raw'])
    def test_weak_compact_suppresses_seed(self):
        g=gates(feature(own_shoulder_distance=.11),CFG,EXTRA,None,None)
        self.assertFalse(g['rules']);self.assertFalse(g['seed']);self.assertIn('SELF_PROXIMAL_HAND',g['refinement_evidence'])
    def test_strong_compact_preserved(self):
        self.assertTrue(gates(feature(own_shoulder_distance=.05,speed=1.1),CFG,EXTRA,None,None)['rules'])
        self.assertTrue(gates(feature(own_shoulder_distance=.05,motion=.11),CFG,EXTRA,None,None)['rules'])
    def test_new_boundaries(self):
        self.assertTrue(gates(feature(own_shoulder_distance=.12),CFG,EXTRA,None,None)['rules'])
        self.assertFalse(gates(feature(own_shoulder_distance=.119999,speed=1.,motion=.1),CFG,EXTRA,None,None)['rules'])
    def test_local_not_roi_veto(self):
        e=self.engine();r=row(0.);p=copy.deepcopy(r['pairs'][0]);p.update(token='S2',orders=[2,3],features=[feature(own_shoulder_distance=.05)])
        r['pairs'].append(p);e.step(r);r['seconds']=.1;v=e.step(r)
        self.assertTrue(v['pairs'][0]['hit']);self.assertFalse(v['pairs'][1]['raw'])
    def test_duplicate_rules_lags_count_once(self):
        e=self.engine();v=e.step(row(0,[feature(),feature(elapsed=.2),feature(limb='RW')]))
        self.assertEqual(v['pairs'][0]['counts'],[1,0]);self.assertFalse(v['hit'])
        self.assertTrue(e.step(row(.1))['hit'])
    def test_different_people_not_merged(self):
        e=self.engine();e.step(row(0));self.assertFalse(e.step(row(.1,[feature(actor=1)]))['hit'])
    def test_missing_is_not_zero_or_quiet(self):
        e=self.engine();v=e.step(row(0,[]));self.assertFalse(v['observable']);self.assertFalse(v['quiet'])
    def test_reset_expires_history(self):
        e=self.engine();e.step(row(0));self.assertTrue(e.step(row(.1))['hit']);self.assertFalse(e.step(row(.5,reset=True))['hit'])
    def test_candidate_hold_not_evidence(self):
        e=self.engine();e.step(row(0));e.step(row(.1));r=row(.2,[]);r['pairs'][0]['candidate_state']='GEOMETRY_HOLD'
        self.assertFalse(e.step(r)['hit'])
    def test_window_expiry(self):
        e=self.engine();e.step(row(0));self.assertFalse(e.step(row(1.1))['hit'])
    def test_foot_unaffected(self):
        f=feature(kind='foot',limb='LF',own_shoulder_distance=None)
        self.assertIn('LEG_DIRECT',gates(f,CFG,EXTRA,None,None)['rules'])
    def test_labels_and_identity_names_not_truth(self):
        a=self.engine();b=self.engine()
        for i in range(5):
            r=row(i/10);s=copy.deepcopy(r);s.update(clip='answer',reference='negative_reference',source_frame=999)
            self.assertEqual(a.step(r),b.step(s))
    def test_prefix_causality(self):
        a=self.engine();b=self.engine();rows=[row(i/10) for i in range(6)]
        x=[a.step(r) for r in rows];y=[b.step(r) for r in rows];b.step(row(.6,[]));self.assertEqual(x,y)
    def test_yellow_expiry_red_and_cooldown(self):
        e=Episode(replace(Settings(),display_hold=1.));self.assertTrue(e.step(0,True,False)['event'])
        self.assertTrue(e.step(.9,False,False)['display']);self.assertFalse(e.step(1.1,False,False)['display'])
        r=e.step(1.2,True,False);self.assertTrue(r['display']);self.assertFalse(r['event'])
    def test_group_does_not_hold_itself(self):
        e=self.engine();h={};r=row(0);group_states(r,e.step(r),0,h);r=row(.1)
        self.assertEqual(group_states(r,e.step(r),.1,h)[0]['state'],'RED_CURRENT')
        r=row(.5,[]);self.assertEqual(group_states(r,e.step(r),.5,h)[0]['state'],'YELLOW_HOLD')
        r=row(1.2,[]);self.assertEqual(group_states(r,e.step(r),1.2,h)[0]['state'],'BLUE_CANDIDATE')
    def test_first_l02_independent_state(self):
        self.assertFalse(self.engine().step(row(105.352669))['hit'])
    def test_score_contract(self):
        from geometry import Geometry,point
        self.assertEqual((Geometry().person_score,Geometry().joint_score),(.25,.10))
        self.assertTrue(point([1,1,.10],10,10));self.assertFalse(point([1,1,.0999],10,10))


if __name__=='__main__':
    result=unittest.TextTestRunner(verbosity=2).run(unittest.defaultTestLoader.loadTestsFromTestCase(Tests))
    (OUT/'reports/unit_tests.json').write_text(json.dumps(dict(tests=result.testsRun,failures=len(result.failures),errors=len(result.errors),status='PASS' if result.wasSuccessful() else 'FAIL'),indent=2))
    raise SystemExit(not result.wasSuccessful())
