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

import { generateFirstPassword } from "@/lib/first-password"

/**
 * A generator has no interesting single case, so these assert the properties
 * that matter to the person typing it and the person who has to trust it.
 */

const SHAPE = /^[A-Z2-9]{4}-[A-Z2-9]{4}-[A-Z2-9]{4}$/

describe("generateFirstPassword", () => {
  test("three groups of four, separated by hyphens", () => {
    for (let i = 0; i < 200; i++) {
      expect(generateFirstPassword()).toMatch(SHAPE)
    }
  })

  test("never the five characters that get misread off paper", () => {
    // O and 0, I and l and 1. HR reads these aloud to somebody at a terminal.
    for (let i = 0; i < 500; i++) {
      expect(generateFirstPassword()).not.toMatch(/[O0Il1]/)
    }
  })

  test("two in a row are not the same", () => {
    expect(generateFirstPassword()).not.toBe(generateFirstPassword())
  })

  test("every letter of the alphabet can turn up", () => {
    const seen = new Set<string>()
    for (let i = 0; i < 2000; i++) {
      for (const c of generateFirstPassword().replaceAll("-", "")) seen.add(c)
    }
    expect(seen.size).toBe(31)
  })

  /**
   * The reason the bytes are drawn against a ceiling rather than folded with a
   * plain `%`.
   *
   * 256 is not a multiple of 31: it is 8 × 31 + 8, so folding a byte with the
   * remainder maps nine byte values onto each of the first eight letters and
   * eight onto each of the other twenty-three. Those eight letters would take
   * 72/256 of the draws instead of 8/31, which is nine percent more than their
   * share.
   *
   * So the assertion is on that group and not on each letter separately, which
   * is what makes it able to see the fault at all. Sixty thousand characters
   * puts a fair generator within about seven tenths of a percent of its share,
   * and puts the folded one thirteen standard deviations outside it. The band
   * below is four percent: wide enough never to trip on chance, narrow enough
   * that the fault cannot hide in it.
   */
  test("the first eight letters take no more than their share", () => {
    const head = new Set("ABCDEFGH")
    const draws = 5000
    let inHead = 0
    let total = 0

    for (let i = 0; i < draws; i++) {
      for (const c of generateFirstPassword().replaceAll("-", "")) {
        if (head.has(c)) inHead++
        total++
      }
    }

    const share = inHead / total
    const fair = 8 / 31
    expect(
      Math.abs(share - fair) / fair,
      `the first eight letters took ${(share * 100).toFixed(2)}% against ${(fair * 100).toFixed(2)}%`,
    ).toBeLessThan(0.04)
  })
})
