import * as OTPAuth from "otpauth";
import { describe, expect, it } from "vitest";
import {
  createTotp,
  hashRecoveryCodes,
  isMfaRequiredForUser,
  verifyRecoveryCode,
  verifyTotpCode,
} from "../../../apps/api/src/plugins/mfa.js";

describe("MFA", () => {
  describe("createTotp", () => {
    it("generates a valid TOTP URI", () => {
      const totp = createTotp("testuser");
      const uri = totp.toString();
      expect(uri).toContain("otpauth://totp/");
      expect(uri).toContain("SnapOtter");
      expect(uri).toContain("testuser");
      expect(uri).toContain("algorithm=SHA1");
      expect(uri).toContain("digits=6");
      expect(uri).toContain("period=30");
    });

    it("generates a TOTP with a valid secret", () => {
      const totp = createTotp("testuser");
      expect(totp.secret.base32).toMatch(/^[A-Z2-7]+=*$/);
      // 20-byte secret = 32 base32 chars
      expect(totp.secret.base32.length).toBe(32);
    });

    it("uses a provided secret when given", () => {
      const knownSecret = "JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP";
      const totp = createTotp("testuser", knownSecret);
      expect(totp.secret.base32).toBe(knownSecret);
    });

    it("generates unique secrets across calls", () => {
      const a = createTotp("user1");
      const b = createTotp("user2");
      expect(a.secret.base32).not.toBe(b.secret.base32);
    });
  });

  describe("verifyTotpCode", () => {
    it("verifies a correct TOTP code", () => {
      const totp = createTotp("testuser");
      const secret = totp.secret.base32;
      const code = totp.generate();
      expect(verifyTotpCode(secret, code)).toBe(true);
    });

    it("rejects an incorrect TOTP code", () => {
      const totp = createTotp("testuser");
      const secret = totp.secret.base32;
      expect(verifyTotpCode(secret, "000000")).toBe(false);
    });

    it("rejects an empty code", () => {
      const totp = createTotp("testuser");
      const secret = totp.secret.base32;
      expect(verifyTotpCode(secret, "")).toBe(false);
    });

    it("rejects a code from a different secret", () => {
      const totp1 = createTotp("user1");
      const totp2 = createTotp("user2");
      const code = totp1.generate();
      expect(verifyTotpCode(totp2.secret.base32, code)).toBe(false);
    });

    it("accepts codes within the 1-step window", () => {
      const secret = new OTPAuth.Secret({ size: 20 });
      const totp = new OTPAuth.TOTP({
        issuer: "SnapOtter",
        label: "test",
        algorithm: "SHA1",
        digits: 6,
        period: 30,
        secret,
      });

      // Generate code for the current period
      const code = totp.generate();
      expect(verifyTotpCode(secret.base32, code)).toBe(true);
    });
  });

  describe("hashRecoveryCodes", () => {
    it("hashes recovery codes into comma-separated SHA-256 hashes", () => {
      const codes = ["abcd1234", "efgh5678"];
      const result = hashRecoveryCodes(codes);
      const parts = result.split(",");
      expect(parts).toHaveLength(2);
      // Each hash should be 64 hex chars (256 bits)
      for (const hash of parts) {
        expect(hash).toMatch(/^[0-9a-f]{64}$/);
      }
    });

    it("produces deterministic hashes", () => {
      const codes = ["code1", "code2", "code3"];
      const a = hashRecoveryCodes(codes);
      const b = hashRecoveryCodes(codes);
      expect(a).toBe(b);
    });

    it("handles a single code", () => {
      const result = hashRecoveryCodes(["onlycode"]);
      expect(result).not.toContain(",");
      expect(result).toMatch(/^[0-9a-f]{64}$/);
    });
  });

  describe("verifyRecoveryCode", () => {
    it("verifies a valid recovery code", () => {
      const codes = ["aaaa1111", "bbbb2222", "cccc3333"];
      const hashList = hashRecoveryCodes(codes);
      const result = verifyRecoveryCode("bbbb2222", hashList);
      expect(result.valid).toBe(true);
    });

    it("rejects an invalid recovery code", () => {
      const codes = ["aaaa1111", "bbbb2222"];
      const hashList = hashRecoveryCodes(codes);
      const result = verifyRecoveryCode("invalid0", hashList);
      expect(result.valid).toBe(false);
      expect(result.remaining).toBe(hashList);
    });

    it("consumes a recovery code on use", () => {
      const codes = ["aaaa1111", "bbbb2222", "cccc3333"];
      const hashList = hashRecoveryCodes(codes);
      const result = verifyRecoveryCode("bbbb2222", hashList);
      expect(result.valid).toBe(true);
      // Remaining should have 2 hashes
      const remaining = result.remaining.split(",");
      expect(remaining).toHaveLength(2);
      // The used code should no longer verify
      const secondTry = verifyRecoveryCode("bbbb2222", result.remaining);
      expect(secondTry.valid).toBe(false);
    });

    it("returns empty remaining when last code is used", () => {
      const codes = ["onlycode"];
      const hashList = hashRecoveryCodes(codes);
      const result = verifyRecoveryCode("onlycode", hashList);
      expect(result.valid).toBe(true);
      expect(result.remaining).toBe("");
    });

    it("preserves other codes when one is consumed", () => {
      const codes = ["first000", "second00", "third000"];
      const hashList = hashRecoveryCodes(codes);

      // Use the first code
      const r1 = verifyRecoveryCode("first000", hashList);
      expect(r1.valid).toBe(true);

      // Second and third should still work
      const r2 = verifyRecoveryCode("second00", r1.remaining);
      expect(r2.valid).toBe(true);

      const r3 = verifyRecoveryCode("third000", r2.remaining);
      expect(r3.valid).toBe(true);
      expect(r3.remaining).toBe("");
    });
  });

  describe("isMfaRequiredForUser", () => {
    it("returns false for optional policy", () => {
      expect(isMfaRequiredForUser("optional", "admin")).toBe(false);
      expect(isMfaRequiredForUser("optional", "editor")).toBe(false);
      expect(isMfaRequiredForUser("optional", "user")).toBe(false);
    });

    it("returns true for required policy regardless of role", () => {
      expect(isMfaRequiredForUser("required", "admin")).toBe(true);
      expect(isMfaRequiredForUser("required", "editor")).toBe(true);
      expect(isMfaRequiredForUser("required", "user")).toBe(true);
    });

    it("returns true for admins_only policy when role is admin", () => {
      expect(isMfaRequiredForUser("admins_only", "admin")).toBe(true);
    });

    it("returns false for admins_only policy when role is not admin", () => {
      expect(isMfaRequiredForUser("admins_only", "editor")).toBe(false);
      expect(isMfaRequiredForUser("admins_only", "user")).toBe(false);
    });
  });
});
