import { describe, expect, it } from "vitest";
import type { Message } from "@slackwsh/contracts";
import {
  channelUnreadCount,
  clampReadSeq,
  mentionBadgeFromRows,
  shouldApplyRevision,
  threadUnreadCount,
} from "./invariants";

function msg(overrides: Partial<Message>): Message {
  return {
    id: 1,
    workspaceId: 1,
    channelId: 1,
    seq: 1,
    clientMsgId: "00000000-0000-4000-8000-000000000001",
    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(0).toISOString(),
    ...overrides,
  };
}

describe("I4 cursor monotonicity", () => {
  it("acks only move last_read_seq forward (GREATEST)", () => {
    expect(clampReadSeq(5, 3, false)).toBe(5);
    expect(clampReadSeq(5, 9, false)).toBe(9);
  });

  it("mark-unread is the explicit decrease (LEAST)", () => {
    expect(clampReadSeq(5, 3, true)).toBe(3);
    expect(clampReadSeq(5, 9, true)).toBe(5);
  });
});

describe("I5 channel unread — v2.0 defect must not return", () => {
  const log = [
    msg({ id: 1, seq: 1, authorId: 1, parentId: null }),
    msg({ id: 2, seq: 2, authorId: 2, parentId: 1, isBroadcast: false }),
    msg({ id: 3, seq: 3, authorId: 2, parentId: 1, isBroadcast: true }),
  ];

  it("does not count a hidden thread reply toward channel unread", () => {
    expect(channelUnreadCount(log, 1, 1)).toBe(1);
  });

  it("does count an also-send-to-channel reply", () => {
    expect(channelUnreadCount(log, 1, 1)).toBe(1);
    expect(log.filter((m) => m.isBroadcast)).toHaveLength(1);
  });

  it("the uncorrected v2.0 formula would have counted both replies", () => {
    const naive = log.filter((m) => m.seq > 1 && m.authorId !== 1 && m.deletedAt == null).length;
    expect(naive).toBe(2);
    expect(channelUnreadCount(log, 1, 1)).toBeLessThan(naive);
  });
});

describe("I5b thread unread", () => {
  it("counts replies past last_read_reply_seq, including non-broadcast", () => {
    const log = [
      msg({ id: 1, seq: 1, authorId: 1, parentId: null }),
      msg({ id: 2, seq: 2, authorId: 2, parentId: 1 }),
      msg({ id: 3, seq: 3, authorId: 2, parentId: 1 }),
    ];
    expect(threadUnreadCount(log, 1, 2, 1)).toBe(1);
    expect(threadUnreadCount(log, 1, 0, 1)).toBe(2);
  });
});

describe("I6 mention fidelity", () => {
  it("counts persisted rows, not the current text", () => {
    const log = [msg({ id: 1, seq: 1, authorId: 2, text: "edited, no mention" })];
    const rows = [{ messageId: 1, targetType: "user" as const, targetId: 1 }];
    expect(mentionBadgeFromRows(log, rows, 1, 0, [1, 2])).toBe(1);
  });

  it("does not count mentions on messages already past last_read_seq", () => {
    const log = [msg({ id: 1, seq: 1, authorId: 2, text: "hey @alice" })];
    const rows = [{ messageId: 1, targetType: "user" as const, targetId: 1 }];
    expect(mentionBadgeFromRows(log, rows, 1, 1, [1, 2])).toBe(0);
  });
});

describe("I8 revision guard", () => {
  it("rejects a strictly older revision", () => {
    expect(shouldApplyRevision(2, 1)).toBe(false);
    expect(shouldApplyRevision(2, 2)).toBe(true);
  });
});
