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

import { DEFAULT_WORK_PATTERN } from "@/lib/calendar"
import {
  coverInForce,
  dutyHolderOn,
  isInForce,
  standingOn,
  type Cover,
  type Holder,
} from "@/lib/cover"

/**
 * The rules of temporary cover, with no database in the room.
 *
 * Written against the three things that would break silently if somebody got
 * them wrong: the window is inclusive at both ends, the working week comes from
 * the stand-in and not from the person being covered, and a cover that has run
 * out stops assigning. None of the three raises an error when it is wrong. Each
 * one just sends work to the wrong person, or to nobody, on days nobody is
 * looking at.
 */

const PORTER = "position-porter"
const OTHER = "position-other"

const cover = (over: Partial<Cover> = {}): Cover => ({
  positionId: PORTER,
  personId: "malee",
  pattern: "mon_sat",
  from: "2026-08-10",
  until: "2026-08-14",
  ...over,
})

const holder = (over: Partial<Holder> = {}): Holder => ({
  id: "somchai",
  pattern: "mon_sat",
  ...over,
})

/**
 * The three standings, which the screen has to tell apart because the End
 * control does two different things: it shortens a running cover to today and
 * it deletes one that has not started.
 */
describe("where today sits relative to the window", () => {
  const window = { from: "2026-08-10", until: "2026-08-14" }

  test("before the first day it has not started", () => {
    expect(standingOn(window, "2026-08-09")).toBe("scheduled")
  })

  test("the first day is already running, not still scheduled", () => {
    expect(standingOn(window, "2026-08-10")).toBe("running")
  })

  test("the last day is still running, not yet over", () => {
    expect(standingOn(window, "2026-08-14")).toBe("running")
  })

  test("the day after the last day is over", () => {
    expect(standingOn(window, "2026-08-15")).toBe("over")
  })

  test("a one-day cover is running on its one day and over the next", () => {
    const one = { from: "2026-08-10", until: "2026-08-10" }
    expect(standingOn(one, "2026-08-09")).toBe("scheduled")
    expect(standingOn(one, "2026-08-10")).toBe("running")
    expect(standingOn(one, "2026-08-11")).toBe("over")
  })

  test("it agrees with isInForce on every day around the window", () => {
    const days = [
      "2026-08-08",
      "2026-08-09",
      "2026-08-10",
      "2026-08-12",
      "2026-08-14",
      "2026-08-15",
      "2026-08-16",
    ]
    for (const day of days) {
      expect(standingOn(window, day) === "running").toBe(
        isInForce(cover(window), day),
      )
    }
  })
})

describe("the window is inclusive at both ends", () => {
  const window = cover()

  test("the first day is covered", () => {
    expect(isInForce(window, "2026-08-10")).toBe(true)
  })

  test("the last day is covered", () => {
    expect(isInForce(window, "2026-08-14")).toBe(true)
  })

  test("every day in between is covered", () => {
    for (const day of ["2026-08-11", "2026-08-12", "2026-08-13"]) {
      expect(isInForce(window, day)).toBe(true)
    }
  })

  test("the day before the window is not", () => {
    expect(isInForce(window, "2026-08-09")).toBe(false)
  })

  test("the day after the window is not", () => {
    expect(isInForce(window, "2026-08-15")).toBe(false)
  })

  test("a single-day cover covers exactly that day", () => {
    const one = cover({ from: "2026-08-10", until: "2026-08-10" })
    expect(isInForce(one, "2026-08-09")).toBe(false)
    expect(isInForce(one, "2026-08-10")).toBe(true)
    expect(isInForce(one, "2026-08-11")).toBe(false)
  })

  test("the comparison is calendar order across a month and a year boundary", () => {
    const newYear = cover({ from: "2026-12-28", until: "2027-01-04" })
    expect(isInForce(newYear, "2026-12-27")).toBe(false)
    expect(isInForce(newYear, "2026-12-31")).toBe(true)
    expect(isInForce(newYear, "2027-01-01")).toBe(true)
    expect(isInForce(newYear, "2027-01-04")).toBe(true)
    expect(isInForce(newYear, "2027-01-05")).toBe(false)
  })
})

