import { describe, expect, test } from "bun:test"

import type { DomainEventType } from "@/lib/db/schema"
import { recipientsOf, type Pending } from "@/lib/services/notifications"

/**
 * Who is told, and who is not.
 *
 * The fan-out is the one decision in the notification half: everything else is
 * a write, an index or a query. It is written out in full below rather than
 * derived from the table it is implemented with, for the same reason
 * `permits` is: a test that recomputed the answer the same way the code does
 * would agree with the code whatever the code said.
 *
 * The database is not involved. What the rule takes is an event with the two
 * people its task names, and those two are exactly what the query hands it.
 */

const ALL: DomainEventType[] = [
  "task.finished",
  "task.checked",
  "task.sent_back",
  "task.reminded",
  "task.missed",
]

const MANAGER = "11111111-1111-1111-1111-111111111111"
const WORKER = "22222222-2222-2222-2222-222222222222"
const TASK = "33333333-3333-3333-3333-333333333333"

function event(over: Partial<Pending> = {}): Pending {
  return {
    id: "event-1",
    type: "task.finished",
    taskInstanceId: TASK,
    actorPersonId: WORKER,
    payload: null,
    assigneePersonId: WORKER,
    checkerPersonId: MANAGER,
    ...over,
  }
}

describe("who each event reaches", () => {
  test("a finished task reaches its checker, and nobody else", () => {
    const to = recipientsOf(event({ type: "task.finished" }))
    expect(to.map((r) => r.personId)).toEqual([MANAGER])
    expect(to[0]!.notice.title).toBe("notify.finished")
    expect(to[0]!.notice.taskId).toBe(TASK)
  })

  test("work sent back reaches the assignee, with the note", () => {
    const to = recipientsOf(
      event({
        type: "task.sent_back",
        actorPersonId: MANAGER,
        payload: { note: "The weights are the last shift's." },
      }),
    )
    expect(to.map((r) => r.personId)).toEqual([WORKER])
    expect(to[0]!.notice.title).toBe("notify.sentBack")
    expect(to[0]!.notice.body).toBe("The weights are the last shift's.")
  })

  test("a reminder reaches the assignee, with what was said", () => {
    const to = recipientsOf(
      event({
        type: "task.reminded",
        actorPersonId: MANAGER,
        payload: { note: "Before the line starts, please." },
      }),
    )
    expect(to.map((r) => r.personId)).toEqual([WORKER])
    expect(to[0]!.notice.title).toBe("notify.reminded")
    expect(to[0]!.notice.body).toBe("Before the line starts, please.")
  })

  test("an approval reaches the assignee", () => {
    const to = recipientsOf(
      event({ type: "task.checked", actorPersonId: MANAGER }),
    )
    expect(to.map((r) => r.personId)).toEqual([WORKER])
    expect(to[0]!.notice.title).toBe("notify.approved")
  })

  /**
   * The deliberate silence. 69 instances are missed in the database today and
   * the nightly sweep marks more every night; the event is recorded for other
   * consumers and reaches nobody.
   */
  test("a missed task is recorded and reaches nobody", () => {
    expect(
      recipientsOf(event({ type: "task.missed", actorPersonId: null })),
    ).toEqual([])
  })
})

describe("who is never told", () => {
  /**
   * The commonest case in the small departments rather than a curiosity: the
   * checker of a duty is often the person holding the position it belongs to.
   */
  test("the actor is never told about their own act", () => {
    for (const type of ALL) {
      expect(
        recipientsOf(
          event({ type, actorPersonId: MANAGER, assigneePersonId: MANAGER }),
        ),
      ).toEqual([])
    }
  })

  test("a vacant position is nobody, and gets nothing", () => {
    expect(
      recipientsOf(
        event({
          type: "task.checked",
          actorPersonId: MANAGER,
          assigneePersonId: null,
        }),
      ),
    ).toEqual([])
  })

  test("a duty naming no checker gets nothing", () => {
    expect(
      recipientsOf(event({ type: "task.finished", checkerPersonId: null })),
    ).toEqual([])
  })
})

describe("every event type is decided", () => {
  /**
   * A type added to the enum and forgotten in `FAN_OUT` would be a `Record`
   * with a missing key, which the compiler catches. What it cannot catch is a
   * type nobody thought about at all, so this asks each of them and only cares
   * that it answered.
   */
  test("none of them throws, and each answers with a list", () => {
    for (const type of ALL) {
      expect(Array.isArray(recipientsOf(event({ type })))).toBe(true)
    }
  })
})
