import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { uuid } from "./uuid.js";

// RFC 4122 v4 UUID pattern
const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

describe("uuid", () => {
  // ---------------------------------------------------------------------------
  // Shape and format
  // ---------------------------------------------------------------------------

  it("returns a string", () => {
    expect(typeof uuid()).toBe("string");
  });

  it("matches the RFC 4122 v4 UUID format", () => {
    expect(uuid()).toMatch(UUID_V4_REGEX);
  });

  it("has the correct length (36 characters)", () => {
    expect(uuid()).toHaveLength(36);
  });

  it("contains hyphens at positions 8, 13, 18, and 23", () => {
    const id = uuid();
    expect(id[8]).toBe("-");
    expect(id[13]).toBe("-");
    expect(id[18]).toBe("-");
    expect(id[23]).toBe("-");
  });

  it("has '4' as the version digit at position 14", () => {
    expect(uuid()[14]).toBe("4");
  });

  it("has a valid variant digit at position 19 (8, 9, a, or b)", () => {
    const variantChar = uuid()[19]!;
    expect(["8", "9", "a", "b"]).toContain(variantChar.toLowerCase());
  });

  // ---------------------------------------------------------------------------
  // Uniqueness
  // ---------------------------------------------------------------------------

  it("generates unique values on successive calls", () => {
    const ids = new Set(Array.from({ length: 1000 }, () => uuid()));
    expect(ids.size).toBe(1000);
  });

  // ---------------------------------------------------------------------------
  // Fallback path (crypto.randomUUID not available)
  // ---------------------------------------------------------------------------

  describe("fallback when crypto.randomUUID is unavailable", () => {
    let originalCrypto: Crypto;

    beforeEach(() => {
      originalCrypto = globalThis.crypto;
    });

    afterEach(() => {
      Object.defineProperty(globalThis, "crypto", {
        value: originalCrypto,
        configurable: true,
        writable: true,
      });
    });

    it("falls back to Math.random-based generation when randomUUID is missing", () => {
      // Stub crypto so that randomUUID is absent
      Object.defineProperty(globalThis, "crypto", {
        value: {},
        configurable: true,
        writable: true,
      });

      const id = uuid();
      expect(id).toMatch(UUID_V4_REGEX);
    });

    it("fallback still produces unique values", () => {
      Object.defineProperty(globalThis, "crypto", {
        value: {},
        configurable: true,
        writable: true,
      });

      const ids = new Set(Array.from({ length: 500 }, () => uuid()));
      expect(ids.size).toBe(500);
    });

    it("falls back when crypto is undefined", () => {
      Object.defineProperty(globalThis, "crypto", {
        value: undefined,
        configurable: true,
        writable: true,
      });

      const id = uuid();
      expect(id).toMatch(UUID_V4_REGEX);
    });
  });

  // ---------------------------------------------------------------------------
  // Native path (crypto.randomUUID explicitly available)
  // ---------------------------------------------------------------------------

  describe("native crypto.randomUUID path", () => {
    it("delegates to crypto.randomUUID when it is available", () => {
      const spy = vi.spyOn(globalThis.crypto, "randomUUID");

      uuid();

      expect(spy).toHaveBeenCalledOnce();
      spy.mockRestore();
    });
  });
});
