/*
 * O tratamento da foto em pixels: paleta, troca de cor, retícula e PNG com DPI
 * (rodar: npx tsx --test src/lib/banner-editor/tratamento-da-foto.test.ts).
 */
import assert from "node:assert/strict";
import { test } from "node:test";
import { MEIO_TOM_PADRAO, labDoRgb, paletaDosPixels, pngComDpi, pontilharPixels, reticulaEmSvg, rgbDoLab, tratamentoLimpo, trocarCoresNosPixels, type MeioTom } from "./tratamento-da-foto";

/* Uma imagem w × h pintada por `cor(x, y)` (RGBA). */
function imagem(w: number, h: number, cor: (x: number, y: number) => [number, number, number, number]) {
  const d = new Uint8ClampedArray(w * h * 4);
  for (let y = 0; y < h; y += 1) for (let x = 0; x < w; x += 1) d.set(cor(x, y), (y * w + x) * 4);
  return d;
}
const pixel = (d: Uint8ClampedArray, w: number, x: number, y: number) => Array.from(d.slice((y * w + x) * 4, (y * w + x) * 4 + 4));

test("o Lab vai e volta na mesma cor", () => {
  const cores: Array<[number, number, number]> = [[0, 0, 0], [255, 255, 255], [200, 30, 60], [12, 180, 240]];
  for (const c of cores) {
    const volta = rgbDoLab(...labDoRgb(...c)).map(Math.round);
    volta.forEach((v, i) => assert.ok(Math.abs(v - c[i]) <= 1, `${c} → ${volta}`));
  }
});

test("a paleta acha as cores de um elemento chapado, da que mais aparece para a que menos", () => {
  /* Metade vermelha, um terço azul, o resto amarelo; uma faixa transparente. */
  const d = imagem(120, 100, (x, y) => (y < 10 ? [0, 0, 0, 0] : x < 60 ? [220, 30, 40, 255] : x < 100 ? [30, 60, 200, 255] : [250, 210, 20, 255]));
  const p = paletaDosPixels(d);
  assert.equal(p.cores.length, 3);
  assert.equal(p.cores[0], "#DC1E28");
  assert.ok(p.cobertura > 0.95, `cobertura ${p.cobertura}`);
  assert.equal(p.transparente, true);
});

test("a foto com degradê tem cobertura baixa (não é elemento chapado)", () => {
  const d = imagem(200, 200, (x, y) => [x, y, (x * y) % 256, 255]);
  assert.ok(paletaDosPixels(d).cobertura < 0.8);
});

test("substituir cor troca só a cor escolhida e mantém a luz", () => {
  /* Vermelho claro e vermelho escuro (sombra) e um azul que não pode mudar. */
  const d = imagem(3, 1, (x) => (x === 0 ? [220, 40, 40, 255] : x === 1 ? [120, 20, 20, 255] : [30, 60, 200, 255]));
  trocarCoresNosPixels(d, [{ de: "#DC2828", para: "#22AA44", tolerancia: 60, suavidade: 30, manterLuz: true }]);
  const [claro, escuro, azul] = [pixel(d, 3, 0, 0), pixel(d, 3, 1, 0), pixel(d, 3, 2, 0)];
  assert.ok(claro[1] > claro[0], `virou verde: ${claro}`);
  assert.ok(escuro[1] > escuro[0] && escuro[1] < claro[1], `a sombra continua mais escura: ${escuro} x ${claro}`);
  assert.deepEqual(azul, [30, 60, 200, 255]);
});

test("substituir cor chapado pinta exatamente a cor nova; tolerância zero só pega a cor exata", () => {
  const d = imagem(2, 1, (x) => (x === 0 ? [220, 40, 40, 255] : [200, 50, 50, 255]));
  trocarCoresNosPixels(d, [{ de: "#DC2828", para: "#22AA44", tolerancia: 0, suavidade: 0, manterLuz: false }]);
  assert.deepEqual(pixel(d, 2, 0, 0), [34, 170, 68, 255]);
  assert.deepEqual(pixel(d, 2, 1, 0), [200, 50, 50, 255]);
});

test("a retícula em pontos: mais tinta no escuro, nada no branco, SVG no tamanho da foto", () => {
  /* Esquerda preta, direita branca. */
  const d = imagem(100, 100, (x) => (x < 50 ? [0, 0, 0, 255] : [255, 255, 255, 255]));
  const m: MeioTom = { ...MEIO_TOM_PADRAO, celulas: 20, angulo: 0 };
  const svg = reticulaEmSvg(d, 100, 100, m, { w: 1000, h: 1000 });
  assert.match(svg, /^<svg[^>]+width="1000" height="1000" viewBox="0 0 100 100"/);
  const pontos = [...svg.matchAll(/M(-?[\d.]+) (-?[\d.]+)a([\d.]+)/g)].map((p) => ({ x: Number(p[1]) + Number(p[3]), r: Number(p[3]) }));
  assert.ok(pontos.length > 0);
  assert.ok(pontos.every((p) => p.x < 52), "só tem ponto do lado preto");
  assert.ok(pontos.some((p) => p.r > 2.4), "no preto o ponto cresce até cobrir");
});

