import { afterEach, describe, expect, it, vi } from "vitest";
import { TriggerQueue, type QueuedTrigger } from "../../src/feishu/triggerQueue.js";
import type { MessageReceiveEvent } from "../../src/feishu/client.js";

describe("TriggerQueue", () => {
  afterEach(() => {
    vi.useRealTimers();
  });

  it("dequeues triggers in FIFO order", () => {
    const queue = new TriggerQueue();

    queue.enqueue("project-1", makeTrigger("first"));
    queue.enqueue("project-1", makeTrigger("second"));

    expect(queue.dequeue("project-1")?.prompt).toBe("first");
    expect(queue.dequeue("project-1")?.prompt).toBe("second");
  });

  it("returns the 1-based queue position from enqueue", () => {
    const queue = new TriggerQueue();

    expect(queue.enqueue("project-1", makeTrigger("first"))).toBe(1);
    expect(queue.enqueue("project-1", makeTrigger("second"))).toBe(2);
    expect(queue.enqueue("project-1", makeTrigger("third"))).toBe(3);
  });

  it("returns 0 when the project queue is full", () => {
    const queue = new TriggerQueue({ maxQueueSize: 2 });

    expect(queue.enqueue("project-1", makeTrigger("first"))).toBe(1);
    expect(queue.enqueue("project-1", makeTrigger("second"))).toBe(2);
    expect(queue.enqueue("project-1", makeTrigger("third"))).toBe(0);
    expect(queue.length("project-1")).toBe(2);
  });

  it("returns null when dequeueing an empty queue", () => {
    const queue = new TriggerQueue();

    expect(queue.dequeue("project-1")).toBeNull();
  });

  it("peeks without removing the trigger", () => {
    const queue = new TriggerQueue();

    queue.enqueue("project-1", makeTrigger("first"));

    expect(queue.peek("project-1")?.prompt).toBe("first");
    expect(queue.length("project-1")).toBe(1);
    expect(queue.dequeue("project-1")?.prompt).toBe("first");
  });

  it("reports length and pending state per project", () => {
    const queue = new TriggerQueue();

    expect(queue.length("project-1")).toBe(0);
    expect(queue.hasPending("project-1")).toBe(false);

    queue.enqueue("project-1", makeTrigger("first"));

    expect(queue.length("project-1")).toBe(1);
    expect(queue.hasPending("project-1")).toBe(true);
  });

  it("purges expired triggers", () => {
    vi.useFakeTimers();
    vi.setSystemTime(1_000);
    const queue = new TriggerQueue({ maxWaitMs: 100 });

    queue.enqueue("project-1", makeTrigger("old"));
    vi.setSystemTime(1_050);
    queue.enqueue("project-1", makeTrigger("fresh"));
    vi.setSystemTime(1_101);

    expect(queue.purgeExpired()).toBe(1);
    expect(queue.length("project-1")).toBe(1);
    expect(queue.dequeue("project-1")?.prompt).toBe("fresh");
  });

  it("clears all triggers for a project", () => {
    const queue = new TriggerQueue();

    queue.enqueue("project-1", makeTrigger("first"));
    queue.enqueue("project-1", makeTrigger("second"));
    queue.enqueue("project-2", makeTrigger("other"));

    expect(queue.clear("project-1")).toBe(2);
    expect(queue.length("project-1")).toBe(0);
    expect(queue.length("project-2")).toBe(1);
  });

  it("clears all project queues", () => {
    const queue = new TriggerQueue();

    queue.enqueue("project-1", makeTrigger("first"));
    queue.enqueue("project-2", makeTrigger("other"));
    queue.clearAll();

    expect(queue.hasPending("project-1")).toBe(false);
    expect(queue.hasPending("project-2")).toBe(false);
  });

  it("keeps multiple project queues independent", () => {
    const queue = new TriggerQueue();

    expect(queue.enqueue("project-1", makeTrigger("p1-first"))).toBe(1);
    expect(queue.enqueue("project-2", makeTrigger("p2-first"))).toBe(1);
    expect(queue.enqueue("project-1", makeTrigger("p1-second"))).toBe(2);

    expect(queue.dequeue("project-2")?.prompt).toBe("p2-first");
    expect(queue.dequeue("project-1")?.prompt).toBe("p1-first");
    expect(queue.dequeue("project-1")?.prompt).toBe("p1-second");
  });
});

function makeTrigger(prompt: string): Omit<QueuedTrigger, "projectId" | "enqueuedAt"> {
  return {
    chatId: "chat-1",
    prompt,
    roleId: "draft",
    executionKind: "conversation",
    msg: makeMessage(prompt),
    senderOpenId: "ou-user",
    actor: { feishuOpenId: "ou-user", chatId: "chat-1" },
  };
}

function makeMessage(prompt: string): MessageReceiveEvent["message"] {
  return {
    message_id: `message-${prompt}`,
    chat_id: "chat-1",
    chat_type: "group",
    message_type: "text",
    content: JSON.stringify({ text: `@_user_1 ${prompt}` }),
    mentions: [{ key: "@_user_1", id: { open_id: "ou-bot" }, name: "Bot" }],
  };
}
