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 decodeObservationSessionCatalog;
let decodeObservationSessionReplay;
let decodeObservationSessionPreparation;
let fetchObservationSessionCatalog;
let deleteObservationSession;
let replayObservationSession;
let fetchObservationSessionPreparation;
let cancelObservationSessionPreparation;
let ObservationSessionApiError;
let ObservationSessionContractError;
let decodeObservationRecordedMediaManifest;
let createObservationReplayCoordinator;
let resolveObservationSessionReplay;
let waitForObservationReplayPreparation;
let storeObservationReplayPreparation;
let loadObservationReplayPreparation;
let observationSessionVisualState;
let observationSessionVisualLabel;

before(async () => {
  server = await createServer({
    appType: "custom",
    logLevel: "silent",
    server: { middlewareMode: true },
  });
  ({
    decodeObservationSessionCatalog,
    decodeObservationSessionReplay,
    decodeObservationSessionPreparation,
    fetchObservationSessionCatalog,
    deleteObservationSession,
    replayObservationSession,
    fetchObservationSessionPreparation,
    cancelObservationSessionPreparation,
    ObservationSessionApiError,
    ObservationSessionContractError,
    decodeObservationRecordedMediaManifest,
  } = await server.ssrLoadModule("/src/core/observation/sessionArchive.ts"));
  ({
    createObservationReplayCoordinator,
    resolveObservationSessionReplay,
    waitForObservationReplayPreparation,
    storeObservationReplayPreparation,
    loadObservationReplayPreparation,
  } = await server.ssrLoadModule(
    "/src/core/observation/useObservationSessions.ts",
  ));
  ({ observationSessionVisualState, observationSessionVisualLabel } = await server.ssrLoadModule(
    "/src/components/ObservationSessionSelect.tsx",
  ));
});

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

function session(overrides = {}) {
  return {
    id: "20260716T205632Z_viewer_live",
    label: "K1 · наблюдение 16 июля",
    started_at_utc: "2026-07-16T20:56:32.635Z",
    completed_at_utc: "2026-07-16T21:20:43.379Z",
    status: "ready",
    modalities: ["point-cloud", "pose"],
    duration_seconds: 1_450.744,
    replayable: true,
    ...overrides,
  };
}

function replay(overrides = {}) {
  const sessionId = overrides.session_id ?? "session-20260716T205632Z";
  const sourceUrl = overrides.source_url ??
    `/api/v1/observation-sessions/${encodeURIComponent(sessionId)}/recording.rrd`;
  const sha256 = overrides.sha256 ?? "a".repeat(64);
  return {
    schema_version: "missioncore.observation-session-replay/v2",
    launch: {
      kind: "rerun-recording",
      session_id: sessionId,
      source_url: sourceUrl,
      viewer_source_url: overrides.viewer_source_url ?? `${sourceUrl}?generation=${sha256}`,
      media_type: "application/vnd.rerun.rrd",
      timeline: "session_time",
      timeline_start_seconds: 0,
      timeline_end_seconds: 1_450.744,
      seekable: true,
      byte_length: 123_456,
      sha256,
      playback: { speed: 1, loop: false },
      media_sources: [],
      ...overrides,
    },
  };
}

function preparation(overrides = {}) {
  const sessionId = overrides.session_id ?? "session-20260716T205632Z";
  return {
    schema_version: "missioncore.observation-session-preparation/v1",
    preparation: {
      preparation_id: "prepare-20260717T131400Z",
      session_id: sessionId,
      state: "queued",
      progress: null,
      updated_at_utc: "2026-07-17T10:14:00Z",
      status_url: `/api/v1/observation-sessions/${encodeURIComponent(sessionId)}/recording-preparation`,
      cancellable: true,
      ...overrides,
    },
  };
}

const preparationEtag = '"prepare-20260717T131400Z"';

test("session catalog decodes canonical snake_case into a path-free camelCase model", () => {
  const catalog = decodeObservationSessionCatalog({ items: [session()] });

  assert.deepEqual(catalog.items, [{
    id: "20260716T205632Z_viewer_live",
    label: "K1 · наблюдение 16 июля",
    startedAtUtc: "2026-07-16T20:56:32.635Z",
    completedAtUtc: "2026-07-16T21:20:43.379Z",
    status: "ready",
    modalities: ["point-cloud", "pose"],
    durationSeconds: 1_450.744,
    replayable: true,
    preparation: null,
    lab: null,
  }]);
  assert.equal("raw_capture" in catalog.items[0], false);
  assert.equal("path" in catalog.items[0], false);
});

