"""Integration tests for Camera API endpoints.

Tests the full request/response cycle for /api/v1/printers/{id}/camera/ endpoints.
"""

from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from httpx import AsyncClient


class TestCameraAPI:
    """Integration tests for /api/v1/printers/{id}/camera/ endpoints."""

    # ========================================================================
    # Camera Stop Endpoint
    # ========================================================================

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_stop_camera_stream_get(self, async_client: AsyncClient, printer_factory):
        """Verify camera stop endpoint works with GET method."""
        printer = await printer_factory()

        response = await async_client.get(f"/api/v1/printers/{printer.id}/camera/stop")

        assert response.status_code == 200
        result = response.json()
        assert "stopped" in result
        assert isinstance(result["stopped"], int)

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_stop_camera_stream_post(self, async_client: AsyncClient, printer_factory):
        """Verify camera stop endpoint works with POST method (sendBeacon compatibility)."""
        printer = await printer_factory()

        response = await async_client.post(f"/api/v1/printers/{printer.id}/camera/stop")

        assert response.status_code == 200
        result = response.json()
        assert "stopped" in result
        assert isinstance(result["stopped"], int)

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_stop_camera_stream_no_active_streams(self, async_client: AsyncClient, printer_factory):
        """Verify stop returns 0 when no active streams exist."""
        printer = await printer_factory()

        response = await async_client.post(f"/api/v1/printers/{printer.id}/camera/stop")

        assert response.status_code == 200
        assert response.json()["stopped"] == 0

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_stop_camera_stream_with_active_stream(self, async_client: AsyncClient, printer_factory):
        """Verify stop terminates active streams for the printer."""
        printer = await printer_factory()

        # Mock an active stream — wait() must be AsyncMock since it's awaited
        mock_process = MagicMock()
        mock_process.returncode = None
        mock_process.pid = 99999
        mock_process.terminate = MagicMock()
        mock_process.wait = AsyncMock()

        with patch("backend.app.api.routes.camera._active_streams", {f"{printer.id}-abc123": mock_process}):
            response = await async_client.post(f"/api/v1/printers/{printer.id}/camera/stop")

        assert response.status_code == 200
        assert response.json()["stopped"] == 1
        mock_process.terminate.assert_called_once()

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_stop_camera_stream_only_stops_matching_printer(self, async_client: AsyncClient, printer_factory):
        """Verify stop only terminates streams for the specified printer."""
        printer1 = await printer_factory(name="Printer 1")
        printer2 = await printer_factory(name="Printer 2")

        # Mock active streams for both printers — wait() must be AsyncMock since it's awaited
        mock_process1 = MagicMock()
        mock_process1.returncode = None
        mock_process1.pid = 99998
        mock_process1.terminate = MagicMock()
        mock_process1.wait = AsyncMock()

        mock_process2 = MagicMock()
        mock_process2.returncode = None
        mock_process2.pid = 99997
        mock_process2.terminate = MagicMock()
        mock_process2.wait = AsyncMock()

        active_streams = {
            f"{printer1.id}-abc123": mock_process1,
            f"{printer2.id}-def456": mock_process2,
        }

        with patch("backend.app.api.routes.camera._active_streams", active_streams):
            response = await async_client.post(f"/api/v1/printers/{printer1.id}/camera/stop")

        assert response.status_code == 200
        assert response.json()["stopped"] == 1
        mock_process1.terminate.assert_called_once()
        mock_process2.terminate.assert_not_called()

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_stop_camera_stream_handles_fanout_stream_id(self, async_client: AsyncClient, printer_factory):
        """Stop must terminate streams keyed with the deterministic
        ``{printer_id}-fanout`` id used by the fan-out broadcaster (#1089).
        Regression guard against the prefix-match drifting away from the
        broadcaster's stream-id convention.
        """
        printer = await printer_factory()
        mock_process = MagicMock()
        mock_process.returncode = None
        mock_process.pid = 99996
        mock_process.terminate = MagicMock()
        mock_process.wait = AsyncMock()

        with patch(
            "backend.app.api.routes.camera._active_streams",
            {f"{printer.id}-fanout": mock_process},
        ):
            response = await async_client.post(f"/api/v1/printers/{printer.id}/camera/stop")

        assert response.status_code == 200
        assert response.json()["stopped"] == 1
        mock_process.terminate.assert_called_once()

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_stop_camera_stream_invokes_broadcaster_shutdown(self, async_client: AsyncClient, printer_factory):
        """Stop must call ``shutdown_broadcaster`` so subscribers wake up via
        the upstream-gone sentinel rather than stalling on the queue (#1089)."""
        printer = await printer_factory()

        with patch(
            "backend.app.api.routes.camera.shutdown_broadcaster",
            AsyncMock(return_value=False),
        ) as mock_shutdown:
            response = await async_client.post(f"/api/v1/printers/{printer.id}/camera/stop")

        assert response.status_code == 200
        mock_shutdown.assert_awaited_once_with(f"printer-{printer.id}")

    # ========================================================================
    # Camera Test Endpoint
    # ========================================================================

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_camera_test_printer_not_found(self, async_client: AsyncClient):
        """Verify 404 when testing camera for non-existent printer."""
        response = await async_client.get("/api/v1/printers/99999/camera/test")

        assert response.status_code == 404
        assert "not found" in response.json()["detail"].lower()

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_camera_test_success(self, async_client: AsyncClient, printer_factory):
        """Verify camera test returns success when camera is accessible."""
        printer = await printer_factory()

        with patch("backend.app.api.routes.camera.test_camera_connection", new_callable=AsyncMock) as mock_test:
            mock_test.return_value = {"success": True, "message": "Camera connected"}

            response = await async_client.get(f"/api/v1/printers/{printer.id}/camera/test")

        assert response.status_code == 200
        result = response.json()
        assert result["success"] is True

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_camera_test_failure(self, async_client: AsyncClient, printer_factory):
        """Verify camera test returns failure when camera is not accessible."""
        printer = await printer_factory()

        with patch("backend.app.api.routes.camera.test_camera_connection", new_callable=AsyncMock) as mock_test:
            mock_test.return_value = {"success": False, "message": "Connection timeout"}

            response = await async_client.get(f"/api/v1/printers/{printer.id}/camera/test")

        assert response.status_code == 200
        result = response.json()
        assert result["success"] is False

    # ========================================================================
    # Camera Diagnose Endpoint (#1395 follow-up)
    # ========================================================================

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_camera_diagnose_printer_not_found(self, async_client: AsyncClient):
        response = await async_client.post("/api/v1/printers/99999/camera/diagnose")
        assert response.status_code == 404

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_camera_diagnose_returns_structured_result(self, async_client: AsyncClient, printer_factory):
        """Endpoint returns the per-stage shape the frontend modal renders."""
        from backend.app.services.camera_diagnose import (
            CameraDiagnoseResult,
            CameraDiagnoseStage,
        )

        printer = await printer_factory()

        fake = CameraDiagnoseResult(
            printer_id=printer.id,
            protocol="rtsp",
            port=322,
            profile="P2S",
            overall_status="failed",
            stages=[
                CameraDiagnoseStage(name="tcp_reachable", status="ok", duration_ms=12),
                CameraDiagnoseStage(name="first_frame", status="failed", duration_ms=15123, code="no_frame"),
            ],
            summary_code="no_frame",
        )
        with patch(
            "backend.app.services.camera_diagnose.diagnose_camera",
            new_callable=AsyncMock,
            return_value=fake,
        ):
            response = await async_client.post(f"/api/v1/printers/{printer.id}/camera/diagnose")

        assert response.status_code == 200
        body = response.json()
        assert body["printer_id"] == printer.id
        assert body["protocol"] == "rtsp"
        assert body["profile"] == "P2S"
        assert body["overall_status"] == "failed"
        assert body["summary_code"] == "no_frame"
        assert [s["name"] for s in body["stages"]] == ["tcp_reachable", "first_frame"]
        assert body["stages"][1]["code"] == "no_frame"

    # ========================================================================
    # Camera Snapshot Endpoint
    # ========================================================================

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_camera_snapshot_printer_not_found(self, async_client: AsyncClient):
        """Verify 404 when capturing snapshot for non-existent printer."""
        response = await async_client.get("/api/v1/printers/99999/camera/snapshot")

        assert response.status_code == 404

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_camera_snapshot_success(self, async_client: AsyncClient, printer_factory):
        """Verify snapshot returns JPEG image when successful."""
        printer = await printer_factory()

        # Create a fake JPEG (starts with FFD8)
        fake_jpeg = b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00"

        with patch("backend.app.api.routes.camera.capture_camera_frame", new_callable=AsyncMock) as mock_capture:
            mock_capture.return_value = True

            # Mock the file read
            with patch("builtins.open", create=True) as mock_open:
                mock_open.return_value.__enter__.return_value.read.return_value = fake_jpeg

                with patch("pathlib.Path.exists", return_value=True), patch("pathlib.Path.unlink"):
                    _response = await async_client.get(f"/api/v1/printers/{printer.id}/camera/snapshot")

        # Note: The actual test might fail due to file operations, but this tests the endpoint structure
        # In production tests, we'd mock more comprehensively

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_camera_snapshot_failure(self, async_client: AsyncClient, printer_factory):
        """Verify 503 when camera capture fails."""
        printer = await printer_factory()

        with patch("backend.app.api.routes.camera.capture_camera_frame", new_callable=AsyncMock) as mock_capture:
            mock_capture.return_value = False

            with patch("pathlib.Path.exists", return_value=False), patch("pathlib.Path.unlink"):
                response = await async_client.get(f"/api/v1/printers/{printer.id}/camera/snapshot")

        assert response.status_code == 503
        assert "Failed to capture" in response.json()["detail"]

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_camera_snapshot_reuses_buffered_frame_when_stream_active(
        self, async_client: AsyncClient, printer_factory
    ):
        """#1271: /camera/snapshot must reuse the broadcaster's buffered frame
        when a live stream is running, instead of opening a second concurrent
        RTSP socket. On printers with strict single-connection enforcement (e.g.
        X2D firmware 01.01.00.00) opening a second socket kicks the live stream.
        """
        printer = await printer_factory()
        fake_jpeg = b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00"

        # Simulate a running broadcaster: one active stream entry + buffered frame.
        active_streams = {f"{printer.id}-fanout": MagicMock()}
        last_frames = {printer.id: fake_jpeg}

        with (
            patch("backend.app.api.routes.camera._active_streams", active_streams),
            patch("backend.app.api.routes.camera._last_frames", last_frames),
            patch("backend.app.api.routes.camera.capture_camera_frame", new_callable=AsyncMock) as mock_capture,
        ):
            response = await async_client.get(f"/api/v1/printers/{printer.id}/camera/snapshot")

        assert response.status_code == 200
        assert response.content == fake_jpeg
        # The fresh-capture path must NOT have been taken — that's the whole point.
        mock_capture.assert_not_called()

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_camera_snapshot_external_camera_success(self, async_client: AsyncClient, printer_factory):
        """Verify snapshot uses external camera when configured."""
        printer = await printer_factory(
            external_camera_enabled=True,
            external_camera_url="http://192.168.1.50/mjpeg",
            external_camera_type="mjpeg",
        )

        fake_jpeg = b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00"

        with patch(
            "backend.app.services.external_camera.capture_frame",
            new_callable=AsyncMock,
            return_value=fake_jpeg,
        ):
            response = await async_client.get(f"/api/v1/printers/{printer.id}/camera/snapshot")

        assert response.status_code == 200
        assert response.headers["content-type"] == "image/jpeg"
        assert response.content == fake_jpeg

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_camera_snapshot_external_camera_failure(self, async_client: AsyncClient, printer_factory):
        """Verify 503 when external camera capture fails."""
        printer = await printer_factory(
            external_camera_enabled=True,
            external_camera_url="http://192.168.1.50/mjpeg",
            external_camera_type="mjpeg",
        )

        with patch(
            "backend.app.services.external_camera.capture_frame",
            new_callable=AsyncMock,
            return_value=None,
        ):
            response = await async_client.get(f"/api/v1/printers/{printer.id}/camera/snapshot")

        assert response.status_code == 503
        assert "external camera" in response.json()["detail"].lower()

    # ========================================================================
    # Camera Stream Endpoint
    # ========================================================================

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_camera_stream_printer_not_found(self, async_client: AsyncClient):
        """Verify 404 when streaming camera for non-existent printer."""
        response = await async_client.get("/api/v1/printers/99999/camera/stream")

        assert response.status_code == 404

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_camera_stream_fps_validation(self, async_client: AsyncClient, printer_factory):
        """Verify FPS parameter is validated and clamped."""
        printer = await printer_factory()

        # FPS should be clamped between 1 and 30
        # Testing that the endpoint accepts various FPS values without error
        # (actual streaming would require mocking ffmpeg)

        with patch("backend.app.api.routes.camera.get_ffmpeg_path", return_value=None):
            # With no ffmpeg, stream should return error message but not crash
            response = await async_client.get(
                f"/api/v1/printers/{printer.id}/camera/stream",
                params={"fps": 100},  # Should be clamped to 30
            )
            # Response will be a streaming response with error
            assert response.status_code == 200

    # ========================================================================
    # Plate Detection Endpoints
    # ========================================================================

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_plate_detection_status_printer_not_found(self, async_client: AsyncClient):
        """Verify 404 when checking plate detection status for non-existent printer."""
        response = await async_client.get("/api/v1/printers/99999/camera/plate-detection/status")

        assert response.status_code == 404

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_plate_detection_status_opencv_not_available(self, async_client: AsyncClient, printer_factory):
        """Verify plate detection status returns unavailable when OpenCV not installed."""
        printer = await printer_factory()

        with patch("backend.app.services.plate_detection.OPENCV_AVAILABLE", False):
            response = await async_client.get(f"/api/v1/printers/{printer.id}/camera/plate-detection/status")

        assert response.status_code == 200
        result = response.json()
        assert result["available"] is False
        assert result["calibrated"] is False

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_plate_detection_status_success(self, async_client: AsyncClient, printer_factory):
        """Verify plate detection status returns correctly when OpenCV available."""
        printer = await printer_factory()

        # OpenCV is available in test environment, just check the response structure
        response = await async_client.get(f"/api/v1/printers/{printer.id}/camera/plate-detection/status")

        assert response.status_code == 200
        result = response.json()
        assert "available" in result
        assert "calibrated" in result

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_check_plate_empty_printer_not_found(self, async_client: AsyncClient):
        """Verify 404 when checking plate for non-existent printer."""
        response = await async_client.get("/api/v1/printers/99999/camera/check-plate")

        assert response.status_code == 404

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_check_plate_empty_success_structure(self, async_client: AsyncClient, printer_factory):
        """Verify check plate returns proper structure when OpenCV available."""
        printer = await printer_factory()

        # Mock PlateDetectionResult to avoid camera timeout
        mock_result = MagicMock()
        mock_result.is_empty = True
        mock_result.confidence = 0.95
        mock_result.difference_percent = 0.5
        mock_result.message = "Plate appears empty"
        mock_result.needs_calibration = False
        mock_result.debug_image = None
        mock_result.to_dict.return_value = {
            "is_empty": True,
            "confidence": 0.95,
            "difference_percent": 0.5,
            "message": "Plate appears empty",
            "has_debug_image": False,
            "needs_calibration": False,
        }

        # Mock PlateDetector for reference count
        mock_detector = MagicMock()
        mock_detector.get_calibration_count.return_value = 0
        mock_detector.MAX_REFERENCES = 5

        with (
            patch("backend.app.services.plate_detection.is_plate_detection_available", return_value=True),
            patch("backend.app.services.plate_detection.check_plate_empty", new_callable=AsyncMock) as mock_check,
            patch("backend.app.services.plate_detection.PlateDetector", return_value=mock_detector),
        ):
            mock_check.return_value = mock_result
            response = await async_client.get(f"/api/v1/printers/{printer.id}/camera/check-plate")

        assert response.status_code == 200
        result = response.json()
        assert "is_empty" in result
        assert "confidence" in result
        assert "message" in result

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_calibrate_plate_printer_not_found(self, async_client: AsyncClient):
        """Verify 404 when calibrating plate for non-existent printer."""
        response = await async_client.post("/api/v1/printers/99999/camera/plate-detection/calibrate")

        assert response.status_code == 404

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_calibrate_plate_success_structure(self, async_client: AsyncClient, printer_factory):
        """Verify calibrate endpoint responds with proper structure."""
        printer = await printer_factory()

        # Mock calibrate_plate at the source module to avoid camera timeout
        with (
            patch("backend.app.services.plate_detection.is_plate_detection_available", return_value=True),
            patch("backend.app.services.plate_detection.calibrate_plate", new_callable=AsyncMock) as mock_calibrate,
        ):
            mock_calibrate.return_value = (True, "Calibration saved (1/5 references)", 0)
            response = await async_client.post(f"/api/v1/printers/{printer.id}/camera/plate-detection/calibrate")

        assert response.status_code == 200
        result = response.json()
        assert result["success"] is True
        assert "index" in result

    # ------------------------------------------------------------------
    # Regression: #1359 — the manual UI check/calibrate routes must derive
    # use_external from the printer's external_camera_enabled setting when
    # the caller omits the flag. Otherwise the UI calibrates against the
    # built-in camera while the runtime auto-check at print start uses the
    # external one, producing a permanent "build plate not empty".
    # ------------------------------------------------------------------

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_check_plate_defaults_use_external_when_external_camera_enabled(
        self, async_client: AsyncClient, printer_factory
    ):
        """Omitting use_external on a printer with external camera enabled
        must call the service with use_external=True."""
        printer = await printer_factory(
            external_camera_enabled=True,
            external_camera_url="http://192.168.1.50/mjpeg",
            external_camera_type="mjpeg",
        )

        mock_result = MagicMock()
        mock_result.to_dict.return_value = {
            "is_empty": True,
            "confidence": 0.95,
            "difference_percent": 0.5,
            "message": "Plate appears empty",
            "has_debug_image": False,
            "needs_calibration": False,
        }
        mock_result.debug_image = None

        mock_detector = MagicMock()
        mock_detector.get_calibration_count.return_value = 0
        mock_detector.MAX_REFERENCES = 5

        with (
            patch("backend.app.services.plate_detection.is_plate_detection_available", return_value=True),
            patch("backend.app.services.plate_detection.check_plate_empty", new_callable=AsyncMock) as mock_check,
            patch("backend.app.services.plate_detection.PlateDetector", return_value=mock_detector),
        ):
            mock_check.return_value = mock_result
            response = await async_client.get(f"/api/v1/printers/{printer.id}/camera/check-plate")

        assert response.status_code == 200
        assert mock_check.await_args.kwargs["use_external"] is True

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_check_plate_defaults_use_external_false_when_external_camera_disabled(
        self, async_client: AsyncClient, printer_factory
    ):
        """Omitting use_external on a printer without an external camera
        must call the service with use_external=False (built-in)."""
        printer = await printer_factory()  # external_camera_enabled defaults to False

        mock_result = MagicMock()
        mock_result.to_dict.return_value = {
            "is_empty": True,
            "confidence": 0.95,
            "difference_percent": 0.5,
            "message": "Plate appears empty",
            "has_debug_image": False,
            "needs_calibration": False,
        }
        mock_result.debug_image = None

        mock_detector = MagicMock()
        mock_detector.get_calibration_count.return_value = 0
        mock_detector.MAX_REFERENCES = 5

        with (
            patch("backend.app.services.plate_detection.is_plate_detection_available", return_value=True),
            patch("backend.app.services.plate_detection.check_plate_empty", new_callable=AsyncMock) as mock_check,
            patch("backend.app.services.plate_detection.PlateDetector", return_value=mock_detector),
        ):
            mock_check.return_value = mock_result
            response = await async_client.get(f"/api/v1/printers/{printer.id}/camera/check-plate")

        assert response.status_code == 200
        assert mock_check.await_args.kwargs["use_external"] is False

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_calibrate_plate_defaults_use_external_when_external_camera_enabled(
        self, async_client: AsyncClient, printer_factory
    ):
        """Calibrating with use_external omitted on an external-camera-enabled
        printer captures the reference from the external camera — matching
        what the runtime check at print start will compare against (#1359)."""
        printer = await printer_factory(
            external_camera_enabled=True,
            external_camera_url="http://192.168.1.50/mjpeg",
            external_camera_type="mjpeg",
        )

        with (
            patch("backend.app.services.plate_detection.is_plate_detection_available", return_value=True),
            patch("backend.app.services.plate_detection.calibrate_plate", new_callable=AsyncMock) as mock_calibrate,
        ):
            mock_calibrate.return_value = (True, "Calibration saved (1/5 references)", 0)
            response = await async_client.post(f"/api/v1/printers/{printer.id}/camera/plate-detection/calibrate")

        assert response.status_code == 200
        assert mock_calibrate.await_args.kwargs["use_external"] is True

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_calibrate_plate_explicit_use_external_false_overrides_default(
        self, async_client: AsyncClient, printer_factory
    ):
        """An explicit use_external=false from the caller still wins even
        when the printer has an external camera configured, so power users
        can force a built-in-camera reference if they ever need to."""
        printer = await printer_factory(
            external_camera_enabled=True,
            external_camera_url="http://192.168.1.50/mjpeg",
            external_camera_type="mjpeg",
        )

        with (
            patch("backend.app.services.plate_detection.is_plate_detection_available", return_value=True),
            patch("backend.app.services.plate_detection.calibrate_plate", new_callable=AsyncMock) as mock_calibrate,
        ):
            mock_calibrate.return_value = (True, "Calibration saved (1/5 references)", 0)
            response = await async_client.post(
                f"/api/v1/printers/{printer.id}/camera/plate-detection/calibrate?use_external=false"
            )

        assert response.status_code == 200
        assert mock_calibrate.await_args.kwargs["use_external"] is False

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_delete_calibration_printer_not_found(self, async_client: AsyncClient):
        """Verify 404 when deleting calibration for non-existent printer."""
        response = await async_client.delete("/api/v1/printers/99999/camera/plate-detection/calibrate")

        assert response.status_code == 404

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_delete_calibration_success(self, async_client: AsyncClient, printer_factory):
        """Verify delete calibration returns proper structure."""
        printer = await printer_factory()

        with patch("backend.app.services.plate_detection.is_plate_detection_available", return_value=True):
            response = await async_client.delete(f"/api/v1/printers/{printer.id}/camera/plate-detection/calibrate")

        assert response.status_code == 200
        result = response.json()
        assert "success" in result
        assert "message" in result

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_get_references_printer_not_found(self, async_client: AsyncClient):
        """Verify 404 when getting references for non-existent printer."""
        response = await async_client.get("/api/v1/printers/99999/camera/plate-detection/references")

        assert response.status_code == 404

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_get_references_opencv_not_available(self, async_client: AsyncClient, printer_factory):
        """Verify get references returns unavailable when OpenCV not installed."""
        printer = await printer_factory()

        with patch("backend.app.services.plate_detection.OPENCV_AVAILABLE", False):
            response = await async_client.get(f"/api/v1/printers/{printer.id}/camera/plate-detection/references")

        assert response.status_code == 503

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_get_references_success(self, async_client: AsyncClient, printer_factory):
        """Verify get references returns proper structure."""
        printer = await printer_factory()

        # Mock OpenCV availability and PlateDetector
        mock_detector = MagicMock()
        mock_detector.get_references.return_value = []
        mock_detector.MAX_REFERENCES = 5

        with (
            patch("backend.app.services.plate_detection.is_plate_detection_available", return_value=True),
            patch("backend.app.services.plate_detection.PlateDetector", return_value=mock_detector),
        ):
            response = await async_client.get(f"/api/v1/printers/{printer.id}/camera/plate-detection/references")

        assert response.status_code == 200
        result = response.json()
        assert "references" in result
        assert "max_references" in result
        assert isinstance(result["references"], list)

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_update_reference_label_printer_not_found(self, async_client: AsyncClient):
        """Verify 404 when updating reference label for non-existent printer."""
        response = await async_client.put(
            "/api/v1/printers/99999/camera/plate-detection/references/0", params={"label": "New Label"}
        )

        assert response.status_code == 404

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_delete_reference_printer_not_found(self, async_client: AsyncClient):
        """Verify 404 when deleting reference for non-existent printer."""
        response = await async_client.delete("/api/v1/printers/99999/camera/plate-detection/references/0")

        assert response.status_code == 404

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_get_reference_thumbnail_printer_not_found(self, async_client: AsyncClient):
        """Verify 404 when getting reference thumbnail for non-existent printer."""
        response = await async_client.get("/api/v1/printers/99999/camera/plate-detection/references/0/thumbnail")

        assert response.status_code == 404

    # ========================================================================
    # USB Camera Endpoint
    # ========================================================================

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_list_usb_cameras_returns_list(self, async_client: AsyncClient):
        """Verify USB cameras endpoint returns a list of cameras."""
        response = await async_client.get("/api/v1/printers/usb-cameras")

        assert response.status_code == 200
        result = response.json()
        assert "cameras" in result
        assert isinstance(result["cameras"], list)

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_list_usb_cameras_structure(self, async_client: AsyncClient):
        """Verify USB cameras endpoint returns proper structure for each camera."""
        with patch("backend.app.services.external_camera.list_usb_cameras") as mock_list:
            mock_list.return_value = [
                {"device": "/dev/video0", "name": "Logitech Webcam C920", "index": 0},
                {"device": "/dev/video2", "name": "USB Camera", "index": 2},
            ]

            response = await async_client.get("/api/v1/printers/usb-cameras")

        assert response.status_code == 200
        result = response.json()
        assert len(result["cameras"]) == 2
        assert result["cameras"][0]["device"] == "/dev/video0"
        assert result["cameras"][0]["name"] == "Logitech Webcam C920"
        assert result["cameras"][1]["device"] == "/dev/video2"

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_list_usb_cameras_empty_on_non_linux(self, async_client: AsyncClient):
        """Verify USB cameras endpoint returns empty list on non-Linux systems."""
        with patch("backend.app.services.external_camera.list_usb_cameras") as mock_list:
            # Simulate non-Linux system (no /dev/video* devices)
            mock_list.return_value = []

            response = await async_client.get("/api/v1/printers/usb-cameras")

        assert response.status_code == 200
        result = response.json()
        assert result["cameras"] == []
