from __future__ import annotations

from dataclasses import replace

import numpy as np
import pytest

from k1link.compute.lidar_contract import (
    K1_LAB_LIDAR_PACK_V1_PROFILE,
    K1_LIVE_LIDAR_PROFILE,
    LidarContractError,
    LidarCoordinateSpace,
    LidarEvidenceProfile,
    LidarPipelineStage,
    LidarPointField,
    LidarPoseStatus,
    LidarQualityMonitor,
    LidarReadiness,
    LidarRepresentation,
    LidarTimeBasis,
    assess_lidar_profile,
    lidar_readiness_document,
    sensor_frame_xyzi,
)
from k1link.data_plane import (
    ConsumerFrameContext,
    DecodedPointCloudView,
    DecodedPoseView,
)


def _context(sequence: int = 1) -> ConsumerFrameContext:
    return ConsumerFrameContext(
        sequence=sequence,
        captured_at_epoch_ns=10,
        received_monotonic_ns=20,
        processing_started_monotonic_ns=21,
        encoded_size_bytes=100,
        live=True,
    )


def _assessment(stage: LidarPipelineStage, *, lab_pack: bool = False) -> object:
    profile = K1_LAB_LIDAR_PACK_V1_PROFILE if lab_pack else K1_LIVE_LIDAR_PROFILE
    return next(item for item in assess_lidar_profile(profile) if item.stage is stage)


def test_k1_profiles_round_trip_and_do_not_claim_navigation_authority() -> None:
    restored = type(K1_LIVE_LIDAR_PROFILE).from_dict(K1_LIVE_LIDAR_PROFILE.to_dict())

    assert restored == K1_LIVE_LIDAR_PROFILE
    document = lidar_readiness_document(restored)
    assert document["authority"] == {
        "commands_enabled": False,
        "navigation_or_safety_accepted": False,
    }


def test_current_k1_live_source_is_detector_degraded_but_odometry_blocked() -> None:
    detector = _assessment(LidarPipelineStage.LIDAR_3D_DETECTION)
    odometry = _assessment(LidarPipelineStage.LIDAR_ODOMETRY)
    lio = _assessment(LidarPipelineStage.LIDAR_INERTIAL_SLAM)

    assert detector.readiness is LidarReadiness.DEGRADED
    assert "pretrained-domain-expects-sensor-scan" in detector.reasons
    assert odometry.readiness is LidarReadiness.BLOCKED
    assert lio.readiness is LidarReadiness.BLOCKED
    assert "per-point-time-unavailable" in lio.reasons


def test_lidar_pack_v1_is_blocked_for_pointpillars_because_it_dropped_intensity() -> None:
    detector = _assessment(LidarPipelineStage.LIDAR_3D_DETECTION, lab_pack=True)

    assert detector.readiness is LidarReadiness.BLOCKED
    assert "admitted-pointpillars-baseline-requires-intensity" in detector.reasons


def test_nvblox_is_blocked_until_k1_scan_geometry_is_known() -> None:
    assessment = _assessment(LidarPipelineStage.NVIDIA_NVBLOX)

    assert assessment.readiness is LidarReadiness.BLOCKED
    assert "lidar-intrinsics-or-scan-geometry-unknown" in assessment.reasons


def test_complete_sensor_profile_is_ready_for_all_admitted_stages() -> None:
    profile = LidarEvidenceProfile(
        profile_id="reference-raw-lidar-imu/v1",
        representation=LidarRepresentation.SENSOR_SCAN,
        coordinate_space=LidarCoordinateSpace.SENSOR,
        coordinate_frame="lidar",
        frame_time_basis=LidarTimeBasis.SENSOR,
        point_fields=(
            LidarPointField.XYZ,
            LidarPointField.INTENSITY,
            LidarPointField.RING,
            LidarPointField.RELATIVE_TIME,
        ),
        pose_status=LidarPoseStatus.SENSOR_SYNCHRONIZED,
        scan_geometry_known=True,
        imu_samples_available=True,
        lidar_imu_extrinsic_available=True,
        camera_extrinsic_available=True,
    )

    assert {
        assessment.readiness for assessment in assess_lidar_profile(profile)
    } == {LidarReadiness.READY}
    assert len(assess_lidar_profile(profile)) == len(LidarPipelineStage)


def test_sensor_frame_xyzi_inverts_map_pose_and_normalizes_intensity() -> None:
    cloud = DecodedPointCloudView(
        context=_context(),
        frame_id="map",
        positions_xyz=((11.0, 2.0, 3.0), (10.0, 3.0, 3.0)),
        intensities=bytes((0, 255)),
    )
    pose = DecodedPoseView(
        context=_context(2),
        frame_id="map",
        child_frame_id="k1-lidar",
        position_xyz=(10.0, 2.0, 3.0),
        orientation_xyzw=(0.0, 0.0, 0.0, 1.0),
    )

    xyzi = sensor_frame_xyzi(cloud, pose)

    np.testing.assert_allclose(
        xyzi,
        np.asarray(
            [
                [1.0, 0.0, 0.0, 0.0],
                [0.0, 1.0, 0.0, 1.0],
            ],
            dtype=np.float32,
        ),
    )


def test_sensor_frame_xyzi_rejects_missing_intensity_and_unbound_frames() -> None:
    cloud = DecodedPointCloudView(
        context=_context(),
        frame_id="map",
        positions_xyz=((1.0, 2.0, 3.0),),
    )
    with pytest.raises(LidarContractError, match="requires intensity"):
        sensor_frame_xyzi(cloud)

    with pytest.raises(LidarContractError, match="diagnostic-only"):
        replace(K1_LIVE_LIDAR_PROFILE, navigation_or_safety_accepted=True)


def test_quality_monitor_is_bounded_and_reports_sensor_relative_range() -> None:
    monitor = LidarQualityMonitor(
        K1_LIVE_LIDAR_PROFILE,
        frame_sample_capacity=2,
        point_sample_capacity=256,
        points_sampled_per_frame=2,
    )
    pose = DecodedPoseView(
        context=_context(20),
        frame_id="map",
        child_frame_id="k1-lidar",
        position_xyz=(10.0, 2.0, 3.0),
        orientation_xyzw=(0.0, 0.0, 0.0, 1.0),
    )
    for sequence, capture_ns in enumerate((10, 110_000_010, 210_000_010), start=1):
        context = replace(_context(sequence), captured_at_epoch_ns=capture_ns)
        monitor.observe(
            DecodedPointCloudView(
                context=context,
                frame_id="map",
                positions_xyz=((11.0, 2.0, 3.0), (10.0, 4.0, 3.0)),
                intensities=bytes((0, 255)),
            ),
            pose=pose,
        )

    report = monitor.snapshot()

    assert report["frames_observed"] == 3
    assert report["points_observed"] == 6
    assert report["point_count_per_frame"]["sample_count"] == 2
    assert report["sensor_range_m"]["minimum"] == pytest.approx(1.0)
    assert report["sensor_range_m"]["maximum"] == pytest.approx(2.0)
    assert report["intensity_0_1"]["minimum"] == pytest.approx(0.0)
    assert report["intensity_0_1"]["maximum"] == pytest.approx(1.0)
    assert report["authority"]["navigation_or_safety_accepted"] is False
