// src/test-hooks/offscreen-hooks.ts — Plan 01-11 Task 2 (offscreen-side test hook).
//
// Installs `globalThis.__mokoshTest` in the offscreen document's isolate.
// The harness reads this surface via `offPage.evaluate(...)` to:
//   - read `getCurrentStream().getVideoTracks()[0].getSettings().displaySurface`
//     (assertion 3 — verifies monitor-only enforcement);
//   - dispatch `new Event('ended')` on the video track (assertion 6 —
//     the Bug B simulation per RESEARCH §7 BLOCKER. `track.stop()` does
//     NOT fire 'ended' per W3C spec, so the production
//     onUserStoppedSharing handler would never run — that is the trap
//     this hook exists to expose);
//   - read `getSegmentCount()` (assertion 11 — verifies 30s ring buffer
//     per D-13).
//
// Plan 01-11 PROTOTYPE addition (synthetic MediaStream bypass + offscreen
// bridge): the `installFakeDisplayMedia()` shim patches
// `navigator.mediaDevices.getDisplayMedia` so the offscreen recorder's
// `startRecording` path resolves WITHOUT spawning Chrome's screen-share
// picker. The `__mokoshOffscreenQuery` chrome.runtime.onMessage bridge
// allows the extension-internal harness page to invoke
// installFakeDisplayMedia + dispatch 'ended' on the active track,
// because page → offscreen direct evaluate is not available; the only
// cross-isolate path is chrome.runtime.sendMessage.
//
// The offscreen recorder wires the runtime references via the two
// setters exported below. These imports are gated by the same
// `__MOKOSH_UAT__` token in src/offscreen/recorder.ts as the SW-side
// hook; production builds tree-shake the entire module away (Tier-1
// grep gate verifies).
//
// Cross-isolate note: SW and offscreen are SEPARATE isolates with
// SEPARATE `globalThis`. The SW-side sw-hooks.ts installs handler
// captures + notification observability on the SW's globalThis;
// this file installs stream + segment-count observability on the
// offscreen's globalThis. The harness queries the appropriate isolate
// per assertion. handlers / notificationCount / notificationIds /
// lastNotificationOptions in this offscreen surface are present-but-
// inert (initialized to empty values) to keep the type uniform across
// isolates — the harness never reads them off the offscreen surface.
//
// References:
//   - MediaStreamTrack 'ended' event:
//     https://developer.mozilla.org/docs/Web/API/MediaStreamTrack/ended_event
//   - HTMLCanvasElement.captureStream:
//     https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement/captureStream
//   - Offscreen document isolation in MV3:
//     https://developer.chrome.com/docs/extensions/reference/api/offscreen

import type { MokoshTestSurface } from './types';

// Module-level mutable cells holding the runtime references. The
// recorder calls the setters; the surface getters close over the cells.
let currentStream: MediaStream | null = null;
let segmentCountGetter: () => number = () => 0;

/**
 * Wire the current MediaStream into the test surface. Called by
 * src/offscreen/recorder.ts:startRecording immediately after the
 * `mediaStream = stream` assignment AND on stream teardown (with
 * null) so the surface tracks the live recording lifecycle.
 *
 * Idempotent — calling with the same value is a no-op equivalent.
 *
 * @param stream - The active MediaStream, or null when teardown.
 */
export function setCurrentStream(stream: MediaStream | null): void {
  currentStream = stream;
}

/**
 * Wire the segment-count getter into the test surface. Called once
 * by src/offscreen/recorder.ts when the test bundle is active. The
 * recorder holds a `let segments: Blob[]` module-local; the getter
 * captures it by closure so the harness reads the live count without
 * needing to import the recorder's source from the harness side.
 *
 * @param getter - Closure returning the current segments.length.
 */
export function setSegmentCountGetter(getter: () => number): void {
  segmentCountGetter = getter;
}