test("opened archive is named in the scene header and trash hover has no pill", async () => {
  const appSource = await readFile(new URL("../src/App.tsx", import.meta.url), "utf8");
  const styles = await readFile(
    new URL("../src/styles/observation-sessions.css", import.meta.url),
    "utf8",
  );
  const spatialControls = await readFile(
    new URL(
      "../../../plugins/xgrids-k1/frontend/src/components/K1SpatialControls.tsx",
      import.meta.url,
    ),
    "utf8",
  );
  const acquisitionPipeline = await readFile(
    new URL(
      "../../../plugins/xgrids-k1/frontend/src/components/K1AcquisitionPipeline.tsx",
      import.meta.url,
    ),
    "utf8",
  );

  assert.match(appSource, /setRecordedReplayLabel\(session\.label\)/);
  assert.match(appSource, /`\$\{activeDefinition\.title\}: \$\{recordedReplayLabel\}`/);
  const deleteStyle = styles.slice(
    styles.indexOf(".observation-session-option__delete"),
    styles.indexOf(".observation-session-option__delete:disabled"),
  );
  assert.match(deleteStyle, /background:\s*transparent/);
  assert.doesNotMatch(deleteStyle, /danger-rgb\) \/ 0\.12/);
  assert.doesNotMatch(spatialControls, /Индикатор постоянно зелёный/);
  assert.doesNotMatch(acquisitionPipeline, /Индикатор постоянно зелёный/);
});

test("session catalog exposes authoritative background preparation state", () => {
  const catalog = decodeObservationSessionCatalog({
    items: [session({
      preparation: {
        preparation_id: "prepare-catalog-1",
        state: "exporting",
        progress: 0.42,
        updated_at_utc: "2026-07-17T10:14:00Z",
        cancellable: true,
        retryable: false,
      },
    })],
  });
  assert.deepEqual(catalog.items[0].preparation, {
    preparationId: "prepare-catalog-1",
    state: "exporting",
    progress: 0.42,
    updatedAtUtc: "2026-07-17T10:14:00Z",
    cancellable: true,
    retryable: false,
    error: null,
  });
});

test("saved-session rows distinguish durable, active, cold and failed states", () => {
  const makeDecoded = (state, overrides = {}) => decodeObservationSessionCatalog({
    items: [session({
      status: "interrupted",
      preparation: state === null ? null : {
        preparation_id: `prepare-${state}`,
        state,
        progress: state === "ready" ? 1 : 0.5,
        updated_at_utc: "2026-07-17T10:14:00Z",
        cancellable: ["queued", "validating", "exporting", "finalizing"].includes(state),
        retryable: state === "failed",
        ...(state === "failed" ? { error: "export failed" } : {}),
      },
      ...overrides,
    })],
  }).items[0];

  assert.equal(observationSessionVisualState(makeDecoded("ready")), "ready");
  for (const state of ["queued", "validating", "exporting", "finalizing"]) {
    assert.equal(observationSessionVisualState(makeDecoded(state)), "processing");
  }
  assert.equal(observationSessionVisualState(makeDecoded("failed")), "error");
  assert.equal(observationSessionVisualState(makeDecoded("cancelled")), "error");
  assert.equal(
    observationSessionVisualState(makeDecoded("failed", { status: "recording" })),
    "error",
  );
  assert.equal(observationSessionVisualState(makeDecoded(null)), "cold");
  assert.equal(
    observationSessionVisualState(makeDecoded(null, { status: "recording" })),
    "processing",
  );
  assert.equal(
    observationSessionVisualState(makeDecoded("ready", { replayable: false })),
    "error",
  );
  assert.equal(
    observationSessionVisualState(makeDecoded("ready"), { failed: true }),
    "error",
  );
  assert.equal(
    observationSessionVisualState(makeDecoded("failed"), { pending: true, failed: true }),
    "error",
  );
  assert.equal(observationSessionVisualLabel("ready"), "Готово");
  assert.equal(observationSessionVisualLabel("processing"), "Обработка");
  assert.equal(observationSessionVisualLabel("cold"), "Подготовить");
  assert.equal(observationSessionVisualLabel("error"), "Ошибка");
});

test("saved-session lamps use green or dim gray only, never warning or danger colors", async () => {
  const css = await readFile(
    new URL("../src/styles/observation-sessions.css", import.meta.url),
    "utf8",
  );
  const start = css.indexOf(".observation-session-option i {");
  const end = css.indexOf(".observation-session-option__state", start);
  assert.notEqual(start, -1);
  assert.notEqual(end, -1);
  const lampRules = css.slice(start, end);
  assert.match(lampRules, /data-session-visual-state="ready"[\s\S]*--nodedc-success-rgb/);
  assert.match(lampRules, /data-session-visual-state="processing"[\s\S]*animation:/);
  assert.match(lampRules, /data-session-visual-state="error"[\s\S]*--nodedc-text-muted/);
  assert.match(lampRules, /data-session-visual-state="error"[\s\S]*opacity:\s*0\.48/);
  assert.doesNotMatch(lampRules, /\b(?:warning|danger|yellow|red)\b/i);
});

