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

import {
  listenerCount,
  publish,
  subscribe,
} from "@/lib/services/notification-hub"

/**
 * The three things a wake-up has to get right.
 *
 * Reaching the person it is for, reaching nobody else, and leaving nothing
 * behind. The third is the one that cannot be seen from outside the process: a
 * closed socket says the client went away, not that the server let go, and a
 * subscription per tab that is never removed is a server that dies quietly
 * after a week.
 */

const ANNA = "11111111-1111-1111-1111-111111111111"
const BEN = "22222222-2222-2222-2222-222222222222"

const undo: (() => void)[] = []

function listen(personId: string, wake: () => void) {
  const off = subscribe(personId, wake)
  undo.push(off)
  return off
}

afterEach(() => {
  while (undo.length > 0) undo.pop()!()
})

describe("waking the streams one person has open", () => {
  test("every tab that person has open is woken, once each", () => {
    let first = 0
    let second = 0
    listen(ANNA, () => first++)
    listen(ANNA, () => second++)

    publish(ANNA)

    expect(first).toBe(1)
    expect(second).toBe(1)
  })

  test("nobody else hears it", () => {
    let anna = 0
    let ben = 0
    listen(ANNA, () => anna++)
    listen(BEN, () => ben++)

    publish(ANNA)

    expect(anna).toBe(1)
    expect(ben).toBe(0)
  })

  test("a person with nothing open costs nothing and throws nothing", () => {
    expect(() => publish(BEN)).not.toThrow()
  })
})

describe("letting go", () => {
  test("an unsubscribed stream is not woken again", () => {
    let woken = 0
    const off = listen(ANNA, () => woken++)

    publish(ANNA)
    off()
    publish(ANNA)

    expect(woken).toBe(1)
  })

  /*
    A stream is torn down from an abort, from a cancel and from its own lifetime
    timer, and there is no ordering between the three. All of them call this.
  */
  test("unsubscribing twice is not a second removal", () => {
    let anna = 0
    const off = listen(ANNA, () => anna++)
    const other = listen(ANNA, () => anna++)

    off()
    off()

    publish(ANNA)
    expect(anna).toBe(1)
    other()
  })

  test("the last one out empties the map rather than leaving a set behind", () => {
    const off = listen(ANNA, () => {})
    expect(listenerCount(ANNA)).toBe(1)

    off()

    expect(listenerCount(ANNA)).toBe(0)
    expect(listenerCount()).toBe(0)
  })

  test("twenty opened and twenty closed leave nothing running", () => {
    const closers = Array.from({ length: 20 }, (_, i) =>
      listen(i % 2 === 0 ? ANNA : BEN, () => {}),
    )
    expect(listenerCount()).toBe(20)

    for (const close of closers) close()

    expect(listenerCount()).toBe(0)
  })
})

describe("one bad stream", () => {
  /*
    A listener that throws is a tab whose controller was closed under it. The
    other tabs are owed the same row, and the throw must not be what stops them
    getting it.
  */
  test("does not stop the others being woken", () => {
    const seen: string[] = []
    listen(ANNA, () => {
      seen.push("first")
      throw new Error("the reader is gone")
    })
    listen(ANNA, () => seen.push("second"))

    expect(() => publish(ANNA)).not.toThrow()
    expect(seen).toEqual(["first", "second"])
  })

  /*
    Tearing itself down is exactly what a dead stream does when it is woken, and
    that removes it from the set being iterated.
  */
  test("may unsubscribe itself from inside the wake-up", () => {
    const seen: string[] = []
    const off: (() => void)[] = []
    off.push(
      listen(ANNA, () => {
        seen.push("first")
        off[0]!()
      }),
    )
    listen(ANNA, () => seen.push("second"))

    publish(ANNA)

    expect(seen).toEqual(["first", "second"])
    expect(listenerCount(ANNA)).toBe(1)
  })
})
