from __future__ import annotations

import json
from pathlib import Path
from typing import Any

import pytest

from k1link.web.plugin_catalog import DevicePluginCatalog, PluginCatalogError


def _manifest(plugin_id: str, model_id: str) -> dict[str, Any]:
    return {
        "apiVersion": "missioncore.nodedc/v1alpha1",
        "kind": "DevicePlugin",
        "metadata": {
            "id": plugin_id,
            "version": "0.1.0",
            "displayName": plugin_id,
        },
        "spec": {
            "hostApiRange": "v1alpha1",
            "runtime": {
                "backendEntrypoint": "example.plugin:adapter",
                "isolation": "transitional-in-process",
            },
            "permissions": [],
            "actions": [{"id": "state.read", "mutating": False, "secretFields": []}],
            "models": [
                {
                    "id": model_id,
                    "vendor": "Example",
                    "displayName": model_id,
                    "category": "Synthetic",
                    "description": "Synthetic contract fixture.",
                    "verified": False,
                    "capabilities": [],
                    "ui": {
                        "slot": "device.connection",
                        "componentKey": "synthetic.connection",
                    },
                }
            ],
        },
    }


def _v1alpha2_manifest(plugin_id: str, model_id: str) -> dict[str, Any]:
    document = _manifest(plugin_id, model_id)
    document["apiVersion"] = "missioncore.nodedc/v1alpha2"
    document["metadata"]["version"] = "0.2.0"
    document["spec"]["hostApiRange"] = "v1alpha2"
    document["spec"]["compatibilityProfiles"] = [
        {
            "profileId": f"{model_id}.fw-1.direct-lan.v1",
            "path": "profiles/fw-1/direct-lan.v1.json",
            "modelId": model_id,
        }
    ]
    return document


def _write_manifest(root: Path, directory: str, document: dict[str, Any]) -> None:
    target = root / "plugins" / directory / "plugin.manifest.json"
    target.parent.mkdir(parents=True)
    target.write_text(json.dumps(document), encoding="utf-8")


def _write_profile(
    root: Path,
    directory: str,
    profile_id: str,
    relative_path: str = "profiles/fw-1/direct-lan.v1.json",
) -> None:
    target = root / "plugins" / directory / relative_path
    target.parent.mkdir(parents=True, exist_ok=True)
    target.write_text(json.dumps({"profile_id": profile_id}), encoding="utf-8")


