import { describe, expect, it } from "vitest";
import type { Call, CalendarEvent, Message, Task } from "@slackwsh/contracts";
import { DEFAULT_NOTIFY_PREFS, describeNotification, type NotifyContext } from "./notify-router";

const ME = 1;
const THEM = 2;

function ctx(overrides: Partial<NotifyContext> = {}): NotifyContext {
  return {
    myUserId: ME,
    myUsername: "yasir",
    prefs: DEFAULT_NOTIFY_PREFS,
    hidden: true, // treat the window as backgrounded unless a test says otherwise
    nameFor: (id) => (id === THEM ? "Basit Khan" : "Yasir Ijaz"),
    ...overrides,
  };
}

function message(overrides: Partial<Message> = {}): Message {
  return {
    id: 10,
    workspaceId: 1,
    channelId: 2,
    seq: 5,
    clientMsgId: "11111111-1111-4111-8111-111111111111",
    authorId: THEM,
    type: "text",
    text: "hello there",
    blocks: { v: 1, doc: {} },
    revision: 0,
    parentId: null,
    isBroadcast: false,
    threadReplyCount: 0,
    threadLastReplyAt: null,
    editedAt: null,
    deletedAt: null,
    createdAt: "2026-08-06T10:00:00.000Z",
    ...overrides,
  } as Message;
}

function task(overrides: Partial<Task> = {}): Task {
  return {
    id: 7,
    workspaceId: 1,
    title: "Ship the report",
    description: null,
    status: "todo",
    assigneeUserId: null,
    dueAt: null,
    createdBy: THEM,
    channelId: null,
    sourceMessageId: null,
    completedAt: null,
    deletedAt: null,
    deletedBy: null,
    createdAt: "2026-08-06T10:00:00.000Z",
    updatedAt: "2026-08-06T10:00:00.000Z",
    ...overrides,
  };
}

function calendarEvent(overrides: Partial<CalendarEvent> = {}): CalendarEvent {
  return {
    id: 3,
    workspaceId: 1,
    title: "Design review",
    description: null,
    location: null,
    startsAt: "2026-08-10T09:00:00.000Z",
    endsAt: "2026-08-10T09:30:00.000Z",
    allDay: false,
    timezone: "UTC",
    recurrenceRule: null,
    channelId: null,
    createdBy: THEM,
    attendees: [{ userId: ME, status: "needs_action", respondedAt: null }],
    seriesId: 3,
    occurrenceDate: "2026-08-10",
    isOccurrence: false,
    isOverride: false,
    createdAt: "2026-08-06T10:00:00.000Z",
    updatedAt: "2026-08-06T10:00:00.000Z",
    ...overrides,
  };
}

function call(overrides: Partial<Call> = {}): Call {
  return {
    id: 4,
    workspaceId: 1,
    kind: "audio",
    status: "ringing",
    channelId: null,
    eventId: null,
    title: null,
    startedBy: THEM,
    startedAt: "2026-08-06T10:00:00.000Z",
    answeredAt: null,
    endedAt: null,
    durationSeconds: null,
    participants: [
      { userId: THEM, state: "joined", invitedAt: "2026-08-06T10:00:00.000Z", joinedAt: "2026-08-06T10:00:00.000Z", leftAt: null, seenAt: null },
      { userId: ME, state: "ringing", invitedAt: "2026-08-06T10:00:00.000Z", joinedAt: null, leftAt: null, seenAt: null },
    ],
    recordingStatus: "idle",
    recordingObjectKey: null,
    transcriptStatus: "idle",
    transcript: null,
    summary: null,
    ...overrides,
  };
}

