import { AgentInference } from "@livekit/protocol";
import { AudioFrame } from "@livekit/rtc-node";
import { describe, expect, it } from "vitest";
import { APIConnectionError } from "../../_exceptions.js";
import { DEFAULT_API_CONNECT_OPTIONS } from "../../types.js";
import { BaseStreamingTurnDetector } from "./base.js";
import { TurnDetectorStreamImpl } from "./detector.js";
import { ThresholdOptions } from "./languages.js";
import { CloudTransport } from "./transports.js";
const { ClientMessage } = AgentInference;
class FakeWS {
  sent = [];
  readyState = 1;
  // OPEN
  closeCbs = [];
  send(data) {
    if (this.readyState !== 1) throw new Error("ws closed");
    this.sent.push(ClientMessage.fromBinary(data));
  }
  close() {
    this.readyState = 3;
    for (const cb of this.closeCbs) cb();
  }
  on(event, cb) {
    if (event === "close") this.closeCbs.push(cb);
  }
}
class FakeDetector extends BaseStreamingTurnDetector {
  get model() {
    return "turn-detector-v1";
  }
  stream() {
    throw new Error("unused");
  }
}
function makeStream(opts) {
  const fakeWs = new FakeWS();
  const script = [...opts.connectScript ?? []];
  const turnOpts = {
    sampleRate: 16e3,
    thresholds: new ThresholdOptions("turn-detector-v1")
  };
  const detector = new FakeDetector(turnOpts);
  const cloudOpts = {
    baseUrl: "",
    apiKey: "x",
    apiSecret: "x",
    connOptions: {
      ...DEFAULT_API_CONNECT_OPTIONS,
      maxRetry: opts.maxRetry ?? 3,
      retryIntervalMs: opts.retryIntervalMs ?? 0
    }
  };
  const connect = async () => {
    if (script.length > 0) {
      const r = script.shift();
      if (r instanceof Error) throw r;
    }
    fakeWs.readyState = 1;
    return fakeWs;
  };
  const transport = new CloudTransport({ detector, opts: turnOpts, cloudOpts, connect });
  const stream = new TurnDetectorStreamImpl({
    detector,
    opts: turnOpts,
    cloudOpts,
    model: "turn-detector-v1",
    transport
  });
  return { stream, fakeWs, transport };
}
async function tick() {
  await new Promise((r) => setImmediate(r));
}
async function waitUntilConnected(transport, ticks = 50) {
  for (let i = 0; i < ticks; i++) {
    if (transport.transportReady()) return;
    await tick();
  }
  throw new Error("transport did not connect within timeout");
}
async function drainSendQueue(_transport, ticks = 50) {
  for (let i = 0; i < ticks; i++) {
    await tick();
  }
}
async function waitForCond(predicate, ticks = 50) {
  for (let i = 0; i < ticks; i++) {
    if (predicate()) return;
    await tick();
  }
}
function pcmFrame(samples = 320) {
  return new AudioFrame(new Int16Array(samples), 16e3, 1, samples);
}
describe("CloudStreamRetry", () => {
  it("num retries resets after a successful connect", async () => {
    const { stream, transport } = makeStream({
      connectScript: [new APIConnectionError({ message: "transient" }), null],
      maxRetry: 3,
      retryIntervalMs: 0
    });
    try {
      await waitUntilConnected(transport);
      expect(transport.connectCalls).toBe(2);
      expect(transport.numRetries).toBe(0);
    } finally {
      await stream.aclose();
    }
  });
});
describe("CloudToLocalFallback", () => {
  it("releases the shared audio reader lock on fallback (regression)", async () => {
    const { stream, transport } = makeStream({ connectScript: [null] });
    try {
      await waitUntilConnected(transport);
      stream.pushAudio(pcmFrame());
      await tick();
      const fut = stream.predict();
      stream.cancelInference({ timedOut: true });
      await fut.await;
      await waitForCond(() => stream.model === "turn-detector-v1-mini");
      expect(stream.isFallback).toBe(true);
      for (let i = 0; i < 10; i++) await tick();
      expect(stream.warnedLocalFailure).toBe(false);
    } finally {
      await stream.aclose();
    }
  });
});
describe("CloudStreamSendOrdering", () => {
  it("inferenceStart precedes inputAudio (FIFO)", async () => {
    const { stream, fakeWs, transport } = makeStream({ connectScript: [null] });
    try {
      await waitUntilConnected(transport);
      stream.predict();
      stream.pushAudio(pcmFrame());
      await drainSendQueue(transport);
      const kinds = fakeWs.sent.map((m) => m.message.case);
      const startIdx = kinds.indexOf("inferenceStart");
      const audioIdx = kinds.indexOf("inputAudio");
      expect(startIdx).toBeGreaterThanOrEqual(0);
      expect(audioIdx).toBeGreaterThanOrEqual(0);
      expect(startIdx).toBeLessThan(audioIdx);
    } finally {
      await stream.aclose();
    }
  });
  it("consecutive inferenceStarts are serialized in call order", async () => {
    const { stream, fakeWs, transport } = makeStream({ connectScript: [null] });
    try {
      await waitUntilConnected(transport);
      stream.predict();
      const firstId = stream._requestId;
      stream.predict();
      const secondId = stream._requestId;
      await drainSendQueue(transport);
      const startIds = [];
      for (const m of fakeWs.sent) {
        if (m.message.case === "inferenceStart") {
          startIds.push(m.message.value.requestId);
        }
      }
      expect(startIds).toEqual([firstId, secondId]);
    } finally {
      await stream.aclose();
    }
  });
});
//# sourceMappingURL=transports.test.js.map