def test_repository_catalog_exposes_xgrids_model() -> None:
    repository_root = Path(__file__).resolve().parents[1]
    catalog = DevicePluginCatalog(repository_root)

    plugins = catalog.plugin_documents()
    models = catalog.model_documents()

    plugin = next(
        item for item in plugins if item["metadata"]["id"] == "nodedc.device.xgrids-lixelkity-k1"
    )
    assert plugin["apiVersion"] == "missioncore.nodedc/v1alpha2"
    assert plugin["metadata"]["version"] == "0.6.0"
    assert plugin["spec"]["hostApiRange"] == "v1alpha2"
    assert plugin["spec"]["compatibilityProfiles"] == [
        {
            "profileId": "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
            "path": "profiles/fw-3.0.2/local-network.v2.json",
            "modelId": "xgrids.lixelkity-k1",
        }
    ]
    action_ids = {action["id"] for action in plugin["spec"]["actions"]}
    assert {
        "device.inspect",
        "sensor.catalog.read",
        "calibration.device-snapshot.read",
        "connection.verify",
        "acquisition.prepare",
        "acquisition.start",
        "acquisition.stop",
        "acquisition.abort",
        "acquisition.state.read",
    } <= action_ids
    assert {
        "stream.start-live",
        "stream.start-replay",
        "stream.stop",
        "camera.preview.select",
        "camera.preview.stop",
        "viewer.settings.update",
        "application-control.shadow-state",
        "application-control.shadow-arm",
        "application-control.shadow-disarm",
        "application-control.session.open",
        "application-control.workspace.enter",
        "application-control.session.close",
    } <= action_ids
    assert next(item for item in models if item["id"] == "xgrids.lixelkity-k1") == {
        "pluginId": "nodedc.device.xgrids-lixelkity-k1",
        "pluginVersion": "0.6.0",
        "id": "xgrids.lixelkity-k1",
        "vendor": "XGRIDS",
        "displayName": "XGRIDS LixelKity K1",
        "category": "Мобильный лидарный сканер",
        "description": (
            "Локальный профиль Bridge, Quick Connect и Direct Connect: "
            "BLE/CoreWLAN, MQTT, камеры и raw-first запись."
        ),
        "verified": True,
        "capabilities": [
            {"id": "device.discovery.ble", "label": "Поиск BLE"},
            {
                "id": "device.provisioning.wifi-over-ble",
                "label": "Wi-Fi через BLE",
            },
            {
                "id": "host.network.wifi-associate-local",
                "label": "Подключение к точке доступа K1",
            },
            {"id": "spatial.point-cloud.live", "label": "Облако точек"},
            {"id": "spatial.pose.live", "label": "Траектория"},
            {"id": "device.modeling.live", "label": "Метрики маршрута"},
            {"id": "device.status.live", "label": "Состояние сканирования"},
            {"id": "camera.preview.live", "label": "Видеокамеры"},
            {"id": "evidence.raw-capture", "label": "Исходная запись"},
            {"id": "evidence.replay", "label": "Повтор записи"},
        ],
        "ui": {
            "slot": "device.connection",
            "componentKey": "xgrids-k1.connection",
        },
    }


def test_catalog_accepts_multiple_distinct_plugins_and_models(tmp_path: Path) -> None:
    _write_manifest(tmp_path, "first", _manifest("example.first", "example.first-model"))
    _write_manifest(tmp_path, "second", _manifest("example.second", "example.second-model"))

    catalog = DevicePluginCatalog(tmp_path)

    assert {item["metadata"]["id"] for item in catalog.plugin_documents()} == {
        "example.first",
        "example.second",
    }
    assert {item["id"] for item in catalog.model_documents()} == {
        "example.first-model",
        "example.second-model",
    }


def test_catalog_accepts_v1alpha2_profile_link(tmp_path: Path) -> None:
    document = _v1alpha2_manifest("example.next", "example.next-model")
    profile_id = document["spec"]["compatibilityProfiles"][0]["profileId"]
    _write_manifest(tmp_path, "next", document)
    _write_profile(tmp_path, "next", profile_id)

    plugin = DevicePluginCatalog(tmp_path).plugin_documents()[0]

    assert plugin["apiVersion"] == "missioncore.nodedc/v1alpha2"
    assert plugin["spec"]["compatibilityProfiles"][0]["profileId"] == profile_id


@pytest.mark.parametrize(
    "mutate",
    [
        lambda document: document["metadata"].update({"id": "invalid plugin"}),
        lambda document: document["spec"]["permissions"].append("invalid permission"),
        lambda document: document["spec"]["actions"][0].update({"id": "invalid action"}),
        lambda document: document["spec"]["models"][0].update({"id": "invalid model"}),
        lambda document: document["spec"]["compatibilityProfiles"][0].update(
            {"profileId": "invalid profile"}
        ),
    ],
)
def test_v1alpha2_rejects_non_identifier_contract_fields(
    tmp_path: Path,
    mutate: Any,
) -> None:
    document = _v1alpha2_manifest("example.strict", "example.strict-model")
    mutate(document)
    _write_manifest(tmp_path, "strict", document)

    with pytest.raises(PluginCatalogError, match="v1alpha2 identifier"):
        DevicePluginCatalog(tmp_path).manifests()


