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

import {
  dutiesIn,
  groupLate,
  groupRuns,
  isLateSort,
  isTeamTab,
  matchesFilter,
  peopleIn,
  personOf,
  type RunRow,
  type ViewRow,
} from "@/lib/team-view"

/**
 * A late row, with only what the view reads. Defaults describe the commonest
 * shape on this screen: a held position, one department, one duty.
 */
function row(over: Partial<ViewRow> = {}): ViewRow {
  return {
    templateId: "duty-a",
    description: "Check the moulds",
    dueDate: "2026-07-24",
    assignee: "สมชาย ใจดี",
    position: "Line Leader",
    department: "Top management",
    ...over,
  }
}

describe("what the url may say", () => {
  test("only the three tabs and the three orderings are accepted", () => {
    expect(isTeamTab("late")).toBe(true)
    expect(isTeamTab("waiting")).toBe(true)
    expect(isTeamTab("banana")).toBe(false)
    expect(isTeamTab(undefined)).toBe(false)
    expect(isLateSort("most")).toBe(true)
    expect(isLateSort("Oldest")).toBe(false)
  })
})

describe("who a row is about", () => {
  test("the holder when there is one", () => {
    expect(personOf(row())).toBe("สมชาย ใจดี")
  })

  /**
   * A duty nobody holds is still somebody's job to fill, so the position stands
   * in for the person rather than the row going blank.
   */
  test("the position when nobody holds it", () => {
    expect(personOf(row({ assignee: null }))).toBe("Line Leader")
  })
})

describe("the filter", () => {
  test("a half-spelled name still matches, which is what applying as you type means", () => {
    expect(matchesFilter(row(), { person: "สมชาย", duty: "" })).toBe(true)
    expect(matchesFilter(row(), { person: "สมหญิง", duty: "" })).toBe(false)
  })

  test("case is folded, for the handful of names written in Latin", () => {
    const latin = row({ assignee: "Narin P." })
    expect(matchesFilter(latin, { person: "narin", duty: "" })).toBe(true)
  })

  test("both fields must match, not either", () => {
    expect(matchesFilter(row(), { person: "สมชาย", duty: "moulds" })).toBe(true)
    expect(matchesFilter(row(), { person: "สมชาย", duty: "invoice" })).toBe(
      false,
    )
  })

  test("an empty field filters nothing away", () => {
    expect(matchesFilter(row(), { person: "", duty: "" })).toBe(true)
  })

  /** A vacant position is filtered on its title, the same string it displays. */
  test("a vacant row matches on the position title", () => {
    const vacant = row({ assignee: null })
    expect(matchesFilter(vacant, { person: "line", duty: "" })).toBe(true)
  })
})

describe("what is offered under the fields", () => {
  test("each person and each duty once, whatever the repetition", () => {
    const rows = [
      row(),
      row({ dueDate: "2026-07-25" }),
      row({ templateId: "duty-b", description: "Sign the log", assignee: "ก" }),
    ]
    expect(peopleIn(rows)).toEqual(["ก", "สมชาย ใจดี"])
    expect(dutiesIn(rows)).toEqual(["Check the moulds", "Sign the log"])
  })
})

