import unittest
from dataclasses import replace
from app.crowd_config import CrowdConfig, CrowdDisplayConfig, FULL_FRAME, duration_ns
from app.crowd_model_contract import CrowdTensorContract, CrowdBuildIntent, unconfirmed_manifest

class ConfigTest(unittest.TestCase):
    def test_defaults(self):
        c=CrowdConfig(); self.assertFalse(c.enabled);self.assertTrue(c.nms_enabled)
        self.assertEqual((c.person_confidence,c.head_confidence,c.nms_iou_threshold),(.45,.45,.85))
        self.assertEqual((c.window_seconds,c.report_seconds,c.stale_seconds,c.minimum_samples),(3,3,6,2))
    def test_roundtrip(self):
        c=CrowdConfig(enabled=True,person_confidence=.8);self.assertEqual(CrowdConfig.parse(c.settings_payload()),c)
    def test_empty_roi(self):
        self.assertEqual(CrowdConfig(roi=[]).roi,FULL_FRAME);self.assertEqual(CrowdConfig(roi=()).roi,FULL_FRAME)
    def test_polygon_invalid(self):
        for roi in (None,{},[[0,0]],[[0,0],[1,1],[0,1],[1,0]],[[0,0],[1,0],[2,0]],[[0,0],[1,0],[1,0]]):
            with self.subTest(roi=roi),self.assertRaises(ValueError):CrowdConfig(roi=roi)
    def test_unknown_keys(self):
        with self.assertRaises(ValueError):CrowdConfig.parse({'crowdCount':1})
        with self.assertRaises(ValueError):CrowdConfig().patched([])
    def test_confidence_boundaries(self):
        c=CrowdConfig(person_confidence=0,head_confidence=1);self.assertEqual(c.confidence_for(0),0);self.assertEqual(c.confidence_for(1),1)
    def test_bad_confidences(self):
        for key in ('person_confidence','head_confidence','nms_iou_threshold'):
            for value in (True,-.1,1.1,float('nan'),float('inf'),'0.5'):
                with self.subTest(key=key,value=value),self.assertRaises(ValueError):CrowdConfig(**{key:value})
    def test_bad_toggles(self):
        for key in ('enabled','nms_enabled','debug_roi'):
            with self.subTest(key=key),self.assertRaises(ValueError):CrowdConfig(**{key:1})
    def test_bad_timers(self):
        for key in ('window_seconds','report_seconds','stale_seconds'):
            for value in (0,-1,True,float('inf'),float('nan'),3601,1e-12):
                with self.subTest(key=key,value=value),self.assertRaises(ValueError):CrowdConfig(**{key:value})
    def test_nanosecond_duration(self):self.assertEqual(duration_ns(1e-9),1)
    def test_minimum_samples(self):
        for value in (0,1,True,2.5,4097):
            with self.subTest(value=value),self.assertRaises(ValueError):CrowdConfig(minimum_samples=value)
    def test_stale_must_cover_report(self):
        with self.assertRaises(ValueError):CrowdConfig(report_seconds=5,stale_seconds=4)
    def test_roi_display_payload_stays_small(self):
        p=CrowdDisplayConfig(CrowdConfig()).payload()
        self.assertEqual(set(p),{'enabled','roi_id','roi_name','roi','debug_roi','confidence'})
        self.assertNotIn('minimum_samples',p);self.assertNotIn('window_seconds',p)
    def test_patching_does_not_mutate(self):
        c=CrowdConfig();new=c.patched({'head_confidence':.7});self.assertEqual(c.head_confidence,.45);self.assertEqual(new.head_confidence,.7)
    def test_labels_validated(self):
        for value in ('','a\nb','x'*65):
            with self.subTest(value=value),self.assertRaises(ValueError):CrowdConfig(roi_name=value)
    def test_class_id_bool_is_invalid(self):
        with self.assertRaises(ValueError):CrowdConfig().confidence_for(True)

class ModelContractTest(unittest.TestCase):
    def contract(self,**changes):
        # Deliberately non-0/1 raw slots and 4 logits. NOT an Awiros observation.
        return CrowdTensorContract(**dict(dict(query_count=17,logit_count=4,person_slot=2,head_slot=0,
            ignored_slots=(1,3),checkpoint_sha256='a'*64,onnx_sha256='b'*64,review_id='SYNTHETIC_ONLY',
            box_format='CXCYWH_NORMALIZED',score_format='RAW_SIGMOID_LOGITS',output_dtype='FP32',
            selection_policy='GLOBAL_TOP300_THEN_MAP'),**changes))
    def test_nonstandard_raw_slots(self):
        c=self.contract();self.assertEqual([c.canonical_class(i) for i in range(4)],[1,None,0,None])
    def test_exact_shapes_all_batches(self):
        c=self.contract()
        for b in range(1,9):c.validate_shapes((b,17,4),(b,17,4),batch=b)
    def test_bad_shape_or_batch(self):
        for b in (0,9,True):
            with self.subTest(b=b),self.assertRaises(ValueError):self.contract().validate_shapes((b,17,4),(b,17,4),batch=b)
        with self.assertRaises(ValueError):self.contract().validate_shapes((1,17,4),(1,17,3),batch=1)
    def test_mapping_complete_unique(self):
        for changes in ({'person_slot':0},{'ignored_slots':(1,)},{'person_slot':True}):
            with self.subTest(changes=changes),self.assertRaises(ValueError):self.contract(**changes)
    def test_provenance_required(self):
        for changes in ({'onnx_sha256':''},{'checkpoint_sha256':'A'*64},{'review_id':' '}):
            with self.subTest(changes=changes),self.assertRaises(ValueError):self.contract(**changes)
    def test_output_semantics_explicit(self):
        for changes in ({'score_format':'PROBABILITY'},{'box_format':'XYXY'},{'output_dtype':'FP16'},
                        {'selection_policy':'ARGMAX_PER_QUERY'}):
            with self.subTest(changes=changes),self.assertRaises(ValueError):self.contract(**changes)
    def test_empty_manifest_not_verified(self):
        m=unconfirmed_manifest();self.assertIsNone(m['raw_contract']);self.assertIsNone(m['checkpoint_sha256'])
        self.assertEqual(m['runtime'],'NOT_RUN');self.assertIsNone(m['preprocessing'])
    def test_per_model_instances(self):
        for n in (1,4,6,8):self.assertEqual(CrowdBuildIntent(instances=n).payload()['instances'],n)
        self.assertEqual(CrowdBuildIntent().payload()['max_batch'],4)
    def test_build_intent_invalid(self):
        for changes in ({'instances':0},{'instances':True},{'max_batch':3},{'resolution':576}):
            with self.subTest(changes=changes),self.assertRaises(ValueError):CrowdBuildIntent(**changes)
