from __future__ import annotations

import json
from pathlib import Path

import pytest

from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
    ApplicationBootstrapError,
    ApplicationControlAuthority,
    LiveDeviceControlBinding,
    ShadowApplicationBootstrapOrchestrator,
    build_canonical_post_start_observation,
    build_shadow_bootstrap,
    decode_and_bind_device_info_response,
)
from k1link.device_plugins.xgrids_k1.protocol.modeling_control import OPENAPI_SUCCESS
from k1link.device_plugins.xgrids_k1.protocol.protobuf_wire import iter_fields

APPLICATION_KEY = "11111111-2222-3333-4444-555555555555"
VENDOR_DEVICE_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
CAPTURE_FACTS = json.loads(
    (
        Path(__file__).parent
        / "fixtures"
        / "xgrids_k1"
        / "device_info_fw3_0_2_semantics.json"
    ).read_text(encoding="utf-8")
)


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


def _uint(number: int, value: int) -> bytes:
    return _varint(number << 3) + _varint(value)


def _bytes(number: int, value: bytes) -> bytes:
    return _varint((number << 3) | 2) + _varint(len(value)) + value


def _text(number: int, value: str) -> bytes:
    return _bytes(number, value.encode())


def _authority() -> ApplicationControlAuthority:
    return ApplicationControlAuthority(openapi_key=APPLICATION_KEY)


def _binding(**changes: object) -> LiveDeviceControlBinding:
    values = {
        "vendor_device_id": VENDOR_DEVICE_ID,
        "device_serial": "K1SERIAL01",
        "software_version": CAPTURE_FACTS["software_version"],
        "system_version": CAPTURE_FACTS["system_version"],
        "device_model": CAPTURE_FACTS["device_model"],
        "device_type": CAPTURE_FACTS["platform_type"],
        "is_activated": CAPTURE_FACTS["is_activated"],
    }
    values.update(changes)
    return LiveDeviceControlBinding(**values)  # type: ignore[arg-type]


def _device_info_response(*, session_id: str = ":DeviceInfoRequest") -> bytes:
    header = _text(4, VENDOR_DEVICE_ID) + _text(5, session_id) + _text(6, APPLICATION_KEY)
    base_info = b"".join(
        (
            _text(1, "2026-01-01T00:00:00"),
            _text(2, CAPTURE_FACTS["software_version"]),
            _text(3, CAPTURE_FACTS["system_version"]),
            _text(4, "V1.2.3"),
            _text(5, "V1.0"),
            _text(6, CAPTURE_FACTS["device_model"]),
            _text(7, "K1SERIAL01"),
            _text(8, CAPTURE_FACTS["platform_type"]),
        )
    )
    working_status = _uint(1, 1) + _uint(2, 0)
    device_info = _bytes(2, base_info) + _bytes(7, working_status)
    error = _uint(1, OPENAPI_SUCCESS) + _text(2, "success")
    return _bytes(1, header) + _bytes(2, device_info) + _bytes(15, error)


def _generic_response(session_id: str) -> bytes:
    header = _text(4, VENDOR_DEVICE_ID) + _text(5, session_id) + _text(6, APPLICATION_KEY)
    error = _uint(1, OPENAPI_SUCCESS) + _text(2, "success")
    repeated_vendor_body = _text(3, "opaque-a") + _text(3, "opaque-b")
    return _bytes(1, header) + repeated_vendor_body + _bytes(15, error)


def test_exact_clean_cycle_pre_start_order_and_wire_lengths() -> None:
    plan = build_shadow_bootstrap(
        _authority(),
        _binding(),
        epoch_seconds=1_752_680_000,
        timezone_name="Europe/Moscow",
    )

    assert [request.message_type for request in plan.requests] == [
        "DeviceInfoRequest",
        "ModelingStatusRequest",
        "GetRtkAdvanceRequest",
        "DeviceConfigRequest",
        "DeviceInfoRequest",
        "GetRtkAdvanceRequest",
        "GetNtripProfileRequest",
        "GetCloudServerConfigRequest",
        "GetRtkAdvanceRequest",
        "DeviceInfoRequest",
    ]
    assert [request.payload_bytes for request in plan.requests] == [
        60,
        64,
        63,
        195,
        135,
        138,
        140,
        145,
        138,
        135,
    ]
    assert sum(request.mutates_device for request in plan.requests) == 1
    assert plan.requests[3].mutates_device
    assert plan.executable is False
    assert plan.blockers == ("vendor-writes-disabled", "publisher-not-installed")
    assert plan.retry_policy == "never-automatic"
    assert APPLICATION_KEY not in repr(plan)
    assert APPLICATION_KEY not in str(plan.as_dict())


