import { describe, expect, it } from "bun:test";
import {
  MIN_PASSWORD_LENGTH,
  missingCriteria,
  passwordSchema,
  scorePassword,
  validatePassword,
} from "./password-policy";

describe("password policy", () => {
  it("exports a 14-char minimum", () => {
    expect(MIN_PASSWORD_LENGTH).toBe(14);
  });

  it("rejects a 13-char password even with all character classes", () => {
    const pw = "Aa1!aaaaaaaaa"; // 13 chars, lower+upper+digit+symbol
    expect(pw).toHaveLength(13);
    const res = validatePassword(pw);
    expect(res.ok).toBe(false);
    if (!res.ok) expect(res.missing).toEqual(["length"]);
    expect(passwordSchema.safeParse(pw).success).toBe(false);
  });

  it("accepts a 14-char password with all character classes", () => {
    const pw = "Aa1!aaaaaaaaaa"; // 14 chars
    expect(pw).toHaveLength(14);
    expect(validatePassword(pw)).toEqual({ ok: true });
    expect(passwordSchema.safeParse(pw).success).toBe(true);
  });

  it("detects a missing symbol", () => {
    const pw = "Aa1aaaaaaaaaaa"; // 14 chars, no symbol
    const res = validatePassword(pw);
    expect(res.ok).toBe(false);
    if (!res.ok) expect(res.missing).toEqual(["symbol"]);
  });

  it("detects a missing uppercase letter", () => {
    const pw = "aa1!aaaaaaaaaa"; // 14 chars, no upper
    const res = validatePassword(pw);
    expect(res.ok).toBe(false);
    if (!res.ok) expect(res.missing).toEqual(["upper"]);
  });

  it("detects a missing digit", () => {
    const pw = "Aaa!aaaaaaaaaa"; // 14 chars, no digit
    const res = validatePassword(pw);
    expect(res.ok).toBe(false);
    if (!res.ok) expect(res.missing).toEqual(["digit"]);
  });

  it("rejects passwords over the max length", () => {
    const pw = `Aa1!${"a".repeat(125)}`; // 129 chars, all classes
    expect(pw.length).toBeGreaterThan(128);
    expect(validatePassword(pw).ok).toBe(false);
    expect(passwordSchema.safeParse(pw).success).toBe(false);
  });

  it("schema issues carry the stable criterion ids", () => {
    const res = passwordSchema.safeParse("aaaa"); // short, no upper/digit/symbol
    expect(res.success).toBe(false);
    if (!res.success) {
      const messages = res.error.issues.map((i) => i.message);
      expect(messages).toContain("length");
    }
  });
});

describe("scorePassword", () => {
  it("is monotonic: weak < fair/good < strong", () => {
    const weak = scorePassword("aaaaaaaaaaaaaa"); // 14 chars, lower only
    const good = scorePassword("Aa1!aaaaaaaaaa"); // 14 chars, all classes
    const strong = scorePassword("Aa1!aaaaaaaaaaAa1!aaaaaa"); // 24 chars, all classes
    expect(weak.score).toBeLessThan(good.score);
    expect(good.score).toBeLessThan(strong.score);
  });

  it("scores 14-char all-class as good (3) and 24-char as strong (4)", () => {
    expect(scorePassword("Aa1!aaaaaaaaaa").score).toBe(3);
    expect(scorePassword("Aa1!aaaaaaaaaaAa1!aaaaaa").score).toBe(4);
  });

  it("never scores above fair (2) while a criterion is missing", () => {
    // 30 chars, both length bonuses, but no symbol.
    const res = scorePassword("Aa1aaaaaaaaaaaaaaaaaaaaaaaaaaa");
    expect(res.missing).toEqual(["symbol"]);
    expect(res.score).toBeLessThanOrEqual(2);
  });

  it("returns the unmet criteria ids in stable order", () => {
    expect(scorePassword("").missing).toEqual([
      "length",
      "lower",
      "upper",
      "digit",
      "symbol",
    ]);
    expect(scorePassword("AAAAAAAAAAAAAA").missing).toEqual([
      "lower",
      "digit",
      "symbol",
    ]);
    expect(missingCriteria("Aa1!aaaaaaaaaa")).toEqual([]);
  });
});