// ─── Synthetic getDisplayMedia (prototype path) ───────────────────────
// State for the canvas-driven fake stream. We retain references at
// module scope so a second installFakeDisplayMedia() call is a no-op
// (idempotent) and so the canvas + animation handle stay alive for the
// lifetime of the offscreen document (canvas-captureStream tracks die
// silently when the source element is GC'd).
//
// The displaySurface override is the critical detail: production code
// in src/offscreen/recorder.ts:294 enforces displaySurface === 'monitor'
// via `track.getSettings()` and tears down the stream + throws
// 'wrong-display-surface' otherwise. Canvas captureStream tracks have
// displaySurface === undefined by default — so we monkey-patch
// getSettings() on the video track to return 'monitor'.

let fakeInstalled = false;
let fakeCanvas: HTMLCanvasElement | null = null;
let fakeAnimationHandle: number | null = null;
let fakeDrawInterval: ReturnType<typeof setInterval> | null = null;

/**
 * Plan 01-14 A23 contract — record the last-received constraints object
 * from every `fakeGetDisplayMedia` invocation. The `installFakeDisplayMedia`
 * shim already accepts the `constraints` parameter (previously prefixed
 * `_constraints` as received-but-unused — see below), so the production
 * call site's `monitorTypeSurfaces: 'include'` sibling lands here. The
 * `get-last-getDisplayMedia-constraints` bridge op (later in this file)
 * reads it for harness inspection.
 *
 * `null` until any `getDisplayMedia` call has been made — A23 always
 * runs AFTER A2's `setupFreshRecording` so the cell is populated by
 * then. Test bundle only; gated identically to the rest of this module
 * by the top-of-module `__MOKOSH_UAT__` import in src/offscreen/recorder.ts.
 */
let lastGetDisplayMediaConstraints: DisplayMediaStreamOptions | null = null;

/**
 * Replace `navigator.mediaDevices.getDisplayMedia` with a synthetic
 * implementation backed by a hidden 30 fps canvas. The returned
 * MediaStream contains exactly one video track; the track's
 * `getSettings()` is monkey-patched to report `displaySurface: 'monitor'`
 * so the production code's post-grant monitor-only validation passes.
 *
 * SAFE to call multiple times — second and subsequent calls are no-ops.
 *
 * The fake stream behaves like a real getDisplayMedia result:
 *   - track.kind === 'video'
 *   - track.readyState === 'live' until stopped (or until 'ended' dispatched)
 *   - track.addEventListener('ended', cb) works as expected
 *   - track.dispatchEvent(new Event('ended')) fires registered listeners
 *     — this is the Bug B simulation path per RESEARCH §7.
 *
 * Called from the harness via the `__mokoshOffscreenQuery` bridge
 * 'install-fake-display-media' op BEFORE triggering the production
 * recording-start flow. The patch persists for the lifetime of the
 * offscreen document.
 */
