import { Heap } from "heap-js";
import { describe, expect, it, vi } from "vitest";
import { ChatContext } from "../llm/chat_context.js";
import { LLM } from "../llm/llm.js";
import { Future, Task } from "../utils.js";
import { _getActivityTaskInfo } from "./agent.js";
import { AgentActivity } from "./agent_activity.js";
import { SpeechHandle } from "./speech_handle.js";
const agentMocks = vi.hoisted(() => ({
  getActivityTaskInfo: vi.fn(() => null)
}));
vi.mock("./agent.js", () => {
  class Agent {
  }
  class AgentTask extends Agent {
  }
  class StopResponse {
  }
  return {
    Agent,
    AgentTask,
    StopResponse,
    _getActivityTaskInfo: agentMocks.getActivityTaskInfo,
    _setActivityTaskInfo: () => {
    },
    functionCallStorage: {
      getStore: () => void 0,
      enterWith: () => {
      },
      run: (_, fn) => fn()
    },
    speechHandleStorage: {
      getStore: () => void 0,
      enterWith: () => {
      }
    }
  };
});
vi.mock("../version.js", () => ({ version: "0.0.0-test" }));
async function raceTimeout(promise, ms) {
  let timer;
  const timeout = new Promise((resolve) => {
    timer = setTimeout(() => resolve("timeout"), ms);
  });
  return Promise.race([promise.then(() => "resolved"), timeout]).finally(
    () => clearTimeout(timer)
  );
}
function buildMainTaskRunner() {
  const q_updated = new Future();
  const speechQueue = new Heap((a, b) => b[0] - a[0] || a[1] - b[1]);
  const speechTasks = /* @__PURE__ */ new Set();
  const fakeActivity = {
    q_updated,
    speechQueue,
    speechTasks,
    _currentSpeech: void 0,
    _schedulingPaused: false,
    _authorizationPaused: false,
    _drainBlockedTasks: [],
    _mainTask: void 0,
    logger: {
      info: () => {
      },
      debug: () => {
      },
      warn: () => {
      },
      error: () => {
      }
    }
  };
  const proto = AgentActivity.prototype;
  fakeActivity.getDrainPendingSpeechTasks = proto.getDrainPendingSpeechTasks.bind(fakeActivity);
  fakeActivity.wakeupMainTask = proto.wakeupMainTask.bind(fakeActivity);
  const mainTask = proto.mainTask;
  const pauseSchedulingTask = proto._pauseSchedulingTask;
  return {
    fakeActivity,
    mainTask: mainTask.bind(fakeActivity),
    pauseSchedulingTask: pauseSchedulingTask.bind(fakeActivity),
    speechQueue,
    speechTasks,
    q_updated
  };
}
describe("AgentActivity - mainTask", () => {
  it("should recover when speech handle is interrupted after authorization", async () => {
    const { fakeActivity, mainTask, speechQueue, q_updated } = buildMainTaskRunner();
    const handle = SpeechHandle.create({ allowInterruptions: true });
    speechQueue.push([SpeechHandle.SPEECH_PRIORITY_NORMAL, 1, handle]);
    handle._markScheduled();
    q_updated.resolve();
    const ac = new AbortController();
    const mainTaskPromise = mainTask(ac.signal);
    await new Promise((r) => setTimeout(r, 50));
    handle.interrupt();
    await new Promise((r) => setTimeout(r, 50));
    fakeActivity._schedulingPaused = true;
    fakeActivity.q_updated = new Future();
    fakeActivity.q_updated.resolve();
    ac.abort();
    const result = await raceTimeout(mainTaskPromise, 2e3);
    expect(result).toBe("resolved");
  });
  it("should process next queued handle after an interrupted one", async () => {
    const { fakeActivity, mainTask, speechQueue, q_updated } = buildMainTaskRunner();
    const handleA = SpeechHandle.create({ allowInterruptions: true });
    const handleB = SpeechHandle.create({ allowInterruptions: true });
    speechQueue.push([SpeechHandle.SPEECH_PRIORITY_NORMAL, 1, handleA]);
    handleA._markScheduled();
    speechQueue.push([SpeechHandle.SPEECH_PRIORITY_NORMAL, 2, handleB]);
    handleB._markScheduled();
    q_updated.resolve();
    const ac = new AbortController();
    const mainTaskPromise = mainTask(ac.signal);
    await new Promise((r) => setTimeout(r, 50));
    handleA.interrupt();
    await new Promise((r) => setTimeout(r, 50));
    try {
      handleB._markGenerationDone();
    } catch {
    }
    await new Promise((r) => setTimeout(r, 50));
    fakeActivity._schedulingPaused = true;
    fakeActivity.q_updated = new Future();
    fakeActivity.q_updated.resolve();
    ac.abort();
    const result = await raceTimeout(mainTaskPromise, 2e3);
    expect(result).toBe("resolved");
  });
  it("should skip handles that were interrupted before being popped", async () => {
    const { fakeActivity, mainTask, speechQueue, q_updated } = buildMainTaskRunner();
    const handle = SpeechHandle.create({ allowInterruptions: true });
    handle.interrupt();
    speechQueue.push([SpeechHandle.SPEECH_PRIORITY_NORMAL, 1, handle]);
    handle._markScheduled();
    q_updated.resolve();
    const ac = new AbortController();
    const mainTaskPromise = mainTask(ac.signal);
    await new Promise((r) => setTimeout(r, 50));
    fakeActivity._schedulingPaused = true;
    fakeActivity.q_updated = new Future();
    fakeActivity.q_updated.resolve();
    ac.abort();
    const result = await raceTimeout(mainTaskPromise, 2e3);
    expect(result).toBe("resolved");
  });
  it("should hold queued speech while reply authorization is paused", async () => {
    const { fakeActivity, mainTask, speechQueue, q_updated } = buildMainTaskRunner();
    const handle = SpeechHandle.create({ allowInterruptions: true });
    fakeActivity._authorizationPaused = true;
    speechQueue.push([SpeechHandle.SPEECH_PRIORITY_NORMAL, 1, handle]);
    handle._markScheduled();
    q_updated.resolve();
    const ac = new AbortController();
    const mainTaskPromise = mainTask(ac.signal);
    await new Promise((r) => setTimeout(r, 50));
    expect(() => handle._markGenerationDone()).toThrow(
      "cannot use mark_generation_done: no active generation is running."
    );
    fakeActivity._authorizationPaused = false;
    fakeActivity.q_updated.resolve();
    await new Promise((r) => setTimeout(r, 50));
    expect(() => handle._markGenerationDone()).not.toThrow();
    fakeActivity._schedulingPaused = true;
    fakeActivity.q_updated = new Future();
    fakeActivity.q_updated.resolve();
    ac.abort();
    const result = await raceTimeout(mainTaskPromise, 2e3);
    expect(result).toBe("resolved");
  });
  it("does not deadlock when a drain runs from inside its own speech task", async () => {
    const { fakeActivity, mainTask, pauseSchedulingTask, speechTasks } = buildMainTaskRunner();
    const ac = new AbortController();
    const mainTaskPromise = mainTask(ac.signal);
    fakeActivity._mainTask = {
      result: mainTaskPromise
    };
    await new Promise((resolve) => setTimeout(resolve, 50));
    const drain = Task.from(
      async () => {
        const current = Task.current();
        speechTasks.add(current);
        await pauseSchedulingTask([]);
      },
      void 0,
      "reentrant-drain"
    );
    try {
      const result = await raceTimeout(drain.result, 2e3);
      expect(result).toBe("resolved");
    } finally {
      speechTasks.clear();
      fakeActivity._schedulingPaused = true;
      fakeActivity.q_updated = new Future();
      fakeActivity.q_updated.resolve();
      ac.abort();
      await raceTimeout(mainTaskPromise, 2e3);
    }
  });
  it("does not deadlock when drain is held only by interrupted speech tasks", async () => {
    const { fakeActivity, mainTask, pauseSchedulingTask, speechTasks } = buildMainTaskRunner();
    const interruptedHandle = { interrupted: true };
    const zombieTask = Task.from(
      () => new Promise(() => {
      }),
      void 0,
      "zombie-speech-task"
    );
    speechTasks.add(zombieTask);
    vi.mocked(_getActivityTaskInfo).mockImplementation(
      (task) => task === zombieTask ? { speechHandle: interruptedHandle } : void 0
    );
    const ac = new AbortController();
    const mainTaskPromise = mainTask(ac.signal);
    fakeActivity._mainTask = {
      result: mainTaskPromise
    };
    await new Promise((resolve) => setTimeout(resolve, 50));
    try {
      const result = await raceTimeout(pauseSchedulingTask([]), 2e3);
      expect(result).toBe("resolved");
    } finally {
      vi.mocked(_getActivityTaskInfo).mockReturnValue(void 0);
      speechTasks.clear();
      fakeActivity._schedulingPaused = true;
      fakeActivity.q_updated = new Future();
      fakeActivity.q_updated.resolve();
      ac.abort();
      await raceTimeout(mainTaskPromise, 2e3);
    }
  });
  it("does not deadlock cancelling a paused speech whose generation never finishes", async () => {
    const handle = SpeechHandle.create({ allowInterruptions: true });
    handle._authorizeGeneration();
    const fakeActivity = {
      cancelSpeechPauseTask: void 0,
      falseInterruptionTimer: void 0,
      pausedSpeech: {
        handle,
        agentState: "speaking",
        timeout: 1e3
      },
      logger: {
        debug: vi.fn()
      },
      agentSession: {
        sessionOptions: {
          turnHandling: {
            interruption: {
              resumeFalseInterruption: false
            }
          }
        },
        output: {
          audio: void 0
        }
      }
    };
    const cancelSpeechPause = AgentActivity.prototype.cancelSpeechPause.bind(fakeActivity);
    const result = await raceTimeout(cancelSpeechPause(), 2e3);
    expect(result).toBe("resolved");
    expect(handle.interrupted).toBe(true);
    expect(fakeActivity.pausedSpeech).toBeUndefined();
  });
});
describe("AgentActivity - speech completion", () => {
  it("ends audio recognition speech when pipeline completion moves session out of speaking", () => {
    const audioRecognition = {
      onEndOfAgentSpeech: vi.fn()
    };
    const fakeActivity = {
      speechQueue: {
        peek: () => void 0
      },
      _currentSpeech: {
        done: () => true
      },
      audioRecognition,
      agentSession: {
        agentState: "speaking",
        _updateAgentState: vi.fn((state) => {
          fakeActivity.agentSession.agentState = state;
        })
      }
    };
    const onPipelineReplyDone = AgentActivity.prototype.onPipelineReplyDone;
    onPipelineReplyDone.call(fakeActivity);
    expect(fakeActivity.agentSession._updateAgentState).toHaveBeenCalledWith("listening");
    expect(audioRecognition.onEndOfAgentSpeech).toHaveBeenCalledTimes(1);
  });
});
class FakePreemptiveLLM extends LLM {
  label() {
    return "fake.LLM";
  }
  chat() {
    throw new Error("not used in these tests");
  }
}
function buildPreemptiveRunner(opts = {}) {
  const preemptiveOpts = {
    enabled: true,
    preemptiveTts: false,
    maxSpeechDuration: 1e4,
    maxRetries: 3,
    ...opts
  };
  const generateReply = vi.fn(
    () => ({ id: "speech_fake", _cancel: () => {
    } })
  );
  const cancelPreemptiveGeneration = vi.fn();
  const fakeChatCtx = new ChatContext();
  const fakeActivity = {
    _preemptiveGenerationCount: 0,
    _preemptiveGeneration: void 0,
    _currentSpeech: void 0,
    schedulingPaused: false,
    llm: new FakePreemptiveLLM(),
    tools: {},
    toolChoice: null,
    agent: { chatCtx: fakeChatCtx },
    agentSession: {
      sessionOptions: {
        turnHandling: { preemptiveGeneration: preemptiveOpts }
      }
    },
    logger: {
      info: () => {
      },
      debug: () => {
      },
      warn: () => {
      },
      error: () => {
      }
    },
    generateReply,
    cancelPreemptiveGeneration
  };
  Object.setPrototypeOf(fakeActivity, AgentActivity.prototype);
  const onPreemptiveGeneration = AgentActivity.prototype.onPreemptiveGeneration;
  return {
    fakeActivity,
    preemptiveOpts,
    generateReply,
    cancelPreemptiveGeneration,
    call: (info = {}) => onPreemptiveGeneration.call(fakeActivity, {
      newTranscript: "hello world",
      transcriptConfidence: 0.95,
      startedSpeakingAt: void 0,
      ...info
    })
  };
}
describe("AgentActivity - onPreemptiveGeneration guards", () => {
  it("increments counter up to maxRetries then skips further calls within the same turn", () => {
    const { fakeActivity, generateReply, call } = buildPreemptiveRunner({ maxRetries: 2 });
    call();
    expect(fakeActivity._preemptiveGenerationCount).toBe(1);
    expect(generateReply).toHaveBeenCalledTimes(1);
    call();
    expect(fakeActivity._preemptiveGenerationCount).toBe(2);
    expect(generateReply).toHaveBeenCalledTimes(2);
    call();
    expect(fakeActivity._preemptiveGenerationCount).toBe(2);
    expect(generateReply).toHaveBeenCalledTimes(2);
    call();
    expect(fakeActivity._preemptiveGenerationCount).toBe(2);
    expect(generateReply).toHaveBeenCalledTimes(2);
  });
  it("resumes preemption after the counter is reset (simulates onEndOfTurn)", () => {
    const { fakeActivity, generateReply, call } = buildPreemptiveRunner({ maxRetries: 2 });
    call();
    call();
    call();
    expect(generateReply).toHaveBeenCalledTimes(2);
    fakeActivity._preemptiveGenerationCount = 0;
    call();
    expect(fakeActivity._preemptiveGenerationCount).toBe(1);
    expect(generateReply).toHaveBeenCalledTimes(3);
  });
  it("skips preemption when startedSpeakingAt exceeds maxSpeechDuration", () => {
    const { fakeActivity, generateReply, call } = buildPreemptiveRunner({
      maxSpeechDuration: 3e3
    });
    call({ startedSpeakingAt: Date.now() - 1e3 });
    expect(fakeActivity._preemptiveGenerationCount).toBe(1);
    expect(generateReply).toHaveBeenCalledTimes(1);
    call({ startedSpeakingAt: Date.now() - 5e3 });
    expect(fakeActivity._preemptiveGenerationCount).toBe(1);
    expect(generateReply).toHaveBeenCalledTimes(1);
  });
  it("skips preemption entirely when enabled is false", () => {
    const { fakeActivity, generateReply, cancelPreemptiveGeneration, call } = buildPreemptiveRunner(
      { enabled: false }
    );
    call();
    call();
    expect(fakeActivity._preemptiveGenerationCount).toBe(0);
    expect(generateReply).not.toHaveBeenCalled();
    expect(cancelPreemptiveGeneration).not.toHaveBeenCalled();
  });
});
//# sourceMappingURL=agent_activity.test.js.map