from copy import deepcopy
from dataclasses import replace
import json
import pytest
from PIL import Image

from app.multi_roi.adapter import normalize
from app.multi_roi.policy import DEFAULT
from app.multi_roi.runtime import RuleRuntime
from app.multi_roi.artifacts import event_snapshot
from app.multi_roi.results import merge
from app.artifact_media import draw_frame
from app.crowd_config import CROWD_MODEL
from app.fire_config import FIRE_MODEL
from app.pose_config import POSE_MODEL
from multi_roi_fixtures import request, frame, observation, pose, LEFT


def runtime(d, clock=lambda: 10.):
    return RuleRuntime(normalize(d, DEFAULT).payload(), clock=clock)


def test_independent_regions_confidence_union_and_no_outside_display():
    d = request('INTRUSION','INTRUSION')
    d['rois'] = [dict(roiId=1, points=LEFT), dict(roiId=2, points=LEFT, outside=True)]
    d['rules'][0].update(roiId=1, parameters={'person_confidence':.8})
    d['rules'][1].update(roiId=2, parameters={'person_confidence':.2})
    r = runtime(d)
    f = frame()
    obs = (observation(f, x=.2, score=.5), observation(f, x=.8, score=.5))
    result = r.person(f, obs, (None,None), [1,2], (0,1))
    assert [c.visible for c in result.commands] == [False, True]
    assert len(result.commands) == 2
    assert len(result.snapshot(visible_only=False)['ruleConfigs']) == 2
    assert r.engines[0] is not r.engines[1]
    r.close()
    assert not r.engines[0].states and not r.engines[1].states


def test_rule_events_and_cooldown_are_independent():
    d = request('INTRUSION','INTRUSION')
    for row in d['rules']:
        row['parameters'] = dict(dwell_seconds=.1, cooldown_seconds=10)
    r = runtime(d)
    result = None
    for i in range(4):
        f = frame(i)
        result = r.person(f, (observation(f),), (None,), [1], (0,))
        if result.events:
            break
    assert {e.rule_id for e in result.events} == {0,1}
    assert len({e.event_id for e in result.events}) == 2
    assert len(result.commands[0].event_ids) == 2
    assert len(r.gates) == 2
    frozen = result.events[0].payload()
    d['rules'].clear()
    assert frozen['ruleContext']['ruleId'] == 0
    scoped = event_snapshot(result.snapshot(), frozen)
    assert all(len(o['scenarioResults']) == 1 for o in scoped['objects'])
    wrong = dict(result.snapshot(), runId='new-run')
    assert event_snapshot(wrong, frozen) is None


@pytest.mark.parametrize('code', ['FALL','FIGHT'])
def test_pose_scenarios_use_rule_roi(code):
    d = request(code)
    d['rois'] = [dict(roiId=1, points=LEFT)]
    d['rules'][0]['roiId'] = 1
    r = runtime(d)
    f = frame()
    obs = (observation(f, POSE_MODEL, x=.2), observation(f, POSE_MODEL, x=.8))
    result = r.person(f, obs, tuple(pose(o) for o in obs), [1,2], (0,1))
    assert [c.visible for c in result.commands] == [True, False]


def test_all_off_is_person_detection_without_events():
    r = runtime(request())
    f = frame()
    obs = (observation(f), observation(f, cls=1))
    result = r.person(f, obs, (None,None), [1,None], (0,))
    assert len(result.commands) == 1 and result.commands[0].detection.class_id == 0
    assert not result.events


def test_fire_and_crowd_rules_use_same_observations_and_outside(monkeypatch):
    # NMS itself is unchanged; these non-overlapping synthetic boxes all survive.
    monkeypatch.setattr('app.crowd_scenario.detection_nms_indices', lambda rows, threshold: tuple(range(len(rows))))
    d = request('FIRE','FIRE','CROWD','CROWD')
    d['rois'] = [dict(roiId=1, points=LEFT), dict(roiId=2, points=LEFT, outside=True)]
    for i, row in enumerate(d['rules']):
        row['roiId'] = 1+i%2
    r = runtime(d)
    f = frame()
    for kind, model in [('fire',FIRE_MODEL), ('crowd',CROWD_MODEL)]:
        obs = (observation(f, model, x=.2), observation(f, model, x=.8))
        result = r.provider(kind, f, obs)
        assert len(result.commands) == 2
        rows = result.snapshot()['objects']
        assert all(len(o['scenarioResults']) == 1 for o in rows)
    r.close()


def test_artifact_hides_other_provider_and_other_rule_color():
    d = request('INTRUSION','FIRE')
    d['rules'][0]['parameters'] = dict(dwell_seconds=.1)
    r = runtime(d)
    for i in range(4):
        f = frame(i)
        person = r.person(f, (observation(f),), (None,), [1], (0,))
        fire = r.provider('fire', f, (observation(f,FIRE_MODEL,x=.8,cls=1),))
        combined = merge(f, [person,fire], camera_id=1)
        if person.events:
            break
    event = person.events[0].payload()
    snapshot = combined.snapshot()
    selected = event_snapshot(snapshot, event)
    assert len(snapshot['objects']) == 2 and len(selected['objects']) == 1
    assert selected['objects'][0]['modelKey'] == 'RFDETRMedium'
    canvas = Image.new('RGB',(1280,720))
    image = draw_frame(canvas, snapshot, scenario='INTRUSION', event=event)
    assert image.getpixel((960,300)) == (0,0,0)
    assert snapshot == combined.snapshot()


def test_more_than_legacy_sixteen_rules_still_keep_event_refs():
    d = request(*(['INTRUSION']*20))
    for row in d['rules']:
        row['parameters'] = dict(dwell_seconds=.1)
    r = runtime(d)
    for i in range(4):
        f = frame(i)
        result = r.person(f, (observation(f),), (None,), [1], (0,))
        if result.events:
            break
    assert len(result.events) == len(result.commands[0].event_ids) == 20
