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

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

describe("MessageStore — I1 total order", () => {
  it("always returns messages ordered by seq regardless of insertion order", () => {
    const store = new MessageStore();
    store.upsert("1", msg({ id: 3, seq: 3 }));
    store.upsert("1", msg({ id: 1, seq: 1 }));
    store.upsert("1", msg({ id: 2, seq: 2 }));

    expect(store.getMessages("1").map((m) => m.seq)).toEqual([1, 2, 3]);
  });
});

describe("MessageStore — I3 idempotency", () => {
  it("upserting the same id twice never produces a duplicate", () => {
    const store = new MessageStore();
    store.upsert("1", msg({ id: 1, seq: 1, text: "first" }));
    store.upsert("1", msg({ id: 1, seq: 1, text: "first" }));

    expect(store.getMessages("1")).toHaveLength(1);
  });
});

describe("MessageStore — I8 mutation convergence", () => {
  it("does not let a lower-revision message overwrite a newer edit, regardless of delivery order", () => {
    const store = new MessageStore();
    store.upsert("1", msg({ id: 1, seq: 1, revision: 2, text: "edited twice" }));
    store.upsert("1", msg({ id: 1, seq: 1, revision: 1, text: "edited once" })); // stale, arrives late

    expect(store.getMessage("1", 1)?.text).toBe("edited twice");
  });

  it("applies a higher-revision message even if it arrives after a lower one", () => {
    const store = new MessageStore();
    store.upsert("1", msg({ id: 1, seq: 1, revision: 0, text: "original" }));
    store.upsert("1", msg({ id: 1, seq: 1, revision: 1, text: "edited" }));

    expect(store.getMessage("1", 1)?.text).toBe("edited");
  });

  it("does not let a stale edit resurrect a deleted message", () => {
    const store = new MessageStore();
    store.upsert("1", msg({ id: 1, seq: 1, revision: 1, text: "before delete" }));
    store.applyDelete("1", 1, 1);
    store.upsert("1", msg({ id: 1, seq: 1, revision: 1, text: "stale edit" }));

    expect(store.getMessage("1", 1)?.deletedAt).not.toBeNull();
    expect(store.getMessage("1", 1)?.text).toBe("");
  });
});

describe("MessageStore — bounded window", () => {
  it("evicts the oldest messages by seq once the per-channel window is exceeded", () => {
    const store = new MessageStore({ messagesPerChannel: 3 });
    for (let seq = 1; seq <= 5; seq++) {
      store.upsert("1", msg({ id: seq, seq }));
    }

    expect(store.getMessages("1").map((m) => m.seq)).toEqual([3, 4, 5]);
  });

  it("evicts the least-recently-touched channel once maxChannels is exceeded", () => {
    const store = new MessageStore({ maxChannels: 2 });
    store.upsert("1", msg({ id: 1, channelId: 1, seq: 1 }));
    store.upsert("2", msg({ id: 2, channelId: 2, seq: 1 }));
    store.upsert("3", msg({ id: 3, channelId: 3, seq: 1 }));

    expect(store.getMessages("1")).toHaveLength(0); // evicted
    expect(store.getMessages("2")).toHaveLength(1);
    expect(store.getMessages("3")).toHaveLength(1);
  });
});
