from __future__ import annotations

import hashlib
import json
import re
import struct
import subprocess
import sys
import threading
from pathlib import Path

import pytest

import k1link.device_plugins.xgrids_k1.rrd_export as export_module
import k1link.viewer.recorded as viewer_recorded_module
from k1link.device_plugins.xgrids_k1.mqtt.capture import (
    CAPTURE_CLOCK_FILENAME,
    FRAME_HEADER,
    RAW_MAGIC,
)
from k1link.device_plugins.xgrids_k1.rrd_export import (
    RECORDED_CAMERA_VIEW_ID,
    RECORDED_METRICS_VIEW_ID,
    RECORDED_PERCEPTION_3D_VIEW_ID,
    RECORDED_POINTS_VISUALIZER_ID,
    RECORDED_ROOT_CONTAINER_ID,
    RECORDED_SPATIAL_VIEW_ID,
    RECORDED_VIEW_POINT_DECIMATION_THRESHOLD,
    RECORDED_VIEW_POINT_FRAME_STRIDE,
    RECORDED_VIEW_POINT_STRIDE,
    SESSION_TIMELINE,
    RrdExportCancelled,
    RrdExportError,
    _recorded_blueprint,
    _recorded_view_points,
    _should_publish_recorded_point_frame,
    export_k1mqtt_to_rrd,
    recorded_blueprint_rrd,
)
from k1link.viewer.recorded import (
    recorded_blueprint as viewer_recorded_blueprint,
)
from k1link.viewer.recorded import (
    recorded_blueprint_rrd as viewer_recorded_blueprint_rrd,
)
from k1link.viewer.rerun_bridge import RerunSceneSettings


def _point_payload(x: float) -> bytes:
    return struct.pack("<III", 16, 0, 0) + struct.pack(
        "<fffBBBB",
        x,
        -2.0,
        3.0,
        10,
        20,
        30,
        40,
    )


def test_recorded_operator_projection_thins_only_dense_point_batches() -> None:
    assert RECORDED_VIEW_POINT_FRAME_STRIDE == 5
    count = RECORDED_VIEW_POINT_DECIMATION_THRESHOLD + 3
    positions = export_module.np.arange(
        count * 3,
        dtype=export_module.np.float32,
    ).reshape((-1, 3))
    intensities = export_module.np.arange(count, dtype=export_module.np.uint8)
    rgb = export_module.np.arange(count * 3, dtype=export_module.np.uint8).reshape((-1, 3))

    projected_positions, projected_intensities, projected_rgb = _recorded_view_points(
        positions,
        intensities,
        rgb,
    )

    assert projected_rgb is not None
    assert export_module.np.array_equal(
        projected_positions,
        positions[::RECORDED_VIEW_POINT_STRIDE],
    )
    assert export_module.np.array_equal(
        projected_intensities,
        intensities[::RECORDED_VIEW_POINT_STRIDE],
    )
    assert export_module.np.array_equal(projected_rgb, rgb[::RECORDED_VIEW_POINT_STRIDE])

    small_positions = positions[:RECORDED_VIEW_POINT_DECIMATION_THRESHOLD]
    small_intensities = intensities[:RECORDED_VIEW_POINT_DECIMATION_THRESHOLD]
    same_positions, same_intensities, no_rgb = _recorded_view_points(
        small_positions,
        small_intensities,
        None,
    )
    assert same_positions is small_positions
    assert same_intensities is small_intensities
    assert no_rgb is None


def test_recorded_operator_projection_uses_stable_two_hz_point_cadence() -> None:
    published = [
        frame_number
        for frame_number in range(1, 13)
        if _should_publish_recorded_point_frame(frame_number)
    ]

    assert published == [1, 6, 11]
    with pytest.raises(ValueError, match="positive"):
        _should_publish_recorded_point_frame(0)


def _pose_payload(x: float) -> bytes:
    return struct.pack("<ffffffff", x, 2.0, 3.0, 99.0, 1.0, 0.0, 0.0, 0.0)


def _varint(value: int) -> bytes:
    encoded = bytearray()
    while value > 0x7F:
        encoded.append((value & 0x7F) | 0x80)
        value >>= 7
    encoded.append(value)
    return bytes(encoded)


def _proto_key(number: int, wire_type: int) -> bytes:
    return _varint((number << 3) | wire_type)


