import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import { buildMapSubjectCardModel, DEFAULT_MAP_SUBJECT_DETAIL_PROFILE, normalizeTelemetryReadings } from "../apps/catalog/src/mapSubjectCard.mjs";

const fact = {
  sourceId: "gelios-unit-555590",
  semanticType: "map.moving_object",
  presentationStatus: "current",
  observedAt: "2026-07-22T08:42:03.000Z",
  receivedAt: "2026-07-22T08:42:05.000Z",
  geometry: { type: "Point", coordinates: [37.6176, 55.7558] },
  attributes: {
    display_name: "555590 B2 867236078012345",
    signal_state: "active",
    movement_state: "moving",
    speed_kph: 17.4,
    course_degrees: 211,
    elevation_meters: 142,
    satellite_count: 12,
    hdop: 0.7,
    horizontal_accuracy_meters: 3.5,
    mileage_km: 4832.25,
    engine_hours: 912.5,
    object_kind: "trike",
    position_source: "gelios",
    sensor_readings: [
      { id: "sensor.temperature", label: "Температура", value: 21.5, unit: "°C", observedAt: "2026-07-22T08:42:02.000Z" },
    ],
  },
};

test("subject card exposes the full provider-neutral telemetry projection", () => {
  const profile = structuredClone(DEFAULT_MAP_SUBJECT_DETAIL_PROFILE);
  profile.tabs.find((tab) => tab.id === "telemetry").sections[0].fields[0].allowedReadingIds = ["sensor.temperature"];
  const card = buildMapSubjectCardModel(fact, {
    bindingId: "trike-current-positions",
    dataProductId: "fleet.positions.current.v5",
    profile,
  });

  assert.ok(card);
  assert.equal(card.title, "555590 B2 867236078012345");
  assert.equal(card.sourceId, fact.sourceId);
  assert.equal(card.defaultTabId, "overview");
  assert.deepEqual(card.tabs.map((tab) => tab.id), ["overview", "position", "telemetry", "counters", "equipment", "settings", "maintenance", "diagnostics"]);
  const values = new Map(card.tabs.flatMap((tab) => tab.sections.flatMap((section) => section.rows.map((row) => [row.key, row.value]))));
  assert.equal(values.get("diagnostics.source-id"), fact.sourceId);
  assert.equal(values.get("diagnostics.product"), "fleet.positions.current.v5");
  assert.equal(values.get("position.latitude"), "55,75580");
  assert.equal(values.get("position.longitude"), "37,61760");
  assert.equal(values.get("overview.signal"), "На связи");
  assert.equal(values.get("overview.movement"), "В движении");
  assert.equal(values.get("overview.speed"), "17,4 км/ч");
  assert.equal(values.get("counters.mileage"), "4 832,25 км");
  assert.equal(values.get("counters.engine-hours"), "912,5 ч");
  const readings = card.tabs.find((tab) => tab.id === "telemetry").sections[0].readings;
  assert.equal(readings[0]?.value, "21,5 °C");
  assert.equal(card.tabs.find((tab) => tab.id === "equipment").empty, true);
});

test("sensor readings remain bounded and reject duplicate, complex and restricted values", () => {
  const readings = normalizeTelemetryReadings([
    { id: "sensor.voltage", label: "Напряжение", value: 48.2, unit: "V" },
    { id: "sensor.voltage", label: "Повтор", value: 49 },
    { id: "sensor.imei", label: "IMEI", value: "forbidden" },
    { id: "sensor.payload", label: "Payload", value: { nested: true } },
    { id: "Sensor Bad", label: "Bad id", value: 1 },
    { id: "sensor.account", label: "Аккаунт", value: "+79991234567" },
  ], ["sensor.voltage", "sensor.imei", "sensor.payload", "sensor.account"]);

  assert.deepEqual(readings, [{ id: "sensor.voltage", label: "Напряжение", value: "48,2 V", observedAt: undefined }]);
});

test("card model never renders provider secrets or direct identifiers from attributes", () => {
  const card = buildMapSubjectCardModel({
    ...fact,
    attributes: {
      ...fact.attributes,
      api_key: "forbidden",
      imei: "forbidden",
      phone: "+70000000000",
      raw_params: "forbidden",
    },
  });
  const keys = card.tabs.flatMap((tab) => tab.sections.flatMap((section) => section.rows.map((row) => row.key)));
  assert.ok(!keys.includes("api_key"));
  assert.ok(!keys.includes("imei"));
  assert.ok(!keys.includes("phone"));
  assert.ok(!keys.includes("raw_params"));
});

test("unknown fields are fail-closed instead of becoming an automatic Other section", () => {
  const card = buildMapSubjectCardModel({
    ...fact,
    attributes: { ...fact.attributes, unclassified_payload: { arbitrary: true }, surprise_value: "must not render" },
  });
  const keys = card.tabs.flatMap((tab) => tab.sections.flatMap((section) => section.rows.map((row) => row.key)));
  assert.ok(!keys.includes("unclassified_payload"));
  assert.ok(!keys.includes("surprise_value"));
  assert.equal(card.tabs.some((tab) => tab.id === "other"), false);
});

