import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { createWebcamCapture } from "../profile/index.ts";

function deferred<T>() {
  let resolve!: (value: T) => void;
  let reject!: (reason?: unknown) => void;
  const promise = new Promise<T>((yes, no) => { resolve = yes; reject = no; });
  return { promise, resolve, reject };
}

class FakeTrack {
  stops = 0;
  stop(): void { this.stops += 1; }
}

class FakeStream {
  readonly tracks: FakeTrack[];
  constructor(tracks: FakeTrack[]) { this.tracks = tracks; }
  getTracks(): FakeTrack[] { return this.tracks; }
}

class FakeVideo {
  srcObject: MediaStream | null = null;
  pauses = 0;
  removals = 0;
  readonly playing: Promise<void>;
  constructor(playing: Promise<void> = Promise.resolve()) { this.playing = playing; }
  play(): Promise<void> { return this.playing; }
  pause(): void { this.pauses += 1; }
  remove(): void { this.removals += 1; }
}

describe("ephemeral webcam capture ownership", () => {
  it("stops a permission result that arrives after explicit Stop", async () => {
    const acquired = deferred<MediaStream>();
    const track = new FakeTrack();
    let videos = 0;
    const capture = createWebcamCapture({
      acquire: () => acquired.promise,
      createVideo: () => { videos += 1; return new FakeVideo() as unknown as HTMLVideoElement; },
    });
    const start = capture.start();
    assert.equal(capture.status(), "requesting");
    capture.stop();
    acquired.resolve(new FakeStream([track]) as unknown as MediaStream);
    assert.equal(await start, null);
    assert.equal(track.stops, 1);
    assert.equal(videos, 0, "cancelled permission never creates a video surface");
    assert.equal(capture.binding(), null);
  });

  it("owns the stream before play settles so pagehide-style Stop is immediate", async () => {
    const playing = deferred<void>();
    const tracks = [new FakeTrack(), new FakeTrack()];
    const video = new FakeVideo(playing.promise);
    const capture = createWebcamCapture({
      acquire: async () => new FakeStream(tracks) as unknown as MediaStream,
      createVideo: () => video as unknown as HTMLVideoElement,
    });
    const start = capture.start();
    await Promise.resolve();
    assert.ok(capture.binding(), "pending playback is already caller-owned");
    capture.stop();
    assert.deepEqual(tracks.map((track) => track.stops), [1, 1]);
    assert.equal(video.pauses, 1);
    assert.equal(video.srcObject, null);
    assert.equal(video.removals, 1);
    playing.resolve();
    assert.equal(await start, null);
    assert.equal(capture.status(), "off");
  });

  it("releases every active track on Stop and dispose without retaining media", async () => {
    const tracks = [new FakeTrack(), new FakeTrack()];
    const video = new FakeVideo();
    const stream = new FakeStream(tracks) as unknown as MediaStream;
    const capture = createWebcamCapture({
      acquire: async () => stream,
      createVideo: () => video as unknown as HTMLVideoElement,
    });
    const binding = await capture.start();
    assert.equal(binding?.stream, stream);
    assert.equal(capture.status(), "active");
    const exposed = capture.binding();
    assert.notEqual(exposed, binding, "binding snapshots do not expose mutable controller state");
    capture.dispose();
    capture.dispose();
    assert.deepEqual(tracks.map((track) => track.stops), [1, 1]);
    assert.equal(capture.binding(), null);
    assert.equal(capture.status(), "disposed");
    await assert.rejects(capture.start(), /disposed/);
  });
});
