import ast
from dataclasses import FrozenInstanceError
from pathlib import Path
import unittest
from unittest.mock import Mock

from app.application.streams import StreamApplicationService
from app.contracts.cameras import CameraConfigError, CameraStart, source_identity
from app.contracts.frames import FrameEnvelope, DetectionObservation
from app.stream_lifecycle import StreamLifecycle


class IdentityContractsTest(unittest.TestCase):
    def test_camera_id_preserves_string_semantics(self):
        self.assertEqual(source_identity(12), "12")
        self.assertEqual(source_identity("0012"), "0012")
        for value in (None, True, False, 1.2, "", "  "):
            with self.assertRaises(CameraConfigError):
                source_identity(value)

    def test_request_repr_does_not_expose_source(self):
        uri = "rtsp://" + "private-user:private-pass@camera.invalid/live"
        request = CameraStart("0012", uri)
        self.assertNotIn("private-pass", repr(request))
        self.assertNotIn(uri, repr(request))
        self.assertEqual(request.runtime_spec("server")["source_id"], "0012")

    def test_no_input_roi_or_frame_timestamp_invention(self):
        frame = FrameEnvelope("server", "source", "stream", "run", 1, 3, None, 10, 1920, 1080)
        result = DetectionObservation(frame, "model", 0, .8, [0, 0, 1, 1])
        self.assertIsNone(frame.frame_time)
        self.assertIsInstance(result.bbox_xyxy, tuple)
        self.assertNotIn("pixel", frame.__dataclass_fields__)
        with self.assertRaises(FrozenInstanceError):
            frame.frame_seq = 4
        with self.assertRaises(ValueError):
            DetectionObservation(frame, "model", 0, float("nan"), (0, 0, 1, 1))

    def test_run_uuid_changes_on_begin_and_not_stop(self):
        lifecycle = StreamLifecycle()
        self.assertIsNone(lifecycle.snapshot()["run_id"])
        first = lifecycle.begin()
        run_id = lifecycle.snapshot()["run_id"]
        lifecycle.finish_stop(first)
        self.assertEqual(run_id, lifecycle.snapshot()["run_id"])
        lifecycle.begin()
        self.assertNotEqual(run_id, lifecycle.snapshot()["run_id"])
        self.assertFalse(lifecycle.signal_frame(first, "det"))

    def test_all_skip_detection_only_and_disabled_input(self):
        manager = Mock()
        service = StreamApplicationService(lambda: manager, "server")
        request = CameraStart("camera", "rtsp://camera.invalid/live", scenarios=(("FALL", True),))
        service.start_camera(request)
        self.assertEqual(manager.apply_and_start.call_args.args[0]["scenarios"], "off")
        for scenarios in (None, (), (("UNKNOWN", False),)):
            service.start_camera(CameraStart("camera", "rtsp://camera.invalid/live", scenarios=scenarios))
        self.assertEqual(manager.apply_and_start.call_count, 4)

    def test_factory_does_not_mount_unapproved_web_wire(self):
        source = Path("app/main.py").read_text()
        ast.parse(source)
        self.assertNotIn("build_web_router", source)
        self.assertIn("include_router(streams_router)", source)
