import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { after, before, test } from "node:test";

import { createServer } from "vite";

let server;
let decodeObservationWorkspaceLayoutProfile;
let encodeObservationWorkspaceLayoutProfile;
let fetchObservationWorkspaceLayoutProfile;
let normalizeObservationWindowRect;
let projectObservationLayoutSnapshot;
let saveObservationWorkspaceLayoutProfile;
let WorkspaceLayoutApiError;
let WorkspaceLayoutContractError;
let observationPresentationSourceAfterLayoutApply;
let visibleSourceIdsAfterRecordedCatalogActivation;

before(async () => {
  server = await createServer({
    appType: "custom",
    logLevel: "silent",
    server: { middlewareMode: true },
  });
  ({
    decodeObservationWorkspaceLayoutProfile,
    encodeObservationWorkspaceLayoutProfile,
    fetchObservationWorkspaceLayoutProfile,
    normalizeObservationWindowRect,
    projectObservationLayoutSnapshot,
    saveObservationWorkspaceLayoutProfile,
    WorkspaceLayoutApiError,
    WorkspaceLayoutContractError,
  } = await server.ssrLoadModule("/src/core/observation/workspaceLayout.ts"));
  ({
    observationPresentationSourceAfterLayoutApply,
    visibleSourceIdsAfterRecordedCatalogActivation,
  } = await server.ssrLoadModule("/src/core/observation/useObservationLayout.ts"));
});

after(async () => {
  await server?.close();
});

function wireProfile(overrides = {}) {
  return {
    version: 2,
    revision: 7,
    workspace_id: "observation.spatial",
    scene_settings: {
      projection: "3d",
      point_size: 2.5,
      color_mode: "intensity",
      palette: "turbo",
      custom_color: "#35d7c1",
      accumulation_seconds: 12,
      show_points: true,
      show_trajectory: true,
      show_grid: true,
      show_labels: false,
      show_camera_frustums: true,
    },
    visible_source_ids: ["spatial.point-cloud.live", "camera.left"],
    active_floating_source_id: "camera.left",
    window_rects: {
      "camera.left": { x: 0.1, y: 0.1, width: 0.4, height: 0.4 },
      "camera.right": { x: 0.55, y: 0.1, width: 0.4, height: 0.4 },
    },
    viewport_size: { width: 1_000, height: 500 },
    ...overrides,
  };
}

test("workspace layout v2 excludes transient tool-window state", () => {
  const wire = wireProfile();
  const profile = decodeObservationWorkspaceLayoutProfile(wire);

  assert.deepEqual(profile.visibleSourceIds, ["spatial.point-cloud.live", "camera.left"]);
  assert.equal(profile.sceneSettings.pointSize, 2.5);
  assert.deepEqual(encodeObservationWorkspaceLayoutProfile(profile), wire);
  assert.equal("tool_windows" in encodeObservationWorkspaceLayoutProfile(profile), false);
});

test("workspace layout rejects unknown fields, transient transport data and invalid schema values", () => {
  assert.throws(
    () => decodeObservationWorkspaceLayoutProfile({
      ...wireProfile(),
      source_url: "ws://192.168.68.52:9877",
    }),
    WorkspaceLayoutContractError,
  );
  assert.throws(
    () => decodeObservationWorkspaceLayoutProfile(wireProfile({ version: 1 })),
    /Неподдерживаемая версия/,
  );
  assert.throws(
    () => decodeObservationWorkspaceLayoutProfile(wireProfile({ revision: 1.5 })),
    /revision/,
  );
  assert.throws(
    () => decodeObservationWorkspaceLayoutProfile(wireProfile({
      visible_source_ids: ["../camera"],
      active_floating_source_id: null,
    })),
    /стабильный идентификатор/,
  );
  assert.throws(
    () => decodeObservationWorkspaceLayoutProfile(wireProfile({
      window_rects: { "camera.left": { x: 0.8, y: 0, width: 0.4, height: 0.5 } },
    })),
    /выходит за нормализованные границы/,
  );
  assert.throws(
    () => decodeObservationWorkspaceLayoutProfile({
      ...wireProfile(),
      tool_windows: {
        sources_open: true,
        display_open: true,
        layers_open: true,
        order: ["sources", "display", "layers"],
      },
    }),
    /неверная схема/,
  );
  assert.throws(
    () => decodeObservationWorkspaceLayoutProfile(wireProfile({
      scene_settings: { ...wireProfile().scene_settings, point_size: Number.POSITIVE_INFINITY },
    })),
    /point_size/,
  );
});