test("todos os estilos em vetor saem com desenho; inverter põe os pontos no claro", () => {
  const d = imagem(60, 60, (x) => [x * 4, x * 4, x * 4, 255]);
  for (const estilo of ["pontos", "quadrados", "losangos", "elipses", "linhas", "cruzes"] as const) {
    const svg = reticulaEmSvg(d, 60, 60, { ...MEIO_TOM_PADRAO, estilo, celulas: 12 });
    assert.match(svg, /<path fill="#111111" d="M/, estilo);
  }
  const metade = imagem(60, 60, (x) => (x < 30 ? [0, 0, 0, 255] : [255, 255, 255, 255]));
  const xs = (svg: string) => [...svg.matchAll(/M(-?[\d.]+) (-?[\d.]+)a([\d.]+)/g)].map((p) => Number(p[1]) + Number(p[3]));
  const normal = xs(reticulaEmSvg(metade, 60, 60, { ...MEIO_TOM_PADRAO, celulas: 12, angulo: 0 }));
  const invertida = xs(reticulaEmSvg(metade, 60, 60, { ...MEIO_TOM_PADRAO, celulas: 12, angulo: 0, inverter: true }));
  assert.ok(normal.length && normal.every((x) => x < 32), "normal: pontos no preto");
  assert.ok(invertida.length && invertida.every((x) => x > 28), "invertida: pontos no branco");
});

test("CMYK sai em quatro tintas multiplicadas; papel e transparente respeitados", () => {
  const d = imagem(40, 40, (x, y) => (y < 20 ? [200, 40, 120, 255] : [0, 0, 0, 0]));
  const svg = reticulaEmSvg(d, 40, 40, { ...MEIO_TOM_PADRAO, cor: "cmyk", celulas: 10, fundo: "#FFFFFF" });
  assert.match(svg, /<rect width="40" height="40" fill="#FFFFFF"\/>/);
  assert.match(svg, /fill="#EC008C" style="mix-blend-mode:multiply"/);
  /* Nada de ponto na metade transparente. */
  const ys = [...svg.matchAll(/M(-?[\d.]+) (-?[\d.]+)a/g)].map((p) => Number(p[2]));
  assert.ok(ys.every((y) => y < 24), `${Math.max(...ys)}`);
});

test("o pontilhado é só tinta ou papel, com a média de tinta certa", () => {
  const w = 64;
  const d = imagem(w, w, () => [128, 128, 128, 255]);
  for (const estilo of ["pontilhado", "ordenado"] as const) {
    const s = pontilharPixels(d, w, w, { ...MEIO_TOM_PADRAO, estilo, tinta: "#000000", fundo: "#FFFFFF" });
    let tinta = 0;
    for (let i = 0; i < w * w; i += 1) {
      const v = s[i * 4];
      assert.ok(v === 0 || v === 255, `${estilo}: só preto ou branco`);
      if (v === 0) tinta += 1;
    }
    assert.ok(Math.abs(tinta / (w * w) - 0.5) < 0.06, `${estilo}: metade de tinta (${tinta / (w * w)})`);
  }
});

test("o PNG ganha o DPI (pHYs) logo depois do IHDR", () => {
  /* Um PNG mínimo de 1 × 1 (IHDR, IDAT, IEND). */
  const png = Uint8Array.from(Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", "base64"));
  const com = pngComDpi(png, 300);
  assert.equal(String.fromCharCode(...com.slice(37, 41)), "pHYs");
  const porMetro = new DataView(com.buffer, com.byteOffset).getUint32(41);
  assert.equal(porMetro, Math.round(300 / 0.0254));
  assert.equal(com.length, png.length + 21);
  assert.equal(pngComDpi(com, 600).length, com.length, "troca o pHYs que já tinha");
});

test("a limpeza só aceita endereço válido e campos conhecidos", () => {
  const valido = (s: string) => s.startsWith("https://x.supabase.co/");
  assert.equal(tratamentoLimpo({ de: "https://outro.com/a.png", trocas: [{ de: "#000000", para: "#FFFFFF" }] }, valido), null);
  assert.equal(tratamentoLimpo({ de: "https://x.supabase.co/a.png" }, valido), null, "sem nada feito, não guarda");
  const t = tratamentoLimpo({ de: "https://x.supabase.co/a.png", feito: "javascript:1", trocas: [{ de: "#ff0000", para: "#00ff00", tolerancia: 500 }], meioTom: { estilo: "nada", celulas: 9999 } }, valido);
  assert.deepEqual(t, { de: "https://x.supabase.co/a.png", trocas: [{ de: "#FF0000", para: "#00FF00", tolerancia: 100, suavidade: 40, manterLuz: true }], meioTom: { ...MEIO_TOM_PADRAO, celulas: 400 } });
});