def _proto_uint(number: int, value: int) -> bytes:
    return _proto_key(number, 0) + _varint(value)


def _proto_bytes(number: int, value: bytes) -> bytes:
    return _proto_key(number, 2) + _varint(len(value)) + value


def _proto_float32(number: int, value: float) -> bytes:
    return _proto_key(number, 5) + struct.pack("<f", value)


def _modeling_payload(*, distance: float, speed: float, scan_ticks: int) -> bytes:
    status = _proto_float32(1, distance) + _proto_float32(2, speed) + _proto_uint(3, scan_ticks)
    return _proto_uint(2, 0) + _proto_bytes(3, status)


def _write_capture(
    directory: Path,
    frames: list[tuple[str, bytes, int]],
    *,
    capture_clock_offsets_ns: tuple[int, int] | None = None,
) -> Path:
    capture = directory / "mqtt.raw.k1mqtt"
    raw = bytearray(RAW_MAGIC)
    metadata: list[dict[str, object]] = []
    epoch_origin_ns = 1_784_124_315_000_000_000
    monotonic_origin_ns = 9_000_000_000
    for sequence, (topic, payload, session_time_ns) in enumerate(frames, start=1):
        topic_raw = topic.encode("utf-8")
        raw.extend(FRAME_HEADER.pack(len(topic_raw), len(payload)))
        raw.extend(topic_raw)
        raw.extend(payload)
        metadata.append(
            {
                "record_type": "message",
                "sequence": sequence,
                "received_at_epoch_ns": epoch_origin_ns + session_time_ns,
                "received_monotonic_ns": monotonic_origin_ns + session_time_ns,
            }
        )
    capture.write_bytes(raw)
    (directory / "mqtt.metadata.jsonl").write_text(
        "".join(json.dumps(item) + "\n" for item in metadata),
        encoding="utf-8",
    )
    if capture_clock_offsets_ns is not None:
        started_offset_ns, completed_offset_ns = capture_clock_offsets_ns
        (directory / "mqtt.timeline.origin.json").write_text(
            json.dumps(
                {
                    "schema_version": 1,
                    "started_at_epoch_ns": epoch_origin_ns + started_offset_ns,
                    "started_monotonic_ns": monotonic_origin_ns + started_offset_ns,
                },
                separators=(",", ":"),
            )
            + "\n",
            encoding="utf-8",
        )
        (directory / CAPTURE_CLOCK_FILENAME).write_text(
            json.dumps(
                {
                    "schema_version": 1,
                    "started_at_epoch_ns": epoch_origin_ns + started_offset_ns,
                    "started_monotonic_ns": monotonic_origin_ns + started_offset_ns,
                    "completed_at_epoch_ns": epoch_origin_ns + completed_offset_ns,
                    "completed_monotonic_ns": monotonic_origin_ns + completed_offset_ns,
                },
                separators=(",", ":"),
            )
            + "\n",
            encoding="utf-8",
        )
    return capture


