import colorsys
import json
import os
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch

import numpy as np
from app.contracts.frames import FrameEnvelope, DetectionObservation
from app.contracts.visualization import FrameResult, VisualizationCommand
from app.metadata_render import class_color, candidate_summary, label_text, render, HEADER
from app.frame_ingress import FrameIngress
from app.test_frame_time import synthetic_anchor


def frame():
    return FrameEnvelope('server', 'source', 'stream', 'run', 1, 0, 0, 123, 640, 360)


class RenderTest(unittest.TestCase):
    def test_stable_palette_avoids_person_green(self):
        self.assertEqual(class_color(0), (60, 235, 145))
        colors = [class_color(i) for i in range(80)]
        self.assertEqual(len(set(colors)), 80)
        self.assertEqual(colors, [class_color(i) for i in range(80)])
        for color in colors[1:]:
            hue = colorsys.rgb_to_hsv(*(x/255 for x in color))[0]*360
            self.assertFalse(66 < hue < 174)

    def test_counts_use_visibility_not_a_second_threshold(self):
        f = frame()
        commands = tuple(VisualizationCommand(DetectionObservation(f, 'RFDETRMedium', i, .1, (.1,.1,.8,.8)),
            'person' if i == 0 else 'other', visible=i%2==0) for i in range(8))
        result = FrameResult(f, commands)
        before = result.snapshot()
        counts = candidate_summary(result)
        self.assertEqual((counts['total'], counts['shown'], counts['hidden']), (8,4,4))
        image, stats = render(result, np.zeros((360,640,3), dtype=np.uint8))
        self.assertEqual(image.size, (640,360+HEADER))
        self.assertEqual(stats['classes_omitted'], 2)
        self.assertEqual(result.snapshot(), before)
        self.assertNotIn('id=None', label_text(commands[0]))
        self.assertEqual(image.getpixel((512, HEADER+100)), class_color(0))

    def test_label_bounds_overlap_and_omission(self):
        f = frame()
        commands = tuple(VisualizationCommand(DetectionObservation(f,'RFDETRMedium',4,.8,(0,0,1,1)),
            'long label '*8, track_id=i if i else None) for i in range(70))
        # Contract permits bounded labels only.
        commands = tuple(VisualizationCommand(c.detection,'airplane label',track_id=c.track_id) for c in commands)
        image, stats = render(FrameResult(f,commands),np.zeros((360,640,3),dtype=np.uint8))
        self.assertGreater(stats['labels_omitted'],0)
        for i,a in enumerate(stats['label_rects']):
            self.assertTrue(0<=a[0]<=a[2]<=640 and HEADER<=a[1]<=a[3]<=image.height)
            for b in stats['label_rects'][:i]:
                self.assertFalse(a[0]<b[2] and a[2]>b[0] and a[1]<b[3] and a[3]>b[1])


class TestAnchorTest(unittest.TestCase):
    def test_explicit_scoped_anchor_does_not_invent_rtp(self):
        with tempfile.TemporaryDirectory() as root:
            path=Path(root)/'anchor.json'
            path.write_text(json.dumps(dict(sourceId='source',startWallTimeMs=123456,rtpClockRate=90000,synthetic=True)))
            with patch.dict(os.environ,{'CONTEXTRA_TEST_FRAME_ANCHOR_FILE':str(path)}):
                value=synthetic_anchor('source')
                self.assertEqual(value.anchor_wall_time_ms,123456)
                self.assertIsNone(value.source_rtp_timestamp)
                self.assertIsNone(value.source_time_wall_ms)
                self.assertEqual(value.time_source,'SYNTHETIC_TEST_ANCHOR')
                self.assertEqual(value.sync_status,'UNAVAILABLE')
                self.assertIsNone(synthetic_anchor('other'))
                ingress=FrameIngress('server','source','stream','run',1,clock_ns=lambda:999000000)
                self.assertEqual(ingress.frame_time,value)
                path.write_text('{"synthetic":false}')
                with self.assertRaisesRegex(ValueError,'Invalid explicit test Anchor'):
                    synthetic_anchor('source')

    def test_default_is_unavailable(self):
        with patch.dict(os.environ,{},clear=True):
            self.assertIsNone(synthetic_anchor('source'))


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