from dataclasses import replace
import json
import unittest
from unittest.mock import Mock, patch

import numpy as np

from app.contracts.frames import FrameTime
from app.det_result_path import DetResultPath
from app.frame_ingress import FrameIngress
from app.intrusion import Intrusion, IntrusionConfig
from app.metadata_render import render, time_label, HEADER
from app.native_metadata import NativeFrame, NativeDetection
from app.stream_settings import patch_settings, visual_options
from test_intrusion import frame
import test_metadata_connection as connection


class OutputTest(unittest.TestCase):
    def test_capture_failure_bound_and_known_surface_resize(self):
        import tempfile
        from pathlib import Path
        from app.metadata_visual import MetadataVisual
        from app.contracts.visualization import FrameResult
        with tempfile.TemporaryDirectory() as root:
            Path(root).chmod(0o700)
            (Path(root)/'capture.enabled').touch()
            now = [0.]
            visual = MetadataVisual(root, clock=lambda: now[0])
            f = frame(0,0)
            try:
                for _ in range(3):
                    self.assertTrue(visual.reserve_image(f))
                    visual.note_failure('extract_failed')
                    now[0] += .5
                self.assertFalse(visual.reserve_image(f))
                self.assertEqual(visual.status()['captures'], 3)
                result = FrameResult(f, ())
                visual.submit_result(result)
                self.assertTrue(visual.submit_image(f, np.zeros((720,1280,3),np.uint8), (1280,720)))
                connection.wait_for(lambda: visual.status().get('saved')==1)
                snapshot = json.loads(next(Path(root).glob('capture-*/*.json')).read_text())
                self.assertEqual(snapshot['frameWidth'],640)
                self.assertFalse(visual.submit_image(f,np.zeros((700,1280,3),np.uint8),(1280,700)))
            finally:
                self.assertTrue(visual.close(1))

    def test_unspecified_none_empty_and_validation(self):
        spec = {'server_id':'s', 'source_id':'c'}
        self.assertEqual(visual_options(spec), (None, False))
        self.assertEqual(patch_settings(spec, {'display_categories':None}), spec)
        updated = patch_settings(spec, {'display_categories':[], 'debug_all_candidates':True})
        self.assertEqual(visual_options(updated), ([], True))
        self.assertEqual(visual_options(patch_settings(updated, {'display_categories':['car','person']})),
                         (['car','person'], True))
        for value in (['unknown'], ['person','person'], True):
            with self.assertRaises(ValueError):
                patch_settings(spec, {'display_categories':value})
        with self.assertRaises(ValueError):
            patch_settings(spec, {'debug_all_candidates':1})
        self.assertEqual(spec, {'server_id':'s','source_id':'c'})

    def test_scenario_priority_keeps_tracking_candidates(self):
        for categories, expected in ((None, [True,False,True]), ([], [True,False,False]),
                                     (['person'], [True,False,False]), (['car'], [True,False,True])):
            tracker = Mock(update=lambda obs: [None]*len(obs))
            path = DetResultPath(FrameIngress('server','source','stream','run',1), tracker,
                'RFDETRMedium', .5, Mock(), lambda: True, Mock(), display_threshold=.25,
                intrusion=Intrusion(IntrusionConfig(enabled=True)), display_categories=categories)
            try:
                path.start()
                path.submit(frame(0,0), NativeFrame(0,0,640,360,
                    tuple(NativeDetection(cls, score, (.1,.1,.3,.8)) for cls,score in ((0,.9),(0,.4),(2,.8))),640,360))
                connection.wait_for(lambda: path.status().get('processed')==1)
                result = path.latest_result()
                self.assertEqual([c.visible for c in result.commands], expected)
                self.assertEqual(len(result.commands), 3)
                self.assertEqual(len(result.snapshot()['objects']), sum(expected))
            finally:
                path.close(1)

    def test_time_sources_and_roi_debug(self):
        f = frame(0,.5)
        self.assertIn('Decision PTS: 0.500s', time_label(f)[0])
        self.assertIn('Calculated UTC: UNAVAILABLE', time_label(f)[0])
        time = FrameTime(source_time_wall_ms=1000, time_source='SYNTHETIC_TEST_ANCHOR', sync_status='ESTIMATED')
        labels = time_label(replace(f, frame_time=time))
        self.assertIn('1970-01-01T00:00:01.000+00:00', labels[0])
        self.assertIn('SYNTHETIC_TEST_ANCHOR', labels[1])
        self.assertIn('ESTIMATED', labels[1])
        from app.contracts.visualization import FrameResult
        cfg = IntrusionConfig(enabled=True, roi_name='ROI A')
        result = FrameResult(f, (), scenario_config=cfg)
        image, _ = render(result, np.zeros((360,640,3), np.uint8))
        self.assertEqual(image.getpixel((639,HEADER+5)), (255,159,50))
        hidden, _ = render(replace(result, scenario_config=replace(cfg, debug_roi=False)), np.zeros((360,640,3), np.uint8))
        self.assertEqual(hidden.getpixel((639,HEADER+5)), (0,0,0))
        with self.assertRaises(ValueError):
            replace(cfg, roi_name='bad\nname')


class OutputSettingsAPI(connection.SettingsTest):
    def test_new_options_cas_and_persistence_failure(self):
        rev = self.manager.settings('stream')['configured']['revision']
        changes = {'display_categories':[], 'debug_all_candidates':True,
                   'intrusion':{'roi_name':'Entry A', 'debug_roi':False}}
        from app.stream_config_store import ConfigPersistenceError
        with patch.object(self.manager.config_store, 'update', side_effect=ConfigPersistenceError()):
            with self.assertRaises(ConfigPersistenceError):
                self.manager.patch_settings('stream', rev, changes)
        self.assertIsNone(self.manager.settings('stream')['configured']['display_categories'])
        value = self.manager.patch_settings('stream', rev, changes)
        self.assertEqual(value['configured']['display_categories'], [])
        self.assertEqual(value['configured']['intrusion']['roi_name'], 'Entry A')
        with self.assertRaises(ValueError):
            self.manager.patch_settings('stream', rev, changes)


if __name__ == '__main__':
    unittest.main()
