import { describe, expect, test } from "bun:test";
import { EventEmitter } from "node:events";
import { RingCentralSoftphoneAdapter } from "./ringcentral-softphone-adapter.ts";
import type { RingCentralSipConfig } from "./config.ts";
import type { AudioFrame } from "./types.ts";

describe("RingCentralSoftphoneAdapter", () => {
  test("registers the documented softphone config and maps inbound invites", async () => {
    const fake = new FakeSoftphone(fakeSip());
    const AdapterSoftphone = fake.constructorForTest();
    const adapter = new RingCentralSoftphoneAdapter({
      sip: fakeSip(),
      softphoneConstructor: AdapterSoftphone,
    });
    const calls: string[] = [];
    let resolveInvite: () => void = () => undefined;
    let rejectInvite: (err: unknown) => void = () => undefined;
    const inviteHandled = new Promise<void>((resolve, reject) => {
      resolveInvite = resolve;
      rejectInvite = reject;
    });

    adapter.onInvite(async (invite, call) => {
      try {
        calls.push(invite.callId);
        expect(invite.callerNumber).toBe("+19725550100");
        expect(invite.calledNumber).toBe("+15550999000");
        await call.sendAudio(audioFrame([0, 1000, 2000, 3000]));
        resolveInvite();
      } catch (err) {
        rejectInvite(err);
      }
    });

    await adapter.register();
    await fake.receiveInvite();
    await inviteHandled;

    expect(fake.registered).toBe(true);
    expect(fake.config.codec).toBe("OPUS/16000");
    expect(calls).toEqual(["call-1"]);
    expect(fake.session.streamedBuffers).toHaveLength(1);
    expect(fake.session.streamedBuffers[0]).toBeInstanceOf(Buffer);
  });

  test("converts decoded RTP payloads into telephony PCM frames", async () => {
    const fake = new FakeSoftphone(fakeSip());
    const adapter = new RingCentralSoftphoneAdapter({
      sip: fakeSip(),
      softphoneConstructor: fake.constructorForTest(),
    });
    const audioFrames: AudioFrame[] = [];

    adapter.onInvite(async (_invite, call) => {
      call.onAudio((frame) => {
        audioFrames.push(frame);
      });
    });

    await adapter.register();
    await fake.receiveInvite();
    fake.session.emit("audioPacket", { payload: new Uint8Array([1, 0, 2, 0]) });

    expect(audioFrames[0]?.format).toEqual({ codec: "pcm16", sampleRateHz: 16000, channels: 1 });
    expect([...audioFrames[0]!.data]).toEqual([1, 0, 2, 0]);
  });

  test("keeps streaming after one outgoing audio frame fails", async () => {
    const fake = new FakeSoftphone(fakeSip());
    const adapter = new RingCentralSoftphoneAdapter({
      sip: fakeSip(),
      softphoneConstructor: fake.constructorForTest(),
    });
    let activeCall: Parameters<Parameters<typeof adapter.onInvite>[0]>[1] | undefined;

    adapter.onInvite(async (_invite, call) => {
      activeCall = call;
    });

    await adapter.register();
    await fake.receiveInvite();
    fake.session.failNextStream = true;

    await activeCall!.sendAudio(audioFrame([1, 2]));
    await activeCall!.sendAudio(audioFrame([3, 4]));

    expect(fake.session.streamedBuffers).toHaveLength(1);
    expect([...fake.session.streamedBuffers[0]!]).toEqual([3, 4]);
  });
});

function fakeSip(): RingCentralSipConfig {
  return {
    domain: "sip.ringcentral.com",
    outboundProxy: "sip.ringcentral.com",
    username: "user",
    password: "pass",
    authorizationId: "auth",
    codec: "OPUS/16000",
    debug: false,
  };
}

function audioFrame(values: number[]): AudioFrame {
  return {
    data: new Uint8Array(values),
    format: { codec: "pcm16", sampleRateHz: 16000, channels: 1 },
  };
}

class FakeSoftphone extends EventEmitter {
  registered = false;
  session = new FakeCallSession();

  constructor(readonly config: FakeSoftphoneConfig) {
    super();
  }

  constructorForTest(): new (config: FakeSoftphoneConfig) => FakeSoftphone {
    const instance = this;
    return class extends FakeSoftphone {
      constructor(config: FakeSoftphoneConfig) {
        super(config);
        return instance;
      }
    };
  }

  async register(): Promise<void> {
    this.registered = true;
  }

  async answer(): Promise<FakeCallSession> {
    return this.session;
  }

  async receiveInvite(): Promise<void> {
    this.emit("invite", {
      getHeader(name: string) {
        if (name === "From") return "<sip:+19725550100@sip.ringcentral.com>";
        if (name === "To") return "<sip:+15550999000@sip.ringcentral.com>";
        return undefined;
      },
    });
    await Promise.resolve();
  }
}

type FakeSoftphoneConfig = Omit<RingCentralSipConfig, "debug" | "codec"> & {
  codec?: RingCentralSipConfig["codec"];
};

class FakeCallSession extends EventEmitter {
  callId = "call-1";
  streamedBuffers: Buffer[] = [];
  failNextStream = false;

  streamAudio(buffer: Buffer): EventEmitter {
    if (this.failNextStream) {
      this.failNextStream = false;
      throw new Error("stream failed");
    }
    this.streamedBuffers.push(buffer);
    const streamer = new EventEmitter();
    queueMicrotask(() => streamer.emit("finished"));
    return streamer;
  }

  async transfer(): Promise<void> {}

  async hangup(): Promise<void> {}
}
