import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
  createWebcamFaceConsent,
  createWebcamFaceTexture,
  type WebcamFaceTextureState,
} from "../profile/index.ts";

function media() {
  let stops = 0;
  let pauses = 0;
  const track = {
    kind: "video",
    stop: () => { stops += 1; },
  } as unknown as MediaStreamTrack;
  const stream = {
    getVideoTracks: () => [track],
    getTracks: () => [track],
  } as unknown as MediaStream;
  const video = {
    srcObject: stream,
    pause: () => { pauses += 1; },
  } as unknown as HTMLVideoElement;
  return { stream, video, stops: () => stops, pauses: () => pauses };
}

describe("ephemeral webcam face texture", () => {
  it("is off by default and refuses media before explicit active consent", () => {
    const consent = createWebcamFaceConsent();
    const adapter = createWebcamFaceTexture({ consent });
    const input = media();
    assert.deepEqual(adapter.state(), {
      status: "off",
      active: false,
      indicatorVisible: false,
      consentRevision: 0,
      ephemeral: true,
      persistence: "none",
      recording: false,
      uploading: false,
    });
    assert.throws(() => adapter.bind(input), /active consent/);
    assert.equal(adapter.texture(), null);
    adapter.dispose();
  });

  it("accepts only caller-attached video after consent and owns only its texture", () => {
    const consent = createWebcamFaceConsent();
    consent.requestStart();
    consent.start(true);
    const input = media();
    const wrongVideo = { srcObject: null } as HTMLVideoElement;
    const adapter = createWebcamFaceTexture({ consent });
    assert.throws(() => adapter.bind({ stream: input.stream, video: wrongVideo }), /attach/);
    const texture = adapter.bind(input);
    let textureDisposals = 0;
    const dispose = texture.dispose.bind(texture);
    texture.dispose = () => { textureDisposals += 1; dispose(); };
    assert.equal(adapter.texture(), texture);
    assert.equal(adapter.state().active, true);
    adapter.clear();
    assert.equal(textureDisposals, 1);
    assert.equal(adapter.texture(), null);
    assert.equal(input.stops(), 0, "caller-owned track was stopped");
    assert.equal(input.pauses(), 0, "caller-owned video was paused");
    adapter.dispose();
  });

  it("clears on stop/revoke and publishes defensive active-indicator transitions", () => {
    const consent = createWebcamFaceConsent();
    consent.requestStart();
    consent.start(true);
    const input = media();
    const indicators: WebcamFaceTextureState[] = [];
    const adapter = createWebcamFaceTexture({
      consent,
      onIndicatorChange: (state) => {
        state.status = "disposed";
        indicators.push({ ...state });
      },
    });
    adapter.bind(input);
    assert.equal(adapter.state().status, "active", "callback mutation escaped into adapter");
    consent.stop();
    assert.equal(adapter.sync().active, false);
    assert.equal(adapter.texture(), null);
    assert.equal(input.stops(), 0);
    assert.deepEqual(indicators.map((state) => state.indicatorVisible), [true, false]);
    adapter.dispose();
  });

  it("is idempotent after disposal and rejects reuse", () => {
    const consent = createWebcamFaceConsent();
    consent.requestStart();
    consent.start(true);
    const input = media();
    const adapter = createWebcamFaceTexture({ consent });
    adapter.bind(input);
    adapter.dispose();
    adapter.dispose();
    assert.equal(adapter.state().status, "disposed");
    assert.equal(adapter.texture(), null);
    assert.throws(() => adapter.bind(input), /disposed/);
    assert.equal(input.stops(), 0);
    assert.equal(input.pauses(), 0);
  });
});