export function installFakeDisplayMedia(): void {
  if (fakeInstalled) {
    return;
  }
  fakeInstalled = true;

  // Build a 320x180 canvas drawing a frame counter — the actual pixel
  // content is irrelevant for A6 (the test only cares about the
  // recording state machine, not the video content) but giving the
  // canvas a moving update keeps the captureStream track in a 'live'
  // state for the rotation-segments lifecycle.
  //
  // Plan 01-13 Wave 3B contract: the canvas + drawing loop are persistent
  // across MULTIPLE recording lifecycles within the same offscreen
  // document (A6 tears recording down via dispatch-ended, A7 starts a
  // FRESH recording — both share the same canvas). Each
  // `fakeGetDisplayMedia` call mints a fresh `MediaStream` via
  // `canvas.captureStream(30)` so the per-call track is in 'live' state
  // even after the previous recording's tracks were `.stop()`-ed by the
  // teardown path (real getDisplayMedia returns a new stream per call;
  // the fake matches that contract).
  const canvas = document.createElement('canvas');
  canvas.width = 320;
  canvas.height = 180;
  canvas.style.position = 'fixed';
  canvas.style.top = '-9999px';
  canvas.style.left = '-9999px';
  document.body.appendChild(canvas);
  fakeCanvas = canvas;

  const ctx = canvas.getContext('2d');
  let frameCount = 0;
  /**
   * Draw one frame on the synthetic canvas. Keeps the captureStream
   * track from going silent (which can cause MediaRecorder to stop
   * emitting dataavailable events on some Chrome versions).
   */
  const drawFrame = (): void => {
    if (ctx !== null) {
      ctx.fillStyle = '#222';
      ctx.fillRect(0, 0, 320, 180);
      ctx.fillStyle = '#fff';
      ctx.font = '20px sans-serif';
      ctx.fillText(`frame ${frameCount}`, 20, 100);
      frameCount += 1;
    }
    fakeAnimationHandle = requestAnimationFrame(drawFrame);
  };
  drawFrame();

  // Belt-and-suspenders frame driver: requestAnimationFrame fires on
  // page-visibility heuristics in headless Chrome (offscreen documents
  // are not "visible" tabs — RAF cadence drops to near-zero under
  // certain throttling regimes, producing 0-frame segments that then
  // crash ts-ebml's VINT decode in `webm-remux.extractFramesFromSegment`
  // with "Unrepresentable length: Infinity" on the malformed empty
  // bytes). A 33ms setInterval (~30fps) drives drawFrame regardless of
  // RAF throttling — it's redundant for normal RAF but guarantees the
  // captureStream track sees real pixel mutations every tick. Both
  // timers are cleaned up in `uninstallFakeDisplayMedia`.
  fakeDrawInterval = setInterval(drawFrame, 33);

  /**
   * Apply the displaySurface monkey-patch to a freshly-minted stream's
   * video track. Production code's post-grant validation reads
   * `getSettings().displaySurface` and tears down + throws
   * 'wrong-display-surface' on anything but 'monitor' — the patch makes
   * the synthetic canvas stream satisfy that gate.
   *
   * @param stream - The stream whose first video track is patched in-place.
   */
  const patchDisplaySurface = (stream: MediaStream): void => {
    const videoTrack = stream.getVideoTracks()[0];
    if (videoTrack !== undefined) {
      const originalGetSettings = videoTrack.getSettings.bind(videoTrack);
      videoTrack.getSettings = ((): MediaTrackSettings => {
        const real = originalGetSettings();
        return {
          ...real,
          displaySurface: 'monitor',
        };
      }) as typeof videoTrack.getSettings;
    }
  };

  /**
   * Mint a FRESH MediaStream from the persistent canvas. Each invocation
   * generates new tracks in 'live' state — required for the multi-
   * recording-lifecycle pattern (A6 stops the first stream's tracks via
   * dispatchEvent('ended'); A7 starts a new recording which calls
   * getDisplayMedia → must get a live stream, NOT the dead one A6
   * teardown left behind). Closure variables (fakeCanvas above) persist
   * across calls; track refs do not.
   *
   * @returns Fresh MediaStream with displaySurface monkey-patch applied
   *          to its video track.
   */
  const mintStream = (): MediaStream => {
    const stream = canvas.captureStream(30);
    patchDisplaySurface(stream);
    return stream;
  };

  // Replace navigator.mediaDevices.getDisplayMedia with a function
  // that mints a FRESH synthetic stream on each call. Production code's
  // `await navigator.mediaDevices.getDisplayMedia(...)` resolves with a
  // newly-minted stream immediately — no picker.
  //
  // Cast through `unknown` because the MediaDevices.getDisplayMedia
  // type has multiple overloads (with/without constraints) and a
  // straight assignment would trip the type checker. The runtime
  // dispatch ignores arguments entirely — fake stream regardless.
  const fakeGetDisplayMedia = async (
    constraints?: DisplayMediaStreamOptions,
  ): Promise<MediaStream> => {
    // Plan 01-14 A23: capture the production call's constraints object
    // so the harness can verify `monitorTypeSurfaces: 'include'` reached
    // the call site. Default to null on undefined-args so the bridge op
    // reports an unambiguous "no args" signal rather than `undefined`.
    lastGetDisplayMediaConstraints = constraints ?? null;
    return mintStream();
  };
  (navigator.mediaDevices as unknown as {
    getDisplayMedia: typeof fakeGetDisplayMedia;
  }).getDisplayMedia = fakeGetDisplayMedia;
}

