import { describe, expect, test } from "bun:test";
import { FailoverVoiceProvider } from "./failover-voice-provider.ts";
import { FakeVoiceProvider } from "./fake-voice-provider.ts";
import { VoicePolicyProxy, type SchedulingToolPort } from "./policy-proxy.ts";
import { RingCentralBridge } from "./ringcentral-bridge.ts";
import { SimulatedRingCentralClient } from "./simulated-media.ts";
import type {
  VoiceCallAuditRecord,
  VoiceProvider,
  VoiceProviderConnectInput,
  VoiceProviderKind,
  VoiceProviderSession,
} from "./types.ts";

describe("RingCentralBridge", () => {
  test("answers simulated call and routes reschedule intent through policy proxy", async () => {
    const toolCalls: string[] = [];
    const audits: VoiceCallAuditRecord[] = [];
    const { client } = startBridge({
      scheduling: schedulingPort(toolCalls),
      audits,
    });

    const call = await client.receiveCall({ callId: "call-1", callerNumber: "+19725550100" });
    await call.callerSays("I need to reschedule my appointment");
    await call.close();

    expect(call.answered).toBe(true);
    expect(call.transferredTo).toBeUndefined();
    expect(toolCalls).toEqual(["find_reschedule_options"]);
    expect(audits[0]!.disposition).toBe("resolved_by_ai");
    expect(audits[0]!.transcripts).toMatchObject([
      {
        role: "caller",
        text: "I need to reschedule my appointment",
        isFinal: true,
      },
      {
        role: "agent",
        isFinal: true,
      },
    ]);
    expect(audits[0]!.toolCalls).toMatchObject([
      {
        id: "tool-options",
        name: "find_reschedule_options",
        arguments: { patient_id: "test-patient", appointment_id: "test-appointment" },
        result: { ok: true, action: "offer_slots" },
      },
    ]);
  });

  test("transfers simulated urgent language to staff", async () => {
    const audits: VoiceCallAuditRecord[] = [];
    const { client } = startBridge({
      scheduling: schedulingPort([]),
      audits,
    });

    const call = await client.receiveCall({ callId: "call-2", callerNumber: "+19725550100" });
    await call.callerSays("I have chest pain and need to reschedule");
    await call.close();

    expect(call.transferredTo).toBe("+15550999000");
    expect(audits[0]!.disposition).toBe("transferred");
    expect(audits[0]!.ringCentral.transferDestination).toBe("+15550999000");
  });

  test("audits the actual fallback voice provider after failover", async () => {
    const audits: VoiceCallAuditRecord[] = [];
    const { client } = startBridge({
      scheduling: schedulingPort([]),
      audits,
      voiceProvider: new FailoverVoiceProvider(
        new ThrowingVoiceProvider("openai_realtime"),
        [new FakeVoiceProvider("grok_voice")],
      ),
    });

    const call = await client.receiveCall({ callId: "call-3", callerNumber: "+19725550100" });
    await call.callerSays("I need to reschedule");
    await call.close();

    expect(audits[0]!.voiceProvider).toBe("grok_voice");
  });

  test("transfers and audits when all voice providers fail to connect", async () => {
    const audits: VoiceCallAuditRecord[] = [];
    const { client } = startBridge({
      scheduling: schedulingPort([]),
      audits,
      voiceProvider: new ThrowingVoiceProvider("openai_realtime"),
    });

    const call = await client.receiveCall({ callId: "call-4", callerNumber: "+19725550100" });

    expect(call.transferredTo).toBe("+15550999000");
    expect(audits).toHaveLength(1);
    expect(audits[0]).toMatchObject({
      disposition: "voice_provider_failed",
      voiceProvider: "openai_realtime",
      ringCentral: { transferDestination: "+15550999000" },
      notes: ["Voice provider connection failed before audio streaming started."],
    });
  });

  test("sends initial recording disclosure prompt after provider connect", async () => {
    const texts: string[] = [];
    const { client } = startBridge({
      scheduling: schedulingPort([]),
      audits: [],
      voiceProvider: new RecordingVoiceProvider(texts),
      initialPrompt: "Greet the caller and disclose that this call may be recorded.",
    });

    await client.receiveCall({ callId: "call-5", callerNumber: "+19725550100" });

    expect(texts).toEqual([
      "Greet the caller and disclose that this call may be recorded.",
    ]);
  });

  test("closes voice provider session before transfer on provider error", async () => {
    const audits: VoiceCallAuditRecord[] = [];
    const provider = new ErroringVoiceProvider();
    const { client } = startBridge({
      scheduling: schedulingPort([]),
      audits,
      voiceProvider: provider,
    });

    const call = await client.receiveCall({ callId: "call-6", callerNumber: "+15550100" });
    await provider.fail();

    expect(provider.closedWith).toBe("voice provider error");
    expect(call.transferredTo).toBe("+15550999000");
    expect(audits[0]!.disposition).toBe("rc_media_failed");
    expect(audits[0]!.ringCentral.transferDestination).toBe("+15550999000");
  });
});

