from __future__ import annotations

import json
import struct
import zipfile
from collections.abc import Mapping
from pathlib import Path

import numpy as np
import numpy.typing as npt
import pytest

from k1link.datasets import goose_qualification
from k1link.datasets.goose_qualification import (
    DegradationProfile,
    qualify_goose_ground,
)
from k1link.ground_segmentation import GroundSegmentation
from k1link.simulation import QualificationRunStore, RunState


class _NeverGround:
    @property
    def identity(self) -> Mapping[str, object]:
        return {"provider_id": "test-current", "revision": "1"}

    def segment(self, xyzi: npt.NDArray[np.float32]) -> GroundSegmentation:
        count = xyzi.shape[0]
        return GroundSegmentation(
            ground_mask=np.zeros(count, dtype=np.bool_),
            assigned_mask=np.ones(count, dtype=np.bool_),
            latency_ms=2.0,
        )


class _NegativeZGround:
    @property
    def identity(self) -> Mapping[str, object]:
        return {"provider_id": "test-patchwork", "revision": "1"}

    def segment(self, xyzi: npt.NDArray[np.float32]) -> GroundSegmentation:
        count = xyzi.shape[0]
        return GroundSegmentation(
            ground_mask=np.asarray(xyzi[:, 2] < 0, dtype=np.bool_),
            assigned_mask=np.ones(count, dtype=np.bool_),
            latency_ms=3.0,
        )


def _frame_bytes(offset: float) -> tuple[bytes, bytes]:
    points = (
        (-1.0 + offset, 0.0, -2.0, 0.1),
        (0.0 + offset, 0.0, -2.0, 0.2),
        (1.0 + offset, 0.0, 2.0, 0.3),
        (2.0 + offset, 0.0, 2.0, 0.4),
    )
    labels = (1, 3, 2, 2)
    return (
        b"".join(struct.pack("<ffff", *point) for point in points),
        b"".join(struct.pack("<I", label) for label in labels),
    )


def _dataset(tmp_path: Path) -> Path:
    root = tmp_path / "datasets"
    archive_sha = "a" * 64
    archive = root / "goose-3d/v2025-08-22/archives/goose_3d_val.zip"
    archive.parent.mkdir(parents=True)
    with zipfile.ZipFile(archive, "w") as target:
        for index in range(2):
            frame_id = f"2022-01-01_flight__0001_{index:019d}"
            points, labels = _frame_bytes(index * 0.1)
            target.writestr(f"goose/lidar/val/{frame_id}_vls128.bin", points)
            target.writestr(f"goose/labels/val/{frame_id}_goose.label", labels)
    state = root / "state/goose-3d-v2025-08-22.json"
    state.parent.mkdir(parents=True)
    state.write_text(json.dumps({"archive": {"sha256": archive_sha}}), encoding="utf-8")
    install = root / "goose-3d/v2025-08-22/installs" / archive_sha
    install.mkdir(parents=True)
    (install / "goose_label_mapping.csv").write_text(
        "class_name,label_key,hex\nasphalt,1,#111111\nrock,2,#222222\nsoil,3,#333333\n",
        encoding="utf-8",
    )
    return root


def test_full_split_qualification_is_sealed_and_resumable(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    root = _dataset(tmp_path)
    runs_root = tmp_path / "runs"
    monkeypatch.setattr(goose_qualification, "_is_worker_dataset_root", lambda _: True)

    report = qualify_goose_ground(
        root,
        runs_root,
        mission_core_commit="1" * 40,
        parallel_workers=1,
        expected_frame_count=2,
        current_segmenter=_NeverGround(),
        patchwork_segmenter=_NegativeZGround(),
        degradations=(DegradationProfile("range-20m", "maximum-range-m", 20.0),),
    )

    assert report["frame_count"] == 2
    assert report["decision"]["status"] == "shadow-candidate"
    assert report["aggregates"]["current"]["micro"]["ground_iou"] == 0
    assert report["aggregates"]["patchworkpp"]["micro"]["ground_iou"] == 1
    assert len(report["frames"]) == 2
    run = QualificationRunStore(runs_root, read_only=True).load(report["run_id"])
    assert run.state is RunState.COMPLETED
    assert {artifact.kind for artifact in run.artifacts} == {
        "goose-ground-qualification-report",
        "goose-ground-qualification-failure-preview",
    }

    resumed = qualify_goose_ground(
        root,
        runs_root,
        mission_core_commit="1" * 40,
        parallel_workers=1,
        expected_frame_count=2,
        current_segmenter=_NeverGround(),
        patchwork_segmenter=_NegativeZGround(),
        degradations=(DegradationProfile("range-20m", "maximum-range-m", 20.0),),
    )
    assert resumed["identity_sha256"] == report["identity_sha256"]