test("subject details compose declared aspect bindings by stable source id", () => {
  const profile = {
    id: "map.subject-detail.composed",
    version: "1.0.0",
    title: "Composed",
    semanticTypes: ["map.moving_object"],
    defaultTabId: "equipment",
    tabs: [{
      id: "equipment",
      label: "Оборудование",
      emptyMessage: "Нет данных",
      sections: [{
        id: "identity",
        label: "Профиль",
        fields: [{ id: "equipment.kind", aspectId: "technical", source: "attribute", field: "equipment_kind", label: "Тип", format: "text" }],
      }],
    }],
  };
  const card = buildMapSubjectCardModel(fact, {
    profile,
    aspects: {
      technical: {
        bindingId: "trike-technical-profile",
        dataProductId: "fleet.technical-profile.current.v1",
        fact: { sourceId: fact.sourceId, semanticType: "asset.technical_profile", attributes: { equipment_kind: "cargo_trike" } },
      },
    },
  });
  assert.equal(card.tabs[0].sections[0].rows[0].value, "cargo_trike");
});

test("restricted joined aspects render declared identifiers and remain fail-closed otherwise", () => {
  const profile = {
    id: "map.subject-detail.restricted",
    version: "1.0.0",
    title: "Restricted",
    semanticTypes: ["map.moving_object"],
    defaultTabId: "contacts",
    tabs: [{
      id: "contacts",
      label: "Контакты",
      emptyMessage: "Нет данных",
      sections: [{
        id: "device",
        label: "Контакты устройства",
        fields: [
          {
            id: "contacts.phone",
            aspectId: "unit_contacts",
            source: "attribute",
            field: "device_phone_primary",
            label: "Телефон 1",
            format: "text",
            dataClass: "restricted",
          },
          {
            id: "contacts.imei",
            aspectId: "unit_contacts",
            source: "attribute",
            field: "device_imei",
            label: "IMEI",
            format: "text",
            dataClass: "restricted",
          },
        ],
      }],
    }],
  };
  const contactAspect = {
    bindingId: "trike-unit-contacts",
    dataProductId: "fleet.units.contacts.current.v1",
    dataClass: "restricted",
    fact: {
      sourceId: fact.sourceId,
      semanticType: "map.moving_object",
      attributes: {
        device_phone_primary: "+79991234567",
        device_imei: "867236078012345",
      },
    },
  };
  const card = buildMapSubjectCardModel(fact, {
    profile,
    aspects: { unit_contacts: contactAspect },
  });
  assert.deepEqual(
    card.tabs[0].sections[0].rows.map((row) => row.value),
    ["+79991234567", "867236078012345"],
  );
  const rejected = buildMapSubjectCardModel(fact, {
    profile,
    aspects: {
      unit_contacts: { ...contactAspect, dataClass: "operational" },
    },
  });
  assert.equal(rejected.tabs[0].empty, true);
});

test("restricted identity aspects render full identifiers and bounded string lists", () => {
  const profile = {
    id: "map.subject-detail.identity",
    version: "1.0.0",
    title: "Identity",
    semanticTypes: ["map.moving_object"],
    defaultTabId: "identity",
    tabs: [{
      id: "identity",
      label: "Идентификация",
      emptyMessage: "Нет данных",
      sections: [{
        id: "device",
        label: "Устройство и доступ",
        fields: [
          {
            id: "identity.imei",
            aspectId: "identity",
            source: "attribute",
            field: "device_imei",
            label: "IMEI",
            format: "text",
            dataClass: "restricted",
          },
          {
            id: "identity.users",
            aspectId: "identity",
            source: "attribute",
            field: "available_user_logins",
            label: "Доступно пользователям",
            format: "string_list",
            dataClass: "restricted",
          },
          {
            id: "identity.custom",
            aspectId: "identity",
            source: "attribute",
            field: "custom_fields",
            label: "Пользовательские поля",
            format: "string_list",
            dataClass: "restricted",
          },
        ],
      }],
    }],
  };
  const card = buildMapSubjectCardModel(fact, {
    profile,
    aspects: {
      identity: {
        bindingId: "trike-unit-identity",
        dataProductId: "fleet.units.identity.current.v1",
        dataClass: "restricted",
        fact: {
          sourceId: fact.sourceId,
          semanticType: "map.moving_object",
          attributes: {
            device_imei: "867236078012345",
            available_user_logins: ["vasya", "dispatcher"],
            custom_fields: ["Serial number=5015", "Battery number=8032"],
          },
        },
      },
    },
  });
  assert.deepEqual(
    card.tabs[0].sections[0].rows.map((row) => row.value),
    [
      "867236078012345",
      "vasya • dispatcher",
      "Serial number: 5015 • Battery number: 8032",
    ],
  );
});

test("Cesium pin and label share one entity selection that opens the subject card", async () => {
  const [renderer, preview] = await Promise.all([
    readFile(new URL("../apps/catalog/src/CesiumMapRenderer.tsx", import.meta.url), "utf8"),
    readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8"),
  ]);

  assert.match(renderer, /viewer\?\.scene\.pick/);
  assert.match(renderer, /pickedId instanceof Entity/);
  assert.match(renderer, /onSelectRef\.current\?\.\(pickedId\.id\)/);
  assert.match(preview, /const handleSelect = useCallback/);
  assert.match(preview, /setSubjectCardOpen\(true\)/);
  assert.match(preview, /title=\{selectedSubjectCard\.title\}/);
  assert.match(preview, /Карточка объекта:/);
  assert.match(preview, /SegmentedControl/);
});