/**
 * Uninstall the fake getDisplayMedia. Used for cleanup between test
 * runs if multiple recordings need to start fresh. Not called by the
 * A6 prototype (single recording lifecycle).
 */
export function uninstallFakeDisplayMedia(): void {
  if (!fakeInstalled) {
    return;
  }
  fakeInstalled = false;
  if (fakeAnimationHandle !== null) {
    cancelAnimationFrame(fakeAnimationHandle);
    fakeAnimationHandle = null;
  }
  if (fakeDrawInterval !== null) {
    clearInterval(fakeDrawInterval);
    fakeDrawInterval = null;
  }
  if (fakeCanvas !== null) {
    fakeCanvas.remove();
    fakeCanvas = null;
  }
  // We deliberately do NOT restore the original getDisplayMedia — the
  // offscreen document is throwaway and gets a fresh navigator on the
  // next createDocument() anyway.
}

/**
 * Dispatch a synthetic 'ended' event on the active stream's video
 * track. This is the Bug B simulation path per RESEARCH §7 BLOCKER —
 * `track.stop()` does NOT fire 'ended' per W3C spec; only
 * dispatchEvent does.
 *
 * Used by A6: the harness calls this after the recording is live;
 * the production `onUserStoppedSharing` handler fires; the SW state
 * machine routes through setIdleMode.
 *
 * @returns Result with ok status; ok=false when no current stream.
 */
export function dispatchEndedOnTrack(): { ok: boolean; error?: string } {
  if (currentStream === null) {
    return {
      ok: false,
      error: 'no current MediaStream — recording must be active',
    };
  }
  const track = currentStream.getVideoTracks()[0];
  if (track === undefined) {
    return { ok: false, error: 'no video track in stream' };
  }
  track.dispatchEvent(new Event('ended'));
  return { ok: true };
}

// ─── Install the global surface ───────────────────────────────────────
// Note: the offscreen isolate's globalThis is FRESH per offscreen
// document creation (each createDocument restart resets it). The
// gated dynamic import in recorder.ts top-of-module runs once per
// offscreen lifetime, so each new offscreen document gets a fresh
// surface install — there is no cross-lifetime contamination.
//
// Augment the surface with the installFakeDisplayMedia entrypoint so
// the harness can invoke it via offPage.evaluate. The MokoshTestSurface
// type widens to include this method via a cross-cast at install time
// — keeping the type clean while still exposing the prototype hook.
globalThis.__mokoshTest = {
  handlers: {
    onClicked: null,
    onStartup: null,
    notificationOnClicked: null,
  },
  notificationCount: 0,
  lastNotificationOptions: null,
  get notificationIds() {
    return [];
  },
  getCurrentStream: () => currentStream,
  getSegmentCount: () => segmentCountGetter(),
  installFakeDisplayMedia,
  uninstallFakeDisplayMedia,
  dispatchEndedOnTrack,
} as MokoshTestSurface & {
  installFakeDisplayMedia: typeof installFakeDisplayMedia;
  uninstallFakeDisplayMedia: typeof uninstallFakeDisplayMedia;
  dispatchEndedOnTrack: typeof dispatchEndedOnTrack;
};

