import { describe, expect, it, vi } from "vitest";
import { ChatContext } from "../llm/chat_context.js";
import { initializeLogger } from "../log.js";
import { SpeechEventType } from "../stt/stt.js";
import { AudioRecognition, STTPipeline } from "./audio_recognition.js";
function createHooks() {
  const hooks = {
    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(),
    onUserTurnExceeded: vi.fn(),
    retrieveChatCtx: () => ChatContext.empty()
  };
  return hooks;
}
async function flushTasks() {
  await new Promise((resolve) => setTimeout(resolve, 0));
}
async function waitFor(check, timeoutMs = 200) {
  const startedAt = Date.now();
  while (!check()) {
    if (Date.now() - startedAt > timeoutMs) {
      throw new Error("timed out waiting for condition");
    }
    await flushTasks();
  }
}
function createRecognition(sttNode, hooks = createHooks()) {
  return {
    hooks,
    recognition: new AudioRecognition({
      recognitionHooks: hooks,
      stt: sttNode,
      minEndpointingDelay: 0,
      maxEndpointingDelay: 0
    })
  };
}
describe("AudioRecognition STT pipeline handoff", () => {
  initializeLogger({ pretty: false, level: "silent" });
  it("reuses an injected STT pipeline instead of opening a second STT stream", async () => {
    let sttNodeCalls = 0;
    const sttNode = async () => {
      sttNodeCalls += 1;
      return new ReadableStream({
        start() {
        }
      });
    };
    const pipeline = new STTPipeline(sttNode);
    const { recognition } = createRecognition(sttNode);
    try {
      await recognition.start({ sttPipeline: pipeline });
      await waitFor(() => sttNodeCalls === 1);
      expect(sttNodeCalls).toBe(1);
    } finally {
      await recognition.close();
      await pipeline.close();
    }
  });
  it("detaches the pipeline so a new consumer can receive subsequent STT events", async () => {
    let controller;
    const sttNode = async () => new ReadableStream({
      start(ctrl) {
        controller = ctrl;
      }
    });
    const pipeline = new STTPipeline(sttNode);
    const first = createRecognition(sttNode);
    const second = createRecognition(sttNode);
    try {
      await first.recognition.start({ sttPipeline: pipeline });
      await waitFor(() => controller !== void 0);
      const detachedPipeline = await first.recognition.detachSttPipeline();
      await first.recognition.close();
      await second.recognition.start({ sttPipeline: detachedPipeline });
      await flushTasks();
      controller == null ? void 0 : controller.enqueue({
        type: SpeechEventType.FINAL_TRANSCRIPT,
        alternatives: [{ text: "reused pipeline", confidence: 0.9 }]
      });
      await waitFor(() => second.hooks.onFinalTranscript.mock.calls.length === 1);
      expect(first.hooks.onFinalTranscript).not.toHaveBeenCalled();
      expect(second.hooks.onFinalTranscript).toHaveBeenCalledTimes(1);
    } finally {
      controller == null ? void 0 : controller.close();
      await first.recognition.close();
      await second.recognition.close();
      await pipeline.close();
    }
  });
  it("resets handoff-sensitive STT state when attaching a pipeline", async () => {
    const sttNode = async () => new ReadableStream({
      start() {
      }
    });
    const pipeline = new STTPipeline(sttNode);
    const { recognition } = createRecognition(sttNode);
    recognition.transcriptBuffer = [
      { type: SpeechEventType.FINAL_TRANSCRIPT, alternatives: [{ text: "stale transcript" }] }
    ];
    recognition.ignoreUserTranscriptUntil = Date.now();
    recognition._inputStartedAt = Date.now();
    try {
      await recognition.start({ sttPipeline: pipeline });
      expect(recognition.transcriptBuffer).toEqual([]);
      expect(recognition.ignoreUserTranscriptUntil).toBeUndefined();
      expect(recognition._inputStartedAt).toBeUndefined();
    } finally {
      await recognition.close();
      await pipeline.close();
    }
  });
  it("recreates the owned STT pipeline when clearing the user turn", async () => {
    let sttNodeCalls = 0;
    const sttNode = async () => {
      sttNodeCalls += 1;
      return new ReadableStream({
        start() {
        }
      });
    };
    const { recognition } = createRecognition(sttNode);
    try {
      await recognition.start();
      await waitFor(() => sttNodeCalls === 1);
      recognition.clearUserTurn();
      await waitFor(() => sttNodeCalls === 2);
    } finally {
      await recognition.close();
    }
  });
  it("keeps an STT pipeline alive across overlapping clearUserTurn calls", async () => {
    let sttNodeCalls = 0;
    const sttNode = async () => {
      sttNodeCalls += 1;
      return new ReadableStream({
        start() {
        }
      });
    };
    const { recognition } = createRecognition(sttNode);
    try {
      await recognition.start();
      await waitFor(() => sttNodeCalls === 1);
      recognition.clearUserTurn();
      recognition.clearUserTurn();
      await flushTasks();
      await flushTasks();
      await flushTasks();
      expect(recognition.sttPipeline).toBeDefined();
    } finally {
      await recognition.close();
    }
  });
  it("does not accumulate user turn limit state after clearing the user turn", async () => {
    const hooks = createHooks();
    const recognition = new AudioRecognition({
      recognitionHooks: hooks,
      minEndpointingDelay: 0,
      maxEndpointingDelay: 0,
      userTurnLimit: { maxWords: 5, maxDuration: null }
    });
    try {
      recognition.checkUserTurnLimit("one two three");
      expect(recognition.userTurnTracker.words).toBe(3);
      expect(hooks.onUserTurnExceeded).not.toHaveBeenCalled();
      recognition.clearUserTurn();
      recognition.checkUserTurnLimit("four five six");
      expect(recognition.userTurnTracker.words).toBe(3);
      expect(hooks.onUserTurnExceeded).not.toHaveBeenCalled();
    } finally {
      await recognition.close();
    }
  });
  it("does not recreate a new pipeline after ownership was detached for handoff", async () => {
    let sttNodeCalls = 0;
    const sttNode = async () => {
      sttNodeCalls += 1;
      return new ReadableStream({
        start() {
        }
      });
    };
    const { recognition } = createRecognition(sttNode);
    try {
      await recognition.start();
      await waitFor(() => sttNodeCalls === 1);
      await recognition.detachSttPipeline();
      recognition.clearUserTurn();
      await flushTasks();
      await flushTasks();
      await flushTasks();
      expect(sttNodeCalls).toBe(1);
      expect(recognition.sttPipeline).toBeUndefined();
    } finally {
      await recognition.close();
    }
  });
});
//# sourceMappingURL=audio_recognition_handoff.test.js.map