describe("describeNotification — messages", () => {
  it("notifies for someone else's message in mode 'all'", () => {
    const result = describeNotification({ type: "message:created", message: message(), channelName: "first-room" }, ctx());
    expect(result).not.toBeNull();
    expect(result!.title).toBe("Basit in #first-room");
    expect(result!.body).toBe("hello there");
    expect(result!.route).toBe("/channel?workspaceId=1&channelId=2");
  });

  it("never notifies for my own message", () => {
    expect(
      describeNotification({ type: "message:created", message: message({ authorId: ME }) }, ctx()),
    ).toBeNull();
  });

  it("stays silent entirely in mode 'off'", () => {
    expect(
      describeNotification(
        { type: "message:created", message: message() },
        ctx({ prefs: { ...DEFAULT_NOTIFY_PREFS, messages: "off" } }),
      ),
    ).toBeNull();
  });

  describe("mode 'mentions'", () => {
    const mentionsOnly = ctx({ prefs: { ...DEFAULT_NOTIFY_PREFS, messages: "mentions" } });

    it("skips an ordinary room message", () => {
      expect(describeNotification({ type: "message:created", message: message() }, mentionsOnly)).toBeNull();
    });

    it("notifies on @myhandle", () => {
      const result = describeNotification(
        { type: "message:created", message: message({ text: "can @yasir take this" }) },
        mentionsOnly,
      );
      expect(result).not.toBeNull();
    });

    it("matches the handle case-insensitively", () => {
      expect(
        describeNotification({ type: "message:created", message: message({ text: "@Yasir ping" }) }, mentionsOnly),
      ).not.toBeNull();
    });

    it("ignores a mention of somebody else", () => {
      expect(
        describeNotification({ type: "message:created", message: message({ text: "@basit ping" }) }, mentionsOnly),
      ).toBeNull();
    });

    it("notifies on @here and @channel", () => {
      for (const text of ["@here standup", "@channel heads up"]) {
        expect(describeNotification({ type: "message:created", message: message({ text }) }, mentionsOnly)).not.toBeNull();
      }
    });

    it("always notifies for a DM", () => {
      const result = describeNotification(
        { type: "message:created", message: message(), channelType: "dm" },
        mentionsOnly,
      );
      expect(result).not.toBeNull();
      expect(result!.title).toBe("Basit"); // no "#channel" for a DM
    });
  });

  it("falls back to a placeholder body for an empty message", () => {
    const result = describeNotification({ type: "message:created", message: message({ text: "" }) }, ctx());
    expect(result!.body).toBe("(attachment)");
  });

  it("routes a thread reply directly to its root and groups it as a thread", () => {
    const result = describeNotification(
      { type: "message:created", message: message({ parentId: 44 }), channelName: "developer" },
      ctx(),
    );
    expect(result).toMatchObject({
      title: "Basit replied in #developer",
      route: "/channel?workspaceId=1&channelId=2&threadRootId=44",
      tag: "thread-44",
      group: "threads",
    });
  });
});

describe("describeNotification — the focused-route gate", () => {
  it("stays quiet when the window is focused on exactly that route", () => {
    expect(
      describeNotification(
        { type: "message:created", message: message() },
        ctx({ hidden: false, currentRoute: "/channel?workspaceId=1&channelId=2" }),
      ),
    ).toBeNull();
  });

  it("ignores parameter order when comparing routes", () => {
    expect(
      describeNotification(
        { type: "message:created", message: message() },
        ctx({ hidden: false, currentRoute: "/channel?channelId=2&workspaceId=1" }),
      ),
    ).toBeNull();
  });

  it("still notifies when focused on a different channel", () => {
    expect(
      describeNotification(
        { type: "message:created", message: message() },
        ctx({ hidden: false, currentRoute: "/channel?workspaceId=1&channelId=99" }),
      ),
    ).not.toBeNull();
  });

  it("still notifies when the window is hidden on that very route", () => {
    expect(
      describeNotification(
        { type: "message:created", message: message() },
        ctx({ hidden: true, currentRoute: "/channel?workspaceId=1&channelId=2" }),
      ),
    ).not.toBeNull();
  });

  it("stays quiet when already focused on the exact thread", () => {
    expect(
      describeNotification(
        { type: "message:created", message: message({ parentId: 44 }) },
        ctx({ hidden: false, currentRoute: "/channel?channelId=2&threadRootId=44&workspaceId=1" }),
      ),
    ).toBeNull();
  });

  it("rings through the gate for an incoming call", () => {
    const result = describeNotification(
      { type: "call:started", call: call() },
      ctx({ hidden: false, currentRoute: "/calls?workspaceId=1&callId=4" }),
    );
    expect(result).not.toBeNull();
    expect(result!.urgent).toBe(true);
  });
});

