import { AudioFrame } from "@livekit/rtc-node";
import { describe, expect, it, vi } from "vitest";
import { ChatContext } from "../llm/chat_context.js";
import { initializeLogger } from "../log.js";
import { AudioRecognition } from "./audio_recognition.js";
import { createSilenceFrame, createSilenceFrameLike } from "./utils.js";
function createHooks() {
  return {
    onInterruption: vi.fn(),
    onStartOfSpeech: vi.fn(),
    onVADInferenceDone: vi.fn(),
    onEndOfSpeech: vi.fn(),
    onInterimTranscript: vi.fn(),
    onFinalTranscript: vi.fn(),
    onEndOfTurn: vi.fn(async () => true),
    onPreemptiveGeneration: vi.fn(),
    onAgentBackchannelOpportunity: vi.fn(),
    retrieveChatCtx: () => ChatContext.empty()
  };
}
function markerFrame(byte, samples = 160, sampleRate = 16e3) {
  const data = new Int16Array(samples).fill(byte);
  return new AudioFrame(data, sampleRate, 1, samples);
}
function isAllZero(frame) {
  for (const v of frame.data) {
    if (v !== 0) return false;
  }
  return true;
}
async function flushTasks() {
  await new Promise((resolve) => setTimeout(resolve, 0));
}
async function waitFor(check, timeoutMs = 1e3) {
  const startedAt = Date.now();
  while (!check()) {
    if (Date.now() - startedAt > timeoutMs) {
      throw new Error("timed out waiting for condition");
    }
    await flushTasks();
  }
}
describe("createSilenceFrame helpers", () => {
  initializeLogger({ pretty: false, level: "silent" });
  it("produces a zeroed mono frame of the requested duration in ms", () => {
    const frame = createSilenceFrame(500, 16e3);
    expect(frame.sampleRate).toBe(16e3);
    expect(frame.channels).toBe(1);
    expect(frame.samplesPerChannel).toBe(8e3);
    expect(frame.data.length).toBe(8e3);
    expect(isAllZero(frame)).toBe(true);
  });
  it("honours numChannels for multi-channel silence", () => {
    const frame = createSilenceFrame(100, 24e3, 2);
    expect(frame.samplesPerChannel).toBe(2400);
    expect(frame.channels).toBe(2);
    expect(frame.data.length).toBe(4800);
    expect(isAllZero(frame)).toBe(true);
  });
  it("createSilenceFrameLike matches the source shape with zeroed samples", () => {
    const src = markerFrame(17, 160, 16e3);
    const silence = createSilenceFrameLike(src);
    expect(silence.sampleRate).toBe(src.sampleRate);
    expect(silence.channels).toBe(src.channels);
    expect(silence.samplesPerChannel).toBe(src.samplesPerChannel);
    expect(silence.data.length).toBe(src.data.length);
    expect(isAllZero(silence)).toBe(true);
  });
});
describe("AudioRecognition substitutes silence on the STT path only", () => {
  initializeLogger({ pretty: false, level: "silent" });
  it("forwards the real frame to every consumer when no discard predicate is set", async () => {
    const sttFrames = [];
    const sttNode = async (audioStream) => {
      void (async () => {
        const reader = audioStream.getReader();
        try {
          while (true) {
            const { done, value } = await reader.read();
            if (done) return;
            sttFrames.push(value);
          }
        } catch {
        }
      })();
      return new ReadableStream({ start() {
      } });
    };
    const recognition = new AudioRecognition({
      recognitionHooks: createHooks(),
      stt: sttNode,
      minEndpointingDelay: 0,
      maxEndpointingDelay: 0
    });
    const subscriberStream = recognition.subscribeAudioStream();
    const subscriberFrames = [];
    const subscriberDone = (async () => {
      const reader = subscriberStream.getReader();
      try {
        while (true) {
          const { done, value } = await reader.read();
          if (done) return;
          subscriberFrames.push(value);
        }
      } catch {
      }
    })();
    try {
      await recognition.start();
      recognition.setInputAudioStream(
        new ReadableStream({
          start(controller) {
            controller.enqueue(markerFrame(17));
            controller.enqueue(markerFrame(34));
            controller.close();
          }
        })
      );
      await waitFor(() => sttFrames.length === 2 && subscriberFrames.length === 2);
      expect(sttFrames.map((f) => f.data[0])).toEqual([17, 34]);
      expect(subscriberFrames.map((f) => f.data[0])).toEqual([17, 34]);
    } finally {
      await recognition.close();
      await subscriberDone;
    }
  });
  it("substitutes silence on the STT path while subscribers still see real frames", async () => {
    const DISCARD_MARKER = 34;
    const sttFrames = [];
    const sttNode = async (audioStream) => {
      void (async () => {
        const reader = audioStream.getReader();
        try {
          while (true) {
            const { done, value } = await reader.read();
            if (done) return;
            sttFrames.push(value);
          }
        } catch {
        }
      })();
      return new ReadableStream({ start() {
      } });
    };
    const recognition = new AudioRecognition({
      recognitionHooks: createHooks(),
      stt: sttNode,
      shouldDiscardAudioForStt: (frame) => frame.data[0] === DISCARD_MARKER,
      minEndpointingDelay: 0,
      maxEndpointingDelay: 0
    });
    const subscriberStream = recognition.subscribeAudioStream();
    const subscriberFrames = [];
    const subscriberDone = (async () => {
      const reader = subscriberStream.getReader();
      try {
        while (true) {
          const { done, value } = await reader.read();
          if (done) return;
          subscriberFrames.push(value);
        }
      } catch {
      }
    })();
    try {
      await recognition.start();
      recognition.setInputAudioStream(
        new ReadableStream({
          start(controller) {
            controller.enqueue(markerFrame(17));
            controller.enqueue(markerFrame(DISCARD_MARKER));
            controller.enqueue(markerFrame(DISCARD_MARKER));
            controller.enqueue(markerFrame(68));
            controller.close();
          }
        })
      );
      await waitFor(() => sttFrames.length === 4 && subscriberFrames.length === 4);
      expect(sttFrames[0].data[0]).toBe(17);
      expect(isAllZero(sttFrames[1])).toBe(true);
      expect(isAllZero(sttFrames[2])).toBe(true);
      expect(sttFrames[3].data[0]).toBe(68);
      for (const frame of sttFrames) {
        expect(frame.sampleRate).toBe(16e3);
        expect(frame.channels).toBe(1);
        expect(frame.samplesPerChannel).toBe(160);
      }
      expect(subscriberFrames.map((f) => f.data[0])).toEqual([
        17,
        DISCARD_MARKER,
        DISCARD_MARKER,
        68
      ]);
    } finally {
      await recognition.close();
      await subscriberDone;
    }
  });
});
//# sourceMappingURL=audio_recognition_push_audio.test.js.map