from __future__ import annotations

import asyncio
import stat
import threading
import time
from pathlib import Path

import numpy as np

from k1link.compute.live_perception import (
    LivePerceptionIngress,
    encode_live_perception_result,
)
from k1link.device_plugins.xgrids_k1.live_perception_shadow import (
    build_live_perception_shadow_router,
    ensure_live_shadow_token,
)


def test_shadow_token_is_stable_and_private(tmp_path: Path) -> None:
    path, token = ensure_live_shadow_token(tmp_path)
    repeated_path, repeated_token = ensure_live_shadow_token(tmp_path)

    assert repeated_path == path
    assert repeated_token == token
    assert stat.S_IMODE(path.stat().st_mode) == 0o600
    assert stat.S_IMODE(path.parent.stat().st_mode) == 0o700


def test_shadow_router_exposes_only_the_exclusive_binary_stream() -> None:
    ingress = LivePerceptionIngress()
    router = build_live_perception_shadow_router(
        ingress,
        "test-plugin",
        bearer_token="x" * 43,
    )
    assert len(router.routes) == 1
    assert router.routes[0].path == (
        "/api/v1/device-plugins/test-plugin/live-perception-shadow"
    )


def test_shadow_router_accepts_only_validated_diagnostic_results_back() -> None:
    ingress = LivePerceptionIngress()
    received: list[bytes] = []
    published = threading.Event()

    def receive_result(payload: bytes) -> bool:
        received.append(payload)
        published.set()
        return True

    router = build_live_perception_shadow_router(
        ingress,
        "test-plugin",
        bearer_token="x" * 43,
        result_receiver=receive_result,
    )
    ingress.begin_session("session-1")
    encoded = encode_live_perception_result(
        frame_index=0,
        source_frame_index=0,
        session_seconds=0.0,
        captured_at_epoch_ns=1,
        image_jpeg=bytes.fromhex("ffd878ffd9"),
        segmentation_mask=np.zeros((600, 800), dtype=np.uint8),
        objects=[],
        delivery={"health": "healthy"},
    )

    class FakeWebSocket:
        def __init__(self) -> None:
            self.headers = {"authorization": f"Bearer {'x' * 43}"}
            self.messages = [
                {"type": "websocket.receive", "bytes": encoded},
                {"type": "websocket.disconnect"},
            ]
            self.sent: list[bytes] = []
            self.accepted = False

        async def accept(self) -> None:
            self.accepted = True

        async def receive(self) -> dict[str, object]:
            await asyncio.sleep(0)
            return self.messages.pop(0)

        async def send_bytes(self, payload: bytes) -> None:
            self.sent.append(payload)

        async def close(self, **_: object) -> None:
            return

    websocket = FakeWebSocket()
    asyncio.run(router.routes[0].endpoint(websocket))  # type: ignore[attr-defined]

    assert websocket.accepted is True
    assert published.is_set()
    assert received == [encoded]


def test_shadow_router_does_not_discard_ingress_while_receiving_results() -> None:
    class SlowIngress(LivePerceptionIngress):
        def take_next(
            self,
            consumer_id: str,
            *,
            timeout: float | None = None,
        ):
            time.sleep(0.01)
            return super().take_next(consumer_id, timeout=timeout)

    ingress = SlowIngress()
    received: list[bytes] = []
    router = build_live_perception_shadow_router(
        ingress,
        "test-plugin",
        bearer_token="x" * 43,
        result_receiver=lambda payload: not received.append(payload),
    )
    ingress.begin_session("session-1")
    for modality, sequence in (
        ("camera-init", 0),
        ("camera-frame", 1),
        ("lidar", 1),
        ("pose", 1),
    ):
        assert ingress.publish(
            modality=modality,
            source_id="sensor.camera.right",
            source_sequence=sequence,
            captured_at_epoch_ns=sequence,
            received_monotonic_ns=sequence,
            payload=modality.encode(),
        )
    ingress.end_session("session-1")
    expected_events = sum(
        int(queue["depth"]) for queue in ingress.snapshot()["queues"].values()
    )

    class DuplexFakeWebSocket:
        def __init__(self) -> None:
            self.headers = {"authorization": f"Bearer {'x' * 43}"}
            self.results_remaining = 8
            self.sent: list[bytes] = []

        async def accept(self) -> None:
            return

        async def receive(self) -> dict[str, object]:
            if self.results_remaining:
                self.results_remaining -= 1
                await asyncio.sleep(0)
                return {"type": "websocket.receive", "bytes": b"result"}
            deadline = asyncio.get_running_loop().time() + 1
            while len(self.sent) < expected_events:
                if asyncio.get_running_loop().time() >= deadline:
                    break
                await asyncio.sleep(0.005)
            return {"type": "websocket.disconnect"}

        async def send_bytes(self, payload: bytes) -> None:
            self.sent.append(payload)

        async def close(self, **_: object) -> None:
            return

    websocket = DuplexFakeWebSocket()
    asyncio.run(router.routes[0].endpoint(websocket))  # type: ignore[attr-defined]

    assert len(received) == 8
    assert len(websocket.sent) == expected_events
    snapshot = ingress.snapshot()
    assert sum(
        int(queue["consumed"]) for queue in snapshot["queues"].values()
    ) == expected_events