describe("describeNotification — calls", () => {
  it("notifies when a call is ringing for me", () => {
    const result = describeNotification({ type: "call:started", call: call() }, ctx());
    expect(result!.title).toBe("Incoming call");
    expect(result!.body).toBe("Basit is calling");
    expect(result!.route).toBe("/calls?workspaceId=1&callId=4");
    // The in-app ringer already plays a sound; a second one is noise.
    expect(result!.silent).toBe(true);
  });

  it("labels a video call", () => {
    expect(describeNotification({ type: "call:started", call: call({ kind: "video" }) }, ctx())!.title).toBe(
      "Incoming video call",
    );
  });

  it("does not re-notify when an update republishes a call already ringing", () => {
    const ringing = call();
    expect(
      describeNotification({ type: "call:updated", call: ringing }, ctx({ previousCall: ringing })),
    ).toBeNull();
  });

  it("ignores a call I'm not part of", () => {
    expect(
      describeNotification({ type: "call:started", call: call({ participants: [] }) }, ctx()),
    ).toBeNull();
  });

  it("ignores a call I started myself", () => {
    expect(
      describeNotification({ type: "call:started", call: call({ startedBy: ME }) }, ctx()),
    ).toBeNull();
  });

  it("does not notify for a call I already joined", () => {
    const joined = call({
      participants: [{ userId: ME, state: "joined", invitedAt: "x", joinedAt: "y", leftAt: null, seenAt: null } as never],
    });
    expect(describeNotification({ type: "call:updated", call: joined }, ctx())).toBeNull();
  });

  it("reports a missed call when it ends unanswered", () => {
    const missed = call({
      status: "missed",
      endedAt: "2026-08-06T10:01:00.000Z",
      participants: [{ userId: ME, state: "missed", invitedAt: "x", joinedAt: null, leftAt: null, seenAt: null } as never],
    });
    const result = describeNotification({ type: "call:ended", call: missed }, ctx());
    expect(result!.title).toBe("Missed call");
    expect(result!.route).toBe("/calls?workspaceId=1");
  });

  it("says nothing when a call I took ends", () => {
    const ended = call({
      status: "ended",
      participants: [{ userId: ME, state: "left", invitedAt: "x", joinedAt: "y", leftAt: "z", seenAt: null } as never],
    });
    expect(describeNotification({ type: "call:ended", call: ended }, ctx())).toBeNull();
  });

  it("honours the calls preference", () => {
    expect(
      describeNotification({ type: "call:started", call: call() }, ctx({ prefs: { ...DEFAULT_NOTIFY_PREFS, calls: false } })),
    ).toBeNull();
  });
});

describe("describeNotification — tasks", () => {
  it("notifies when a task is created already assigned to me", () => {
    const result = describeNotification({ type: "task:created", task: task({ assigneeUserId: ME }) }, ctx());
    expect(result!.title).toBe("New task assigned to you");
    expect(result!.body).toBe("Ship the report");
    expect(result!.route).toBe("/tasks?workspaceId=1");
    expect(result!.sound).toBe("cheerful");
  });

  it("notifies when an existing task is reassigned to me", () => {
    const result = describeNotification(
      { type: "task:updated", task: task({ assigneeUserId: ME }) },
      ctx({ previousTask: task({ assigneeUserId: THEM }) }),
    );
    expect(result!.title).toBe("New task assigned to you");
    expect(result!.sound).toBe("short");
    expect(result!.chimeWhileLooking).toBe(true);
  });

  it("still rings a reassignment when I am already looking at Tasks", () => {
    const result = describeNotification(
      { type: "task:updated", task: task({ assigneeUserId: ME }) },
      ctx({
        previousTask: task({ assigneeUserId: THEM }),
        hidden: false,
        currentRoute: "/tasks?workspaceId=1",
      }),
    );
    expect(result!.sound).toBe("short");
    expect(result!.skipToast).toBe(true);
  });

  it("notifies when a task I created is later assigned to me", () => {
    const result = describeNotification(
      { type: "task:updated", task: task({ createdBy: ME, assigneeUserId: ME }) },
      ctx({ previousTask: task({ createdBy: ME, assigneeUserId: THEM }) }),
    );
    expect(result!.title).toBe("New task assigned to you");
    expect(result!.sound).toBe("short");
  });

  it("uses the cheerful tone when an unassigned task is given to me", () => {
    const result = describeNotification(
      { type: "task:updated", task: task({ assigneeUserId: ME }) },
      ctx({ previousTask: task({ assigneeUserId: null }) }),
    );
    expect(result!.title).toBe("New task assigned to you");
    expect(result!.sound).toBe("cheerful");
  });

  it("does not re-notify when an unrelated field changes on my task", () => {
    expect(
      describeNotification(
        { type: "task:updated", task: task({ assigneeUserId: ME, title: "Ship the report v2" }) },
        ctx({ previousTask: task({ assigneeUserId: ME }) }),
      ),
    ).toBeNull();
  });

  it("notifies on a status change to a task I'm assigned", () => {
    const result = describeNotification(
      { type: "task:updated", task: task({ assigneeUserId: ME, status: "in_progress" }) },
      ctx({ previousTask: task({ assigneeUserId: ME, status: "todo" }) }),
    );
    expect(result!.title).toBe("Task moved to in progress");
  });

  it("notifies the creator when someone else moves their task", () => {
    const result = describeNotification(
      { type: "task:updated", task: task({ createdBy: ME, status: "done" }) },
      ctx({ previousTask: task({ createdBy: ME, status: "todo" }) }),
    );
    expect(result!.title).toBe("Task moved to done");
  });

  it("ignores a status change on a task that is nothing to do with me", () => {
    expect(
      describeNotification(
        { type: "task:updated", task: task({ status: "done" }) },
        ctx({ previousTask: task({ status: "todo" }) }),
      ),
    ).toBeNull();
  });

  it("reports a task being taken off me", () => {
    const result = describeNotification(
      { type: "task:updated", task: task({ createdBy: ME, assigneeUserId: THEM }) },
      ctx({ previousTask: task({ createdBy: ME, assigneeUserId: ME }) }),
    );
    expect(result!.title).toBe("Task reassigned");
  });

  it("suppresses the echo of my own edit", () => {
    expect(
      describeNotification(
        { type: "task:updated", task: task({ assigneeUserId: ME, status: "done" }) },
        ctx({ previousTask: task({ assigneeUserId: ME, status: "todo" }), isSelfEcho: () => true }),
      ),
    ).toBeNull();
  });

  it("does not notify me for a task I assigned to myself", () => {
    expect(
      describeNotification({ type: "task:created", task: task({ createdBy: ME, assigneeUserId: ME }) }, ctx()),
    ).toBeNull();
  });

  it("honours the tasks preference", () => {
    expect(
      describeNotification(
        { type: "task:created", task: task({ assigneeUserId: ME }) },
        ctx({ prefs: { ...DEFAULT_NOTIFY_PREFS, tasks: false } }),
      ),
    ).toBeNull();
  });

  it("notifies the creator when someone else deletes their task", () => {
    const result = describeNotification(
      {
        type: "task:deleted",
        workspaceId: 1,
        taskId: 7,
        title: "Ship the report",
        createdBy: ME,
        deletedByUserId: THEM,
      },
      ctx(),
    );
    expect(result!.title).toBe("Task deleted");
    expect(result!.body).toBe("Basit deleted “Ship the report”");
  });

  it("honours the tasks preference for a delete", () => {
    expect(
      describeNotification(
        {
          type: "task:deleted",
          workspaceId: 1,
          taskId: 7,
          title: "Ship the report",
          createdBy: ME,
          deletedByUserId: THEM,
        },
        ctx({ prefs: { ...DEFAULT_NOTIFY_PREFS, tasks: false } }),
      ),
    ).toBeNull();
  });

  it("does not notify the person who deleted the task", () => {
    expect(
      describeNotification(
        {
          type: "task:deleted",
          workspaceId: 1,
          taskId: 7,
          title: "Ship the report",
          createdBy: THEM,
          deletedByUserId: ME,
        },
        ctx(),
      ),
    ).toBeNull();
  });
});

