import unittest
from dataclasses import asdict
from unittest.mock import patch
from app.frame_time import FrameAnchor, RtpFrameClock, validate_carried_time
from app.stream_config_store import ConfigPersistenceError
from app.stream_manager import StreamManager
import tempfile


class FrameTimeTest(unittest.TestCase):
    def packet(self, ts, rate=90000, ssrc=1):
        return dict(timestamp=ts, clock_rate=rate, ssrc=ssrc)

    def test_zero_and_integer_precision_and_null(self):
        anchor = FrameAnchor(startWallTimeMs=1000, synthetic=True).frame_time()
        clock = RtpFrameClock(anchor)
        self.assertEqual(clock.resolve(None), anchor)
        for raw, expected in ((0, 1000), (90001, 2000)):
            t = clock.resolve(self.packet(raw))
            self.assertEqual(t.source_time_wall_ms, expected)
            self.assertEqual(t.sync_status, 'ESTIMATED')
            self.assertEqual(t.time_source, 'SYNTHETIC_TEST_ANCHOR')
            self.assertEqual(validate_carried_time(asdict(t), anchor), t)

    def test_wrap_waits_for_new_anchor_and_does_not_double_count(self):
        a = FrameAnchor(startWallTimeMs=1000).frame_time()
        c = RtpFrameClock(a)
        prior = c.resolve(self.packet(2**32 - 1))
        for raw in (0, 1, 90000):
            t = c.resolve(self.packet(raw))
            self.assertIsNone(t.source_time_wall_ms)
            self.assertEqual(t.sync_status, 'RAW_ONLY')
            self.assertEqual(validate_carried_time(asdict(t), a), t)
        self.assertEqual(validate_carried_time(asdict(prior), a), prior)
        b = FrameAnchor(startWallTimeMs=2000).frame_time()
        t = RtpFrameClock(b).resolve(self.packet(90000))
        self.assertEqual(t.source_time_wall_ms, 3000)
        with self.assertRaises(ValueError): validate_carried_time(asdict(prior), b)

    def test_restart_rate_or_ssrc_change_not_silently_synchronized(self):
        for next_packet in (self.packet(8), self.packet(10, 8000), self.packet(10, ssrc=2)):
            c = RtpFrameClock(FrameAnchor(startWallTimeMs=10).frame_time())
            c.resolve(self.packet(9))
            self.assertEqual(c.resolve(next_packet).sync_status, 'RAW_ONLY')

    def test_invalid_values_and_no_anchor(self):
        for packet in (self.packet(True), self.packet(-1), self.packet(2**32), self.packet(1, 0), {}):
            with self.assertRaises(ValueError): RtpFrameClock(None).resolve(packet)
        t = RtpFrameClock(None).resolve(self.packet(0))
        self.assertEqual(t.sync_status, 'RAW_ONLY')
        self.assertIsNone(t.source_time_wall_ms)
        for value in (True, -1, '1000', 2**53):
            with self.assertRaises(ValueError): FrameAnchor(startWallTimeMs=value)
        with self.assertRaises(ValueError):
            RtpFrameClock(FrameAnchor(startWallTimeMs=2**53-1).frame_time()).resolve(self.packet(90000))

    def test_settings_cas_save_failure_and_old_snapshot_preserved(self):
        with tempfile.TemporaryDirectory() as root:
            m = StreamManager(root+'/base', root+'/overlay')
            g = m.add(dict(stream_id='s', server_id='server', source_id='camera', uri='private', store=False))
            rev = m.settings('s')['configured']['revision']
            changes = dict(frame_time_anchor=dict(startWallTimeMs=1000, synthetic=True))
            with patch.object(m.config_store, 'update', side_effect=ConfigPersistenceError()):
                with self.assertRaises(ConfigPersistenceError): m.patch_settings('s', rev, changes)
            self.assertNotIn('frame_time_anchor', g.spec)
            with patch.object(g, 'start', side_effect=AssertionError('unexpected start')):
                response = m.patch_settings('s', rev, changes)
            self.assertEqual(response['configured']['frame_time_anchor'], changes['frame_time_anchor'])
            with self.assertRaises(ValueError): m.patch_settings('s', rev, dict(frame_time_anchor=None))
            m.patch_settings('s', response['configured']['revision'], dict(frame_time_anchor=None))
            self.assertIsNone(g.spec['frame_time_anchor'])

    def test_http_anchor_and_null(self):
        from app.api.streams import SettingsPatch
        p = SettingsPatch(expected_revision='a'*64, frame_time_anchor=dict(startWallTimeMs=1000, synthetic=True))
        self.assertEqual(p.model_dump()['frame_time_anchor']['startWallTimeMs'], 1000)
        self.assertIn('frame_time_anchor', SettingsPatch(expected_revision='a'*64, frame_time_anchor=None).model_dump(exclude_unset=True))
        with self.assertRaises(ValueError): SettingsPatch(expected_revision='a'*64, frame_time_anchor={'bad': 1})