test("restored desired layout survives an empty catalog and reveals only known stable sources later", () => {
  const profile = decodeObservationWorkspaceLayoutProfile(wireProfile());
  const snapshot = {
    visibleSourceIds: profile.visibleSourceIds,
    activeFloatingSourceId: profile.activeFloatingSourceId,
    windowRects: profile.windowRects,
    viewportSize: profile.viewportSize,
  };

  const empty = projectObservationLayoutSnapshot(snapshot, new Set(), { width: 500, height: 1_000 });
  assert.deepEqual(empty, {
    visibleSourceIds: [],
    activeFloatingSourceId: null,
    windowRects: {},
  });
  assert.deepEqual(snapshot.visibleSourceIds, ["spatial.point-cloud.live", "camera.left"]);

  const cameraAppeared = projectObservationLayoutSnapshot(
    snapshot,
    new Set(["camera.left"]),
    { width: 500, height: 1_000 },
  );
  assert.deepEqual(cameraAppeared.visibleSourceIds, ["camera.left"]);
  assert.equal(cameraAppeared.activeFloatingSourceId, "camera.left");
  assert.deepEqual(cameraAppeared.windowRects["camera.left"], {
    x: 50,
    y: 100,
    width: 200,
    height: 400,
  });
});

test("window rectangles normalize once and project proportionally into a different viewport", () => {
  assert.deepEqual(
    normalizeObservationWindowRect(
      { x: 100, y: 50, width: 400, height: 200 },
      { width: 1_000, height: 500 },
    ),
    { x: 0.1, y: 0.1, width: 0.4, height: 0.4 },
  );
});

test("fullscreen presentation survives the viewport resize it causes", () => {
  assert.equal(
    observationPresentationSourceAfterLayoutApply("camera.left", "preserve"),
    "camera.left",
  );
  assert.equal(
    observationPresentationSourceAfterLayoutApply("camera.left", "reset"),
    null,
  );
});

test("opening a recorded catalog reveals its sealed cameras beside the point cloud", () => {
  const source = (id, modality, transport = "recording") => ({
    id,
    modality,
    transport,
    availability: "available",
    previewUrl: null,
    delivery: modality === "video" ? { kind: "recorded-fmp4-manifest" } : null,
    activation: null,
    capabilities: {
      defaultVisible: true,
      overlay: modality === "video",
    },
  });
  const sources = [
    source("recorded.spatial.primary", "point-cloud"),
    source("recorded.camera.left", "video"),
    source("recorded.camera.right", "video"),
    source("live.camera", "video", "websocket"),
  ];

  assert.deepEqual(
    visibleSourceIdsAfterRecordedCatalogActivation(
      ["recorded.spatial.primary"],
      sources,
    ),
    ["recorded.spatial.primary", "recorded.camera.left", "recorded.camera.right"],
  );
});

test("workspace layout API uses the canonical endpoint and optimistic revision", async () => {
  const calls = [];
  const current = decodeObservationWorkspaceLayoutProfile(wireProfile());
  const saved = await saveObservationWorkspaceLayoutProfile(current, {
    fetcher: async (input, init) => {
      calls.push({ input: String(input), init, body: JSON.parse(String(init.body)) });
      return new Response(JSON.stringify(wireProfile({ revision: 8 })), {
        status: 200,
        headers: { "Content-Type": "application/json" },
      });
    },
  });

  assert.equal(calls.length, 1);
  assert.equal(calls[0].input, "/api/v1/workspace-layouts/observation.spatial");
  assert.equal(calls[0].init.method, "PUT");
  assert.equal(calls[0].init.headers["If-Match"], '"7"');
  assert.equal(calls[0].body.revision, 7);
  assert.equal(calls[0].body.workspace_id, "observation.spatial");
  assert.equal(saved.revision, 8);
});

test("workspace layout API treats 404 as no profile and exposes revision conflicts", async () => {
  assert.equal(
    await fetchObservationWorkspaceLayoutProfile({
      fetcher: async () => new Response(null, { status: 404 }),
    }),
    null,
  );

  const current = decodeObservationWorkspaceLayoutProfile(wireProfile());
  await assert.rejects(
    saveObservationWorkspaceLayoutProfile(current, {
      fetcher: async () => new Response(JSON.stringify({ detail: "revision mismatch" }), {
        status: 412,
        headers: { "Content-Type": "application/json" },
      }),
    }),
    (error) => error instanceof WorkspaceLayoutApiError &&
      error.conflict && error.status === 412 && error.message === "revision mismatch",
  );
});

test("scene tool windows are route-scoped and toolbar actions are not duplicated", async () => {
  const appSource = await readFile(new URL("../src/App.tsx", import.meta.url), "utf8");
  const utilityStart = appSource.indexOf("const contentActions");
  const utilityEnd = appSource.indexOf("const header", utilityStart);
  const utilitySource = appSource.slice(utilityStart, utilityEnd);

  assert.match(appSource, /open=\{spatialWorkspaceActive && sourceWindowOpen\}/);
  assert.match(appSource, /open=\{spatialWorkspaceActive && displayWindowOpen\}/);
  assert.match(appSource, /open=\{spatialWorkspaceActive && layerInspectorOpen\}/);
  assert.match(
    appSource,
    /if \(spatialWorkspaceActive\) return;[\s\S]*setSceneWindowOrder\(\[\]\)/,
  );
  assert.doesNotMatch(utilitySource, /Настроить визуальный движок/);
  assert.doesNotMatch(utilitySource, /Настроить отображение/);
  assert.doesNotMatch(utilitySource, /Открыть слои/);
});