describe("describeNotification — calendar", () => {
  it("notifies on a new invitation", () => {
    const result = describeNotification({ type: "event:created", event: calendarEvent() }, ctx());
    expect(result!.title).toBe("New event invitation");
    expect(result!.body).toContain("Design review");
    expect(result!.route).toBe("/calendar?workspaceId=1");
  });

  it("ignores an event I'm not invited to", () => {
    expect(
      describeNotification({ type: "event:created", event: calendarEvent({ attendees: [] }) }, ctx()),
    ).toBeNull();
  });

  it("ignores an event I created", () => {
    expect(
      describeNotification({ type: "event:created", event: calendarEvent({ createdBy: ME }) }, ctx()),
    ).toBeNull();
  });

  it("notifies when an event I'm on is moved", () => {
    const result = describeNotification(
      { type: "event:updated", event: calendarEvent({ startsAt: "2026-08-10T14:00:00.000Z" }) },
      ctx({ previousEvent: calendarEvent() }),
    );
    expect(result!.title).toBe("Event moved");
  });

  it("stays quiet when only the description changed", () => {
    expect(
      describeNotification(
        { type: "event:updated", event: calendarEvent({ description: "agenda added" }) },
        ctx({ previousEvent: calendarEvent() }),
      ),
    ).toBeNull();
  });

  it("treats being added to an existing event as an invitation", () => {
    const result = describeNotification(
      { type: "event:updated", event: calendarEvent() },
      ctx({ previousEvent: calendarEvent({ attendees: [] }) }),
    );
    expect(result!.title).toBe("New event invitation");
  });

  it("suppresses the echo of my own edit", () => {
    expect(
      describeNotification(
        { type: "event:updated", event: calendarEvent({ startsAt: "2026-08-10T14:00:00.000Z" }) },
        ctx({ previousEvent: calendarEvent(), isSelfEcho: () => true }),
      ),
    ).toBeNull();
  });

  it("honours the calendar preference", () => {
    expect(
      describeNotification(
        { type: "event:created", event: calendarEvent() },
        ctx({ prefs: { ...DEFAULT_NOTIFY_PREFS, calendar: false } }),
      ),
    ).toBeNull();
  });
});

describe("describeNotification — signed-out safety", () => {
  it("says nothing when the user id isn't known yet", () => {
    for (const event of [
      { type: "message:created", message: message() },
      { type: "call:started", call: call() },
      { type: "task:created", task: task({ assigneeUserId: ME }) },
      { type: "event:created", event: calendarEvent() },
    ] as const) {
      expect(describeNotification(event, ctx({ myUserId: null }))).toBeNull();
    }
  });
});
