from __future__ import annotations

import base64
import hashlib
import json
from datetime import UTC, datetime, timedelta
from pathlib import Path

import pytest
from missioncore_plugin_sdk import v0alpha2 as sdk
from pydantic import TypeAdapter, ValidationError

NOW = datetime(2026, 7, 16, 12, 0, tzinfo=UTC)


def _model() -> sdk.DeviceModelRef:
    return sdk.DeviceModelRef(
        plugin_id="nodedc.device.synthetic",
        plugin_version="0.2.0",
        model_id="synthetic.sensor",
    )


def _device() -> sdk.DeviceInstanceRef:
    return sdk.DeviceInstanceRef(
        device_id="synthetic.device-001",
        model=_model(),
        stability=sdk.IdentityStability.STABLE,
        basis=sdk.IdentityBasis.PLUGIN_DERIVED,
    )


def _session_ref() -> sdk.DeviceSessionRef:
    return sdk.DeviceSessionRef(
        session_id="session-001",
        device_id="synthetic.device-001",
    )


def _header() -> sdk.CanonicalStreamHeader:
    return sdk.CanonicalStreamHeader(
        message_id="message-001",
        session=_session_ref(),
        acquisition_id="acquisition-001",
        source_id="lidar-primary",
        channel_id="points",
        sequence=1,
        captured_at=NOW,
        observed_at=NOW + timedelta(milliseconds=3),
        clock_domain=sdk.ClockDomain.UTC,
        compatibility_profile_id="synthetic.fw-1.v1",
    )


def _inline_payload(data: bytes) -> sdk.InlinePayload:
    return sdk.InlinePayload(
        media_type="application/x-pointcloud-struct",
        byte_length=len(data),
        sha256=f"sha256:{hashlib.sha256(data).hexdigest()}",
        data=base64.b64encode(data).decode("ascii"),
    )


def test_plugin_sdk_v0alpha2_identity_separates_transport_alias_from_stable_id() -> None:
    alias = sdk.TransportAlias(
        transport="ble.corebluetooth",
        scope_id="agent.macbook-01",
        value="SYNTHETIC-K1-0001",
        observed_at=NOW,
    )
    provisional = sdk.DeviceInstanceRef(
        device_id="observation.ble-synthetic-k1-0001",
        model=_model(),
        stability=sdk.IdentityStability.PROVISIONAL,
        basis=sdk.IdentityBasis.TRANSPORT_LOCAL,
        aliases=(alias,),
    )

    assert provisional.aliases == (alias,)
    with pytest.raises(ValidationError, match="transport-local identifier"):
        sdk.DeviceInstanceRef(
            device_id="incorrectly-stable",
            model=_model(),
            stability=sdk.IdentityStability.STABLE,
            basis=sdk.IdentityBasis.TRANSPORT_LOCAL,
        )


def test_plugin_sdk_v0alpha2_session_tracks_three_independent_state_machines() -> None:
    context = sdk.DeviceSessionContext(
        session_id="session-001",
        device=_device(),
        execution=sdk.ExecutionBinding(
            node_id="mission-node.local-01",
            agent_instance_id="agent-process-001",
            platform=sdk.ExecutionPlatform.MACOS,
        ),
        compatibility_profile_id="synthetic.fw-1.v1",
        opened_at=NOW,
    )
    snapshot = sdk.DeviceSessionSnapshot(
        context=context,
        revision=4,
        enrollment=sdk.EnrollmentState.ENROLLED,
        connectivity=sdk.ConnectivityState.CONNECTED,
        acquisition=sdk.AcquisitionState.IDLE,
        observed_at=NOW,
        active_operation_ids=("operation-health",),
    )

    assert snapshot.enrollment is sdk.EnrollmentState.ENROLLED
    assert snapshot.connectivity is sdk.ConnectivityState.CONNECTED
    assert snapshot.acquisition is sdk.AcquisitionState.IDLE
    with pytest.raises(ValidationError, match="active operation ids must be unique"):
        sdk.DeviceSessionSnapshot.model_validate(
            {
                **snapshot.model_dump(),
                "active_operation_ids": ("operation-health", "operation-health"),
            }
        )


