import { describe, expect, test } from "bun:test";
import {
  CAPTCHA_CONCRETE_IMAGE_STYLES,
  CAPTCHA_IMAGE_STYLES,
  normalizeCaptchaImageStyleList,
  resolveCaptchaImageStyle,
} from "../src/challenges/styles.ts";
import { renderTextChallenge } from "../src/challenges/visual.ts";
import type { CaptchaConcreteImageStyle, CaptchaGeneratorContext } from "../src/types.ts";

function random(seedValue = 17) {
  let seed = seedValue >>> 0;
  const next = () => {
    seed = (seed * 1664525 + 1013904223) >>> 0;
    return seed;
  };
  return {
    int(min: number, max: number) {
      return min + (next() % (max - min + 1));
    },
    float() {
      return next() / 0xffff_ffff;
    },
  };
}

function context(style: CaptchaConcreteImageStyle): CaptchaGeneratorContext {
  const source = random(CAPTCHA_CONCRETE_IMAGE_STYLES.indexOf(style) + 101);
  return {
    difficulty: "normal",
    disturbance: 50,
    imageStyle: style,
    requestedImageStyle: style,
    imageStylePool: [style],
    locale: "en",
    length: 6,
    caseSensitive: false,
    minCompletionMs: 800,
    randomInt: source.int,
    randomFloat: source.float,
    randomId: (bytes = 18) => `${style}-${bytes}`,
  };
}

function decodeDataUri(uri: string): Uint8Array {
  const encoded = uri.slice(uri.indexOf(",") + 1);
  return Uint8Array.from(atob(encoded), (character) => character.charCodeAt(0));
}

describe("CAPTCHA image renderer styles", () => {
  test("publishes the complete renderer catalog", () => {
    expect(CAPTCHA_IMAGE_STYLES).toContain("random");
    expect(CAPTCHA_CONCRETE_IMAGE_STYLES).toHaveLength(18);
    expect(CAPTCHA_CONCRETE_IMAGE_STYLES).toEqual([
      "classic",
      "collision",
      "snow",
      "corrosion",
      "spiderweb",
      "cross-shadow",
      "split",
      "split2",
      "cut",
      "darts",
      "distortion",
      "stitch",
      "striped",
      "wave",
      "grid-noise",
      "scribble",
      "pixel",
      "broken-lines",
    ]);
  });

  test("renders a valid PNG for every concrete style", () => {
    const images = new Set<string>();
    for (const style of CAPTCHA_CONCRETE_IMAGE_STYLES) {
      const image = renderTextChallenge("A7K93P", context(style));
      expect(image.startsWith("data:image/png;base64,")).toBe(true);
      const bytes = decodeDataUri(image);
      expect([...bytes.slice(0, 8)]).toEqual([137, 80, 78, 71, 13, 10, 26, 10]);
      expect(bytes.byteLength).toBeGreaterThan(1_000);
      images.add(image);
    }
    expect(images.size).toBe(CAPTCHA_CONCRETE_IMAGE_STYLES.length);
  });

  test("normalizes comma-separated renderer pools", () => {
    expect(normalizeCaptchaImageStyleList("snow, wave, snow", "allowedStyles")).toEqual([
      "snow",
      "wave",
    ]);
    expect(() => normalizeCaptchaImageStyleList("snow,unknown", "allowedStyles")).toThrow(
      "allowedStyles contains an unknown image style",
    );
  });

  test("selects random styles only from the active pool", () => {
    const resolved = resolveCaptchaImageStyle({
      imageStyle: "random",
      allowedStyles: ["classic", "snow", "wave"],
      excludedStyles: ["snow"],
      randomInt: () => 1,
    });
    expect(resolved.pool).toEqual(["classic", "wave"]);
    expect(resolved.resolved).toBe("wave");
  });
});
