import { describe, expect, it, vi } from "vitest";
import { ChatContext } from "../llm/chat_context.js";
import { RealtimeModel } from "../llm/realtime.js";
import { initializeLogger, log } from "../log.js";
import { Agent } from "./agent.js";
import {
  AgentActivity,
  cleanupReusableResources
} from "./agent_activity.js";
initializeLogger({ pretty: false, level: "silent" });
function createFakeActivity(agent, stt, inputStartedAt) {
  const detachedPipeline = { id: Symbol("pipeline") };
  const activity = {
    agent,
    audioRecognition: {
      detachSttPipeline: vi.fn(async () => detachedPipeline),
      inputStartedAt
    },
    stt,
    llm: void 0,
    tools: [],
    realtimeSession: void 0
  };
  return { activity, detachedPipeline };
}
async function detachResources(oldActivity, newActivity) {
  const fn = AgentActivity.prototype._detachReusableResources;
  return await fn.call(oldActivity, newActivity);
}
describe("AgentActivity STT handoff reuse eligibility", () => {
  it("reuses the pipeline when both activities share the same STT instance and sttNode", async () => {
    var _a;
    const sharedStt = { id: "shared-stt" };
    const oldActivity = createFakeActivity(new Agent({ instructions: "a" }), sharedStt);
    const newActivity = createFakeActivity(new Agent({ instructions: "b" }), sharedStt);
    const resources = await detachResources(oldActivity.activity, newActivity.activity);
    expect(resources.sttPipeline).toBe(oldActivity.detachedPipeline);
    expect((_a = oldActivity.activity.audioRecognition) == null ? void 0 : _a.detachSttPipeline).toHaveBeenCalledTimes(1);
  });
  it("carries the original input start time with a reused STT pipeline", async () => {
    const sharedStt = { id: "shared-stt" };
    const oldInputStartedAt = Date.now() - 6e4;
    const oldActivity = createFakeActivity(
      new Agent({ instructions: "a" }),
      sharedStt,
      oldInputStartedAt
    );
    const newActivity = createFakeActivity(new Agent({ instructions: "b" }), sharedStt);
    const resources = await detachResources(oldActivity.activity, newActivity.activity);
    expect(resources.sttPipeline).toBe(oldActivity.detachedPipeline);
    expect(resources.sttInputStartedAt).toBe(oldInputStartedAt);
  });
  it("does not reuse when the STT instances differ", async () => {
    var _a;
    const oldActivity = createFakeActivity(new Agent({ instructions: "a" }), { id: "stt-a" });
    const newActivity = createFakeActivity(new Agent({ instructions: "b" }), { id: "stt-b" });
    const resources = await detachResources(oldActivity.activity, newActivity.activity);
    expect(resources.sttPipeline).toBeUndefined();
    expect((_a = oldActivity.activity.audioRecognition) == null ? void 0 : _a.detachSttPipeline).not.toHaveBeenCalled();
  });
  it("does not reuse when either activity has no STT", async () => {
    var _a;
    const sharedStt = { id: "shared-stt" };
    const oldActivity = createFakeActivity(new Agent({ instructions: "a" }), void 0);
    const newActivity = createFakeActivity(new Agent({ instructions: "b" }), sharedStt);
    const resources = await detachResources(oldActivity.activity, newActivity.activity);
    expect(resources.sttPipeline).toBeUndefined();
    expect((_a = oldActivity.activity.audioRecognition) == null ? void 0 : _a.detachSttPipeline).not.toHaveBeenCalled();
  });
  it("does not reuse when the agents override sttNode differently", async () => {
    var _a;
    const sharedStt = { id: "shared-stt" };
    class AgentA extends Agent {
      async sttNode(_audio, _modelSettings) {
        return null;
      }
    }
    class AgentB extends Agent {
      async sttNode(_audio, _modelSettings) {
        return null;
      }
    }
    const oldActivity = createFakeActivity(new AgentA({ instructions: "a" }), sharedStt);
    const newActivity = createFakeActivity(new AgentB({ instructions: "b" }), sharedStt);
    const resources = await detachResources(oldActivity.activity, newActivity.activity);
    expect(resources.sttPipeline).toBeUndefined();
    expect((_a = oldActivity.activity.audioRecognition) == null ? void 0 : _a.detachSttPipeline).not.toHaveBeenCalled();
  });
  it("does not reuse when the new agent inherits the same custom sttNode implementation", async () => {
    var _a;
    const sharedStt = { id: "shared-stt" };
    class AgentA extends Agent {
      async sttNode(_audio, _modelSettings) {
        return null;
      }
    }
    class AgentB extends AgentA {
    }
    const oldActivity = createFakeActivity(new AgentA({ instructions: "a" }), sharedStt);
    const newActivity = createFakeActivity(new AgentB({ instructions: "b" }), sharedStt);
    const resources = await detachResources(oldActivity.activity, newActivity.activity);
    expect(resources.sttPipeline).toBeUndefined();
    expect((_a = oldActivity.activity.audioRecognition) == null ? void 0 : _a.detachSttPipeline).not.toHaveBeenCalled();
  });
  it("does not reuse when the old activity has no audioRecognition", async () => {
    const sharedStt = { id: "shared-stt" };
    const oldActivity = createFakeActivity(new Agent({ instructions: "a" }), sharedStt);
    const newActivity = createFakeActivity(new Agent({ instructions: "b" }), sharedStt);
    oldActivity.activity.audioRecognition = void 0;
    const resources = await detachResources(oldActivity.activity, newActivity.activity);
    expect(resources.sttPipeline).toBeUndefined();
  });
});
describe("AgentActivity RT session reuse eligibility", () => {
  function createFakeRtSession() {
    return {
      chatCtx: ChatContext.empty(),
      off: vi.fn(),
      on: vi.fn(),
      interrupt: vi.fn(),
      clearAudio: vi.fn(),
      close: vi.fn(async () => {
      })
    };
  }
  class FakeRealtimeModel extends RealtimeModel {
    get model() {
      return "fake";
    }
    label() {
      return "fake.RealtimeModel";
    }
    session() {
      throw new Error("not implemented");
    }
    async close() {
    }
  }
  function createFakeRealtimeModel(capabilitiesOverrides = {}) {
    const capabilities = {
      messageTruncation: false,
      turnDetection: false,
      userTranscription: false,
      autoToolReplyGeneration: false,
      audioOutput: true,
      manualFunctionCalls: false,
      midSessionChatCtxUpdate: false,
      midSessionInstructionsUpdate: false,
      midSessionToolsUpdate: false,
      ...capabilitiesOverrides
    };
    return new FakeRealtimeModel(capabilities);
  }
  function createRtActivity(agent, llm, rtSession) {
    return {
      agent,
      audioRecognition: void 0,
      stt: void 0,
      llm,
      tools: agent.toolCtx,
      realtimeSession: rtSession
    };
  }
  it("reuses RT session when same LLM, same instructions, equivalent context, and same tools", async () => {
    const sharedLlm = createFakeRealtimeModel();
    const rtSession = createFakeRtSession();
    const oldAgent = new Agent({ instructions: "hello" });
    const newAgent = new Agent({ instructions: "hello" });
    const oldActivity = createRtActivity(oldAgent, sharedLlm, rtSession);
    const newActivity = createRtActivity(newAgent, sharedLlm);
    const resources = await detachResources(oldActivity, newActivity);
    expect(resources.rtSession).toBe(rtSession);
    expect(oldActivity.realtimeSession).toBeUndefined();
    expect(rtSession.off).toHaveBeenCalled();
  });
  it("does not reuse RT session when LLM instances differ", async () => {
    const rtSession = createFakeRtSession();
    const oldLlm = createFakeRealtimeModel();
    const newLlm = createFakeRealtimeModel();
    const oldActivity = createRtActivity(new Agent({ instructions: "a" }), oldLlm, rtSession);
    const newActivity = createRtActivity(new Agent({ instructions: "a" }), newLlm);
    const resources = await detachResources(oldActivity, newActivity);
    expect(resources.rtSession).toBeUndefined();
  });
  it("does not reuse RT session when instructions differ and midSessionInstructionsUpdate is false", async () => {
    const sharedLlm = createFakeRealtimeModel({ midSessionInstructionsUpdate: false });
    const rtSession = createFakeRtSession();
    const oldActivity = createRtActivity(new Agent({ instructions: "old" }), sharedLlm, rtSession);
    const newActivity = createRtActivity(new Agent({ instructions: "new" }), sharedLlm);
    const resources = await detachResources(oldActivity, newActivity);
    expect(resources.rtSession).toBeUndefined();
  });
  it("reuses RT session when instructions differ but midSessionInstructionsUpdate is true", async () => {
    const sharedLlm = createFakeRealtimeModel({ midSessionInstructionsUpdate: true });
    const rtSession = createFakeRtSession();
    const oldActivity = createRtActivity(new Agent({ instructions: "old" }), sharedLlm, rtSession);
    const newActivity = createRtActivity(new Agent({ instructions: "new" }), sharedLlm);
    const resources = await detachResources(oldActivity, newActivity);
    expect(resources.rtSession).toBe(rtSession);
  });
  it("reuses RT session when context differs but midSessionChatCtxUpdate is true", async () => {
    const sharedLlm = createFakeRealtimeModel({ midSessionChatCtxUpdate: true });
    const rtSession = createFakeRtSession();
    const oldActivity = createRtActivity(new Agent({ instructions: "same" }), sharedLlm, rtSession);
    const newActivity = createRtActivity(new Agent({ instructions: "same" }), sharedLlm);
    const resources = await detachResources(oldActivity, newActivity);
    expect(resources.rtSession).toBe(rtSession);
  });
  it("does not reuse when no RT session exists", async () => {
    const sharedLlm = createFakeRealtimeModel();
    const oldActivity = createRtActivity(new Agent({ instructions: "a" }), sharedLlm, void 0);
    const newActivity = createRtActivity(new Agent({ instructions: "a" }), sharedLlm);
    const resources = await detachResources(oldActivity, newActivity);
    expect(resources.rtSession).toBeUndefined();
  });
  it("does not reuse when LLM is not a RealtimeModel", async () => {
    const rtSession = createFakeRtSession();
    const nonRealtimeLlm = { id: "plain-llm" };
    const oldActivity = createRtActivity(
      new Agent({ instructions: "a" }),
      nonRealtimeLlm,
      rtSession
    );
    const newActivity = createRtActivity(new Agent({ instructions: "a" }), nonRealtimeLlm);
    const resources = await detachResources(oldActivity, newActivity);
    expect(resources.rtSession).toBeUndefined();
  });
});
describe("AgentActivity blockNewTurns (handoff transition)", () => {
  function createBareActivity() {
    const activity = Object.create(AgentActivity.prototype);
    activity.logger = { warn: vi.fn(), debug: vi.fn(), info: vi.fn(), error: vi.fn() };
    activity.cancelPreemptiveGeneration = vi.fn();
    activity.createSpeechTask = vi.fn(() => ({ cancel: vi.fn() }));
    return activity;
  }
  const endOfTurnInfo = {
    newTranscript: "hello again",
    transcriptConfidence: 1,
    transcriptionDelay: 0,
    endOfUtteranceDelay: 0,
    startedSpeakingAt: void 0,
    stoppedSpeakingAt: void 0
  };
  it("onEndOfTurn skips a new user turn while new turns are blocked, even when scheduling is still running", async () => {
    const activity = createBareActivity();
    activity._schedulingPaused = false;
    activity.newTurnsBlocked = false;
    activity.blockNewTurns();
    const handled = await activity.onEndOfTurn(endOfTurnInfo);
    expect(handled).toBe(true);
    expect(activity.cancelPreemptiveGeneration).toHaveBeenCalledTimes(1);
    expect(activity.createSpeechTask).not.toHaveBeenCalled();
  });
  it("onEndOfTurn schedules the turn normally when new turns are not blocked", async () => {
    const activity = createBareActivity();
    activity._schedulingPaused = false;
    activity.newTurnsBlocked = false;
    activity.agent = { stt: void 0 };
    activity.agentSession = { stt: void 0 };
    activity._currentSpeech = void 0;
    activity._userTurnCompletedTask = void 0;
    const handled = await activity.onEndOfTurn(endOfTurnInfo);
    expect(handled).toBe(true);
    expect(activity.createSpeechTask).toHaveBeenCalledTimes(1);
  });
  it("onUserTurnExceeded is independent of newTurnsBlocked", () => {
    const activity = createBareActivity();
    activity._schedulingPaused = false;
    activity.newTurnsBlocked = false;
    activity.userTurnExceededLocked = false;
    activity.userTurnExceededTask = void 0;
    activity.blockNewTurns();
    const ev = {
      type: "user_turn_exceeded",
      transcript: "hi",
      accumulatedTranscript: "hi",
      accumulatedWordCount: 10,
      duration: 5e3,
      createdAt: Date.now()
    };
    activity.onUserTurnExceeded(ev);
    expect(activity.createSpeechTask).toHaveBeenCalledTimes(1);
  });
  it("userTurnCompleted skips before the callback when new turns are blocked (not closing)", async () => {
    const activity = createBareActivity();
    activity._schedulingPaused = false;
    activity.newTurnsBlocked = false;
    activity._currentSpeech = void 0;
    const onUserTurnCompleted = vi.fn(async () => {
    });
    activity.agent = { llm: void 0, chatCtx: ChatContext.empty(), onUserTurnCompleted };
    activity.agentSession = { llm: void 0, _closing: false, _conversationItemAdded: vi.fn() };
    activity.blockNewTurns();
    await activity.userTurnCompleted(endOfTurnInfo);
    expect(onUserTurnCompleted).not.toHaveBeenCalled();
    expect(activity.agentSession._conversationItemAdded).not.toHaveBeenCalled();
    expect(activity.createSpeechTask).not.toHaveBeenCalled();
  });
  it("userTurnCompleted commits the skipped message to chat ctx when closing", async () => {
    const activity = createBareActivity();
    activity._schedulingPaused = false;
    activity.newTurnsBlocked = false;
    activity._currentSpeech = void 0;
    const push = vi.fn();
    const conversationItemAdded = vi.fn();
    activity.agent = {
      llm: void 0,
      chatCtx: ChatContext.empty(),
      _chatCtx: { items: { push } },
      onUserTurnCompleted: vi.fn(async () => {
      })
    };
    activity.agentSession = {
      llm: void 0,
      _closing: true,
      _conversationItemAdded: conversationItemAdded
    };
    activity.blockNewTurns();
    await activity.userTurnCompleted(endOfTurnInfo);
    expect(push).toHaveBeenCalledTimes(1);
    expect(conversationItemAdded).toHaveBeenCalledTimes(1);
    expect(activity.createSpeechTask).not.toHaveBeenCalled();
  });
  it("userTurnCompleted re-checks after the callback when a handoff blocks new turns mid-callback", async () => {
    const activity = createBareActivity();
    activity._schedulingPaused = false;
    activity.newTurnsBlocked = false;
    activity._currentSpeech = void 0;
    const onUserTurnCompleted = vi.fn(async () => {
      activity.newTurnsBlocked = true;
    });
    const plainLlm = { id: "plain-llm" };
    activity.agent = { llm: plainLlm, chatCtx: ChatContext.empty(), onUserTurnCompleted };
    activity.agentSession = { llm: void 0, _closing: false, _conversationItemAdded: vi.fn() };
    await activity.userTurnCompleted(endOfTurnInfo);
    expect(onUserTurnCompleted).toHaveBeenCalledTimes(1);
    expect(activity.createSpeechTask).not.toHaveBeenCalled();
  });
});
describe("cleanupReusableResources", () => {
  it("closes both STT pipeline and RT session", async () => {
    const sttClose = vi.fn(async () => {
    });
    const rtClose = vi.fn(async () => {
    });
    const resources = {
      sttPipeline: { close: sttClose },
      rtSession: { close: rtClose }
    };
    await cleanupReusableResources(resources, log());
    expect(sttClose).toHaveBeenCalledTimes(1);
    expect(rtClose).toHaveBeenCalledTimes(1);
    expect(resources.sttPipeline).toBeUndefined();
    expect(resources.rtSession).toBeUndefined();
  });
  it("handles partial resources (only STT)", async () => {
    const sttClose = vi.fn(async () => {
    });
    const resources = {
      sttPipeline: { close: sttClose }
    };
    await cleanupReusableResources(resources, log());
    expect(sttClose).toHaveBeenCalledTimes(1);
    expect(resources.sttPipeline).toBeUndefined();
  });
  it("handles empty resources", async () => {
    const resources = {};
    await cleanupReusableResources(resources, log());
  });
});
//# sourceMappingURL=agent_activity_handoff.test.js.map