def test_plugin_sdk_v0alpha2_operation_ack_is_not_completion() -> None:
    policy = sdk.OperationPolicy(
        action_id="acquisition.start",
        safety_class=sdk.OperationSafetyClass.STATE_CHANGING,
        idempotency=sdk.IdempotencyMode.IDEMPOTENT_WITH_KEY,
        timeout_seconds=30,
        retry_limit=1,
        preconditions=("device.connected", "calibration.ready"),
        secret_fields=("device_credential",),
    )
    request = sdk.OperationRequest(
        operation_id="operation-start-001",
        session=_session_ref(),
        action_id="acquisition.start",
        requested_at=NOW,
        deadline_at=NOW + timedelta(seconds=30),
        idempotency_key="acquisition.start:001",
        parameters={"project_id": "project-001"},
        secret_refs={
            "device_credential": sdk.SecretReference(
                provider="missioncore.secret-store",
                reference="devices/synthetic-001/credential",
            )
        },
    )
    sdk.validate_operation_request(policy, request)

    adapter = TypeAdapter(sdk.OperationEvent)
    acknowledgement = adapter.validate_python(
        {
            "event_type": "acknowledgement",
            "operation_id": request.operation_id,
            "session": request.session.model_dump(),
            "sequence": 0,
            "recorded_at": NOW,
            "disposition": "accepted",
        }
    )
    completion = adapter.validate_python(
        {
            "event_type": "completion",
            "operation_id": request.operation_id,
            "session": request.session.model_dump(),
            "sequence": 2,
            "recorded_at": NOW + timedelta(seconds=1),
            "result": {"acquisition_id": "acquisition-001"},
        }
    )

    assert isinstance(acknowledgement, sdk.OperationAcknowledgement)
    assert isinstance(completion, sdk.OperationCompletion)
    assert acknowledgement.event_type != completion.event_type


def test_plugin_sdk_v0alpha2_operation_policy_fails_closed() -> None:
    with pytest.raises(ValidationError, match="automatic retries"):
        sdk.OperationPolicy(
            action_id="calibration.start",
            safety_class=sdk.OperationSafetyClass.SAFETY_CRITICAL,
            idempotency=sdk.IdempotencyMode.NON_IDEMPOTENT,
            timeout_seconds=60,
            retry_limit=1,
        )

    policy = sdk.OperationPolicy(
        action_id="camera.catalog.read",
        safety_class=sdk.OperationSafetyClass.READ_ONLY,
        idempotency=sdk.IdempotencyMode.IDEMPOTENT,
        timeout_seconds=5,
    )
    request = sdk.OperationRequest(
        operation_id="operation-camera-001",
        session=_session_ref(),
        action_id="camera.preview.start",
        requested_at=NOW,
        deadline_at=NOW + timedelta(seconds=5),
    )
    with pytest.raises(ValueError, match="action_id"):
        sdk.validate_operation_request(policy, request)


def test_plugin_sdk_v0alpha2_point_cloud_layout_and_payload_are_verified() -> None:
    payload = _inline_payload(b"\0" * 24)
    frame = sdk.PointCloudFrame(
        header=_header(),
        coordinate_frame="sensor.lidar-primary",
        width=2,
        point_count=2,
        point_step=12,
        row_step=24,
        dense=False,
        fields=(
            sdk.PointField(name="x", offset=0, data_type=sdk.PointFieldType.FLOAT32),
            sdk.PointField(name="y", offset=4, data_type=sdk.PointFieldType.FLOAT32),
            sdk.PointField(name="z", offset=8, data_type=sdk.PointFieldType.FLOAT32),
        ),
        payload=payload,
    )

    assert frame.payload.byte_length == frame.point_count * frame.point_step
    with pytest.raises(ValidationError, match="point_count"):
        sdk.PointCloudFrame.model_validate({**frame.model_dump(), "point_count": 3})
    with pytest.raises(ValidationError, match="sha256"):
        sdk.InlinePayload.model_validate({**payload.model_dump(), "sha256": f"sha256:{'0' * 64}"})