test("session catalog sorts newest first and uses id as a deterministic tie-breaker", () => {
  const catalog = decodeObservationSessionCatalog({
    items: [
      session({ id: "same-b", started_at_utc: "2026-07-16T19:00:00Z", completed_at_utc: null }),
      session({ id: "newest", started_at_utc: "2026-07-17T00:00:00Z", completed_at_utc: null }),
      session({ id: "same-a", started_at_utc: "2026-07-16T19:00:00Z", completed_at_utc: null }),
    ],
  });

  assert.deepEqual(catalog.items.map(({ id }) => id), ["newest", "same-a", "same-b"]);
});

test("session catalog rejects raw storage fields, unsafe ids and duplicate ids", () => {
  assert.throws(
    () => decodeObservationSessionCatalog({
      items: [session({ raw_capture: "sessions/private/mqtt.raw.k1mqtt" })],
    }),
    ObservationSessionContractError,
  );
  assert.throws(
    () => decodeObservationSessionCatalog({ items: [session({ id: "../private" })] }),
    ObservationSessionContractError,
  );
  assert.throws(
    () => decodeObservationSessionCatalog({ items: [session(), session()] }),
    /повторяющийся id/,
  );
});

test("session catalog rejects invalid temporal, status and duration values", () => {
  assert.throws(
    () => decodeObservationSessionCatalog({
      items: [session({ started_at_utc: "16 July 2026" })],
    }),
    /ISO-датой/,
  );
  assert.throws(
    () => decodeObservationSessionCatalog({ items: [session({ status: "complete" })] }),
    /неизвестное состояние/,
  );
  assert.throws(
    () => decodeObservationSessionCatalog({ items: [session({ duration_seconds: -1 })] }),
    /неотрицательным числом/,
  );
  assert.throws(
    () => decodeObservationSessionCatalog({
      items: [session({ completed_at_utc: "2026-07-16T19:00:00Z" })],
    }),
    /раньше времени начала/,
  );
});

test("catalog API preserves HTTP detail without accepting a malformed success body", async () => {
  await assert.rejects(
    fetchObservationSessionCatalog({
      fetcher: async () => new Response(JSON.stringify({ detail: "storage unavailable" }), {
        status: 503,
        headers: { "Content-Type": "application/json" },
      }),
    }),
    (error) => error instanceof ObservationSessionApiError &&
      error.status === 503 && error.message === "storage unavailable",
  );

  await assert.rejects(
    fetchObservationSessionCatalog({
      fetcher: async () => new Response(JSON.stringify({ items: "not-an-array" }), {
        status: 200,
        headers: { "Content-Type": "application/json" },
      }),
    }),
    ObservationSessionContractError,
  );
});

test("delete API uses one opaque same-origin target and requires an empty 204", async () => {
  const calls = [];
  await deleteObservationSession("session-20260716T205632Z", {
    fetcher: async (input, init) => {
      calls.push({ input: String(input), init });
      return new Response(null, { status: 204 });
    },
  });

  assert.equal(calls.length, 1);
  assert.equal(
    calls[0].input,
    "/api/v1/observation-sessions/session-20260716T205632Z",
  );
  assert.equal(calls[0].init.method, "DELETE");
  await assert.rejects(
    deleteObservationSession("../private", { fetcher: async () => new Response(null) }),
    ObservationSessionContractError,
  );
  await assert.rejects(
    deleteObservationSession("session-20260716T205632Z", {
      fetcher: async () => new Response(JSON.stringify({ detail: "recording is open" }), {
        status: 409,
        headers: { "Content-Type": "application/json" },
      }),
    }),
    (error) => error instanceof ObservationSessionApiError &&
      error.status === 409 && error.message === "recording is open",
  );
});

