import { describe, it, expect, expectTypeOf } from "vitest"
import {
  generateTotpSecret,
  buildOtpAuthUrl,
  verifyTotpCode,
  generateBackupCodes,
  hashBackupCodes,
  verifyBackupCode,
  consumeBackupCode,
  countRemainingBackupCodes,
} from "./totp"

describe("totp - generateTotpSecret", () => {
  it("returns a non-empty base32 string", () => {
    const secret = generateTotpSecret()
    expect(typeof secret).toBe("string")
    expect(secret.length).toBeGreaterThan(0)
  })

  it("generates different secrets on each call", () => {
    const s1 = generateTotpSecret()
    const s2 = generateTotpSecret()
    expect(s1).not.toBe(s2)
  })

  it("only contains base32 characters (A-Z, 2-7)", () => {
    const secret = generateTotpSecret()
    expect(secret).toMatch(/^[A-Z2-7]+$/)
  })
})

describe("totp - buildOtpAuthUrl", () => {
  it("builds a valid otpauth:// URL for TOTP", () => {
    const secret = "JBSWY3DPEHPK3PXP"
    const url = buildOtpAuthUrl({
      serviceName: "Next_Edu",
      accountName: "user@example.com",
      secret,
    })
    expect(url).toContain("otpauth://totp/")
    expect(url).toContain("issuer=Next_Edu")
    expect(url).toContain(`secret=${secret}`)
  })

  it("URL-encodes the issuer and label", () => {
    const url = buildOtpAuthUrl({
      serviceName: "My Service",
      accountName: "user@example.com",
      secret: "JBSWY3DPEHPK3PXP",
    })
    // Spaces in issuer should be encoded
    expect(url).toContain("My%20Service")
  })
})

describe("totp - verifyTotpCode", () => {
  it("returns false for an invalid token", () => {
    const secret = generateTotpSecret()
    expect(verifyTotpCode("000000", secret)).toBe(false)
  })

  it("returns false for empty token", () => {
    const secret = generateTotpSecret()
    expect(verifyTotpCode("", secret)).toBe(false)
  })

  it("returns false for malformed token", () => {
    const secret = generateTotpSecret()
    expect(verifyTotpCode("not-a-code", secret)).toBe(false)
  })

  it("handles whitespace in token", () => {
    const secret = generateTotpSecret()
    // Should not throw even with whitespace
    expect(() => verifyTotpCode("  123 456  ", secret)).not.toThrow()
  })
})

describe("totp - generateBackupCodes", () => {
  it("generates 10 backup codes by default", () => {
    const codes = generateBackupCodes()
    expect(codes).toHaveLength(10)
  })

  it("each code is 8 characters long", () => {
    const codes = generateBackupCodes()
    for (const code of codes) {
      expect(code).toHaveLength(8)
    }
  })

  it("codes only contain safe alphabet characters (no 0/O/1/I)", () => {
    const codes = generateBackupCodes()
    const safeAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
    for (const code of codes) {
      for (const ch of code) {
        expect(safeAlphabet).toContain(ch)
      }
    }
  })

  it("generates different codes on each call", () => {
    const c1 = generateBackupCodes()
    const c2 = generateBackupCodes()
    // Extremely unlikely all 10 codes match
    expect(c1).not.toEqual(c2)
  })
})

describe("totp - hashBackupCodes & verifyBackupCode", () => {
  it("hashes codes and verifies a correct code", async () => {
    const codes = ["ABCDEFGH", "JKLMNOPQ"]
    const hashed = await hashBackupCodes(codes)
    expect(typeof hashed).toBe("string")
    const parsed = JSON.parse(hashed) as unknown
    expect(Array.isArray(parsed)).toBe(true)
    expect(parsed).toHaveLength(2)

    const idx = await verifyBackupCode("ABCDEFGH", hashed)
    expect(idx).toBe(0)
  })

  it("returns -1 for incorrect code", async () => {
    const codes = ["ABCDEFGH"]
    const hashed = await hashBackupCodes(codes)
    const idx = await verifyBackupCode("WRONGCODE", hashed)
    expect(idx).toBe(-1)
  })

  it("is case-insensitive (normalizes to uppercase)", async () => {
    const codes = ["ABCDEFGH"]
    const hashed = await hashBackupCodes(codes)
    const idx = await verifyBackupCode("abcdefgh", hashed)
    expect(idx).toBe(0)
  })

  it("trims whitespace before verifying", async () => {
    const codes = ["ABCDEFGH"]
    const hashed = await hashBackupCodes(codes)
    const idx = await verifyBackupCode("  ABCDEFGH  ", hashed)
    expect(idx).toBe(0)
  })

  it("returns -1 for invalid JSON storage", async () => {
    const idx = await verifyBackupCode("ABCDEFGH", "not-json")
    expect(idx).toBe(-1)
  })

  it("returns -1 for non-array JSON", async () => {
    const idx = await verifyBackupCode("ABCDEFGH", '{"valid": false}')
    expect(idx).toBe(-1)
  })

  it("returns correct index for second code in list", async () => {
    const codes = ["FIRSTCD", "SECONDAB", "THIRDXYZ"]
    const hashed = await hashBackupCodes(codes)
    const idx = await verifyBackupCode("SECONDAB", hashed)
    expect(idx).toBe(1)
  })
})

describe("totp - consumeBackupCode", () => {
  it("removes the used code from the list", async () => {
    const codes = ["AAAA1111", "BBBB2222", "CCCC3333"]
    const hashed = await hashBackupCodes(codes)
    const next = await consumeBackupCode(hashed, 1)
    const remaining = JSON.parse(next) as unknown[]
    expect(remaining).toHaveLength(2)
  })

  it("returns original JSON for invalid index (-1)", async () => {
    const hashed = await hashBackupCodes(["AAAA1111"])
    const next = await consumeBackupCode(hashed, -1)
    expect(next).toBe(hashed)
  })

  it("returns original JSON for out-of-bounds index", async () => {
    const hashed = await hashBackupCodes(["AAAA1111"])
    const next = await consumeBackupCode(hashed, 99)
    expect(next).toBe(hashed)
  })

  it("returns original JSON for invalid JSON input", async () => {
    const next = await consumeBackupCode("not-json", 0)
    expect(next).toBe("not-json")
  })
})

describe("totp - countRemainingBackupCodes", () => {
  it("counts codes in a valid array", async () => {
    const hashed = await hashBackupCodes(["A", "B", "C"])
    expect(countRemainingBackupCodes(hashed)).toBe(3)
  })

  it("returns 0 for empty array", () => {
    expect(countRemainingBackupCodes("[]")).toBe(0)
  })

  it("returns 0 for invalid JSON", () => {
    expect(countRemainingBackupCodes("not-json")).toBe(0)
  })

  it("returns 0 for non-array JSON", () => {
    expect(countRemainingBackupCodes('{"a":1}')).toBe(0)
  })
})

describe("totp - type safety", () => {
  it("verifyTotpCode returns boolean", () => {
    const secret = generateTotpSecret()
    const result = verifyTotpCode("123456", secret)
    expectTypeOf(result).toBeBoolean()
  })
})
