import unittest
from copy import deepcopy
from app.crowd_config import CROWD_MODEL, CrowdConfig, FULL_FRAME
from app.crowd_runtime import (MODEL_PENDING,STANDALONE_REQUIRED,TRACKING_UNSUPPORTED,
    project_crowd_candidate,patch_crowd_candidate,crowd_plan)
from app.secure_control import validate_start, ControlError, ControlService, Start
from app.pose_config import execution_plan
import json

def scenarios(enabled=('CROWD',), **crowd_changes):
    rows=[dict(scenarioCode=k,enabled=k in enabled,confidence=None,rois=[])
          for k in ('FIRE','FALL','INTRUSION','LOITERING','FIGHT','CROWD')]
    rows[-1].update(crowd_changes);return rows

def spec():return dict(server_id='server',source_id='1',tracking=dict(enabled=False),store=False,active_learning=False)
def request(enabled=('CROWD',)):
    return dict(cameraId=1,rtspUrl='rtsp://example.invalid/live',startWallTimeMs=1,rtpClockRate=90000,scenarios=scenarios(enabled))

class AdmissionTest(unittest.TestCase):
    def test_web_start_validated(self):
        validate_start(request())
    def test_all_mix_combinations_rejected(self):
        for k in ('FIRE','FALL','INTRUSION','LOITERING','FIGHT'):
            with self.subTest(k=k),self.assertRaisesRegex(ControlError,STANDALONE_REQUIRED):validate_start(request(('CROWD',k)))
    def test_disabled_web_unchanged(self):
        for enable in ((),('INTRUSION',),('LOITERING',),('FIRE',),('FALL',)):
            d=request(enable);before=deepcopy(d);validate_start(d);self.assertEqual(d,before)
    def test_web_rejects_before_manager_mutation(self):
        service=ControlService(object(),'server')
        with self.assertRaisesRegex(ControlError,STANDALONE_REQUIRED):service.start_camera(Start(json.dumps(request(('CROWD','FIGHT')))))
    def test_local_execution_gate(self):
        s=spec();s['crowd']=dict(enabled=True)
        self.assertEqual(execution_plan(s)['model'],CROWD_MODEL)
    def test_raw_model_cannot_fallback_to_coco(self):
        with self.assertRaisesRegex(ValueError,MODEL_PENDING):execution_plan(dict(inference_profile=dict(det_model=CROWD_MODEL)))
    def test_tracking_on_explicit_rejection(self):
        s=spec();s.update(crowd=dict(enabled=True),tracking=dict(enabled=True))
        with self.assertRaisesRegex(ValueError,TRACKING_UNSUPPORTED):crowd_plan(s)
    def test_local_enabled_others_rejected(self):
        for k in ('intrusion','loitering','fall','fire','fight','pose'):
            s=spec();s.update(crowd=dict(enabled=True));s[k]=dict(enabled=True)
            with self.subTest(k=k),self.assertRaisesRegex(ValueError,STANDALONE_REQUIRED):crowd_plan(s)
    def test_disabled_absent_local_guard_no_change(self):
        for s in ({},dict(crowd=dict(enabled=False)),dict(inference_profile=dict(det_model='RFDETRMedium'))):
            before=deepcopy(s);crowd_plan(s);self.assertEqual(s,before)
    def test_pure_web_projection(self):
        s=spec();before=deepcopy(s);v=project_crowd_candidate(s,scenarios())
        self.assertEqual(s,before);self.assertEqual(v['inference_profile']['det_model'],CROWD_MODEL)
        self.assertFalse(v['tracking']['enabled']);self.assertTrue(v['crowd']['enabled'])
    def test_null_confidence_preserves_ai(self):
        s=spec();s['crowd']=dict(person_confidence=.7,head_confidence=.6,window_seconds=5)
        v=project_crowd_candidate(s,scenarios())['crowd']
        self.assertEqual((v['person_confidence'],v['head_confidence'],v['window_seconds']),(.7,.6,5))
    def test_explicit_confidence_applies_both(self):
        v=project_crowd_candidate(spec(),scenarios(confidence=.45))['crowd']
        self.assertEqual((v['person_confidence'],v['head_confidence']),(.45,.45))
    def test_first_roi_only(self):
        rois=[dict(label='first',points=[[0,0],[.5,0],[.5,1],[0,1]]),dict(label='second',points=[[.5,0],[1,0],[1,1],[.5,1]])]
        v=project_crowd_candidate(spec(),scenarios(rois=rois))['crowd'];self.assertEqual(v['roi'],rois[0]['points']);self.assertEqual(v['roi_name'],'first')
    def test_empty_roi_full_frame(self):
        self.assertEqual(project_crowd_candidate(spec(),scenarios())['crowd']['roi'],[list(x) for x in FULL_FRAME])
    def test_disabled_absent_projection_no_change(self):
        s=spec();self.assertEqual(project_crowd_candidate(s,scenarios(())),s)
    def test_saved_other_scenario_never_silently_disabled(self):
        s=spec();s['fire']=dict(enabled=True)
        with self.assertRaisesRegex(ValueError,STANDALONE_REQUIRED):project_crowd_candidate(s,scenarios())
        self.assertTrue(s['fire']['enabled'])
    def test_patch_pure_and_preserves_noop(self):
        s=spec();self.assertEqual(patch_crowd_candidate(s,{}),s)
        new=patch_crowd_candidate(s,{'window_seconds':5});self.assertNotIn('crowd',s);self.assertEqual(new['crowd']['window_seconds'],5)
    def test_no_verification_flag_bypass(self):
        s=spec();s['crowd']=dict(enabled=True,verified=True)
        with self.assertRaises(ValueError):crowd_plan(s)