test("replay API accepts only a same-origin seekable recording descriptor", async () => {
  const calls = [];
  const launch = await replayObservationSession("session-20260716T205632Z", {
    fetcher: async (input, init) => {
      calls.push({ input: String(input), init });
      return new Response(JSON.stringify(replay()), {
        status: 200,
        headers: { "Content-Type": "application/json" },
      });
    },
  });

  assert.equal(calls.length, 1);
  assert.equal(calls[0].input, "/api/v1/observation-sessions/session-20260716T205632Z/replay");
  assert.equal(calls[0].init.method, "POST");
  assert.equal(launch.kind, "ready");
  assert.equal(launch.launch.timeline, "session_time");
  assert.equal(
    launch.launch.sourceUrl,
    "/api/v1/observation-sessions/session-20260716T205632Z/recording.rrd",
  );
  await assert.rejects(
    replayObservationSession("../private", { fetcher: async () => new Response(null) }),
    ObservationSessionContractError,
  );
});

test("preparation contract is strict, session-scoped and same-origin", async () => {
  const sessionId = "session-20260716T205632Z";
  const decoded = decodeObservationSessionPreparation(preparation(), sessionId);
  assert.equal(decoded.sessionId, sessionId);
  assert.equal(decoded.state, "queued");
  assert.equal(decoded.progress, null);

  assert.throws(
    () => decodeObservationSessionPreparation(
      preparation({ status_url: "https://invalid.test/status" }),
      sessionId,
    ),
    /канонический same-origin/,
  );
  assert.throws(
    () => decodeObservationSessionPreparation(preparation({ debug_path: "/tmp/raw" }), sessionId),
    /неизвестные поля/,
  );
  assert.throws(
    () => decodeObservationSessionPreparation(preparation({ progress: 1.1 }), sessionId),
    /недопустимое число/,
  );
  await assert.rejects(
    replayObservationSession(sessionId, {
      fetcher: async () => new Response(JSON.stringify(preparation()), {
        status: 202,
        headers: { "Content-Type": "application/json" },
      }),
    }),
    /preparation ETag/,
  );
});

test("202 preparation polls through explicit phases and reveals launch only when ready", async () => {
  const sessionId = "session-20260716T205632Z";
  const calls = [];
  const phases = [];
  let poll = 0;
  let clock = 0;
  let previousViewerMounted = true;
  const launch = await resolveObservationSessionReplay(sessionId, {
    signal: new AbortController().signal,
    requestTimeoutMs: 1_000,
    heartbeatStallMs: 5_000,
    maximumWaitMs: 10_000,
    initialPollIntervalMs: 50,
    maximumPollIntervalMs: 50,
    now: () => clock,
    sleep: async (milliseconds) => {
      assert.equal(previousViewerMounted, true);
      clock += milliseconds;
    },
    onUpdate: (value) => {
      assert.equal(previousViewerMounted, true);
      phases.push([value.state, value.progress]);
    },
    fetcher: async (input, init) => {
      calls.push([String(input), init.method, new Headers(init.headers).get("If-Match")]);
      if (init.method === "POST") {
        return new Response(JSON.stringify(preparation()), {
          status: 202,
          headers: { "Content-Type": "application/json", ETag: preparationEtag },
        });
      }
      poll += 1;
      if (poll === 1) {
        return new Response(JSON.stringify(preparation({
          state: "exporting",
          progress: 0.5,
          updated_at_utc: "2026-07-17T10:14:01Z",
        })), {
          status: 202,
          headers: { "Content-Type": "application/json", ETag: preparationEtag },
        });
      }
      return new Response(JSON.stringify(replay({ session_id: sessionId })), {
        status: 200,
        headers: { "Content-Type": "application/json", ETag: preparationEtag },
      });
    },
  });
  // This is the point where App is allowed to unmount the old viewer.
  previousViewerMounted = false;
  assert.equal(launch.sessionId, sessionId);
  assert.deepEqual(phases, [["queued", null], ["exporting", 0.5]]);
  assert.deepEqual(calls.map((entry) => entry[1]), ["POST", "GET", "GET"]);
  assert.deepEqual(calls.map((entry) => entry[2]), [null, preparationEtag, preparationEtag]);
});

test("initial replay restore is not mistaken for a stalled preparation poll", async () => {
  const sessionId = "session-20260716T205632Z";
  let requestSignal;
  const launch = await resolveObservationSessionReplay(sessionId, {
    signal: new AbortController().signal,
    requestTimeoutMs: 100,
    fetcher: async (_input, init) => {
      requestSignal = init.signal;
      await new Promise((resolve, reject) => {
        const timer = setTimeout(resolve, 150);
        init.signal.addEventListener("abort", () => {
          clearTimeout(timer);
          reject(new DOMException("cancelled", "AbortError"));
        }, { once: true });
      });
      return new Response(JSON.stringify(replay({ session_id: sessionId })), {
        status: 200,
        headers: { "Content-Type": "application/json" },
      });
    },
  });
  assert.equal(requestSignal.aborted, false);
  assert.equal(launch.sessionId, sessionId);
});