function startBridge(input: {
  scheduling: SchedulingToolPort;
  audits: VoiceCallAuditRecord[];
  voiceProvider?: VoiceProvider;
  initialPrompt?: string;
}): { client: SimulatedRingCentralClient } {
  const client = new SimulatedRingCentralClient();
  const bridge = new RingCentralBridge(client, {
    voiceProvider: input.voiceProvider ?? new FakeVoiceProvider("openai_realtime"),
    policyProxy: new VoicePolicyProxy(input.scheduling),
    humanTransferDestination: "+15550999000",
    instructions: "Exult test agent.",
    initialPrompt: input.initialPrompt,
    onAudit: (record) => {
      input.audits.push(record);
    },
  });
  bridge.start();
  return { client };
}

class ThrowingVoiceProvider implements VoiceProvider {
  constructor(readonly kind: VoiceProviderKind) {}

  async connect(_input: VoiceProviderConnectInput): Promise<VoiceProviderSession> {
    throw new Error("connect failed");
  }
}

class RecordingVoiceProvider implements VoiceProvider {
  readonly kind = "openai_realtime" as const;

  constructor(private readonly texts: string[]) {}

  async connect(input: VoiceProviderConnectInput): Promise<VoiceProviderSession> {
    return {
      provider: this.kind,
      sessionId: input.callId,
      sendAudio: async () => {},
      sendText: async (text) => {
        this.texts.push(text);
      },
      sendToolResult: async () => {},
      interrupt: async () => {},
      close: async () => {},
    };
  }
}

class ErroringVoiceProvider implements VoiceProvider {
  readonly kind = "openai_realtime" as const;
  private events?: VoiceProviderConnectInput["events"];
  closedWith?: string;

  async connect(input: VoiceProviderConnectInput): Promise<VoiceProviderSession> {
    this.events = input.events;
    return {
      provider: this.kind,
      sessionId: input.callId,
      sendAudio: async () => {},
      sendText: async () => {},
      sendToolResult: async () => {},
      interrupt: async () => {},
      close: async (reason) => {
        this.closedWith = reason;
      },
    };
  }

  async fail(): Promise<void> {
    await this.events?.onError?.(new Error("provider failed"));
  }
}

function schedulingPort(calls: string[]): SchedulingToolPort {
  return {
    async verifyPatientIdentity() {
      calls.push("verify_patient_identity");
      return { ok: true, action: "continue", message: "verified" };
    },
    async findRescheduleOptions() {
      calls.push("find_reschedule_options");
      return { ok: true, action: "offer_slots", message: "slots" };
    },
    async requestReschedule() {
      calls.push("request_reschedule");
      return { ok: false, action: "approval_required", message: "approval" };
    },
    async createCallback() {
      calls.push("create_callback");
      return { ok: true, action: "callback_required", message: "callback" };
    },
    async transferToStaff() {
      calls.push("transfer_to_staff");
      return { ok: true, action: "transfer_to_staff", message: "transfer" };
    },
  };
}
