"""CPU-only timestamp contract checks; no video decode or model execution."""
import csv
import bisect
import json
from pathlib import Path
import unittest
from part_plan import make_part_plan
from sampling import make_plan

MAP=Path('/data/codex-artifacts/contextra/action-model-eval/2026/2026-10-29_contextra-self-filming-original01-clips_r01/아이서티_컨텍스트라_싸움_쓰러짐_자체촬영_원본01_클립자료/metadata/frame_maps/L01.csv')

class PlanTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        with MAP.open(encoding='utf-8-sig') as f:
            cls.pts=[float(r['clip_pts_seconds']) for r in csv.DictReader(f)]

    def test_unsplit_is_still_rejected(self):
        with self.assertRaisesRegex(ValueError,'duplicate'):
            make_plan(self.pts,None)

    def test_parts_keep_global_frame_and_time(self):
        for name,bounds,count,first,last in [('L01_P01',[1,2542],1044,1,2541),('L01_P02',[2543,19975],7261,2543,19974)]:
            p=make_part_plan(self.pts,bounds,name)
            self.assertEqual(len(p['target_seconds']),count)
            self.assertEqual(p['source_frame_indices'][0],first)
            self.assertEqual(p['source_frame_indices'][-1],last)
            self.assertEqual(p['display_samples'],list(range(count)))
            self.assertAlmostEqual(p['target_seconds'][0],self.pts[first-1])
            self.assertTrue(all(bounds[0]<=f<=bounds[1] for f in p['source_frame_indices']))
            self.assertTrue(all(abs(self.pts[f-1]-t)<.03 for f,t in zip(p['source_frame_indices'],p['target_seconds'])))

    def test_original_and_observed_gaps_differ(self):
        a=make_part_plan(self.pts,[1,2542],'L01_P01')
        b=make_part_plan(self.pts,[2543,19975],'L01_P02')
        self.assertAlmostEqual(self.pts[2542]-self.pts[2541],1.016668)
        self.assertAlmostEqual(b['relative_seconds'][0]-a['relative_seconds'][-1],1.057729)

    def test_bad_bounds_fail(self):
        for bounds in ([0,10],[4,3],[1,20000]):
            with self.assertRaises(ValueError):make_part_plan(self.pts,bounds,'invalid')

    def test_global_nearest_ties(self):
        p=make_part_plan(self.pts,[2543,19975],'L01_P02')
        for target,index in zip(p['target_seconds'],p['source_frame_indices']):
            j=bisect.bisect_left(self.pts,target)
            candidates=[i for i in (j-1,j) if 0<=i<len(self.pts)]
            expected=min(candidates,key=lambda i:(abs(self.pts[i]-target),i))+1
            self.assertEqual(index,expected)

    def test_completed_P01_selection_unchanged(self):
        root=Path(__file__).resolve().parent.parent
        saved=json.loads((root/'clips/L01_P01/inputs/sampling_plan.json').read_text())
        updated=make_part_plan(self.pts,[1,2542],'L01_P01')
        for field in ('source_frame_indices','target_seconds','relative_seconds'):
            self.assertEqual(saved[field],updated[field])

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