test("status-ready response must match the preparation ETag before launch is accepted", async () => {
  const decoded = decodeObservationSessionPreparation(
    preparation({ state: "finalizing", progress: 0.95 }),
    "session-20260716T205632Z",
  );
  let receivedIfMatch;
  await assert.rejects(
    fetchObservationSessionPreparation(decoded, {
      fetcher: async (_input, init) => {
        receivedIfMatch = new Headers(init.headers).get("If-Match");
        return new Response(JSON.stringify(replay()), {
          status: 200,
          headers: { "Content-Type": "application/json", ETag: '"replacement-job"' },
        });
      },
    }),
    /актуальный preparation ETag/,
  );
  assert.equal(receivedIfMatch, preparationEtag);
});

test("preparation cancellation is conditional on the same preparation ETag", async () => {
  const decoded = decodeObservationSessionPreparation(
    preparation({ state: "exporting", progress: 0.5 }),
    "session-20260716T205632Z",
  );
  let request;
  const result = await cancelObservationSessionPreparation(decoded, {
    fetcher: async (input, init) => {
      request = {
        input: String(input),
        method: init.method,
        ifMatch: new Headers(init.headers).get("If-Match"),
      };
      return new Response(null, { status: 204 });
    },
  });
  assert.equal(result, null);
  assert.deepEqual(request, {
    input: decoded.statusUrl,
    method: "DELETE",
    ifMatch: preparationEtag,
  });
});

test("preparation polling fails explicitly when the server heartbeat stalls", async () => {
  const decoded = decodeObservationSessionPreparation(
    preparation({ state: "exporting", progress: 0.1 }),
    "session-20260716T205632Z",
  );
  let clock = 0;
  await assert.rejects(
    waitForObservationReplayPreparation(decoded, {
      signal: new AbortController().signal,
      requestTimeoutMs: 1_000,
      heartbeatStallMs: 250,
      maximumWaitMs: 5_000,
      initialPollIntervalMs: 150,
      maximumPollIntervalMs: 150,
      now: () => clock,
      sleep: async (milliseconds) => { clock += milliseconds; },
      fetcher: async () => new Response(JSON.stringify(preparation({
        state: "exporting",
        progress: 0.1,
      })), {
        status: 202,
        headers: { "Content-Type": "application/json", ETag: preparationEtag },
      }),
    }),
    /перестала обновляться/,
  );
});

test("queued preparation can wait behind the bounded backend worker without false heartbeat failure", async () => {
  const decoded = decodeObservationSessionPreparation(
    preparation({ state: "queued", progress: 0 }),
    "session-20260716T205632Z",
  );
  let clock = 0;
  let polls = 0;
  const launch = await waitForObservationReplayPreparation(decoded, {
    signal: new AbortController().signal,
    requestTimeoutMs: 1_000,
    heartbeatStallMs: 250,
    maximumWaitMs: 5_000,
    initialPollIntervalMs: 150,
    maximumPollIntervalMs: 150,
    now: () => clock,
    sleep: async (milliseconds) => { clock += milliseconds; },
    fetcher: async () => {
      polls += 1;
      if (polls <= 2) {
        return new Response(JSON.stringify(preparation({ state: "queued", progress: 0 })), {
          status: 202,
          headers: { "Content-Type": "application/json", ETag: preparationEtag },
        });
      }
      return new Response(JSON.stringify(replay()), {
        status: 200,
        headers: { "Content-Type": "application/json", ETag: preparationEtag },
      });
    },
  });
  assert.equal(clock, 450);
  assert.equal(launch.sessionId, "session-20260716T205632Z");
});

test("terminal preparation failure is explicit and retryable", async () => {
  const sessionId = "session-20260716T205632Z";
  const response = await replayObservationSession(sessionId, {
    fetcher: async () => new Response(JSON.stringify(preparation({
      state: "failed",
      progress: 0.4,
      cancellable: false,
      retryable: true,
      error: "Повреждён индекс исходной записи.",
    })), {
      status: 409,
      headers: { "Content-Type": "application/json", ETag: preparationEtag },
    }),
  });
  assert.equal(response.kind, "preparing");
  assert.equal(response.preparation.retryable, true);
  assert.equal(response.preparation.error, "Повреждён индекс исходной записи.");
});