def test_v1alpha1_keeps_legacy_nonblank_identifier_compatibility(tmp_path: Path) -> None:
    document = _manifest("legacy plugin", "legacy model")
    _write_manifest(tmp_path, "legacy", document)

    plugin = DevicePluginCatalog(tmp_path).plugin_documents()[0]

    assert plugin["metadata"]["id"] == "legacy plugin"
    assert plugin["spec"]["models"][0]["id"] == "legacy model"


def test_catalog_accepts_multiple_profiled_models_in_v1alpha2(tmp_path: Path) -> None:
    document = _v1alpha2_manifest("example.family", "example.model-a")
    model_b = {
        **document["spec"]["models"][0],
        "id": "example.model-b",
        "displayName": "Example model B",
    }
    document["spec"]["models"].append(model_b)
    profile_b = "example.model-b.fw-1.direct-lan.v1"
    document["spec"]["compatibilityProfiles"].append(
        {
            "profileId": profile_b,
            "path": "profiles/fw-1/model-b.v1.json",
            "modelId": "example.model-b",
        }
    )
    _write_manifest(tmp_path, "family", document)
    profile_a = document["spec"]["compatibilityProfiles"][0]["profileId"]
    _write_profile(tmp_path, "family", profile_a)
    _write_profile(
        tmp_path,
        "family",
        profile_b,
        relative_path="profiles/fw-1/model-b.v1.json",
    )

    plugin = DevicePluginCatalog(tmp_path).plugin_documents()[0]

    assert [model["id"] for model in plugin["spec"]["models"]] == [
        "example.model-a",
        "example.model-b",
    ]


def test_catalog_rejects_api_and_host_contract_mismatch(tmp_path: Path) -> None:
    document = _manifest("example.mismatch", "example.mismatch-model")
    document["spec"]["hostApiRange"] = "v1alpha2"
    _write_manifest(tmp_path, "mismatch", document)

    with pytest.raises(PluginCatalogError, match="Invalid device-plugin manifest"):
        DevicePluginCatalog(tmp_path).manifests()


def test_v1alpha2_catalog_rejects_missing_profile_file(tmp_path: Path) -> None:
    document = _v1alpha2_manifest("example.missing", "example.missing-model")
    _write_manifest(tmp_path, "missing", document)

    with pytest.raises(PluginCatalogError, match="Compatibility profile does not exist"):
        DevicePluginCatalog(tmp_path).manifests()


def test_v1alpha2_catalog_rejects_profile_path_outside_plugin(tmp_path: Path) -> None:
    document = _v1alpha2_manifest("example.escape", "example.escape-model")
    profile_id = document["spec"]["compatibilityProfiles"][0]["profileId"]
    document["spec"]["compatibilityProfiles"][0]["path"] = "../outside.json"
    outside = tmp_path / "plugins" / "outside.json"
    outside.parent.mkdir(parents=True, exist_ok=True)
    outside.write_text(json.dumps({"profile_id": profile_id}), encoding="utf-8")
    _write_manifest(tmp_path, "escape", document)

    with pytest.raises(PluginCatalogError, match="escapes plugin directory"):
        DevicePluginCatalog(tmp_path).manifests()


def test_v1alpha2_catalog_rejects_profile_id_mismatch(tmp_path: Path) -> None:
    document = _v1alpha2_manifest("example.mismatch", "example.mismatch-model")
    _write_manifest(tmp_path, "mismatch", document)
    _write_profile(tmp_path, "mismatch", "different.profile.id")

    with pytest.raises(PluginCatalogError, match="Compatibility profile id mismatch"):
        DevicePluginCatalog(tmp_path).manifests()


def test_v1alpha2_catalog_rejects_duplicate_profile_json_keys(tmp_path: Path) -> None:
    document = _v1alpha2_manifest("example.duplicate", "example.duplicate-model")
    profile_id = document["spec"]["compatibilityProfiles"][0]["profileId"]
    _write_manifest(tmp_path, "duplicate", document)
    target = tmp_path / "plugins" / "duplicate" / "profiles" / "fw-1" / "direct-lan.v1.json"
    target.parent.mkdir(parents=True, exist_ok=True)
    target.write_text(
        f'{{"profile_id": "{profile_id}", "profile_id": "{profile_id}"}}',
        encoding="utf-8",
    )

    with pytest.raises(PluginCatalogError, match="Duplicate JSON key"):
        DevicePluginCatalog(tmp_path).manifests()


