import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { MessageQueue } from "@/core/net/message-queue";

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

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

  it("sends the first message immediately", async () => {
    const consumer = vi.fn();
    const queue = new MessageQueue();
    queue.setConsumer(consumer);

    queue.enqueue("hello");
    await Promise.resolve();

    expect(consumer).toHaveBeenCalledTimes(1);
    expect(consumer).toHaveBeenCalledWith("hello");

    queue.dispose();
  });

  it("merges pending messages after the interval", async () => {
    const consumer = vi.fn();
    const queue = new MessageQueue({ intervalMs: 1000 });
    queue.setConsumer(consumer);

    queue.enqueue("first");
    await Promise.resolve();
    queue.enqueue("second");
    queue.enqueue("third");

    expect(consumer).toHaveBeenCalledTimes(1);

    await vi.advanceTimersByTimeAsync(999);
    expect(consumer).toHaveBeenCalledTimes(1);

    await vi.advanceTimersByTimeAsync(1);
    expect(consumer).toHaveBeenCalledTimes(2);
    expect(consumer).toHaveBeenLastCalledWith("second\nthird");

    queue.dispose();
  });

  it("does not send a pending batch while a previous send is still running", async () => {
    let resolveFirstSend: () => void = () => undefined;
    const consumer = vi.fn(
      () =>
        new Promise<void>((resolve) => {
          resolveFirstSend = resolve;
        }),
    );
    const queue = new MessageQueue({ intervalMs: 1000 });
    queue.setConsumer(consumer);

    queue.enqueue("first");
    await Promise.resolve();
    queue.enqueue("second");
    queue.enqueue("third");

    await vi.advanceTimersByTimeAsync(1000);
    expect(consumer).toHaveBeenCalledTimes(1);

    resolveFirstSend();
    await Promise.resolve();
    await vi.advanceTimersByTimeAsync(1000);

    expect(consumer).toHaveBeenCalledTimes(2);
    expect(consumer).toHaveBeenLastCalledWith("second\nthird");

    queue.dispose();
  });

  it("does not retry a failed batch", async () => {
    const consumer = vi
      .fn()
      .mockResolvedValueOnce(undefined)
      .mockRejectedValueOnce(new Error("send failed"))
      .mockResolvedValue(undefined);
    const queue = new MessageQueue({ intervalMs: 1000 });
    queue.setConsumer(consumer);

    queue.enqueue("first");
    await Promise.resolve();
    queue.enqueue("second");
    queue.enqueue("third");

    await vi.advanceTimersByTimeAsync(1000);
    expect(consumer).toHaveBeenLastCalledWith("second\nthird");

    queue.enqueue("fourth");
    await vi.advanceTimersByTimeAsync(1000);

    expect(consumer).toHaveBeenCalledTimes(3);
    expect(consumer).toHaveBeenLastCalledWith("fourth");

    queue.dispose();
  });

  it("supports a custom joiner", async () => {
    const consumer = vi.fn();
    const queue = new MessageQueue({ intervalMs: 1000, joiner: " " });
    queue.setConsumer(consumer);

    queue.enqueue("one");
    await Promise.resolve();
    queue.enqueue("two");
    queue.enqueue("three");

    await vi.advanceTimersByTimeAsync(1000);

    expect(consumer).toHaveBeenLastCalledWith("two three");

    queue.dispose();
  });
});
