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

import type { Relation, TaskStatus } from "@/lib/task-actions"
import { permits, type Verb } from "@/lib/services/task-commands"

/**
 * The gate every write goes through.
 *
 * The page hides buttons; this is what actually refuses. So the matrix below is
 * written out in full rather than derived from `availableActions`: a test that
 * recomputed the answer the same way the code does would agree with the code
 * whatever the code said, including after somebody inverts the comparison.
 */

const STATUSES: TaskStatus[] = [
  "todo",
  "started",
  "finished",
  "checked",
  "missed",
]

const VERBS: Verb[] = [
  "start",
  "finish",
  "report_issue",
  "approve",
  "send_back",
  "verify",
  "close",
  "remind",
]

/** Everything this viewer would be allowed to do, at each of the five statuses. */
const allowed = (relations: Relation[], isOneTime = false) =>
  Object.fromEntries(
    STATUSES.map((status) => [
      status,
      VERBS.filter((verb) => permits(verb, status, { relations, isOneTime })),
    ]),
  )

const nothing = {
  todo: [],
  started: [],
  finished: [],
  checked: [],
  missed: [],
}

describe("permits", () => {
  /**
   * The case the whole file exists for. Somebody who is named nowhere on the
   * duty and runs no department can post any id with any verb, and every one of
   * the forty combinations has to come back refused.
   */
  test("somebody with no relation to the task may do nothing to it, ever", () => {
    expect(allowed([])).toEqual(nothing)
  })

  test("an assignee works, and only while there is work to do", () => {
    expect(allowed(["assignee"])).toEqual({
      todo: ["start", "report_issue"],
      started: ["finish", "report_issue"],
      finished: [],
      checked: [],
      // Still owed, so still theirs to hand in. `report_issue` is not here:
      // `lifecycle.json` names it on `todo|started` only.
      missed: ["start", "finish"],
    })
  })

  test("a checker decides, and only once the work has been handed in", () => {
    expect(allowed(["checker"])).toEqual({
      todo: [],
      started: [],
      finished: ["approve", "send_back"],
      checked: [],
      missed: [],
    })
  })

  test("a verifier audits, and only after the checker has finished with it", () => {
    expect(allowed(["verifier"])).toEqual({
      todo: [],
      started: [],
      finished: [],
      checked: ["verify"],
      missed: [],
    })
  })

  test("a manager of the department nudges owed work and nothing else", () => {
    expect(allowed(["manager_of_dept"])).toEqual({
      todo: ["remind"],
      started: ["remind"],
      finished: [],
      checked: [],
      // The chase follows the debt. Losing it at the cut-off would take the
      // reminder away exactly when it began to be worth sending.
      missed: ["remind"],
    })
  })

  /**
   * `close` ends something nothing will reissue. On a recurring duty tomorrow
   * brings another instance, so the creator holds no relation at all and the
   * gate refuses the verb they would have been offered on a one-time task.
   */
  test("the creator of a one-time task closes it, and gets nothing on a recurring one", () => {
    expect(allowed(["one_time_creator"], true)).toEqual({
      todo: [],
      started: [],
      // One verb, per `actions_by_relation`. It read `send_back, close` until
      // a review demonstrated a staff member of another department returning a
      // task with a creator relation acquired months earlier.
      finished: ["close"],
      checked: [],
      missed: [],
    })
    expect(allowed(["one_time_creator"], false)).toEqual(nothing)
  })

  /**
   * A one-time creator who is also its checker approves rather than closes, so
   * `close` is refused for them: two words for one transition, and the gate
   * accepts exactly the one the page was told to draw.
   */
  test("a creator who is also the checker may approve and may close", () => {
    const both: Relation[] = ["one_time_creator", "checker"]
    expect(
      permits("approve", "finished", { relations: both, isOneTime: true }),
    ).toBe(true)
    // Both relations are held, so both verbs are theirs. Refusing `close` here
    // meant refusing it to every admin as well, since `has` counts an admin as
    // the checker.
    expect(
      permits("close", "finished", { relations: both, isOneTime: true }),
    ).toBe(true)
    // And the creator alone still gets only the one verb the spec names.
    const alone: Relation[] = ["one_time_creator"]
    expect(
      permits("close", "finished", { relations: alone, isOneTime: true }),
    ).toBe(true)
    expect(
      permits("send_back", "finished", { relations: alone, isOneTime: true }),
    ).toBe(false)
    expect(
      permits("approve", "finished", { relations: alone, isOneTime: true }),
    ).toBe(false)
  })

  test("an admin may do whatever the status allows, holding no relation at all", () => {
    expect(allowed(["admin"])).toEqual({
      todo: ["start", "report_issue", "remind"],
      started: ["finish", "report_issue", "remind"],
      finished: ["approve", "send_back"],
      checked: ["verify"],
      missed: ["start", "finish", "remind"],
    })
  })

  /**
   * Two people in a department means one person doing the work and approving
   * it, which the roster already contains. Both relations have to be read
   * together or half the lifecycle is refused to the only person who can move
   * it.
   */
  test("somebody who checks their own work is gated on both relations at once", () => {
    expect(allowed(["assignee", "checker"])).toEqual({
      todo: ["start", "report_issue"],
      started: ["finish", "report_issue"],
      finished: ["approve", "send_back"],
      checked: [],
      missed: ["start", "finish"],
    })
  })

  /**
   * A missed task is work that ran out of time and is still owed, which is what
   * the client answered on 4 August. The gate has to let the debt be paid: the
   * assignee hands it in, the manager chases it, and nobody else gains anything
   * from the day having passed.
   *
   * The row that matters most is the checker's. There is no work to judge until
   * somebody hands it in, so `missed` gives them nothing, exactly as `todo`
   * does. The gate widening for the assignee must not widen for them.
   */
  test("a missed task is owed by its assignee and by nobody else", () => {
    expect(allowed(["assignee"]).missed).toEqual(["start", "finish"])
    expect(allowed(["manager_of_dept"]).missed).toEqual(["remind"])
    expect(allowed(["checker"]).missed).toEqual([])
    expect(allowed(["verifier"]).missed).toEqual([])
    expect(allowed(["one_time_creator"], true).missed).toEqual([])
    expect(allowed([]).missed).toEqual([])
  })

  /**
   * The gate is what refuses, so the two verbs the answer adds are asserted
   * against it directly and not only through the matrix above.
   */
  test("the gate accepts the two verbs that clear a missed task", () => {
    const assignee: Relation[] = ["assignee"]
    expect(
      permits("finish", "missed", { relations: assignee, isOneTime: false }),
    ).toBe(true)
    expect(
      permits("start", "missed", { relations: assignee, isOneTime: false }),
    ).toBe(true)
    // And nothing beyond them. A missed task is not a way round the checker.
    expect(
      permits("approve", "missed", {
        relations: ["checker"],
        isOneTime: false,
      }),
    ).toBe(false)
    expect(
      permits("verify", "missed", {
        relations: ["verifier"],
        isOneTime: false,
      }),
    ).toBe(false)
    expect(
      permits("close", "missed", {
        relations: ["one_time_creator"],
        isOneTime: true,
      }),
    ).toBe(false)
  })
})
