import { AgentSession as pb } from "@livekit/protocol";
import * as net from "node:net";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { initializeLogger } from "../log.js";
import { SessionHost, SessionTransport, TcpSessionTransport } from "./remote_session.js";
beforeAll(() => {
  initializeLogger({ pretty: true, level: "info" });
});
function frame(msg) {
  const data = msg.toBinary();
  const header = Buffer.allocUnsafe(4);
  header.writeUInt32BE(data.length, 0);
  return Buffer.concat([header, Buffer.from(data)]);
}
function pingMessage(requestId) {
  return new pb.AgentSessionMessage({
    message: {
      case: "request",
      value: new pb.SessionRequest({
        requestId,
        request: { case: "ping", value: new pb.SessionRequest_Ping() }
      })
    }
  });
}
async function listen(server) {
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
  return server.address().port;
}
describe("TcpSessionTransport framing", () => {
  let server;
  let transport;
  afterEach(async () => {
    await (transport == null ? void 0 : transport.close());
    transport = void 0;
    if (server) {
      await new Promise((resolve) => server.close(() => resolve()));
      server = void 0;
    }
  });
  it("decodes a single framed message", async () => {
    server = net.createServer((sock) => {
      sock.write(frame(pingMessage("r1")));
    });
    const port = await listen(server);
    transport = new TcpSessionTransport("127.0.0.1", port);
    await transport.start();
    const it2 = transport[Symbol.asyncIterator]();
    const { value, done } = await it2.next();
    expect(done).toBe(false);
    expect(value.message.case).toBe("request");
    expect(value.message.value.requestId).toBe("r1");
  });
  it("reassembles a frame delivered across multiple chunks", async () => {
    const buf = frame(pingMessage("split"));
    server = net.createServer((sock) => {
      sock.write(buf.subarray(0, 5));
      setTimeout(() => sock.write(buf.subarray(5)), 10);
    });
    const port = await listen(server);
    transport = new TcpSessionTransport("127.0.0.1", port);
    await transport.start();
    const it2 = transport[Symbol.asyncIterator]();
    const { value } = await it2.next();
    expect(value.message.value.requestId).toBe("split");
  });
  it("decodes multiple frames coalesced into one chunk", async () => {
    server = net.createServer((sock) => {
      sock.write(Buffer.concat([frame(pingMessage("a")), frame(pingMessage("b"))]));
    });
    const port = await listen(server);
    transport = new TcpSessionTransport("127.0.0.1", port);
    await transport.start();
    const it2 = transport[Symbol.asyncIterator]();
    const first = await it2.next();
    const second = await it2.next();
    expect(first.value.message.value.requestId).toBe("a");
    expect(second.value.message.value.requestId).toBe("b");
  });
  it("round-trips a sent message back through the wire", async () => {
    const received = [];
    const sawFullFrame = new Promise((resolve) => {
      server = net.createServer((sock) => {
        sock.on("data", (d) => {
          received.push(d);
          resolve();
        });
      });
    });
    const port = await listen(server);
    transport = new TcpSessionTransport("127.0.0.1", port);
    await transport.start();
    await transport.sendMessage(pingMessage("outbound"));
    await sawFullFrame;
    const wire = Buffer.concat(received);
    const length = wire.readUInt32BE(0);
    const decoded = pb.AgentSessionMessage.fromBinary(wire.subarray(4, 4 + length));
    expect(decoded.message.value.requestId).toBe("outbound");
  });
  it("rejects start() when nothing is listening", async () => {
    const t = new TcpSessionTransport("127.0.0.1", 1);
    await expect(t.start()).rejects.toThrow();
  });
  it("unblocks a backpressured sendMessage when the transport closes", async () => {
    let serverSocket;
    const localServer = net.createServer((sock) => {
      serverSocket = sock;
    });
    const port = await listen(localServer);
    const t = new TcpSessionTransport("127.0.0.1", port);
    await t.start();
    try {
      const huge = "x".repeat(4 * 1024 * 1024);
      const msg = new pb.AgentSessionMessage({
        message: {
          case: "request",
          value: new pb.SessionRequest({
            requestId: huge,
            request: { case: "ping", value: new pb.SessionRequest_Ping() }
          })
        }
      });
      const sendPromise = t.sendMessage(msg);
      await new Promise((r) => setTimeout(r, 50));
      await t.close();
      await expect(sendPromise).resolves.toBeUndefined();
    } finally {
      await t.close();
      serverSocket == null ? void 0 : serverSocket.destroy();
      await new Promise((resolve) => localServer.close(() => resolve()));
    }
  });
});
class FakeTransport extends SessionTransport {
  sent = [];
  inbound = [];
  waitingResolve = null;
  closed = false;
  push(msg) {
    if (this.waitingResolve) {
      const resolve = this.waitingResolve;
      this.waitingResolve = null;
      resolve({ value: msg, done: false });
    } else {
      this.inbound.push(msg);
    }
  }
  async sendMessage(msg) {
    this.sent.push(msg);
  }
  async close() {
    this.closed = true;
    if (this.waitingResolve) {
      const resolve = this.waitingResolve;
      this.waitingResolve = null;
      resolve({ value: void 0, done: true });
    }
  }
  [Symbol.asyncIterator]() {
    return {
      next: () => {
        const pending = this.inbound.shift();
        if (pending) return Promise.resolve({ value: pending, done: false });
        if (this.closed) {
          return Promise.resolve({
            value: void 0,
            done: true
          });
        }
        return new Promise((resolve) => {
          this.waitingResolve = resolve;
        });
      }
    };
  }
}
describe("SessionHost updateIo", () => {
  it("toggles input/output enabled flags and acks", async () => {
    const setInputAudio = vi.fn();
    const setOutputAudio = vi.fn();
    const setTranscription = vi.fn();
    const fakeSession = {
      on: () => {
      },
      off: () => {
      },
      input: { setAudioEnabled: setInputAudio },
      output: {
        setAudioEnabled: setOutputAudio,
        setTranscriptionEnabled: setTranscription
      }
    };
    const transport = new FakeTransport();
    const host = new SessionHost(transport);
    host.registerSession(fakeSession);
    await host.start();
    transport.push(
      new pb.AgentSessionMessage({
        message: {
          case: "request",
          value: new pb.SessionRequest({
            requestId: "io1",
            request: {
              case: "updateIo",
              value: new pb.SessionRequest_UpdateIO({
                input: new pb.SessionRequest_UpdateIO_Input({ audioEnabled: false }),
                output: new pb.SessionRequest_UpdateIO_Output({
                  audioEnabled: true,
                  transcriptionEnabled: false
                })
              })
            }
          })
        }
      })
    );
    await vi.waitFor(() => expect(transport.sent.length).toBe(1));
    expect(setInputAudio).toHaveBeenCalledWith(false);
    expect(setOutputAudio).toHaveBeenCalledWith(true);
    expect(setTranscription).toHaveBeenCalledWith(false);
    const resp = transport.sent[0].message.value;
    expect(resp.requestId).toBe("io1");
    expect(resp.response.case).toBe("updateIo");
    await host.close();
  });
  it("ignores unset optional fields", async () => {
    const setInputAudio = vi.fn();
    const setOutputAudio = vi.fn();
    const setTranscription = vi.fn();
    const fakeSession = {
      on: () => {
      },
      off: () => {
      },
      input: { setAudioEnabled: setInputAudio },
      output: {
        setAudioEnabled: setOutputAudio,
        setTranscriptionEnabled: setTranscription
      }
    };
    const transport = new FakeTransport();
    const host = new SessionHost(transport);
    host.registerSession(fakeSession);
    await host.start();
    transport.push(
      new pb.AgentSessionMessage({
        message: {
          case: "request",
          value: new pb.SessionRequest({
            requestId: "io2",
            request: {
              case: "updateIo",
              value: new pb.SessionRequest_UpdateIO({
                output: new pb.SessionRequest_UpdateIO_Output({ transcriptionEnabled: true })
              })
            }
          })
        }
      })
    );
    await vi.waitFor(() => expect(transport.sent.length).toBe(1));
    expect(setInputAudio).not.toHaveBeenCalled();
    expect(setOutputAudio).not.toHaveBeenCalled();
    expect(setTranscription).toHaveBeenCalledWith(true);
    await host.close();
  });
});
//# sourceMappingURL=remote_session.test.js.map