import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { MessageBatcher, messageBatchKey } from "../../src/feishu/messageBatcher.js";

describe("MessageBatcher", () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it("flushes a single message after the debounce period", async () => {
    const flushed: string[] = [];
    const batcher = new MessageBatcher(async (text) => {
      flushed.push(text);
    }, { debounceMs: 25 });

    await batcher.enqueue("chat-1", "sender-1", "hello");

    expect(flushed).toEqual([]);
    await vi.advanceTimersByTimeAsync(24);
    expect(flushed).toEqual([]);
    await vi.advanceTimersByTimeAsync(1);
    expect(flushed).toEqual(["hello"]);
  });

  it("merges two quick messages with a newline", async () => {
    const flushed: string[] = [];
    const batcher = new MessageBatcher(async (text) => {
      flushed.push(text);
    }, { debounceMs: 25 });

    await batcher.enqueue("chat-1", "sender-1", "first");
    await batcher.enqueue("chat-1", "sender-1", "second");
    await vi.advanceTimersByTimeAsync(25);

    expect(flushed).toEqual(["first\nsecond"]);
  });

  it("flushes immediately when the max message count is reached", async () => {
    const flushed: string[] = [];
    const batcher = new MessageBatcher(async (text) => {
      flushed.push(text);
    }, { debounceMs: 1000, maxMessages: 2 });

    await batcher.enqueue("chat-1", "sender-1", "first");
    expect(flushed).toEqual([]);
    await batcher.enqueue("chat-1", "sender-1", "second");

    expect(flushed).toEqual(["first\nsecond"]);
  });

  it("flushes immediately when the max character count is reached", async () => {
    const flushed: string[] = [];
    const batcher = new MessageBatcher(async (text) => {
      flushed.push(text);
    }, { debounceMs: 1000, maxChars: 5 });

    await batcher.enqueue("chat-1", "sender-1", "abc");
    expect(flushed).toEqual([]);
    await batcher.enqueue("chat-1", "sender-1", "d");

    expect(flushed).toEqual(["abc\nd"]);
  });

  it("uses the extended debounce for a chunk near the split threshold", async () => {
    const flushed: string[] = [];
    const batcher = new MessageBatcher(async (text) => {
      flushed.push(text);
    }, { debounceMs: 10, splitThreshold: 5, extendedDebounceMs: 50 });

    await batcher.enqueue("chat-1", "sender-1", "12345");
    await vi.advanceTimersByTimeAsync(49);
    expect(flushed).toEqual([]);
    await vi.advanceTimersByTimeAsync(1);

    expect(flushed).toEqual(["12345"]);
  });

  it("keeps different chat/sender pairs in separate batches", async () => {
    const flushed: string[] = [];
    const batcher = new MessageBatcher(async (text) => {
      flushed.push(text);
    }, { debounceMs: 25 });

    await batcher.enqueue("chat-1", "sender-1", "one");
    await batcher.enqueue("chat-1", "sender-2", "two");
    await batcher.enqueue("chat-2", "sender-1", "three");
    await vi.advanceTimersByTimeAsync(25);

    expect(flushed.sort()).toEqual(["one", "three", "two"]);
  });

  it("flushAll flushes every pending batch", async () => {
    const flushed: string[] = [];
    const batcher = new MessageBatcher(async (text) => {
      flushed.push(text);
    }, { debounceMs: 1000 });

    await batcher.enqueue("chat-1", "sender-1", "one");
    await batcher.enqueue("chat-2", "sender-2", "two");
    await batcher.flushAll();

    expect(flushed.sort()).toEqual(["one", "two"]);
  });

  it("builds keys from chat and sender open ids", () => {
    expect(messageBatchKey("chat-1", "ou-1")).toBe("chat-1:ou-1");
  });

  // Slash-command bypass is owned by trigger.ts, not MessageBatcher. Existing
  // trigger integration tests cover /new, /resume, /reset as immediate paths.
});
