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

import {
  foldGroups,
  percentDone,
  totalOf,
  type KpiFigures,
  type KpiGroup,
} from "@/lib/kpi-view"

/**
 * The arithmetic /kpi is read as fact.
 *
 * ELEVA-116 is "a person is evaluated on KPI they cannot see", and the fix is
 * only worth anything if the figure a manager reads about somebody and the
 * figure that person reads about themselves are the same figure. What is tested
 * here is the half of that which does not need a database: that a person is one
 * row however the query hands them over, that the rows add up to the total, and
 * that an empty plate is not a row of noughts.
 */

function figures(due: number, done: number, late = 0, missed = 0): KpiFigures {
  return { due, done, late, missed }
}

function group(over: Partial<KpiGroup> = {}): KpiGroup {
  return {
    personId: "p1",
    name: "Anna",
    positionId: "pos1",
    position: "Clerk",
    left: false,
    figures: figures(10, 6, 1, 3),
    ...over,
  }
}

describe("foldGroups", () => {
  test("a person is one row", () => {
    const rows = foldGroups([group()])
    expect(rows).toHaveLength(1)
    expect(rows[0]!.name).toBe("Anna")
    expect(rows[0]!.figures).toEqual(figures(10, 6, 1, 3))
  })

  /**
   * The query groups on the position as well as the person, because vacant work
   * has no person to group on. Somebody holding two positions therefore arrives
   * as two groups, and their own screen shows them one number: folding on the
   * person is what stops their manager's line disagreeing with it.
   */
  test("one person holding two positions is still one row", () => {
    const rows = foldGroups([
      group({ positionId: "pos1", figures: figures(10, 6, 1, 3) }),
      group({ positionId: "pos2", position: "Stock", figures: figures(4, 4) }),
    ])
    expect(rows).toHaveLength(1)
    expect(rows[0]!.figures).toEqual(figures(14, 10, 1, 3))
  })

  /**
   * Two empty chairs are two lines. There is no person to fold them onto and
   * the position is the only thing telling them apart, so folding them together
   * would hide one vacancy behind another.
   */
  test("two vacant positions stay two rows", () => {
    const rows = foldGroups([
      group({
        personId: null,
        name: null,
        positionId: "pos1",
        position: "Laser",
      }),
      group({
        personId: null,
        name: null,
        positionId: "pos2",
        position: "Stock",
      }),
    ])
    expect(rows).toHaveLength(2)
    expect(rows.map((r) => r.position).sort()).toEqual(["Laser", "Stock"])
    expect(rows.every((r) => r.name === null)).toBe(true)
  })

  /** Vacant rows and people are never merged, whatever order they arrive in. */
  test("a vacancy is not folded into a person", () => {
    const rows = foldGroups([
      group({ personId: null, name: null }),
      group({ personId: "p1", name: "Anna" }),
    ])
    expect(rows).toHaveLength(2)
  })

  test("somebody who has left keeps their row and is marked", () => {
    const rows = foldGroups([group({ name: "Nachrida", left: true })])
    expect(rows[0]!.left).toBe(true)
    expect(rows[0]!.figures.due).toBe(10)
  })

  /**
   * Name order, and a vacant row sorts on the position title it shows, because
   * that is the text somebody reading the column is scanning.
   */
  test("rows come back in name order, vacancies under their title", () => {
    const rows = foldGroups([
      group({ personId: "p2", name: "Zara", positionId: "pos2" }),
      group({
        personId: null,
        name: null,
        positionId: "pos3",
        position: "Laser",
      }),
      group({ personId: "p1", name: "Anna", positionId: "pos1" }),
    ])
    expect(rows.map((r) => r.name ?? r.position)).toEqual([
      "Anna",
      "Laser",
      "Zara",
    ])
  })

  test("nothing in, nothing out", () => {
    expect(foldGroups([])).toEqual([])
  })
})

describe("totalOf", () => {
  /** The one thing a reader does with a table under a total is check it adds up. */
  test("the total is the sum of the rows", () => {
    const rows = foldGroups([
      group({ personId: "p1", name: "Anna", figures: figures(28, 16, 1, 10) }),
      group({
        personId: "p2",
        name: "Malai",
        positionId: "pos2",
        figures: figures(5, 4, 0, 1),
      }),
    ])
    expect(totalOf(rows)).toEqual(figures(33, 20, 1, 11))
  })

  /** Vacant work belongs to the department, so it is in the total and in a row. */
  test("work on an empty chair is counted in the total", () => {
    const rows = foldGroups([
      group({ personId: "p1", name: "Anna", figures: figures(17, 9, 0, 8) }),
      group({
        personId: null,
        name: null,
        positionId: "pos9",
        position: "Laser",
        figures: figures(7, 0, 1, 5),
      }),
    ])
    expect(totalOf(rows)).toEqual(figures(24, 9, 1, 13))
  })

  /**
   * Null and not a row of noughts. Somebody who was owed nothing has not failed
   * at anything, and `ram` holds a position in a department outside the pilot.
   */
  test("nothing due is null rather than zero", () => {
    expect(totalOf([])).toBeNull()
  })
})

describe("percentDone", () => {
  test("done over due, as whole percent", () => {
    expect(percentDone(figures(33, 20))).toBe(61)
    expect(percentDone(figures(28, 16))).toBe(57)
  })

  /** Both ends, because both ends are the ones somebody screenshots. */
  test("all of it and none of it", () => {
    expect(percentDone(figures(4, 4))).toBe(100)
    expect(percentDone(figures(7, 0))).toBe(0)
  })

  /**
   * The table sorts on the ratio and shows this, so a person with 3 of 4 must
   * sort above one with 30 of 60 while both read as whole numbers.
   */
  test("a small denominator is not rounded into a different number", () => {
    expect(percentDone(figures(4, 3))).toBe(75)
    expect(percentDone(figures(60, 30))).toBe(50)
  })
})