def _sha256(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def _print_rrd_entity(path: Path, entity: str) -> str:
    completed = subprocess.run(
        [
            sys.executable,
            "-m",
            "rerun_cli",
            "rrd",
            "print",
            "-vvv",
            "--entity",
            entity,
            str(path),
        ],
        check=True,
        capture_output=True,
        text=True,
    )
    return completed.stdout


def test_recorded_blueprint_accumulates_point_frames_on_session_timeline() -> None:
    blueprint = _recorded_blueprint(
        RerunSceneSettings(
            point_size=7.5,
            palette="custom",
            custom_color="#112233",
            accumulation_seconds=18.5,
            show_points=False,
            show_trajectory=True,
            show_grid=False,
        )
    )
    spatial_view = blueprint.root_container.contents[0]
    visible_ranges = spatial_view.properties["VisibleTimeRanges"]
    line_grid = spatial_view.properties["LineGrid3D"]

    assert visible_ranges.ranges.as_arrow_array().to_pylist() == []
    assert line_grid.visible.as_arrow_array().to_pylist() == [False]
    point_behavior, point_visualizer, point_time_ranges = spatial_view.visualizer_overrides[
        "/world/points"
    ]
    trajectory_behavior, trajectory_time_ranges = spatial_view.visualizer_overrides[
        "/world/trajectory"
    ]
    expected_accumulation = [
        {
            "timeline": SESSION_TIMELINE,
            "range": {
                "start": -18_500_000_000,
                "end": 0,
            },
        }
    ]
    assert point_time_ranges.ranges.as_arrow_array().to_pylist() == expected_accumulation
    assert trajectory_time_ranges.ranges.as_arrow_array().to_pylist() == expected_accumulation
    perception_behavior = spatial_view.visualizer_overrides["/world/perception"]
    assert point_behavior.visible.as_arrow_array().to_pylist() == [False]
    assert trajectory_behavior.visible.as_arrow_array().to_pylist() == [True]
    assert perception_behavior.visible.as_arrow_array().to_pylist() == [False]
    perception_3d_view = blueprint.root_container.contents[2]
    assert perception_3d_view.origin == "/world"
    assert "VisibleTimeRanges" not in perception_3d_view.properties
    assert perception_3d_view.visualizer_overrides[
        "/world/perception"
    ].visible.as_arrow_array().to_pylist() == [True]
    assert perception_3d_view.visualizer_overrides[
        "/world/perception/lidar"
    ].visible.as_arrow_array().to_pylist() == [False]
    point_overrides = {
        str(batch.component_descriptor()): batch.as_arrow_array().to_pylist()
        for batch in point_visualizer.overrides
    }
    assert point_overrides == {
        "Points3D:colors": [0x112233FF],
        # Negative values are Rerun's encoding for screen-space UI points.
        "Points3D:radii": [-7.5],
    }


def test_zero_accumulation_uses_latest_frame_instead_of_empty_time_range() -> None:
    blueprint = _recorded_blueprint(RerunSceneSettings(accumulation_seconds=0.0, show_grid=True))
    spatial_view = blueprint.root_container.contents[0]

    assert spatial_view.properties["VisibleTimeRanges"].ranges.as_arrow_array().to_pylist() == []
    point_behavior, point_visualizer = spatial_view.visualizer_overrides["/world/points"]
    (trajectory_behavior,) = spatial_view.visualizer_overrides["/world/trajectory"]
    assert point_behavior.visible.as_arrow_array().to_pylist() == [True]
    assert trajectory_behavior.visible.as_arrow_array().to_pylist() == [True]
    point_components = {str(batch.component_descriptor()) for batch in point_visualizer.overrides}
    assert point_components == {"Points3D:radii"}


def test_cuboids_are_overlaid_on_the_stable_spatial_view_without_unifying_video() -> None:
    baseline = viewer_recorded_blueprint(
        RerunSceneSettings(accumulation_seconds=12.0),
        include_initial_playback_state=False,
        unified_perception=False,
        show_cuboids_3d=False,
    )
    blueprint = viewer_recorded_blueprint(
        RerunSceneSettings(accumulation_seconds=12.0),
        include_initial_playback_state=False,
        unified_perception=False,
        show_cuboids_3d=True,
    )
    spatial_view = blueprint.root_container.contents[0]

    assert spatial_view.id == baseline.root_container.contents[0].id
    assert spatial_view.visualizer_overrides[
        "/world/perception"
    ].visible.as_arrow_array().to_pylist() == [True]
    assert spatial_view.visualizer_overrides[
        "/world/perception/lidar"
    ].visible.as_arrow_array().to_pylist() == [False]
    assert spatial_view.visualizer_overrides[
        "/world/perception/support"
    ].visible.as_arrow_array().to_pylist() == [False]
    assert spatial_view.visualizer_overrides[
        "/world/perception/semantic_points"
    ].visible.as_arrow_array().to_pylist() == [False]
    assert spatial_view.visualizer_overrides[
        "/world/perception/boxes3d"
    ].visible.as_arrow_array().to_pylist() == [True]


def test_dynamic_blueprint_reuses_scene_ids_without_playback_mutation() -> None:
    first = _recorded_blueprint(
        RerunSceneSettings(show_points=False, show_trajectory=True),
        include_initial_playback_state=False,
    )
    second = _recorded_blueprint(
        RerunSceneSettings(show_points=True, show_trajectory=False),
        include_initial_playback_state=False,
    )

    assert not hasattr(first, "time_panel")
    assert not hasattr(second, "time_panel")
    assert first.root_container.id == second.root_container.id == RECORDED_ROOT_CONTAINER_ID
    first_view = first.root_container.contents[0]
    second_view = second.root_container.contents[0]
    assert first_view.id == second_view.id == RECORDED_SPATIAL_VIEW_ID
    assert first.root_container.contents[1].id == RECORDED_CAMERA_VIEW_ID
    assert second.root_container.contents[1].id == RECORDED_CAMERA_VIEW_ID
    assert first.root_container.contents[2].id == RECORDED_PERCEPTION_3D_VIEW_ID
    assert second.root_container.contents[2].id == RECORDED_PERCEPTION_3D_VIEW_ID
    assert first.root_container.contents[3].id == RECORDED_METRICS_VIEW_ID
    assert second.root_container.contents[3].id == RECORDED_METRICS_VIEW_ID

    first_point_behavior, first_point_visualizer, first_point_time_ranges = (
        first_view.visualizer_overrides["/world/points"]
    )
    second_point_behavior, second_point_visualizer, second_point_time_ranges = (
        second_view.visualizer_overrides["/world/points"]
    )
    first_trajectory, first_trajectory_time_ranges = first_view.visualizer_overrides[
        "/world/trajectory"
    ]
    second_trajectory, second_trajectory_time_ranges = second_view.visualizer_overrides[
        "/world/trajectory"
    ]
    assert first_point_visualizer.id == RECORDED_POINTS_VISUALIZER_ID
    assert second_point_visualizer.id == RECORDED_POINTS_VISUALIZER_ID
    assert first_point_behavior.visible.as_arrow_array().to_pylist() == [False]
    assert second_point_behavior.visible.as_arrow_array().to_pylist() == [True]
    assert first_trajectory.visible.as_arrow_array().to_pylist() == [True]
    assert second_trajectory.visible.as_arrow_array().to_pylist() == [False]
    assert (
        first_point_time_ranges.ranges.as_arrow_array().to_pylist()
        == first_trajectory_time_ranges.ranges.as_arrow_array().to_pylist()
    )
    assert (
        second_point_time_ranges.ranges.as_arrow_array().to_pylist()
        == second_trajectory_time_ranges.ranges.as_arrow_array().to_pylist()
    )

    payload = recorded_blueprint_rrd(
        RerunSceneSettings(show_points=False, show_trajectory=False),
        recording_id="stable-recording",
    )
    assert b"PlayState" not in payload
    assert b"play_state" not in payload
    assert str(RECORDED_ROOT_CONTAINER_ID).encode() in payload
    assert str(RECORDED_SPATIAL_VIEW_ID).encode() in payload
    assert str(RECORDED_POINTS_VISUALIZER_ID).encode() in payload


def test_viewer_blueprint_reset_is_bounded_and_recreates_render_views() -> None:
    initial = viewer_recorded_blueprint(
        RerunSceneSettings(accumulation_seconds=12.0),
        include_initial_playback_state=False,
        active_view="perception",
        view_reset_generation=0,
    )
    reset = viewer_recorded_blueprint(
        RerunSceneSettings(accumulation_seconds=12.0),
        include_initial_playback_state=False,
        active_view="perception",
        view_reset_generation=1,
    )
    restored = viewer_recorded_blueprint(
        RerunSceneSettings(accumulation_seconds=12.0),
        include_initial_playback_state=False,
        active_view="perception",
        view_reset_generation=0,
    )

    assert initial.root_container.id == restored.root_container.id
    assert reset.root_container.id != initial.root_container.id
    assert initial.root_container.contents[0].id != reset.root_container.contents[0].id
    assert restored.root_container.contents[0].id == initial.root_container.contents[0].id
    assert initial.root_container.active_tab == 1
    assert reset.root_container.active_tab == 1
    assert initial.root_container.contents[1].id != reset.root_container.contents[1].id
    assert initial.root_container.contents[2].id != reset.root_container.contents[2].id
    assert restored.root_container.contents[1].id == initial.root_container.contents[1].id
    assert restored.root_container.contents[2].id == initial.root_container.contents[2].id
    cuboids = viewer_recorded_blueprint(
        RerunSceneSettings(accumulation_seconds=12.0),
        include_initial_playback_state=False,
        active_view="perception3d",
    )
    assert cuboids.root_container.active_tab == 2
    assert cuboids.root_container.id != initial.root_container.id
    cuboid_view = cuboids.root_container.contents[2]
    assert cuboid_view.origin == "/world"
    assert "VisibleTimeRanges" not in cuboid_view.properties
    cuboid_points, cuboid_point_visualizer = cuboid_view.visualizer_overrides["/world/points"]
    cuboid_perception = cuboid_view.visualizer_overrides["/world/perception"]
    cuboid_diagnostic_lidar = cuboid_view.visualizer_overrides["/world/perception/lidar"]
    assert cuboid_points.visible.as_arrow_array().to_pylist() == [True]
    assert (
        cuboid_point_visualizer.id
        != initial.root_container.contents[0].visualizer_overrides["/world/points"][1].id
    )
    assert cuboid_perception.visible.as_arrow_array().to_pylist() == [True]
    assert cuboid_diagnostic_lidar.visible.as_arrow_array().to_pylist() == [False]
    perception_behavior = initial.root_container.contents[0].visualizer_overrides[
        "/world/perception"
    ]
    assert perception_behavior.visible.as_arrow_array().to_pylist() == [False]


def test_recorded_follow_mode_tracks_sensor_pose_with_the_same_orbital_eye() -> None:
    normal = viewer_recorded_blueprint(
        RerunSceneSettings(),
        include_initial_playback_state=False,
    )
    followed = viewer_recorded_blueprint(
        RerunSceneSettings(),
        include_initial_playback_state=False,
        follow_trajectory=True,
    )
    followed_again = viewer_recorded_blueprint(
        RerunSceneSettings(show_grid=False),
        include_initial_playback_state=False,
        follow_trajectory=True,
    )
    layer_only_update = viewer_recorded_blueprint(
        RerunSceneSettings(show_grid=False),
        include_initial_playback_state=False,
        follow_trajectory=True,
        update_eye_controls=False,
    )

    normal_view = normal.root_container.contents[0]
    followed_view = followed.root_container.contents[0]
    normal_eye = normal_view.properties["EyeControls3D"]
    followed_eye = followed_view.properties["EyeControls3D"]
    normal_components = {
        str(batch.component_descriptor()): batch.as_arrow_array().to_pylist()
        for batch in normal_eye.as_component_batches()
    }
    followed_components = {
        str(batch.component_descriptor()): batch.as_arrow_array().to_pylist()
        for batch in followed_eye.as_component_batches()
    }

    assert followed_view.id == RECORDED_SPATIAL_VIEW_ID
    assert followed_again.root_container.contents[0].id == RECORDED_SPATIAL_VIEW_ID
    assert followed_view.id == normal_view.id
    assert "EyeControls3D" not in layer_only_update.root_container.contents[0].properties
    assert "EyeControls3D" not in layer_only_update.root_container.contents[2].properties
    assert normal_components == {
        "EyeControls3D:tracking_entity": [""],
    }
    assert followed_components == {
        "EyeControls3D:kind": [2],
        "EyeControls3D:tracking_entity": ["/world/sensor_pose"],
    }


def test_recorded_blueprint_layer_updates_preserve_store_until_explicit_reset(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    activations: list[tuple[object, bool, bool]] = []
    eye_control_updates: list[bool] = []
    send_blueprint = viewer_recorded_module.bindings.send_blueprint
    make_blueprint = viewer_recorded_module.recorded_blueprint

    def capture_activation(
        storage: object,
        make_active: bool,
        make_default: bool,
        recording: object,
    ) -> None:
        activations.append((storage, make_active, make_default))
        send_blueprint(storage, make_active, make_default, recording)

    monkeypatch.setattr(
        viewer_recorded_module.bindings,
        "send_blueprint",
        capture_activation,
    )

    def capture_eye_control_update(*args: object, **kwargs: object) -> object:
        eye_control_updates.append(bool(kwargs["update_eye_controls"]))
        return make_blueprint(*args, **kwargs)

    monkeypatch.setattr(
        viewer_recorded_module,
        "recorded_blueprint",
        capture_eye_control_update,
    )
    session_id = "b" * 32
    initial = viewer_recorded_blueprint_rrd(
        RerunSceneSettings(accumulation_seconds=12.0),
        recording_id="stable-camera",
        blueprint_session_id=session_id,
        unified_perception=True,
    )
    layers_enabled = viewer_recorded_blueprint_rrd(
        RerunSceneSettings(accumulation_seconds=12.0),
        recording_id="stable-camera",
        blueprint_session_id=session_id,
        unified_perception=True,
        show_detections_2d=True,
        show_segmentation=True,
        show_cuboids_3d=True,
    )
    follow_enabled = viewer_recorded_blueprint_rrd(
        RerunSceneSettings(accumulation_seconds=12.0),
        recording_id="stable-camera",
        blueprint_session_id=session_id,
        unified_perception=True,
        show_detections_2d=True,
        show_segmentation=True,
        show_cuboids_3d=True,
        follow_trajectory=True,
    )
    layer_disabled_while_following = viewer_recorded_blueprint_rrd(
        RerunSceneSettings(accumulation_seconds=12.0),
        recording_id="stable-camera",
        blueprint_session_id=session_id,
        unified_perception=True,
        show_detections_2d=False,
        show_segmentation=True,
        show_cuboids_3d=True,
        follow_trajectory=True,
    )
    reset = viewer_recorded_blueprint_rrd(
        RerunSceneSettings(accumulation_seconds=12.0),
        recording_id="stable-camera",
        blueprint_session_id=session_id,
        view_reset_generation=1,
        unified_perception=True,
        show_detections_2d=True,
        show_segmentation=True,
        show_cuboids_3d=True,
        follow_trajectory=True,
    )

    store_pattern = rb"rec_[0-9a-f]{32}"
    initial_store_ids = set(re.findall(store_pattern, initial))
    layer_store_ids = set(re.findall(store_pattern, layers_enabled))
    follow_store_ids = set(re.findall(store_pattern, follow_enabled))
    followed_layer_store_ids = set(re.findall(store_pattern, layer_disabled_while_following))
    reset_store_ids = set(re.findall(store_pattern, reset))
    assert len(initial_store_ids) == 1
    assert layer_store_ids == initial_store_ids
    assert follow_store_ids == initial_store_ids
    assert followed_layer_store_ids == initial_store_ids
    assert len(reset_store_ids) == 1
    assert reset_store_ids.isdisjoint(initial_store_ids)
    assert eye_control_updates == [True, False, True, False, True]
    assert [activation[1:] for activation in activations] == [
        (True, False),
        (True, False),
        (True, False),
        (True, False),
        (True, False),
    ]
    assert activations[0][0] is activations[1][0]
    assert activations[2][0] is activations[0][0]
    assert activations[3][0] is activations[0][0]
    assert activations[4][0] is not activations[0][0]
    assert len(initial) < 350_000
    assert len(layers_enabled) < 350_000
    assert len(follow_enabled) < 350_000
    assert len(layer_disabled_while_following) < 350_000
    assert len(reset) < 350_000


def test_viewer_blueprint_unifies_original_video_and_independent_ai_layers() -> None:
    blueprint = viewer_recorded_blueprint(
        RerunSceneSettings(accumulation_seconds=12.0),
        include_initial_playback_state=False,
        unified_perception=True,
        show_detections_2d=True,
        show_segmentation=True,
        show_cuboids_3d=False,
    )

    assert type(blueprint.root_container).__name__ == "Horizontal"
    camera_view, spatial_view = blueprint.root_container.contents
    assert camera_view.origin == "/perception/camera"
    assert spatial_view.origin == "/world"
    assert camera_view.visualizer_overrides[
        "/perception/camera/image"
    ].visible.as_arrow_array().to_pylist() == [True]
    assert camera_view.visualizer_overrides[
        "/perception/camera/detections"
    ].visible.as_arrow_array().to_pylist() == [True]
    assert camera_view.visualizer_overrides[
        "/perception/camera/segmentation"
    ].visible.as_arrow_array().to_pylist() == [True]
    (semantic_behavior,) = spatial_view.visualizer_overrides["/world/perception/semantic_points"]
    (cuboid_behavior,) = spatial_view.visualizer_overrides["/world/perception/boxes3d"]
    assert semantic_behavior.visible.as_arrow_array().to_pylist() == [True]
    assert cuboid_behavior.visible.as_arrow_array().to_pylist() == [False]
    visible_ranges = spatial_view.properties["VisibleTimeRanges"]
    assert visible_ranges.ranges.as_arrow_array().to_pylist() == []
    _point_behavior, _point_visualizer, point_time_ranges = spatial_view.visualizer_overrides[
        "/world/points"
    ]
    assert point_time_ranges.ranges.as_arrow_array().to_pylist() == [
        {
            "timeline": SESSION_TIMELINE,
            "range": {"start": -12_000_000_000, "end": 0},
        }
    ]


def test_export_preserves_every_decodable_frame_and_source_timeline(tmp_path: Path) -> None:
    capture = _write_capture(
        tmp_path,
        [
            ("lixel/application/report/heartbeat", b"opaque", 0),
            ("RealtimePointcloud", _point_payload(1.0), 100_000_000),
            ("RealtimePath", _pose_payload(1.0), 600_000_000),
            ("RealtimePointcloud", _point_payload(2.0), 900_000_000),
            ("RealtimePath", _pose_payload(1.2), 1_200_000_000),
            ("lixel/application/report/heartbeat", b"opaque-tail", 9_500_000_000),
        ],
        capture_clock_offsets_ns=(-500_000_000, 10_000_000_000),
    )
    output = tmp_path / "session.rrd"

    summary = export_k1mqtt_to_rrd(capture, output)

    assert summary["source_messages"] == 6
    assert summary["decoded_messages"] == 4
    assert summary["point_frames"] == 2
    assert summary["pose_frames"] == 2
    assert summary["ignored_messages"] == 2
    assert summary["points"] == 2
    assert summary["trajectory_poses"] == 2
    assert summary["trajectory_updates"] == 2
    assert summary["session_origin_monotonic_ns"] == 8_500_000_000
    assert summary["timeline"] == "session_time"
    assert summary["timeline_start_ns"] == 0
    assert summary["timeline_end_ns"] == 10_500_000_000
    assert summary["timeline_span_ns"] == 10_500_000_000
    assert summary["first_decoded_time_ns"] == 600_000_000
    assert summary["last_decoded_time_ns"] == 1_700_000_000
    assert summary["source_sha256"] == _sha256(capture)
    assert summary["rrd_bytes"] == output.stat().st_size
    assert summary["rrd_sha256"] == _sha256(output)
    assert output.stat().st_size > 0
    assert list(tmp_path.glob(".session.rrd.*.tmp")) == []

    origin_table = _print_rrd_entity(output, "/__mission_core/session_origin")
    assert "/__mission_core/session_origin" in origin_table
    assert "session_time" in origin_table
    assert "P0D" in origin_table
    assert "[true]" in origin_table
    end_table = _print_rrd_entity(output, "/__mission_core/session_end")
    assert "/__mission_core/session_end" in end_table
    assert "PT10.5S" in end_table


def test_export_records_device_route_time_series_on_shared_timeline(tmp_path: Path) -> None:
    capture = _write_capture(
        tmp_path,
        [
            ("RealtimePointcloud", _point_payload(1.0), 0),
            (
                "lixel/application/report/modeling",
                _modeling_payload(distance=2.25, speed=0.75, scan_ticks=20),
                500_000_000,
            ),
            (
                "lixel/application/report/modeling",
                _modeling_payload(distance=3.5, speed=1.25, scan_ticks=22),
                1_000_000_000,
            ),
        ],
    )
    output = tmp_path / "session.rrd"

    summary = export_k1mqtt_to_rrd(capture, output)

    assert summary["modeling_reports"] == 2
    assert summary["decoded_messages"] == 1
    assert summary["ignored_messages"] == 0
    assert summary["timeline_end_ns"] == 1_000_000_000
    distance_table = _print_rrd_entity(output, "/metrics/device/route_distance_meters")
    elapsed_table = _print_rrd_entity(output, "/metrics/device/scan_elapsed_seconds")
    assert "2.25" in distance_table and "3.5" in distance_table
    assert "10.0" in elapsed_table and "11.0" in elapsed_table


def test_export_uses_explicit_catalog_bound_sealed_clock(tmp_path: Path) -> None:
    capture = _write_capture(
        tmp_path,
        [("RealtimePointcloud", _point_payload(1.0), 500_000_000)],
    )
    origin = {
        "schema_version": 1,
        "started_at_epoch_ns": 1_784_124_314_000_000_000,
        "started_monotonic_ns": 8_000_000_000,
    }
    origin_path = tmp_path / "mqtt.timeline.origin.json"
    origin_path.write_text(json.dumps(origin, separators=(",", ":")) + "\n")
    provisional = {
        **origin,
        "completed_at_epoch_ns": 1_784_124_316_000_000_000,
        "completed_monotonic_ns": 10_000_000_000,
    }
    (tmp_path / "mqtt.timeline.json").write_text(
        json.dumps(provisional, separators=(",", ":")) + "\n"
    )
    sealed = {
        **origin,
        "completed_at_epoch_ns": 1_784_124_321_000_000_000,
        "completed_monotonic_ns": 15_000_000_000,
    }
    sealed_payload = (json.dumps(sealed, separators=(",", ":")) + "\n").encode()
    sealed_digest = hashlib.sha256(sealed_payload).hexdigest()
    sealed_path = tmp_path / f"mqtt.timeline.session-{sealed_digest}.json"
    sealed_path.write_bytes(sealed_payload)
    output = tmp_path / "session.rrd"

    summary = export_k1mqtt_to_rrd(
        capture,
        output,
        capture_clock_path=sealed_path,
        capture_clock_origin_path=origin_path,
    )

    assert summary["session_origin_monotonic_ns"] == 8_000_000_000
    assert summary["timeline_end_ns"] == 7_000_000_000


def test_export_rejects_malformed_known_modeling_report(tmp_path: Path) -> None:
    capture = _write_capture(
        tmp_path,
        [
            ("RealtimePointcloud", _point_payload(1.0), 0),
            ("lixel/application/report/modeling", b"malformed", 1_000_000),
        ],
    )
    output = tmp_path / "session.rrd"
    output.write_bytes(b"trusted-existing-recording")

    with pytest.raises(RrdExportError, match="modeling report"):
        export_k1mqtt_to_rrd(capture, output)

    assert output.read_bytes() == b"trusted-existing-recording"


def test_atomic_rename_failure_preserves_existing_recording(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    capture = _write_capture(
        tmp_path,
        [("RealtimePointcloud", _point_payload(1.0), 0)],
    )
    output = tmp_path / "session.rrd"
    trusted = b"existing-good-recording"
    output.write_bytes(trusted)

    def fail_replace(_source: Path, _destination: Path) -> None:
        raise OSError("synthetic rename failure")

    monkeypatch.setattr(export_module.os, "replace", fail_replace)

    with pytest.raises(RrdExportError, match="synthetic rename failure"):
        export_k1mqtt_to_rrd(capture, output)

    assert output.read_bytes() == trusted
    assert list(tmp_path.glob(".session.rrd.*.tmp")) == []


def test_missing_monotonic_metadata_never_replaces_existing_recording(tmp_path: Path) -> None:
    capture = _write_capture(
        tmp_path,
        [("RealtimePointcloud", _point_payload(1.0), 0)],
    )
    (tmp_path / "mqtt.metadata.jsonl").unlink()
    output = tmp_path / "session.rrd"
    output.write_bytes(b"existing-good-recording")

    with pytest.raises(RrdExportError, match="received_monotonic_ns"):
        export_k1mqtt_to_rrd(capture, output)

    assert output.read_bytes() == b"existing-good-recording"
    assert list(tmp_path.glob(".session.rrd.*.tmp")) == []


def test_export_preserves_valid_prefix_before_crash_metadata_tail(tmp_path: Path) -> None:
    capture = _write_capture(
        tmp_path,
        [
            ("RealtimePointcloud", _point_payload(1.0), 100_000_000),
            ("RealtimePointcloud", _point_payload(2.0), 200_000_000),
        ],
    )
    metadata_path = tmp_path / "mqtt.metadata.jsonl"
    first_line = metadata_path.read_text(encoding="utf-8").splitlines(keepends=True)[0]
    metadata_path.write_text(
        first_line + '{"record_type":"message"',
        encoding="utf-8",
    )
    output = tmp_path / "session.rrd"

    summary = export_k1mqtt_to_rrd(capture, output)

    assert summary["source_messages"] == 1
    assert summary["decoded_messages"] == 1
    assert summary["point_frames"] == 1
    assert summary["timeline_end_ns"] == 0
    assert output.stat().st_size > 0


def test_export_honors_cooperative_cancellation_without_publishing(tmp_path: Path) -> None:
    capture = _write_capture(
        tmp_path,
        [("RealtimePointcloud", _point_payload(1.0), 0)],
    )
    output = tmp_path / "session.rrd"
    output.write_bytes(b"trusted-existing-recording")
    cancelled = threading.Event()
    cancelled.set()

    with pytest.raises(RrdExportCancelled):
        export_k1mqtt_to_rrd(capture, output, cancel_event=cancelled)

    assert output.read_bytes() == b"trusted-existing-recording"
    assert list(tmp_path.glob(".session.rrd.*.tmp")) == []