test("pending preparation survives reload only through its strict canonical contract", () => {
  const values = new Map();
  const storage = {
    get length() { return values.size; },
    clear: () => values.clear(),
    getItem: (key) => values.get(key) ?? null,
    key: (index) => [...values.keys()][index] ?? null,
    removeItem: (key) => values.delete(key),
    setItem: (key, value) => values.set(key, String(value)),
  };
  const decoded = decodeObservationSessionPreparation(
    preparation({ state: "exporting", progress: 0.7 }),
    "session-20260716T205632Z",
  );
  storeObservationReplayPreparation(decoded, storage);
  assert.deepEqual(loadObservationReplayPreparation(storage), decoded);

  const key = storage.key(0);
  storage.setItem(key, JSON.stringify({ ...preparation(), raw_path: "/private/raw" }));
  assert.equal(loadObservationReplayPreparation(storage), null);
  assert.equal(storage.length, 0);
});

test("replay API forwards cancellation and preserves AbortError semantics", async () => {
  const controller = new AbortController();
  let receivedSignal;
  const pending = replayObservationSession("session-20260716T205632Z", {
    signal: controller.signal,
    fetcher: async (_input, init) => {
      receivedSignal = init.signal;
      return await new Promise((_resolve, reject) => {
        init.signal.addEventListener("abort", () => {
          reject(new DOMException("cancelled", "AbortError"));
        }, { once: true });
      });
    },
  });

  controller.abort();
  assert.equal(receivedSignal, controller.signal);
  await assert.rejects(pending, (error) => error?.name === "AbortError");
});

test("replay coordinator makes rapid saved-session selection latest-request-wins", () => {
  const coordinator = createObservationReplayCoordinator();
  const first = coordinator.begin();
  assert.equal(first.isCurrent(), true);

  const second = coordinator.begin();
  assert.equal(first.signal.aborted, true);
  assert.equal(first.isCurrent(), false);
  assert.equal(first.finish(), false);
  assert.equal(second.isCurrent(), true);
  assert.equal(second.finish(), true);
  assert.equal(second.isCurrent(), false);

  const third = coordinator.begin();
  coordinator.cancel();
  assert.equal(third.signal.aborted, true);
  assert.equal(third.isCurrent(), false);
  assert.equal(third.finish(), true);
  assert.equal(third.finish(), false);
});

test("an acquisition guard cancellation keeps settlement ownership", () => {
  const coordinator = createObservationReplayCoordinator();
  const archiveSwitch = coordinator.begin();
  let replacementBlanked = false;
  let replacementSettled = false;

  // Models the point after onReplayBegin has released the previous viewer.
  replacementBlanked = true;
  coordinator.cancel();

  assert.equal(archiveSwitch.signal.aborted, true);
  assert.equal(archiveSwitch.isCurrent(), false);
  if (archiveSwitch.finish()) replacementSettled = true;
  assert.equal(replacementBlanked, true);
  assert.equal(replacementSettled, true);
});

