import { describe, expect, it, vi } from "vitest";
import { ChannelSync } from "./channel-sync";
import { MessageStore } from "./message-store";
import type { Message } from "@slackwsh/contracts";

function msg(seq: number, overrides: Partial<Message> = {}): Message {
  return {
    id: seq,
    workspaceId: 1,
    channelId: 1,
    seq,
    clientMsgId: `00000000-0000-0000-0000-00000000000${seq}`,
    authorId: 1,
    type: "text",
    text: `msg ${seq}`,
    blocks: { v: 1, doc: {} },
    revision: 0,
    parentId: null,
    isBroadcast: false,
    threadReplyCount: 0,
    threadLastReplyAt: null,
    editedAt: null,
    deletedAt: null,
    createdAt: new Date().toISOString(),
    ...overrides,
  };
}

describe("ChannelSync — I7 catch-up/live boundary", () => {
  it("buffers live events during catch-up and flushes them in seq order after backfill", async () => {
    const store = new MessageStore();
    let resolveBackfill!: (v: Message[]) => void;
    const fetchScrollback = vi.fn().mockImplementation(
      () => new Promise<Message[]>((resolve) => (resolveBackfill = resolve)),
    );
    const sync = new ChannelSync("1", store, fetchScrollback, 0);

    const startPromise = sync.start();
    // Live events arrive while backfill is still genuinely in flight —
    // buffered, not rendered, per I7.
    await sync.onLiveMessage(msg(3));
    await sync.onLiveMessage(msg(2));
    expect(store.getMessages("1")).toHaveLength(0);

    resolveBackfill([msg(1)]);
    await startPromise;

    expect(fetchScrollback).toHaveBeenCalledWith({ limit: 50 });
    expect(store.getMessages("1").map((m) => m.seq)).toEqual([1, 2, 3]);
    expect(sync.getState()).toBe("live");
  });

  it("resyncs when a live message arrives past a gap", async () => {
    const store = new MessageStore();
    const fetchScrollback = vi
      .fn()
      .mockResolvedValueOnce([msg(1)])
      .mockResolvedValueOnce([msg(2), msg(3)]);
    const sync = new ChannelSync("1", store, fetchScrollback, 0);
    await sync.start();

    await sync.onLiveMessage(msg(4));

    expect(fetchScrollback).toHaveBeenCalledWith(
      expect.objectContaining({ afterSeq: 1, beforeSeq: 5, limit: 200 }),
    );
    expect(store.getMessages("1").map((m) => m.seq)).toEqual([1, 2, 3, 4]);
  });

  it("prepends older pages without moving the live cursor backwards", async () => {
    const store = new MessageStore();
    const latestPage = Array.from({ length: 50 }, (_, index) => msg(index + 51));
    const fetchScrollback = vi
      .fn()
      .mockResolvedValueOnce(latestPage)
      .mockResolvedValueOnce([msg(49), msg(50)]);
    const sync = new ChannelSync("1", store, fetchScrollback, 0);

    await sync.start();
    const liveCursor = sync.getCursor();
    const older = await sync.loadOlder(50);

    expect(fetchScrollback).toHaveBeenLastCalledWith({ beforeSeq: 51, limit: 50 });
    expect(older.map((message) => message.seq)).toEqual([49, 50]);
    expect(store.getMessages("1").slice(0, 4).map((message) => message.seq)).toEqual([49, 50, 51, 52]);
    expect(sync.getCursor()).toBe(liveCursor);
    expect(sync.hasOlderMessages()).toBe(false);
  });
});