def test_bootstrap_headers_switch_from_unbound_to_live_device_identity() -> None:
    plan = build_shadow_bootstrap(
        _authority(),
        _binding(),
        epoch_seconds=1_752_680_000,
        timezone_name="Europe/Moscow",
    )

    first_header = next(iter_fields(plan.requests[0].payload)).value
    assert isinstance(first_header, bytes)
    first_fields = {field.number: field.value for field in iter_fields(first_header)}
    assert 4 not in first_fields
    assert first_fields[5] == b":DeviceInfoRequest"

    config_header = next(iter_fields(plan.requests[3].payload)).value
    assert isinstance(config_header, bytes)
    config_fields = {field.number: field.value for field in iter_fields(config_header)}
    assert config_fields[4] == VENDOR_DEVICE_ID.encode()
    assert (
        config_fields[5]
        == (f"{VENDOR_DEVICE_ID}:DeviceConfigRequest:Publish_Proto_DeviceConfig_SetTime").encode()
    )


def test_device_config_timestamp_uses_captured_sint64_zigzag_wire_semantics() -> None:
    captured_epoch_seconds = 1_784_215_030
    plan = build_shadow_bootstrap(
        _authority(),
        _binding(),
        epoch_seconds=captured_epoch_seconds,
        timezone_name="Europe/Moscow",
    )

    config_request = plan.requests[3]
    top_fields = {field.number: field.value for field in iter_fields(config_request.payload)}
    time_config = top_fields[4]
    assert isinstance(time_config, bytes)
    time_fields = {field.number: field.value for field in iter_fields(time_config)}

    assert time_fields == {
        1: 3_568_430_060,
        2: b"Europe/Moscow",
    }


def test_canonical_post_start_observation_preserves_retained_operations_12_to_14() -> None:
    plan = build_canonical_post_start_observation(_authority(), _binding())

    assert [request.ordinal for request in plan.requests] == [12, 13, 14]
    assert [request.message_type for request in plan.requests] == [
        "ModelingStatusRequest",
        "DeviceInfoRequest",
        "ModelingStatusRequest",
    ]
    assert [request.payload_bytes for request in plan.requests] == [139, 135, 139]
    assert [request.response_required for request in plan.requests] == [False, True, True]
    assert all(request.phase == "post-start-observation" for request in plan.requests)
    assert all(not request.mutates_device for request in plan.requests)


def test_device_info_response_produces_live_binding_without_a_saved_device_profile() -> None:
    response = decode_and_bind_device_info_response(_device_info_response(), _authority())

    assert response.result_code == OPENAPI_SUCCESS
    assert response.binding.ready_for_reviewed_profile
    assert response.binding.matches_reviewed_device
    assert response.binding.matches_reviewed_firmware
    assert response.binding.is_activated
    assert VENDOR_DEVICE_ID not in repr(response)
    assert APPLICATION_KEY not in repr(response)


def test_device_info_response_correlation_and_profile_attestation_fail_closed() -> None:
    with pytest.raises(ApplicationBootstrapError, match="session correlation"):
        decode_and_bind_device_info_response(
            _device_info_response(session_id=":AnotherRequest"),
            _authority(),
        )
    with pytest.raises(ApplicationBootstrapError, match="does not attest"):
        build_shadow_bootstrap(
            _authority(),
            _binding(system_version="V3.0.3"),
            epoch_seconds=1_752_680_000,
            timezone_name="Europe/Moscow",
        )