test("switching saved sessions aborts only local polling and never cancels shared backend work", async () => {
  const hookSource = await readFile(
    new URL("../src/core/observation/useObservationSessions.ts", import.meta.url),
    "utf8",
  );
  const selectorSource = await readFile(
    new URL("../src/components/ObservationSessionSelect.tsx", import.meta.url),
    "utf8",
  );
  assert.doesNotMatch(hookSource, /cancelObservationSessionPreparation|method:\s*["']DELETE/);
  assert.doesNotMatch(selectorSource, /cancelReplay|>\s*Отменить\s*</);
});

test("recording preparation never presents phase heartbeats as fake percentages", async () => {
  const selectorSource = await readFile(
    new URL("../src/components/ObservationSessionSelect.tsx", import.meta.url),
    "utf8",
  );

  assert.match(selectorSource, /exporting:\s*["']Готовим операторскую сцену["']/);
  assert.doesNotMatch(selectorSource, /Math\.round\(progress\s*\*\s*100\)/);
});

test("unified recorded AI view keeps the raw camera mounted but does not cover overlays", async () => {
  const workspaceSource = await readFile(
    new URL("../src/workspaces/Workspaces.tsx", import.meta.url),
    "utf8",
  );

  assert.match(workspaceSource, /hidden=\{pointCloudFocused \|\| unifiedPerception\}/);
  assert.match(
    workspaceSource,
    /presentedMediaSourceCount = unifiedPerception \? 0 : visibleMediaSources\.length/,
  );
});

test("replay descriptor rejects path leaks, mismatched sessions and non-seekable data", () => {
  assert.throws(
    () => decodeObservationSessionReplay(replay({ source_url: "file:///private/session.rrd" })),
    /same-origin/,
  );
  assert.throws(
    () => decodeObservationSessionReplay(replay({ source_url: "/api/v1/observation-sessions/other/recording.rrd" })),
    /same-origin/,
  );
  assert.throws(
    () => decodeObservationSessionReplay(replay({ seekable: false })),
    /seekable/,
  );
  assert.throws(
    () => decodeObservationSessionReplay(replay({
      viewer_source_url: "/api/v1/observation-sessions/session-20260716T205632Z/recording.rrd",
    })),
    /канонически привязан/,
  );
  assert.throws(
    () => decodeObservationSessionReplay(replay({
      viewer_source_url: `https://foreign.test/recording.rrd?generation=${"a".repeat(64)}`,
    })),
    /канонически привязан/,
  );
  assert.throws(
    () => decodeObservationSessionReplay(replay({ raw_path: "/private/raw.k1mqtt" })),
    /неизвестные поля/,
  );
  assert.throws(
    () => decodeObservationSessionReplay(replay({ byte_length: Number.MAX_SAFE_INTEGER + 1 })),
    /недопустимое число/,
  );
  assert.throws(
    () => decodeObservationSessionReplay(replay({ playback: { speed: 101, loop: false } })),
    /недопустимое число/,
  );
});

test("replay decodes opaque recorded cameras and their same-origin fMP4 manifest", () => {
  const sessionId = "session-20260716T205632Z";
  const source = {
    id: "recorded.camera.5a5a5a5a5a5a5a5a",
    label: "Записанная камера 1",
    modality: "video",
    manifest_url: `/api/v1/observation-sessions/${sessionId}/media/recorded-video-5a5a5a5a5a5a5a5a/manifest`,
    manifest_generation_sha256: "c".repeat(64),
    byte_length: 3_266,
    media_type: "video/mp4",
    timeline_start_seconds: 0.25,
    timeline_end_seconds: 20,
    seekable: true,
    synchronization: "host-arrival-best-effort",
  };
  const decoded = decodeObservationSessionReplay(replay({
    session_id: sessionId,
    timeline_end_seconds: 20,
    media_sources: [source],
  }));
  assert.equal(
    decoded.viewerSourceUrl,
    `/api/v1/observation-sessions/${sessionId}/recording.rrd?generation=${"a".repeat(64)}`,
  );
  assert.equal(decoded.mediaSources.length, 1);
  assert.equal(decoded.mediaSources[0].id, source.id);
  assert.equal(decoded.mediaSources[0].manifestUrl, source.manifest_url);
  assert.equal(
    decoded.mediaSources[0].manifestGenerationSha256,
    source.manifest_generation_sha256,
  );

  const manifest = decodeObservationRecordedMediaManifest({
    schema_version: "missioncore.observation-recorded-media/v3",
    source_id: source.id,
    generation_sha256: "c".repeat(64),
    byte_length: 3_266,
    timeline_start_seconds: 0.25,
    timeline_end_seconds: 20,
    synchronization: "host-arrival-best-effort",
    epochs: [{
      ordinal: 1,
      timeline_start_seconds: 0.25,
      timeline_end_seconds: 20,
      media_type: 'video/mp4; codecs="avc1.640028"',
      byte_length: 3_266,
      stream_url: source.manifest_url.replace(
        "/manifest",
        `/epochs/1/recording.mp4?generation=${"c".repeat(64)}`,
      ),
    }],
  }, decoded.mediaSources[0]);
  assert.equal(manifest.epochs[0].byteLength, 3_266);
  assert.match(manifest.epochs[0].streamUrl, /recording\.mp4\?generation=/);
  assert.equal(manifest.epochs[0].timelineStartSeconds, 0.25);
  assert.throws(
    () => decodeObservationRecordedMediaManifest({
      ...{
        schema_version: "missioncore.observation-recorded-media/v1",
        source_id: source.id,
        generation_sha256: "c".repeat(64),
        byte_length: 3_266,
        timeline_start_seconds: 0.25,
        timeline_end_seconds: 20,
        synchronization: "host-arrival-best-effort",
        epochs: [],
      },
    }, decoded.mediaSources[0]),
    /несовместим/,
  );
  assert.throws(
    () => decodeObservationRecordedMediaManifest({
      schema_version: "missioncore.observation-recorded-media/v3",
      source_id: source.id,
      generation_sha256: "c".repeat(64),
      byte_length: 3_267,
      timeline_start_seconds: 0.25,
      timeline_end_seconds: 20,
      synchronization: "host-arrival-best-effort",
      epochs: [],
    }, decoded.mediaSources[0]),
    /несовместим|launch descriptor/,
  );
});

test("replay decodes a synchronized full-epoch perception video", () => {
  const sessionId = "20260720T065719Z_viewer_live";
  const resultId = `result-${"d".repeat(64)}`;
  const generation = "e".repeat(64);
  const source = {
    id: "recorded.perception.right",
    label: "Сегментация · камера right",
    modality: "video",
    manifest_url: `/api/v1/observation-sessions/${sessionId}/perception-media/${resultId}/manifest`,
    manifest_generation_sha256: generation,
    byte_length: 12_345_678,
    media_type: "video/mp4",
    timeline_start_seconds: 35.421857292,
    timeline_end_seconds: 484.144857292,
    seekable: true,
    synchronization: "host-arrival-best-effort",
  };
  const decoded = decodeObservationSessionReplay(replay({
    session_id: sessionId,
    timeline_end_seconds: 500,
    media_sources: [source],
  }));
  assert.equal(decoded.mediaSources[0].id, "recorded.perception.right");
  assert.equal(decoded.mediaSources[0].manifestUrl, source.manifest_url);

  const manifest = decodeObservationRecordedMediaManifest({
    schema_version: "missioncore.observation-recorded-media/v3",
    source_id: source.id,
    generation_sha256: generation,
    byte_length: source.byte_length,
    timeline_start_seconds: source.timeline_start_seconds,
    timeline_end_seconds: source.timeline_end_seconds,
    synchronization: "host-arrival-best-effort",
    epochs: [{
      ordinal: 1,
      timeline_start_seconds: source.timeline_start_seconds,
      timeline_end_seconds: source.timeline_end_seconds,
      media_type: 'video/mp4; codecs="avc1.640028"',
      byte_length: source.byte_length,
      stream_url: `/api/v1/observation-sessions/${sessionId}/perception-media/${resultId}/recording.mp4?generation=${generation}`,
    }],
  }, decoded.mediaSources[0]);
  assert.equal(manifest.epochs.length, 1);
  assert.equal(manifest.epochs[0].timelineStartSeconds, source.timeline_start_seconds);
  assert.equal(manifest.epochs[0].timelineEndSeconds, source.timeline_end_seconds);
});

test("recorded camera contracts reject foreign origins, path escapes and unknown fields", () => {
  const sessionId = "session-20260716T205632Z";
  const base = {
    id: "recorded.camera.5a5a5a5a5a5a5a5a",
    label: "Записанная камера 1",
    modality: "video",
    manifest_url: `/api/v1/observation-sessions/${sessionId}/media/recorded-video-5a5a5a5a5a5a5a5a/manifest`,
    manifest_generation_sha256: "c".repeat(64),
    byte_length: 384,
    media_type: "video/mp4",
    timeline_start_seconds: 0,
    timeline_end_seconds: 20,
    seekable: true,
    synchronization: "host-arrival-best-effort",
  };
  assert.throws(
    () => decodeObservationSessionReplay(replay({
      session_id: sessionId,
      timeline_end_seconds: 20,
      media_sources: [{ ...base, manifest_url: "https://invalid.test/private" }],
    })),
    /same-origin/,
  );
  assert.throws(
    () => decodeObservationSessionReplay(replay({
      session_id: sessionId,
      timeline_end_seconds: 20,
      media_sources: [{ ...base, path: "/private/archive" }],
    })),
    /неизвестные поля/,
  );
  assert.throws(
    () => decodeObservationSessionReplay(replay({
      session_id: sessionId,
      timeline_end_seconds: 20,
      media_sources: [{ ...base, manifest_generation_sha256: undefined }],
    })),
    /immutable generation/,
  );
  assert.throws(
    () => decodeObservationSessionReplay(replay({
      session_id: sessionId,
      timeline_end_seconds: 20,
      media_sources: [{ ...base, manifest_generation_sha256: "not-a-digest" }],
    })),
    /immutable generation/,
  );
  const decoded = decodeObservationSessionReplay(replay({
    session_id: sessionId,
    timeline_end_seconds: 20,
    media_sources: [base],
  }));
  assert.throws(
    () => decodeObservationRecordedMediaManifest({
      schema_version: "missioncore.observation-recorded-media/v3",
      source_id: base.id,
      generation_sha256: "c".repeat(64),
      byte_length: 384,
      timeline_start_seconds: 0,
      timeline_end_seconds: 20,
      synchronization: "host-arrival-best-effort",
      epochs: [{
        ordinal: 1,
        timeline_start_seconds: 0,
        timeline_end_seconds: 20,
        media_type: 'video/mp4; codecs="avc1.640028"',
        byte_length: 384,
        stream_url: "file:///private/recording.mp4",
      }],
    }, decoded.mediaSources[0]),
    /небезопасный API URL/,
  );
});
