from copy import deepcopy
import json
import pytest

from app.multi_roi.adapter import normalize
from app.multi_roi.errors import RoiError
from app.multi_roi.model import Snapshot
from app.multi_roi.policy import DEFAULT
from app.multi_roi.projection import project
from app.provider_plan import plan
from multi_roi_fixtures import request, LEFT


@pytest.mark.parametrize('codes', [(), ('INTRUSION',), ('LOITERING',), ('FALL',), ('FIGHT',), ('FIRE',), ('CROWD',),
                                  ('INTRUSION','LOITERING','FALL','FIGHT','FIRE','CROWD')])
def test_compile_scenarios_and_no_saved_parameter_inheritance(codes):
    snap = normalize(request(*codes), DEFAULT)
    spec = project(dict(server_id='server', source_id='1', intrusion={'dwell_seconds':99},
                        fall={'person_score':.99}, store=False), snap)
    p = plan(spec)
    assert set(p.requested_scenarios) == set(codes)
    assert spec['store'] is False
    assert Snapshot.restore(snap.payload()) == snap
    if 'INTRUSION' in codes:
        assert spec['intrusion']['dwell_seconds'] == .5


def test_rule_order_defaults_and_independent_duplicate_semantics():
    d = request('INTRUSION', 'INTRUSION')
    a = normalize(d, DEFAULT)
    d['rules'].reverse()
    assert normalize(d, DEFAULT) == a
    d['rules'][0]['ruleId'] = 99
    assert normalize(d, DEFAULT).fingerprint != a.fingerprint
    assert len(a.areas) == 1 and len(a.rules) == 2


def test_polygon_order_numeric_representation_and_full_frame_default_label():
    d = request('INTRUSION')
    before = normalize(d, DEFAULT)
    d['rois'] = [dict(roiId=0, points=[[1,1],[1,0],[0,0],[0,1]])]
    assert normalize(d, DEFAULT) == before
    d['rois'][0]['points'] = [[0,0],[.5,0],[1,0],[1,1],[0,1]]
    d['rois'][0]['outside'] = True
    with pytest.raises(RoiError, match='EMPTY_EFFECTIVE_REGION'):
        normalize(d, DEFAULT)


@pytest.mark.parametrize('mutate,code', [
    (lambda d: d.pop('rules'), 'INVALID_SNAPSHOT'),
    (lambda d: d.update(rules=None), 'INVALID_SNAPSHOT'),
    (lambda d: d['rules'].append(deepcopy(d['rules'][0])), 'DUPLICATE_RULE_ID'),
    (lambda d: d['rules'][0].update(roiId=9), 'ROI_REFERENCE_NOT_FOUND'),
    (lambda d: d['rules'][0].update(ruleId=True), 'INVALID_RULE_CONFIG'),
    (lambda d: d['rules'][0].update(parameters={'password':'do-not-print'}), 'INVALID_RULE_CONFIG'),
    (lambda d: d['rules'][0].update(parameters={'person_confidence':2}), 'INVALID_RULE_CONFIG'),
    (lambda d: d['rules'][0].update(parameters={'person_confidence':None}), 'INVALID_RULE_CONFIG'),
    (lambda d: d.update(rois=[dict(roiId=0, points=LEFT)]), 'FULL_FRAME_ID_CONFLICT'),
    (lambda d: d.update(rois=[dict(roiId=0, outside=True)]), 'EMPTY_EFFECTIVE_REGION'),
    (lambda d: d.update(rois=[dict(roiId=5, points=[[0,0],[1,1],[0,1],[1,0]])]), 'INVALID_ROI'),
])
def test_rejects_precisely_and_redacts(mutate, code):
    d = request('INTRUSION')
    mutate(d)
    with pytest.raises(RoiError) as exc:
        normalize(d, DEFAULT)
    assert exc.value.code == code
    assert 'do-not-print' not in json.dumps(exc.value.payload())
    assert exc.value.payload()['existingRunPreserved'] is True


def test_full_frame_counts_once_and_unknown_ids_never_fallback():
    d = request('FIRE', 'FALL')
    d['rois'] = [dict(roiId=i, points=LEFT) for i in range(1, 9)]
    with pytest.raises(RoiError, match='ROI_LIMIT_EXCEEDED'):
        normalize(d, DEFAULT)
    d['rois'].pop()
    assert len(normalize(d, DEFAULT).areas) == 8
    d['rules'][0]['roiId'] = 100
    with pytest.raises(RoiError, match='ROI_REFERENCE_NOT_FOUND'):
        normalize(d, DEFAULT)


def test_reserved_id_mapping_is_isolated_from_internal_geometry():
    d = request('FIRE')
    d['rules'][0]['roiId'] = 99
    snap = normalize(d, dict(DEFAULT, full_frame_roi_id=99))
    assert snap.areas[0].kind == 'full_frame' and snap.areas[0].roi_id == 99
    with pytest.raises(RoiError, match='ROI_REFERENCE_NOT_FOUND'):
        normalize(request('FIRE'), dict(DEFAULT, full_frame_roi_id=99))


def test_inside_outside_and_positive_full_frame_keep_identity():
    d = request('INTRUSION')
    d['rois'] = [dict(roiId=3, points=LEFT, outside=True)]
    d['rules'][0]['roiId'] = 3
    area = normalize(d, DEFAULT).areas[0]
    assert not area.contains((.2,.5)) and area.contains((.8,.5))
    assert not area.contains((1.2,.5))
    d['rois'][0].update(points=[[0,0],[1,0],[1,1],[0,1]], outside=False)
    assert normalize(d, DEFAULT).areas[0].roi_id == 3
    d['rois'][0]['outside'] = True
    with pytest.raises(RoiError, match='EMPTY_EFFECTIVE_REGION'):
        normalize(d, DEFAULT)


def test_crowd_mandatory_values_and_rule_limit():
    d = request('CROWD')
    d['rules'][0]['parameters'].pop('reference_count')
    with pytest.raises(RoiError, match='INVALID_RULE_CONFIG'):
        normalize(d, DEFAULT)
    d = request(*(['FIRE']*49))
    with pytest.raises(RoiError, match='RULE_LIMIT_EXCEEDED'):
        normalize(d, DEFAULT)
    assert len(normalize(d, dict(DEFAULT, max_rules=49)).rules) == 49


@pytest.mark.parametrize('code', ['INTRUSION','LOITERING','FIRE','FALL','FIGHT','CROWD'])
def test_disabled_rules_do_not_enable_providers(code):
    d = request(code)
    d['rules'][0]['enabled'] = False
    d['rules'][0]['parameters'] = {}
    s = normalize(d, DEFAULT)
    p = plan(project(dict(server_id='server', source_id='1'), s))
    assert p.keys == ('medium',) and not p.requested_scenarios