describe("grouping the late tab", () => {
  /**
   * The measurement the redesign exists for. Seven days of one missed duty are
   * seven rows and one line, and it is the seven rows that used to push the
   * rest of a department onto page two.
   */
  test("a run of missed days collapses to one line carrying the count", () => {
    const rows = [
      row({ dueDate: "2026-07-27" }),
      row({ dueDate: "2026-07-28" }),
      row({ dueDate: "2026-07-29" }),
      row({ dueDate: "2026-07-30" }),
      row({ dueDate: "2026-07-31" }),
      row({ dueDate: "2026-08-01" }),
      row({ dueDate: "2026-08-03" }),
    ]
    const groups = groupLate(rows, "oldest")
    expect(groups).toHaveLength(1)
    expect(groups[0]!.rows).toHaveLength(7)
    expect(groups[0]!.from).toBe("2026-07-27")
    expect(groups[0]!.to).toBe("2026-08-03")
  })

  /**
   * Two departments run duties spelled identically. Grouping on the words would
   * merge one department's missed work into another's and produce a count
   * belonging to nobody.
   */
  test("the same words under two duties stay two groups", () => {
    const groups = groupLate(
      [row(), row({ templateId: "duty-b", department: "Accounting" })],
      "oldest",
    )
    expect(groups).toHaveLength(2)
  })

  /** A position that changes hands mid-run is two people's records, not one. */
  test("the same duty under two people stays two groups", () => {
    const groups = groupLate([row(), row({ assignee: "สมหญิง" })], "oldest")
    expect(groups).toHaveLength(2)
    expect(groups.map((g) => g.person).sort()).toHaveLength(2)
  })

  test("a vacant run is marked so the screen can dim the title", () => {
    const groups = groupLate([row({ assignee: null })], "oldest")
    expect(groups[0]!.vacant).toBe(true)
    expect(groups[0]!.person).toBe("Line Leader")
  })

  test("a group of one has its window collapsed to a single day", () => {
    const groups = groupLate([row()], "oldest")
    expect(groups[0]!.from).toBe(groups[0]!.to)
  })

  test("the dates inside a group read forward whatever the ordering", () => {
    const rows = [
      row({ dueDate: "2026-08-01" }),
      row({ dueDate: "2026-07-24" }),
      row({ dueDate: "2026-07-28" }),
    ]
    for (const sort of ["oldest", "most", "person"] as const) {
      expect(groupLate(rows, sort)[0]!.rows.map((r) => r.dueDate)).toEqual([
        "2026-07-24",
        "2026-07-28",
        "2026-08-01",
      ])
    }
  })

  test("oldest first is the oldest date in each run", () => {
    const groups = groupLate(
      [
        row({
          templateId: "duty-b",
          description: "Sign the log",
          dueDate: "2026-08-01",
        }),
        row({ dueDate: "2026-07-24" }),
      ],
      "oldest",
    )
    expect(groups.map((g) => g.from)).toEqual(["2026-07-24", "2026-08-01"])
  })

  /**
   * Ties go to the older run. Two duties missed twice each are not equally
   * urgent when one of them started a fortnight earlier.
   */
  test("most repeats first, and a tie is broken by the older run", () => {
    const rows = [
      row({ templateId: "a", dueDate: "2026-08-01" }),
      row({ templateId: "a", dueDate: "2026-08-02" }),
      row({ templateId: "b", dueDate: "2026-07-20" }),
      row({ templateId: "b", dueDate: "2026-07-21" }),
      row({ templateId: "c", dueDate: "2026-07-30" }),
      row({ templateId: "c", dueDate: "2026-07-31" }),
      row({ templateId: "c", dueDate: "2026-08-01" }),
    ]
    const groups = groupLate(rows, "most")
    expect(groups.map((g) => g.rows.length)).toEqual([3, 2, 2])
    expect(groups[1]!.from).toBe("2026-07-20")
  })

  test("by person orders on the name the row shows", () => {
    const groups = groupLate(
      [
        row({ templateId: "a", assignee: "ข" }),
        row({ templateId: "b", assignee: "ก" }),
      ],
      "person",
    )
    expect(groups.map((g) => g.person)).toEqual(["ก", "ข"])
  })

  test("no rows, no groups", () => {
    expect(groupLate([], "oldest")).toEqual([])
  })
})

/**
 * The same collapsing, keyed the other way, which is what My tasks does.
 *
 * A row there carries no assignee and no position: the list selects on the
 * reader's own id, so the person the team overview puts in its key is a
 * constant. These rows are deliberately the narrow shape, so a change that
 * quietly made the grouper need a person again would stop this file compiling.
 */
function mine(over: Partial<RunRow> = {}): RunRow & { id: string } {
  return {
    id: `${over.templateId ?? "duty-a"}-${over.dueDate ?? "2026-07-24"}`,
    templateId: "duty-a",
    description: "Check the moulds",
    dueDate: "2026-07-24",
    ...over,
  }
}

