import unittest
from dataclasses import replace
from app.pose_config import PoseConfig,execution_plan
from app.stream_settings import patch_settings,revision
from app.contracts.frames import FrameEnvelope,DetectionObservation,PoseObservation
from app.contracts.visualization import VisualizationCommand,FrameResult
from app.pose_visual import pose_summary


def frame(seq=0,pts=0):
    return FrameEnvelope('s','c','c','r',1,seq,pts,1,1280,720)


def result(count=10):
    f=frame()
    d=DetectionObservation(f,'RFDETRKeypointPreview',0,.5,(.1,.1,.9,.9))
    points=tuple((.5,.5,.15 if i<count else .1) for i in range(17))
    p=PoseObservation(f,d.model_key,d.score,d.bbox_xyxy,points)
    return FrameResult(f,(VisualizationCommand(d,'person',pose=p),),pose_config=PoseConfig(show_joints=True))


class PoseTest(unittest.TestCase):
    def test_plan_and_noop(self):
        spec=dict(server_id='s',source_id='c',store=False,active_learning=False)
        self.assertEqual(execution_plan(spec)['role'],'det')
        changed=patch_settings(spec,{'pose':{'show_joints':True},'display_categories':['person']})
        self.assertEqual(execution_plan(changed)['role'],'pose')
        self.assertEqual(revision(changed),revision(patch_settings(changed,{'pose':{'show_joints':True}})))
        self.assertNotIn('pose',spec)
        with self.assertRaises(ValueError):patch_settings(changed,{'display_categories':['car']})
        with self.assertRaises(ValueError):patch_settings(changed,{'pose':{'joint_score':float('nan')}})

    def test_mask_not_raw_deletion(self):
        r=result(9)
        self.assertEqual(pose_summary(r)['filtered'],1)
        self.assertEqual(len(r.snapshot()['objects'][0]['keypoints']),17)
        self.assertEqual(pose_summary(result(10))['shown'],1)
        self.assertTrue(r.commands[0].visible)

    def test_joint_geometry_identity(self):
        r=result()
        with self.assertRaises(ValueError): replace(r.commands[0],pose=replace(r.commands[0].pose,frame=frame(1)))
        points=((-0.1,.5,.9),)*17
        p=replace(r.commands[0].pose,keypoints=points)
        out=replace(r,commands=(replace(r.commands[0],pose=p),))
        self.assertEqual(pose_summary(out)['filtered'],1)
        self.assertEqual(out.snapshot()['objects'][0]['keypoints'][0][0],-.1)

    def test_frame_specific_detail(self):
        with self.assertRaises(ValueError):PoseConfig(detail_object_ref='0')
        with self.assertRaises(ValueError):PoseConfig(min_joints=18)
        PoseConfig(detail_object_ref='0',detail_frame_seq=0)

    def test_intrusion_provider(self):
        from app.intrusion import Intrusion,IntrusionConfig
        engine=Intrusion(IntrusionConfig(enabled=True))
        events=[]
        for i in range(7):
            f=frame(i,i*100000000)
            d=DetectionObservation(f,'RFDETRKeypointPreview',0,.9,(.1,.1,.9,.9))
            _,e=engine.evaluate(f,(d,),(1,))
            events.extend(e)
        self.assertEqual(len(events),1)
        self.assertEqual(events[0].payload()['modelKey'],'RFDETRKeypointPreview')
        engine.clear()
        self.assertFalse(engine.states)

    def test_pts_capture_interval(self):
        import tempfile
        from pathlib import Path
        from app.metadata_visual import MetadataVisual
        with tempfile.TemporaryDirectory() as path:
            root=Path(path);root.chmod(0o700);(root/'capture.enabled').touch()
            clock=[0.]
            visual=MetadataVisual(root,clock=lambda:clock[0])
            visual.pts_interval_s=3
            try:
                self.assertTrue(visual.reserve_image(frame()))
                clock[0]=1
                self.assertFalse(visual.reserve_image(frame(1,2999999999)))
                self.assertTrue(visual.reserve_image(frame(2,3000000000)))
            finally:self.assertTrue(visual.close(2))

    def test_result_worker(self):
        from app.det_result_path import DetResultPath
        from app.native_metadata import NativeFrame,NativeDetection
        from app.frame_ingress import FrameIngress
        from app.byte_tracking import ByteTracker
        import time
        class Collector:
            def add(self,*args,**kwargs):pass
        ingress=FrameIngress('s','c','c','r',1)
        errors=[]
        path=DetResultPath(ingress,ByteTracker(),'RFDETRKeypointPreview',.5,Collector(),lambda:True,
                           lambda:errors.append(True),pose_config=PoseConfig(enabled=True))
        path.start()
        try:
            r=result();f=r.frame
            d=NativeDetection(0,.5,(.1,.1,.9,.9),r.commands[0].pose.keypoints)
            self.assertTrue(path.submit(f,NativeFrame(0,0,1280,720,(d,),1280,720)))
            deadline=time.monotonic()+3
            while path.latest_result() is None and not errors and time.monotonic()<deadline:time.sleep(.01)
            self.assertFalse(errors)
            self.assertEqual(len(path.latest_result().snapshot()['objects'][0]['keypoints']),17)
            self.assertFalse(path.submit(f,NativeFrame(0,0,1280,720,(d,),1280,720)))
        finally:path.close(3)
        self.assertIsNone(path.latest_result())
