from copy import deepcopy
import importlib
import json
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import Mock

import pytest
from fastapi.testclient import TestClient

from app.multi_roi.adapter import normalize
from app.multi_roi.policy import DEFAULT
from app.multi_roi.projection import project
from app.multi_provider_results import MultiProviderCoordinator, _OpenFrame
from app.provider_plan import plan
from multi_roi_fixtures import request, frame, Manager
from test_pr13_multi_provider import _Ingress, _Collector
from test_secure_bridge import configuration, AI_TOKEN
from control_sidecar.main import create_app
from control_sidecar.protocol import BoundaryError


def test_six_scenarios_share_one_tracker_and_three_provider_results(monkeypatch):
    tracker = SimpleNamespace(update=Mock(return_value=[1]), reset=Mock())
    monkeypatch.setattr('app.byte_tracking.ByteTracker', lambda *a, **k: tracker)
    monkeypatch.setattr(MultiProviderCoordinator, 'pose_nms', lambda self, obs: tuple(range(len(obs))))
    monkeypatch.setattr('app.crowd_scenario.detection_nms_indices', lambda obs, threshold: tuple(range(len(obs))))
    snapshot = normalize(request('INTRUSION','LOITERING','FALL','FIGHT','FIRE','CROWD'), DEFAULT)
    spec = project(dict(server_id='server', source_id='1', stream_id='stream'), snapshot)
    p = plan(spec)
    c = MultiProviderCoordinator(spec, _Ingress(), p, _Collector(), lambda: True, lambda: None, clock=lambda:0.)
    assert p.keys == ('pose','fire','crowd')
    f = frame()
    def copied(pose=False):
        row = SimpleNamespace(class_id=0, score=.9, bbox_xyxy=(.1,.1,.4,.8),
            keypoints=tuple((.2,.4,.9) for _ in range(17)) if pose else None)
        row.downstream = lambda: dict(class_id=0, score=.9, bbox_xyxy=row.bbox_xyxy)
        return SimpleNamespace(detections=(row,))
    parts = {'pose':copied(True),'fire':copied(),'crowd':copied()}
    state = _OpenFrame(f, 0., 1., set(parts), parts, state='FINALIZING')
    c._finalize(state)
    output = c.snapshot()
    assert tracker.update.call_count == 1
    assert len(output['ruleConfigs']) == 6
    assert {r['scenarioCode'] for r in output['ruleConfigs']} == set(p.requested_scenarios)
    assert len(output['objects']) == 3
    assert c.status()['multi_roi']['rules'] == 6
    c.close(1)
    tracker.reset.assert_called_once()


def test_sidecar_policy_requires_auth_and_preserves_new_error_detail():
    from contextlib import asynccontextmanager
    class Auth:
        async def authorize(self, token):
            if token != AI_TOKEN:
                raise BoundaryError('UNAUTHORIZED',401)
        def current(self): return AI_TOKEN
        async def close(self): pass
        async def status(self): return {}
    class Transport:
        async def request(self, method, url, **kwargs):
            if url.endswith('/roi-policy'):
                return 200,b'{"effective":{"mode":"legacy"}}'
            return 422,json.dumps(dict(error='EMPTY_EFFECTIVE_REGION', requestId='request',
                message='빈 영역입니다.', details={'ruleId':2}, existingRunPreserved=True)).encode()
        async def close(self): pass
    app = create_app(configuration(), Transport(), Auth())
    # No lifespan/auth login: all traffic is TestClient -> mocked in-process transport.
    client = TestClient(app)
    assert client.get('/api/v1/maintenance/roi-policy').status_code == 401
    headers={'Authorization':'Bearer '+AI_TOKEN}
    assert client.get('/api/v1/maintenance/roi-policy',headers=headers).status_code == 200
    result = client.post('/api/v1/cameras/start',json=request('FIRE'),headers=headers).json()
    assert result['result'] == 'FAIL' and result['reason'] == 'EMPTY_EFFECTIVE_REGION'
    assert result['message'] == '빈 영역입니다.' and result['existingRunPreserved'] is True
    client.close()


def test_ct_client_uses_stdin_no_token_argument_or_autostart(monkeypatch):
    root=Path(__file__).resolve().parents[1]/'tools/icerti_offline/maintenance'
    monkeypatch.syspath_prepend(str(root))
    field = importlib.import_module('field_roi')
    seen=[]
    def run(args, **kwargs):
        seen.append((args,kwargs))
        return SimpleNamespace(stdout=b'{"effective":{"mode":"legacy"},"error":"INVALID_POLICY"}')
    monkeypatch.setattr('maintenance_process.run',run)
    result=field.call('fixture-container')
    assert result['effective']['mode']=='legacy'
    args,kwargs=seen[0]
    assert args[:4]==['docker','exec','-i','fixture-container']
    assert json.loads(kwargs['input'])=={'method':'GET','body':None}
    assert kwargs['timeout']==8
    assert 'docker start' not in ' '.join(args)


def test_real_manager_overlay_isolated_noop_full_replace_and_stop(tmp_path,monkeypatch):
    from app.stream_manager import StreamManager
    from app.secure_control import ControlService, Start
    from test_web_start_preparation import StubGroup
    from test_multi_roi_control import change
    manager=StreamManager(str(tmp_path/'base.yaml'),str(tmp_path/'overlay.yaml'))
    monkeypatch.setattr(manager,'_new_group',StubGroup)
    service=ControlService(manager,'server',{'store':False},roi_policy_path=tmp_path/'policy.json')
    change(service.roi_policy,mode='multi_roi')
    d=request('INTRUSION','LOITERING')
    first=service.start_camera(Start(json.dumps(d)))
    before=Path(manager.overlay_path).read_bytes()
    d['rules'].reverse()
    second=service.start_camera(Start(json.dumps(d)))
    assert second.group is first.group
    assert first.group.starts==1 and first.group.restarts==0
    assert Path(manager.overlay_path).read_bytes()==before
    d['rules']=[]
    service.start_camera(Start(json.dumps(d)))
    saved=manager.config_store.read()['added'][0]
    assert saved['multi_roi']['rules']==[] and not saved['loitering']['enabled']