describe("finding the cover that applies", () => {
  test("a cover on another position is not this position's", () => {
    const covers = [cover({ positionId: OTHER })]
    expect(coverInForce(covers, PORTER, "2026-08-11")).toBeNull()
  })

  test("the right position on a day inside the window", () => {
    const covers = [cover({ positionId: OTHER }), cover()]
    expect(coverInForce(covers, PORTER, "2026-08-11")?.personId).toBe("malee")
  })

  test("nothing at all when no window contains the day", () => {
    const covers = [cover(), cover({ from: "2026-09-01", until: "2026-09-03" })]
    expect(coverInForce(covers, PORTER, "2026-08-20")).toBeNull()
  })

  test("no covers at all is not an error", () => {
    expect(coverInForce([], PORTER, "2026-08-11")).toBeNull()
  })

  /**
   * The database refuses this pair. The test is here because a pure function
   * that returns whichever element came first in an array returns a different
   * answer for the same data depending on the order the rows arrived in, and
   * two runs of the generator would then disagree.
   */
  test("two overlapping covers resolve to the one that started first, whatever the order", () => {
    const early = cover({
      personId: "malee",
      from: "2026-08-10",
      until: "2026-08-14",
    })
    const late = cover({
      personId: "niran",
      from: "2026-08-12",
      until: "2026-08-20",
    })
    expect(coverInForce([early, late], PORTER, "2026-08-13")?.personId).toBe(
      "malee",
    )
    expect(coverInForce([late, early], PORTER, "2026-08-13")?.personId).toBe(
      "malee",
    )
  })
})

describe("who the duties fall to", () => {
  test("the single holder, on a day with no cover", () => {
    const decided = dutyHolderOn(PORTER, [holder()], [], "2026-08-11")
    expect(decided.assignee).toBe("somchai")
  })

  test("the stand-in, on a day the cover runs", () => {
    const decided = dutyHolderOn(PORTER, [holder()], [cover()], "2026-08-11")
    expect(decided.assignee).toBe("malee")
  })

  test("the holder again on the day after the cover ends", () => {
    const decided = dutyHolderOn(PORTER, [holder()], [cover()], "2026-08-15")
    expect(decided.assignee).toBe("somchai")
  })

  test("the holder still, on the day before it starts", () => {
    const decided = dutyHolderOn(PORTER, [holder()], [cover()], "2026-08-09")
    expect(decided.assignee).toBe("somchai")
  })

  test("the holder keeps the last day of the window to the stand-in", () => {
    expect(
      dutyHolderOn(PORTER, [holder()], [cover()], "2026-08-14").assignee,
    ).toBe("malee")
  })

  test("a cover on somebody else's position leaves this one alone", () => {
    const decided = dutyHolderOn(
      PORTER,
      [holder()],
      [cover({ positionId: OTHER, personId: "niran" })],
      "2026-08-11",
    )
    expect(decided.assignee).toBe("somchai")
  })

  test("a vacant position with a cover gets the stand-in", () => {
    const decided = dutyHolderOn(PORTER, [], [cover()], "2026-08-11")
    expect(decided.assignee).toBe("malee")
  })

  test("a vacant position with no cover gets nobody", () => {
    const decided = dutyHolderOn(PORTER, [], [], "2026-08-11")
    expect(decided.assignee).toBeNull()
  })

  test("a shared position with a cover gets the stand-in, not one of the several", () => {
    const shared = [holder({ id: "a" }), holder({ id: "b" })]
    expect(dutyHolderOn(PORTER, shared, [cover()], "2026-08-11").assignee).toBe(
      "malee",
    )
  })

  test("a shared position with no cover still gets nobody", () => {
    const shared = [holder({ id: "a" }), holder({ id: "b" })]
    expect(dutyHolderOn(PORTER, shared, [], "2026-08-11").assignee).toBeNull()
  })
})

