/**
 * Tests for `src/lib/water-log-pin.ts` — PIN hashing/verification used
 * by the field portal. The hashing layer is critical: a 4-digit PIN is
 * essentially a 10,000-entry dictionary, so a slow KDF and a per-row
 * random salt are the only defense-in-depth that matters if the
 * database leaks.
 *
 * The `server-only` import is stubbed so the test runner can import
 * the module (the pattern matches `tests/unit/passwords.test.ts`).
 */
import { describe, it, expect, vi } from "vitest";

vi.mock("server-only", () => ({}));

import {
  hashPin,
  verifyPin,
  validatePin,
  isWeakPin,
  generatePin,
  PIN_MIN,
  PIN_MAX,
} from "@/lib/water-log-pin";

describe("validatePin", () => {
  it("accepts a 4-digit numeric PIN", () => {
    expect(validatePin("1234")).toBeNull();
  });

  it("accepts the full 8-digit range", () => {
    expect(validatePin("12345678")).toBeNull();
  });

  it("rejects pins shorter than PIN_MIN", () => {
    expect(validatePin("12")).toMatch(/4.*8/);
  });

  it("rejects pins longer than PIN_MAX", () => {
    expect(validatePin("123456789")).toMatch(/4.*8/);
  });

  it("rejects non-numeric input", () => {
    expect(validatePin("123a")).toBe("PIN must be numeric");
  });

  it("rejects non-string types", () => {
    expect(validatePin(1234)).toBe("PIN must be a string");
    expect(validatePin(null)).toBe("PIN must be a string");
    expect(validatePin(undefined)).toBe("PIN must be a string");
  });

  it("accepts leading zeros", () => {
    expect(validatePin("0007")).toBeNull();
  });
});

describe("isWeakPin", () => {
  it("flags repeated digits", () => {
    expect(isWeakPin("1111")).toBe(true);
    expect(isWeakPin("0000")).toBe(true);
  });

  it("flags monotonic runs", () => {
    expect(isWeakPin("1234")).toBe(true);
    expect(isWeakPin("4321")).toBe(true);
  });

  it("flags palindromes", () => {
    expect(isWeakPin("1221")).toBe(true);
    expect(isWeakPin("3443")).toBe(true);
  });

  it("does not flag random-looking pins", () => {
    expect(isWeakPin("4739")).toBe(false);
    expect(isWeakPin("8201")).toBe(false);
  });
});

describe("hashPin / verifyPin", () => {
  it("round-trips a 4-digit PIN", () => {
    const h = hashPin("4739");
    expect(verifyPin("4739", h)).toBe(true);
    expect(verifyPin("4740", h)).toBe(false);
  });

  it("uses a random salt — same PIN produces different hashes", () => {
    const a = hashPin("4739");
    const b = hashPin("4739");
    expect(a).not.toBe(b);
    expect(verifyPin("4739", a)).toBe(true);
    expect(verifyPin("4739", b)).toBe(true);
  });

  it("uses a self-describing format (scrypt$N$r$p$salt$hash)", () => {
    const h = hashPin("4739");
    const parts = h.split("$");
    expect(parts[0]).toBe("scrypt");
    expect(Number.parseInt(parts[1], 10)).toBeGreaterThan(0);
    expect(Number.parseInt(parts[2], 10)).toBeGreaterThan(0);
    expect(Number.parseInt(parts[3], 10)).toBeGreaterThan(0);
    // Salt is 16 bytes base64 — length depends on padding but always > 16
    expect(parts[4].length).toBeGreaterThanOrEqual(16);
    // Hash is 32 bytes base64
    expect(parts[5].length).toBeGreaterThanOrEqual(40);
  });

  it("returns false (no exception) for tampered hashes", () => {
    expect(verifyPin("4739", "not-a-real-hash")).toBe(false);
    expect(verifyPin("4739", "scrypt$1$1$1$xx$yy")).toBe(false);
    expect(verifyPin("4739", "")).toBe(false);
  });

  it("returns false for the wrong PIN (without revealing why)", () => {
    const h = hashPin("4739");
    expect(verifyPin("0000", h)).toBe(false);
    expect(verifyPin("9999", h)).toBe(false);
  });

  it("rejects the empty string (no bypass for missing PIN)", () => {
    const h = hashPin("4739");
    expect(verifyPin("", h)).toBe(false);
  });
});

describe("generatePin", () => {
  it("returns a 4-digit string", () => {
    const pin = generatePin();
    expect(pin).toMatch(/^\d{4}$/);
  });

  it("avoids obvious weak patterns (no 1111, 1234, etc.)", () => {
    // 1000 attempts — should never see a weak pin if the generator works.
    for (let i = 0; i < 1000; i++) {
      const pin = generatePin();
      expect(isWeakPin(pin)).toBe(false);
    }
  });

  it("produces variety across many calls (statistical)", () => {
    const seen = new Set<string>();
    for (let i = 0; i < 200; i++) {
      seen.add(generatePin());
    }
    // With 10k possible 4-digit pins, we'd expect to see well over 100
    // distinct values in 200 draws. This is a sanity check, not a hard
    // guarantee — if it ever flakes, increase the iteration count.
    expect(seen.size).toBeGreaterThan(80);
  });
});

describe("PIN length constants", () => {
  it("PINs are 4–8 digits", () => {
    expect(PIN_MIN).toBe(4);
    expect(PIN_MAX).toBe(8);
  });
});