const byDuty = (row: RunRow) => row.templateId

describe("grouping one person's backlog", () => {
  /** Nine rows of one daily check are what pushes today's work off the screen. */
  test("a duty missed nine days running is one line saying nine", () => {
    const days = [
      "2026-07-21",
      "2026-07-22",
      "2026-07-23",
      "2026-07-24",
      "2026-07-27",
      "2026-07-28",
      "2026-07-29",
      "2026-07-30",
      "2026-08-03",
    ]
    const runs = groupRuns(
      days.map((dueDate) => mine({ dueDate })),
      byDuty,
    )
    expect(runs).toHaveLength(1)
    expect(runs[0]!.rows).toHaveLength(9)
    expect(runs[0]!.from).toBe("2026-07-21")
    expect(runs[0]!.to).toBe("2026-08-03")
  })

  /**
   * The one thing grouping may never do. Collapsing lines is a way of reading
   * the backlog, not a way of shrinking it, so every row that went in comes
   * back out under exactly one line.
   */
  test("every row that went in is inside exactly one run", () => {
    const rows = [
      mine({ dueDate: "2026-07-21" }),
      mine({ dueDate: "2026-07-22" }),
      mine({ templateId: "duty-b", description: "Sign the log" }),
      mine({ templateId: "duty-b", dueDate: "2026-08-01" }),
      mine({ templateId: "duty-c", description: "Count the stock" }),
    ]
    const runs = groupRuns(rows, byDuty)
    const inside = runs.flatMap((run) => run.rows.map((r) => r.id))
    expect(inside).toHaveLength(rows.length)
    expect(new Set(inside).size).toBe(rows.length)
    expect(new Set(inside)).toEqual(new Set(rows.map((r) => r.id)))
  })

  /**
   * A person can hold positions in two departments, and two departments run
   * duties spelled identically. Grouping on the words would put one
   * department's backlog under the other's line.
   */
  test("identical wording under two duties stays two lines", () => {
    const runs = groupRuns([mine(), mine({ templateId: "duty-b" })], byDuty)
    expect(runs).toHaveLength(2)
    expect(runs.map((r) => r.description)).toEqual([
      "Check the moulds",
      "Check the moulds",
    ])
  })

  test("the oldest run leads, which is the order the section already read in", () => {
    const runs = groupRuns(
      [
        mine({
          templateId: "duty-b",
          description: "Sign the log",
          dueDate: "2026-08-01",
        }),
        mine({ dueDate: "2026-07-21" }),
      ],
      byDuty,
    )
    expect(runs.map((r) => r.from)).toEqual(["2026-07-21", "2026-08-01"])
  })

  test("the dates inside a run read forward", () => {
    const runs = groupRuns(
      [
        mine({ dueDate: "2026-08-01" }),
        mine({ dueDate: "2026-07-21" }),
        mine({ dueDate: "2026-07-28" }),
      ],
      byDuty,
    )
    expect(runs[0]!.rows.map((r) => r.dueDate)).toEqual([
      "2026-07-21",
      "2026-07-28",
      "2026-08-01",
    ])
  })

  test("a run of one has its window collapsed to a single day", () => {
    const runs = groupRuns([mine()], byDuty)
    expect(runs[0]!.from).toBe(runs[0]!.to)
    expect(runs[0]!.rows).toHaveLength(1)
  })

  /** Nothing carried over is no runs, and the screen draws no machinery at all. */
  test("no rows, no runs", () => {
    expect(groupRuns([], byDuty)).toEqual([])
  })

  test("the keys are distinct, so React has one identity per line", () => {
    const runs = groupRuns(
      [mine(), mine({ dueDate: "2026-07-25" }), mine({ templateId: "duty-b" })],
      byDuty,
    )
    expect(new Set(runs.map((r) => r.key)).size).toBe(runs.length)
  })
})