def test_v1alpha2_catalog_rejects_profile_for_unknown_model(tmp_path: Path) -> None:
    document = _v1alpha2_manifest("example.unknown", "example.known-model")
    link = document["spec"]["compatibilityProfiles"][0]
    profile_id = link["profileId"]
    link["modelId"] = "example.unknown-model"
    _write_manifest(tmp_path, "unknown", document)
    _write_profile(tmp_path, "unknown", profile_id)

    with pytest.raises(PluginCatalogError, match="references unknown model"):
        DevicePluginCatalog(tmp_path).manifests()


def test_catalog_rejects_duplicate_model_ids(tmp_path: Path) -> None:
    _write_manifest(tmp_path, "first", _manifest("example.first", "example.model"))
    _write_manifest(tmp_path, "second", _manifest("example.second", "example.model"))

    with pytest.raises(PluginCatalogError, match="Duplicate device-model id"):
        DevicePluginCatalog(tmp_path).manifests()


def test_catalog_rejects_unknown_contract_version(tmp_path: Path) -> None:
    document = _manifest("example.invalid", "example.invalid-model")
    document["apiVersion"] = "missioncore.nodedc/v999"
    _write_manifest(tmp_path, "invalid", document)

    with pytest.raises(PluginCatalogError, match="Invalid device-plugin manifest"):
        DevicePluginCatalog(tmp_path).manifests()


def test_v1alpha_catalog_rejects_multiple_models_and_process_isolation(
    tmp_path: Path,
) -> None:
    multiple_models = _manifest("example.multi", "example.model-a")
    model_a = multiple_models["spec"]["models"][0]
    multiple_models["spec"]["models"].append({**model_a, "id": "example.model-b"})
    _write_manifest(tmp_path, "multi", multiple_models)

    with pytest.raises(PluginCatalogError, match="Invalid device-plugin manifest"):
        DevicePluginCatalog(tmp_path).manifests()

    process_root = tmp_path / "process"
    process_manifest = _manifest("example.process", "example.process-model")
    process_manifest["spec"]["runtime"]["isolation"] = "process"
    _write_manifest(process_root, "process", process_manifest)

    with pytest.raises(PluginCatalogError, match="Invalid device-plugin manifest"):
        DevicePluginCatalog(process_root).manifests()


def test_catalog_requires_safe_state_read_action(tmp_path: Path) -> None:
    document = _manifest("example.unsafe", "example.unsafe-model")
    document["spec"]["actions"] = [{"id": "state.read", "mutating": True, "secretFields": []}]
    _write_manifest(tmp_path, "unsafe", document)

    with pytest.raises(PluginCatalogError, match="must declare safe state.read"):
        DevicePluginCatalog(tmp_path).manifests()


@pytest.mark.parametrize(
    ("mutate", "invalid_value"),
    [
        (lambda document, value: document["spec"]["permissions"].append(value), " "),
        (
            lambda document, value: document["spec"]["actions"][0]["secretFields"].append(value),
            "",
        ),
        (
            lambda document, value: document["spec"]["models"][0].update({"description": value}),
            "x" * 1025,
        ),
    ],
)
def test_catalog_rejects_strings_that_frontend_manifest_parser_rejects(
    tmp_path: Path,
    mutate: Any,
    invalid_value: str,
) -> None:
    document = _manifest("example.invalid", "example.invalid-model")
    mutate(document, invalid_value)
    _write_manifest(tmp_path, "invalid", document)

    with pytest.raises(PluginCatalogError, match="Invalid device-plugin manifest"):
        DevicePluginCatalog(tmp_path).manifests()