@pytest.mark.parametrize(
    ("device_model", "device_type"),
    (
        ("lixelkity K1", "A4"),
        ("LixelKity K1 ", "A4"),
        ("LixelKity K11", "A4"),
        ("LixelKity K1", "a4"),
        ("LixelKity K1", " A4"),
        ("LixelKity K1", "K1"),
    ),
)
def test_reviewed_profile_rejects_non_exact_device_model_and_type(
    device_model: str,
    device_type: str,
) -> None:
    binding = _binding(device_model=device_model, device_type=device_type)

    assert binding.matches_reviewed_firmware
    assert not binding.matches_reviewed_device
    assert not binding.ready_for_reviewed_profile
    with pytest.raises(ApplicationBootstrapError, match="does not attest"):
        build_shadow_bootstrap(
            _authority(),
            binding,
            epoch_seconds=1_752_680_000,
            timezone_name="Europe/Moscow",
        )
    with pytest.raises(ApplicationBootstrapError, match="does not attest"):
        build_canonical_post_start_observation(_authority(), binding)


def test_response_barrier_orchestrator_emits_each_retained_batch_once() -> None:
    orchestrator = ShadowApplicationBootstrapOrchestrator(
        _authority(),
        epoch_seconds=1_752_680_000,
        timezone_name="Europe/Moscow",
    )

    identity = orchestrator.next_batch()
    assert [request.ordinal for request in identity] == [1]
    with pytest.raises(ApplicationBootstrapError, match="already issued"):
        orchestrator.next_batch()
    orchestrator.accept_response(
        "bootstrap:1:DeviceInfoRequest",
        _device_info_response(),
    )

    initial_reads = orchestrator.next_batch()
    assert [request.ordinal for request in initial_reads] == [2, 3, 4, 5, 6]
    assert [request.response_required for request in initial_reads] == [
        False,
        True,
        True,
        True,
        True,
    ]
    device_info = initial_reads[3]
    orchestrator.accept_response(
        "bootstrap:5:DeviceInfoRequest",
        _device_info_response(session_id=device_info.session_id),
    )
    with pytest.raises(ApplicationBootstrapError, match="already issued"):
        orchestrator.next_batch()
    for request in (initial_reads[4], initial_reads[1], initial_reads[2]):
        orchestrator.accept_response(
            f"bootstrap:{request.ordinal}:{request.message_type}",
            _generic_response(request.session_id),
        )

    ntrip = orchestrator.next_batch()
    assert [request.ordinal for request in ntrip] == [7]
    orchestrator.accept_response(
        "bootstrap:7:GetNtripProfileRequest",
        _generic_response(ntrip[0].session_id),
    )

    final_reads = orchestrator.next_batch()
    assert [request.ordinal for request in final_reads] == [8, 9, 10]
    for request in final_reads:
        payload = (
            _device_info_response(session_id=request.session_id)
            if request.message_type == "DeviceInfoRequest"
            else _generic_response(request.session_id)
        )
        orchestrator.accept_response(
            f"bootstrap:{request.ordinal}:{request.message_type}",
            payload,
        )

    snapshot = orchestrator.snapshot()
    assert snapshot.bootstrap_complete
    assert snapshot.live_binding_observed
    assert snapshot.pending_response_topics == ()
    assert snapshot.executable is False
    assert snapshot.vendor_writes_enabled is False
    with pytest.raises(ApplicationBootstrapError, match="already complete"):
        orchestrator.next_batch()


def test_orchestrator_rejects_unexpected_response_without_advancing() -> None:
    orchestrator = ShadowApplicationBootstrapOrchestrator(
        _authority(),
        epoch_seconds=1_752_680_000,
        timezone_name="Europe/Moscow",
    )
    orchestrator.next_batch()

    with pytest.raises(ApplicationBootstrapError, match="not required"):
        orchestrator.accept_response(
            "bootstrap:3:GetRtkAdvanceRequest",
            _generic_response(":GetRtkAdvanceRequest"),
        )

    assert orchestrator.snapshot().current_batch == 1
    assert orchestrator.snapshot().batch_issued


def test_application_authority_requires_exact_captured_profile_length() -> None:
    with pytest.raises(ApplicationBootstrapError, match="36-byte"):
        ApplicationControlAuthority(openapi_key="too-short")