def test_plugin_sdk_v0alpha2_evidence_is_immutable_and_lineage_is_explicit() -> None:
    handle = sdk.EvidenceHandle(
        evidence_id="evidence.raw-001",
        store_id="evidence-store.local",
        media_type="application/vnd.tcpdump.pcap",
        byte_length=1024,
        sha256=f"sha256:{'a' * 64}",
        object_key="experiments/2026-07-16/raw-001.pcap",
    )
    record = sdk.EvidenceRecord(
        handle=handle,
        plugin_id="nodedc.device.synthetic",
        session=_session_ref(),
        created_at=NOW,
        source_kind="packet-capture",
        retention=sdk.EvidenceRetention.EXPERIMENT,
        redaction=sdk.RedactionState.UNREVIEWED,
    )

    assert record.handle.sha256.startswith("sha256:")
    with pytest.raises(ValidationError, match="cannot name itself"):
        sdk.EvidenceRecord.model_validate(
            {**record.model_dump(), "parent_evidence_ids": [handle.evidence_id]}
        )
    with pytest.raises(ValidationError, match="frozen"):
        record.redaction = sdk.RedactionState.CLEARED  # type: ignore[misc]


def test_plugin_sdk_v0alpha2_contracts_are_deeply_immutable() -> None:
    source_parameters = {
        "pipeline": {
            "stages": ["capture", "decode"],
            "options": {"raw_first": True},
        }
    }
    request = sdk.OperationRequest(
        operation_id="operation-immutable-001",
        session=_session_ref(),
        action_id="acquisition.prepare",
        requested_at=NOW,
        deadline_at=NOW + timedelta(seconds=30),
        parameters=source_parameters,
    )

    pipeline = request.parameters["pipeline"]
    assert isinstance(pipeline, dict)
    stages = pipeline["stages"]
    assert isinstance(stages, list)
    options = pipeline["options"]
    assert isinstance(options, dict)

    with pytest.raises(TypeError, match="deeply immutable"):
        request.parameters["new"] = True
    with pytest.raises(TypeError, match="deeply immutable"):
        options["raw_first"] = False
    with pytest.raises(TypeError, match="deeply immutable"):
        stages.append("publish")

    source_parameters["pipeline"]["stages"].append("outside-mutation")
    assert stages == ["capture", "decode"]
    wire_document = request.model_dump(mode="json")
    assert wire_document["parameters"] == {
        "pipeline": {
            "stages": ["capture", "decode"],
            "options": {"raw_first": True},
        }
    }
    assert json.loads(json.dumps(wire_document))["parameters"] == wire_document["parameters"]
    assert "parameters" in sdk.OperationRequest.model_json_schema()["properties"]

    copy_update = {"parameters": {"pipeline": {"stages": ["publish"]}}}
    copied = request.model_copy(update=copy_update)
    copied_stages = copied.parameters["pipeline"]["stages"]
    assert copied_stages == ["publish"]
    with pytest.raises(TypeError, match="deeply immutable"):
        copied_stages.append("mutate")
    copy_update["parameters"]["pipeline"]["stages"].append("outside-mutation")
    assert copied_stages == ["publish"]
    with pytest.raises(ValidationError, match="extra"):
        request.model_copy(update={"undeclared": True})