// ─── Offscreen bridge: __mokoshOffscreenQuery ────────────────────────
// The extension-internal harness page cannot evaluate directly in the
// offscreen isolate (separate globalThis; chrome.runtime.sendMessage
// is the only cross-isolate path). So we register a dedicated
// onMessage handler that responds to __mokoshOffscreenQuery messages
// with the requested operation result.
//
// Protocol — page → offscreen message:
//   { type: '__mokoshOffscreenQuery', op: <string> }
// Response shapes (sync via sendResponse, return false):
//   op='install-fake-display-media' → { ok: true } OR { ok: false, error }
//   op='dispatch-ended' → { ok: true } OR { ok: false, error: 'no stream' }
//   op='has-stream' → { hasStream: boolean }
//   op='get-display-surface' → { displaySurface: string|null } OR { ok: false, error }
//     — Plan 01-13 Wave 3A A3 contract. Returns the active track's
//       `getSettings().displaySurface` value (monkey-patched to 'monitor'
//       by `installFakeDisplayMedia`); returns null when no stream is live.
//   op='get-segment-count' → { count: number } OR { count: -1, error }
//     — Plan 01-13 Wave 3D A11 contract. Returns the offscreen recorder's
//       live `segments.length` via the `segmentCountGetter` closure wired
//       at startRecording (see src/offscreen/recorder.ts:284). Before any
//       startRecording, the getter is the default `() => 0` from line 54
//       above — A11 always calls this AFTER setupFreshRecording so a non-
//       zero count is meaningful. The 10s rotation cadence (D-13;
//       SEGMENT_DURATION_MS) means a recording that has been live for
//       ~35s should report count ≥ 3 (3 × 10s = 30s = MAX_SEGMENTS).
//   op='get-last-getDisplayMedia-constraints' → { constraints: object|null }
//     — Plan 01-14 A23 contract. Returns the most-recent constraints
//       object captured by `fakeGetDisplayMedia`. Used by the harness to
//       verify the production call site passes `monitorTypeSurfaces: 'include'`
//       (W3C Screen Capture spec §6.1; Chrome ≥ 119 picker-narrowing
//       semantics). `null` only when no getDisplayMedia call has happened
//       yet — A23 always runs AFTER A2's setupFreshRecording so a non-null
//       value is the expected case.
// Unknown ops respond { ok: false, error: 'unknown-op' }.
//
// The bridge handler MUST run BEFORE the production offscreen bridge
// installed at recorder.ts:838 — but offscreen-hooks runs at top-of-
// module via the gated dynamic import (before bootstrap()), so this
// ordering is satisfied by construction.
//
// IMPORTANT: chrome.runtime.onMessage dispatches to ALL registered
// listeners; our handler returns false for non-matching message types
// so the production handler still sees them. The production handler
// also returns false for unknown types, so there is no two-way
// contention.
chrome.runtime.onMessage.addListener((rawMessage, _sender, sendResponse) => {
  if (rawMessage === null || typeof rawMessage !== 'object') {
    return false;
  }
  const message = rawMessage as { type?: unknown; op?: unknown };
  if (message.type !== '__mokoshOffscreenQuery') {
    return false;
  }
  const op = String(message.op ?? '');
  if (op === 'install-fake-display-media') {
    try {
      installFakeDisplayMedia();
      sendResponse({ ok: true });
    } catch (err) {
      sendResponse({
        ok: false,
        error: err instanceof Error ? err.message : String(err),
      });
    }
    return false;
  }
  if (op === 'dispatch-ended') {
    try {
      const r = dispatchEndedOnTrack();
      sendResponse(r);
    } catch (err) {
      sendResponse({
        ok: false,
        error: err instanceof Error ? err.message : String(err),
      });
    }
    return false;
  }
  if (op === 'has-stream') {
    sendResponse({ hasStream: currentStream !== null });
    return false;
  }
  if (op === 'get-display-surface') {
    // Plan 01-13 Wave 3A A3 contract — return the active track's
    // displaySurface so the harness can verify the offscreen-hooks
    // monkey-patched getSettings() correctly reports 'monitor'.
    // Production code in src/offscreen/recorder.ts enforces this same
    // invariant (tears down + throws 'wrong-display-surface' otherwise),
    // so if recording is live the value is guaranteed monitor — the
    // harness asserts === 'monitor' for explicit empirical verification.
    try {
      if (currentStream === null) {
        sendResponse({ displaySurface: null });
        return false;
      }
      const track = currentStream.getVideoTracks()[0];
      if (track === undefined) {
        sendResponse({ displaySurface: null });
        return false;
      }
      const settings = track.getSettings();
      const displaySurface = settings.displaySurface ?? null;
      sendResponse({ displaySurface });
    } catch (err) {
      sendResponse({
        ok: false,
        error: err instanceof Error ? err.message : String(err),
      });
    }
    return false;
  }
  if (op === 'get-last-getDisplayMedia-constraints') {
    // Plan 01-14 A23 contract — return the most-recent constraints object
    // captured by the `fakeGetDisplayMedia` shim. Production code at
    // src/offscreen/recorder.ts:270 calls `navigator.mediaDevices.getDisplayMedia({
    // video: {...}, monitorTypeSurfaces: 'include', audio: false })`; this op
    // exposes that object so the harness can assert
    // `constraints.monitorTypeSurfaces === 'include'`.
    //
    // `null` is returned in two cases:
    //   (a) No `getDisplayMedia` call has been made yet — A23 always runs
    //       AFTER A2's setupFreshRecording so this is unreachable in the
    //       orchestrated sequence;
    //   (b) The call was made with `undefined` args — also unreachable for
    //       the production call site which always supplies the constraints
    //       object.
    // try/catch is defensive — bridge handlers must never propagate
    // exceptions to chrome.runtime.sendMessage (the dispatcher contract
    // shared by all ops above).
    try {
      sendResponse({ constraints: lastGetDisplayMediaConstraints });
    } catch (err) {
      sendResponse({
        ok: false,
        error: err instanceof Error ? err.message : String(err),
      });
    }
    return false;
  }
  if (op === 'get-segment-count') {
    // Plan 01-13 Wave 3D A11 contract — return the offscreen recorder's
    // live segment count via the `segmentCountGetter` closure wired at
    // startRecording (src/offscreen/recorder.ts:284). The closure
    // captures the recorder's module-local `segments: Blob[]` array,
    // which the rotation lifecycle (D-13; SEGMENT_DURATION_MS = 10s,
    // MAX_SEGMENTS = 3) populates with self-contained WebM segments.
    // After ~35s of continuous recording, A11 asserts count >= 3.
    //
    // The default getter (`() => 0`) at module load returns 0 — A11
    // therefore MUST call this AFTER setupFreshRecording so the
    // recorder has wired the live getter. A pre-recording call would
    // legitimately return 0; the harness orders the assertion so this
    // failure mode is unreachable.
    //
    // -1 sentinel on error preserves the dispatcher contract (every
    // op returns a numeric `count` field on the happy path or -1 +
    // `error` on failure). A `try/catch` is defensive against a future
    // getter that throws (the closure-bound module-level array is a
    // pure read, so no throw is expected, but bridge handlers should
    // never propagate exceptions to chrome.runtime.sendMessage).
    try {
      sendResponse({ count: segmentCountGetter() });
    } catch (err) {
      sendResponse({
        count: -1,
        error: err instanceof Error ? err.message : String(err),
      });
    }
    return false;
  }
  sendResponse({ ok: false, error: 'unknown-op' });
  return false;
});

// ─── Auto-install fake getDisplayMedia at module load ────────────────
// PROTOTYPE: install the fake getDisplayMedia eagerly so production
// recorder.startRecording will use the synthetic stream on its first
// call — no chicken-and-egg with the bridge install op. Wrap in a
// try so any DOM-not-ready edge case does not block module init.
try {
  installFakeDisplayMedia();
} catch (e) {
  console.warn("[offscreen-hooks] eager installFakeDisplayMedia failed:", e);
}


export {};