describe("the working week that shapes the days", () => {
  /**
   * The one that decides whether a Saturday exists at all. The stand-in is the
   * person who has to be there, so it is their week that counts, and taking the
   * covered holder's would put work on a day the person receiving it does not
   * work without anything reporting a problem.
   */
  test("comes from the stand-in while the cover runs", () => {
    const covered = holder({ id: "somchai", pattern: "mon_sat" })
    const standIn = cover({ personId: "malee", pattern: "mon_fri" })
    expect(
      dutyHolderOn(PORTER, [covered], [standIn], "2026-08-11").pattern,
    ).toBe("mon_fri")
  })

  test("and the other way round, so the answer is not the holder's by coincidence", () => {
    const covered = holder({ id: "somchai", pattern: "mon_fri" })
    const standIn = cover({ personId: "malee", pattern: "mon_sat" })
    expect(
      dutyHolderOn(PORTER, [covered], [standIn], "2026-08-11").pattern,
    ).toBe("mon_sat")
  })

  test("goes back to the holder's the day the cover ends", () => {
    const covered = holder({ id: "somchai", pattern: "mon_fri" })
    const standIn = cover({ personId: "malee", pattern: "mon_sat" })
    expect(
      dutyHolderOn(PORTER, [covered], [standIn], "2026-08-15").pattern,
    ).toBe("mon_fri")
  })

  test("is the holder's when nobody is covering", () => {
    expect(
      dutyHolderOn(PORTER, [holder({ pattern: "mon_fri" })], [], "2026-08-11")
        .pattern,
    ).toBe("mon_fri")
  })

  test("falls back to the schema's default when there is no single holder and no cover", () => {
    expect(dutyHolderOn(PORTER, [], [], "2026-08-11").pattern).toBe(
      DEFAULT_WORK_PATTERN,
    )
    expect(DEFAULT_WORK_PATTERN).toBe("mon_sat")
  })

  test("a cover over a vacant position beats the fallback week", () => {
    const standIn = cover({ pattern: "mon_fri" })
    expect(dutyHolderOn(PORTER, [], [standIn], "2026-08-11").pattern).toBe(
      "mon_fri",
    )
  })
})

describe("a cover that has been ended stops assigning", () => {
  /**
   * Ending early is shortening the window and nothing else, so what a test can
   * check is that the shortened window stops on the day it now says. The row
   * whose `until` was moved back to today keeps today and loses tomorrow.
   */
  const shortened = cover({ from: "2026-08-10", until: "2026-08-12" })

  test("today, the day it was cut back to, still goes to the stand-in", () => {
    expect(
      dutyHolderOn(PORTER, [holder()], [shortened], "2026-08-12").assignee,
    ).toBe("malee")
  })

  test("tomorrow goes back to the holder", () => {
    expect(
      dutyHolderOn(PORTER, [holder()], [shortened], "2026-08-13").assignee,
    ).toBe("somchai")
  })

  test("and every day after that, not only the first", () => {
    for (const day of [
      "2026-08-14",
      "2026-08-20",
      "2026-09-01",
      "2027-01-01",
    ]) {
      expect(dutyHolderOn(PORTER, [holder()], [shortened], day).assignee).toBe(
        "somchai",
      )
    }
  })

  test("the week goes back with it", () => {
    const covered = holder({ pattern: "mon_fri" })
    const standIn = cover({
      from: "2026-08-10",
      until: "2026-08-12",
      pattern: "mon_sat",
    })
    expect(
      dutyHolderOn(PORTER, [covered], [standIn], "2026-08-13").pattern,
    ).toBe("mon_fri")
  })
})