def test_plugin_sdk_v0alpha2_compatibility_blocks_unproven_control() -> None:
    pass_rule = sdk.CompatibilityRuleResult(
        rule_id="firmware.exact-match",
        outcome=sdk.RuleOutcome.PASS,
        blocking=True,
        detail="Firmware and protocol fixture match the reviewed profile.",
    )
    assessment = sdk.CompatibilityAssessment(
        assessment_id="compatibility-001",
        device=_device(),
        evaluated_at=NOW,
        observed_firmware="1.0.0",
        profile_id="synthetic.fw-1.v1",
        decision=sdk.CompatibilityDecision.COMPATIBLE,
        permitted_mode=sdk.PermittedMode.ACTIVE_CONTROL,
        rules=(pass_rule,),
        supported_actions=("acquisition.start", "acquisition.stop"),
    )
    assert assessment.permitted_mode is sdk.PermittedMode.ACTIVE_CONTROL

    with pytest.raises(ValidationError, match="active control requires a compatible decision"):
        sdk.CompatibilityAssessment.model_validate(
            {
                **assessment.model_dump(),
                "decision": "unknown",
                "permitted_mode": "active-control",
                "profile_id": None,
            }
        )


def test_plugin_sdk_v0alpha2_runtime_handshake_is_versioned_and_closed() -> None:
    descriptor = sdk.RuntimePluginDescriptor(
        plugin_id="nodedc.device.synthetic",
        plugin_version="0.1.0",
        supported_host_api_versions=("missioncore.nodedc/v1alpha2",),
        action_ids=("state.read", "acquisition.start"),
    )
    request = sdk.RuntimeHandshakeRequest(
        handshake_id="handshake-001",
        plugin_id=descriptor.plugin_id,
        plugin_version=descriptor.plugin_version,
        host_api_version="missioncore.nodedc/v1alpha2",
        requested_at=NOW,
        required_action_ids=descriptor.action_ids,
    )
    result = sdk.RuntimeHandshakeResult(
        handshake_id=request.handshake_id,
        runtime_instance_id="runtime.synthetic-001",
        accepted_host_api_version=request.host_api_version,
        descriptor=descriptor,
        ready_at=NOW,
    )
    health = sdk.RuntimeHealthSnapshot(
        runtime_instance_id=result.runtime_instance_id,
        plugin_id=descriptor.plugin_id,
        plugin_version=descriptor.plugin_version,
        status="ready",
        observed_at=NOW,
    )

    assert descriptor.runtime_protocol_version == sdk.RUNTIME_PROTOCOL_VERSION
    assert result.descriptor == descriptor
    assert health.status == "ready"
    with pytest.raises(ValidationError, match="runtime action ids must be unique"):
        sdk.RuntimePluginDescriptor.model_validate(
            {
                **descriptor.model_dump(),
                "action_ids": ["state.read", "state.read"],
            }
        )
    with pytest.raises(ValidationError, match="extra"):
        sdk.RuntimeHandshakeRequest.model_validate(
            {**request.model_dump(), "process_id": 42}
        )


def test_plugin_sdk_v0alpha2_exports_closed_json_schemas_and_vocabulary() -> None:
    schemas = sdk.contract_json_schemas()

    assert {
        "DeviceInstanceRef",
        "DeviceSessionSnapshot",
        "OperationEvent",
        "RuntimeActionInvocation",
        "RuntimeActionResult",
        "RuntimePluginDescriptor",
        "RuntimeHandshakeRequest",
        "RuntimeHandshakeResult",
        "RuntimeHealthSnapshot",
        "CanonicalStreamEnvelope",
        "EvidenceRecord",
        "CompatibilityAssessment",
    }.issubset(schemas)
    assert "oneOf" in schemas["OperationEvent"]
    with pytest.raises(ValidationError, match="extra"):
        sdk.DeviceSessionRef.model_validate(
            {"session_id": "session-001", "device_id": "device-001", "vendor": "forbidden"}
        )

    repository_root = Path(__file__).resolve().parents[1]
    vocabulary = (
        repository_root / "docs/domain-model/mission-core-experimental-vocabulary-v0alpha2.md"
    ).read_text(encoding="utf-8")
    assert "acknowledgement is not operation completion" in vocabulary
    assert "not yet a NODE.DC platform ontology" in vocabulary
