import { test } from "node:test";
import assert from "node:assert/strict";
import http from "node:http";
import type { AddressInfo } from "node:net";
import zlib from "node:zlib";

import { tirarContatos } from "./contatos.ts";
import { lerShopify } from "./importacao/leitores/shopify.ts";
import { lerVariacoesDoWoo, lerWooCommerce, produtosComVariacaoNoWoo } from "./importacao/leitores/woocommerce.ts";
import { apelidoDaVtex, lerVtex, totalDaVtex } from "./importacao/leitores/vtex.ts";
import { lerPagina, produtosDaLista } from "./importacao/leitores/pagina.ts";
import { detectarPlataforma } from "./importacao/leitores/plataforma.ts";
import { lerSitemap, linksDeProduto, pareceProduto } from "./importacao/leitores/sitemap.ts";
import { chaveDeOrigem, nomeDoEndereco, precoDoDado, precoEmCentavos, precoEscrito, produtoVazio, semEntidades, textoCorrido } from "./importacao/modelo.ts";
import { MAXIMO_DE_FOTOS, limparDescricao, montarProduto, resumoDasVariacoes } from "./importacao/montar.ts";
import { buscar, conferirEndereco, consultaSegura, ErroDeRede, ipProibido, permiteLocal, textoDaResposta } from "./importacao/rede.ts";
import { montarVariacoes } from "./importacao/variacoes.ts";

const LOJA = "https://loja.exemplo.com.br";
let contador = 0;
const novoId = () => `id-${++contador}`;

/* ---------------------------------------------------------------- rede */

test("IP de dentro é recusado; IP de fora passa", () => {
  for (const ip of ["127.0.0.1", "10.1.2.3", "172.20.0.1", "192.168.1.1", "169.254.169.254", "100.64.1.1", "0.0.0.0", "224.0.0.1", "255.255.255.255"]) {
    assert.equal(ipProibido(ip), true, ip);
  }
  for (const ip of ["::1", "fe80::1", "fd00::1", "::ffff:127.0.0.1", "::ffff:7f00:1", "2001:db8::1", "64:ff9b::7f00:1", "::", "ff02::1", "2002:7f00:1::1"]) {
    assert.equal(ipProibido(ip), true, ip);
  }
  for (const ip of ["8.8.8.8", "200.147.67.142", "2606:4700:4700::1111", "2804:14c:1::1"]) {
    assert.equal(ipProibido(ip), false, ip);
  }
  assert.equal(ipProibido("lixo"), true);
});

test("o modo local libera só o próprio computador", () => {
  assert.equal(ipProibido("127.0.0.1", true), false);
  assert.equal(ipProibido("::1", true), false);
  assert.equal(ipProibido("10.0.0.1", true), true);
  assert.equal(ipProibido("169.254.169.254", true), true);
});

test("a variável do modo local nunca vale em produção", () => {
  assert.equal(permiteLocal({ NODE_ENV: "production", ASSISTENTES_IMPORTACAO_LOCAL: "1" }), false);
  assert.equal(permiteLocal({ NODE_ENV: "development", ASSISTENTES_IMPORTACAO_LOCAL: "1" }), true);
  assert.equal(permiteLocal({ NODE_ENV: "development" }), false);
});

test("endereços que não podem ser buscados", () => {
  const motivo = (endereco: string, local = false) => {
    try {
      conferirEndereco(endereco, local);
      return "passou";
    } catch (erro) {
      return erro instanceof ErroDeRede ? erro.motivo : "outro";
    }
  };
  assert.equal(motivo("ftp://loja.com.br/x"), "endereco");
  assert.equal(motivo("https://usuario:senha@loja.com.br/"), "endereco");
  assert.equal(motivo("https://loja.com.br:8443/"), "endereco");
  assert.equal(motivo("http://localhost/"), "bloqueado");
  assert.equal(motivo("http://intranet/"), "bloqueado");
  assert.equal(motivo("http://servidor.local/"), "bloqueado");
  assert.equal(motivo("http://10.0.0.1/"), "bloqueado");
  assert.equal(motivo("http://2130706433/"), "bloqueado");
  assert.equal(motivo("http://[::1]/"), "bloqueado");
  assert.equal(motivo("nada"), "endereco");
  assert.equal(motivo("https://loja.exemplo.com.br/produto/x"), "passou");
  assert.equal(motivo("http://loja.exemplo.com.br:80/"), "passou");
  assert.equal(motivo("http://127.0.0.1:4000/", true), "passou");
  assert.equal(motivo("http://10.0.0.1/", true), "bloqueado");
});

test("a consulta de nome recusa nome que aponta para dentro", async () => {
  const resultado = await new Promise<string>((resolver) => {
    consultaSegura(false)("localhost", { all: true }, (erro) => resolver(erro?.code ?? "passou"));
  });
  assert.equal(resultado, "EKTALOGUBLOQUEADO");
  const local = await new Promise<string>((resolver) => {
    consultaSegura(true)("localhost", { all: true }, (erro, enderecos) => resolver(erro ? erro.code ?? "erro" : String(Array.isArray(enderecos) && enderecos.length)));
  });
  assert.notEqual(local, "EKTALOGUBLOQUEADO");
});

async function servidorDeTeste() {
  const servidor = http.createServer((pedido, resposta) => {
    const caminho = pedido.url ?? "/";
    if (caminho === "/latin1") {
      resposta.writeHead(200, { "content-type": "text/html; charset=iso-8859-1" });
      resposta.end(Buffer.from("<p>Promoção de verão</p>", "latin1"));
    } else if (caminho === "/gzip") {
      resposta.writeHead(200, { "content-type": "application/json", "content-encoding": "gzip" });
      resposta.end(zlib.gzipSync(JSON.stringify({ ok: true })));
    } else if (caminho === "/grande") {
      resposta.writeHead(200, { "content-type": "text/plain" });
      resposta.end(Buffer.alloc(2 * 1024 * 1024, 97));
    } else if (caminho === "/bomba") {
      resposta.writeHead(200, { "content-type": "text/plain", "content-encoding": "gzip" });
      resposta.end(zlib.gzipSync(Buffer.alloc(8 * 1024 * 1024, 0)));
    } else if (caminho === "/para-dentro") {
      resposta.writeHead(302, { location: "http://10.0.0.1/segredo" });
      resposta.end();
    } else if (caminho === "/para-metadados") {
      resposta.writeHead(301, { location: "http://169.254.169.254/latest/meta-data/" });
      resposta.end();
    } else if (caminho === "/volta") {
      resposta.writeHead(302, { location: "/latin1" });
      resposta.end();
    } else if (caminho === "/ciranda") {
      resposta.writeHead(302, { location: "/ciranda" });
      resposta.end();
    } else if (caminho === "/lento") {
      /* nunca responde */
    } else {
      resposta.writeHead(404);
      resposta.end();
    }
  });
  await new Promise<void>((resolver) => servidor.listen(0, "127.0.0.1", () => resolver()));
  const porta = (servidor.address() as AddressInfo).port;
  return { base: `http://127.0.0.1:${porta}`, fechar: () => new Promise<void>((resolver) => { servidor.closeAllConnections(); servidor.close(() => resolver()); }) };
}

const PEDIDO = { limite: 1024 * 1024, tempoMs: 3000, aceita: "*/*" };
async function motivoDa(promessa: Promise<unknown>) {
  try {
    await promessa;
    return "passou";
  } catch (erro) {
    return erro instanceof ErroDeRede ? erro.motivo : `outro: ${(erro as Error).message}`;
  }
}

test("buscar: codificação, compactação, redirecionamento e limites", async () => {
  const { base, fechar } = await servidorDeTeste();
  try {
    /* Sem o modo local, o próprio computador é recusado. */
    assert.equal(await motivoDa(buscar(`${base}/latin1`, PEDIDO, false)), "bloqueado");

    const latin = await buscar(`${base}/latin1`, PEDIDO, true);
    assert.equal(latin.status, 200);
    assert.equal(textoDaResposta(latin), "<p>Promoção de verão</p>");

    const compactada = await buscar(`${base}/gzip`, PEDIDO, true);
    assert.deepEqual(JSON.parse(compactada.corpo.toString("utf8")), { ok: true });

    const voltou = await buscar(`${base}/volta`, PEDIDO, true);
    assert.equal(voltou.endereco, `${base}/latin1`);

    assert.equal(await motivoDa(buscar(`${base}/grande`, PEDIDO, true)), "grande");
    /* O limite conta o que sai da descompactação: 8 MB de zeros cabem em poucos KB. */
    assert.equal(await motivoDa(buscar(`${base}/bomba`, PEDIDO, true)), "grande");
    assert.equal(await motivoDa(buscar(`${base}/para-dentro`, PEDIDO, true)), "bloqueado");
    assert.equal(await motivoDa(buscar(`${base}/para-metadados`, PEDIDO, true)), "bloqueado");
    assert.equal(await motivoDa(buscar(`${base}/ciranda`, PEDIDO, true)), "redirecionamentos");
    assert.equal(await motivoDa(buscar(`${base}/lento`, { ...PEDIDO, tempoMs: 400 }, true)), "tempo");

    const naoTem = await buscar(`${base}/nao-tem`, PEDIDO, true);
    assert.equal(naoTem.status, 404);
  } finally {
    await fechar();
  }
});

/* ---------------------------------------------------------------- preços e textos */

test("preços escritos de todo jeito", () => {
  assert.equal(precoDoDado("59.90"), 59.9);
  assert.equal(precoDoDado(59.9), 59.9);
  assert.equal(precoDoDado("59,90"), 59.9);
  assert.equal(precoDoDado("1234.5"), 1234.5);
  assert.equal(precoDoDado("abc"), null);
  assert.equal(precoDoDado(-1), null);
  assert.equal(precoEscrito("R$ 1.234,56"), 1234.56);
  assert.equal(precoEscrito("R$ 59,90"), 59.9);
  assert.equal(precoEscrito("1,234.56"), 1234.56);
  assert.equal(precoEscrito("R$ 1.200"), 1200);
  assert.equal(precoEscrito("por R$ 7,5 cada"), 7.5);
  assert.equal(precoEmCentavos("1990", 2), 19.9);
  assert.equal(precoEmCentavos("199", 0), 199);
  assert.equal(precoEmCentavos("19.90", 2), null);
});

test("a chave de origem ignora o que não muda o produto", () => {
  assert.equal(chaveDeOrigem("https://www.Loja.com.br/produto/caneca/?utm_source=x&gclid=1"), "loja.com.br/produto/caneca");
  assert.equal(chaveDeOrigem("http://loja.com.br/produto/caneca"), "loja.com.br/produto/caneca");
  assert.equal(chaveDeOrigem("https://loja.com.br/p?id=2&cor=azul"), "loja.com.br/p?cor=azul&id=2");
  assert.equal(chaveDeOrigem("https://loja.com.br:443/produto/caneca"), "loja.com.br/produto/caneca");
  assert.equal(chaveDeOrigem("http://127.0.0.1:4000/produto/caneca"), "127.0.0.1:4000/produto/caneca");
  assert.equal(nomeDoEndereco("https://loja.com.br/produtos/caneca-azul-325ml/"), "Caneca azul 325ml");
  assert.equal(nomeDoEndereco("https://loja.com.br/caneca-magica/p"), "Caneca magica");
});

test("entidades do HTML viram letra", () => {
  assert.equal(semEntidades("Bon&eacute; &#8211; Pe&ccedil;a &Uacute;nica &amp; cia"), "Boné – Peça Única & cia");
});

test("contatos saem do texto, o resto fica", () => {
  const { texto, tirados } = tirarContatos("Ligue (87) 98870-1216 ou vendas@loja.com.br; veja www.lojaantiga.com.br. Período 03/10/2026 - 10/10/2026, 500 un.");
  assert.equal(tirados.length, 3);
  assert.doesNotMatch(texto, /98870|vendas@|lojaantiga/);
  assert.match(texto, /03\/10\/2026 - 10\/10\/2026/);
  assert.match(texto, /500 un/);
  assert.equal(tirarContatos("Chame no WhatsApp (11) 98765-4321 ou na loja.").texto, "Chame no WhatsApp ou na loja.");
  assert.equal(tirarContatos("Ligue +55 (87) 98870-1216.").texto, "Ligue .");
});

/* ---------------------------------------------------------------- Shopify */

const SHOPIFY = {
  products: [
    {
      id: 1,
      title: "Camiseta Aurora",
      handle: "camiseta-aurora",
      body_html: '<p>Camiseta 100% algodão. Fale no WhatsApp (11) 98765-4321 ou <a href="https://wa.me/5511987654321">clique aqui</a>.</p>',
      vendor: "Aurora",
      product_type: "Camisetas",
      options: [
        { name: "Tamanho", position: 1, values: ["P", "M", "G"] },
        { name: "Cor", position: 2, values: ["Azul", "Preto"] },
      ],
      images: [
        { id: 11, src: "https://cdn.shopify.com/s/files/1/azul.jpg", position: 1, variant_ids: [101, 102, 103] },
        { id: 12, src: "https://cdn.shopify.com/s/files/1/preto.jpg", position: 2, variant_ids: [104, 105, 106] },
      ],
      variants: [
        { id: 101, option1: "P", option2: "Azul", price: "50.00", compare_at_price: "60.00", sku: "CA-P-AZ", grams: 200 },
        { id: 102, option1: "M", option2: "Azul", price: "55.00", sku: "CA-M-AZ" },
        { id: 103, option1: "G", option2: "Azul", price: "60.00" },
        { id: 104, option1: "P", option2: "Preto", price: "52.00" },
        { id: 105, option1: "M", option2: "Preto", price: "57.00" },
        { id: 106, option1: "G", option2: "Preto", price: "62.00" },
      ],
    },
    {
      id: 2,
      title: "Caneca",
      handle: "caneca",
      body_html: "",
      options: [{ name: "Title", position: 1, values: ["Default Title"] }],
      variants: [{ id: 201, option1: "Default Title", price: "39.90", sku: "CAN-1" }],
      images: [],
    },
  ],
};

test("Shopify: produto com variações, fotos por variante e produto simples", () => {
  const [camiseta, caneca] = lerShopify(SHOPIFY, LOJA);
  assert.equal(camiseta.referencia, `${LOJA}/products/camiseta-aurora`);
  assert.equal(camiseta.preco, 50);
  assert.equal(camiseta.precoAntigo, 60);
  assert.equal(camiseta.pesoKg, 0.2);
  assert.deepEqual(camiseta.categorias, ["Camisetas"]);
  assert.equal(camiseta.fotos.length, 2);
  assert.deepEqual(camiseta.atributos, [
    { nome: "Tamanho", valores: ["P", "M", "G"] },
    { nome: "Cor", valores: ["Azul", "Preto"] },
  ]);
  assert.equal(camiseta.variantes.length, 6);
  assert.equal(camiseta.variantes[0].foto, "https://cdn.shopify.com/s/files/1/azul.jpg");
  assert.equal(camiseta.variantes[3].foto, "https://cdn.shopify.com/s/files/1/preto.jpg");

  assert.equal(caneca.preco, 39.9);
  assert.equal(caneca.codigo, "CAN-1");
  assert.equal(caneca.variantes.length, 0);
  assert.equal(caneca.atributos.length, 0);
  assert.equal(lerShopify({ product: SHOPIFY.products[1] }, LOJA).length, 1);
  assert.equal(lerShopify({ nada: 1 }, LOJA).length, 0);
});

/* ---------------------------------------------------------------- WooCommerce */

const WOO = [
  {
    id: 10,
    name: "Bon&eacute; Trucker &#8211; Aurora",
    permalink: `${LOJA}/produto/bone-trucker/`,
    description: "<p>Boné com tela.</p>",
    sku: "BT",
    prices: { price: "4990", regular_price: "5990", sale_price: "4990", currency_code: "BRL", currency_minor_unit: 2, price_range: { min_amount: "4990", max_amount: "5490" } },
    images: [{ src: `${LOJA}/wp-content/uploads/bone.jpg` }],
    categories: [{ name: "Bonés" }],
    attributes: [
      { name: "Cor", has_variations: true, terms: [{ name: "Azul marinho", slug: "azul-marinho" }, { name: "Vermelho", slug: "vermelho" }] },
      { name: "Material", has_variations: false, terms: [{ name: "Algodão", slug: "algodao" }] },
    ],
    variations: [
      { id: 11, attributes: [{ name: "Cor", value: "azul-marinho" }] },
      { id: 12, attributes: [{ name: "Cor", value: "vermelho" }] },
    ],
  },
  { id: 20, name: "Adesivo", permalink: `${LOJA}/produto/adesivo/`, prices: { price: "250", regular_price: "250", currency_minor_unit: 2 }, images: [], attributes: [], variations: [] },
];
const WOO_VARIACOES = [
  { id: 11, sku: "BT-AZ", prices: { price: "4990", regular_price: "5990", currency_minor_unit: 2 }, images: [{ src: `${LOJA}/wp-content/uploads/bone-azul.jpg` }] },
  { id: 12, prices: { price: "5490", regular_price: "5490", currency_minor_unit: 2 }, images: [] },
];

test("WooCommerce: centavos, nomes dos termos e preço de cada variação", () => {
  assert.deepEqual(produtosComVariacaoNoWoo(WOO), ["10"]);
  const [bone, adesivo] = lerWooCommerce(WOO, LOJA, lerVariacoesDoWoo(WOO_VARIACOES, LOJA));
  assert.equal(bone.nome, "Boné Trucker – Aurora");
  assert.equal(bone.preco, 49.9);
  assert.equal(bone.precoAntigo, 59.9);
  assert.equal(bone.moeda, "BRL");
  assert.deepEqual(bone.categorias, ["Bonés"]);
  assert.deepEqual(bone.atributos, [{ nome: "Cor", valores: ["Azul marinho", "Vermelho"] }]);
  assert.deepEqual(
    bone.variantes.map((variante) => [variante.opcoes.Cor, variante.preco, variante.foto, variante.codigo]),
    [
      ["Azul marinho", 49.9, `${LOJA}/wp-content/uploads/bone-azul.jpg`, "BT-AZ"],
      ["Vermelho", 54.9, null, null],
    ]
  );
  assert.equal(adesivo.preco, 2.5);
  assert.equal(adesivo.variantes.length, 0);
});

test("WooCommerce: valor vazio na variação vale para todos os valores", () => {
  const produto = {
    ...WOO[0],
    variations: [{ id: 11, attributes: [{ name: "Cor", value: "" }] }],
  };
  const [lido] = lerWooCommerce([produto], LOJA, lerVariacoesDoWoo([WOO_VARIACOES[0]], LOJA));
  assert.deepEqual(
    lido.variantes.map((variante) => variante.opcoes.Cor),
    ["Azul marinho", "Vermelho"]
  );
});

/* ---------------------------------------------------------------- VTEX */

test("VTEX: itens viram variações, com o preço cheio como preço antigo", () => {
  const [mochila] = lerVtex(
    [
      {
        productId: "1",
        productName: "Mochila Aurora",
        brand: "Aurora",
        linkText: "mochila-aurora",
        link: `${LOJA}/mochila-aurora/p`,
        description: "Mochila resistente.",
        categories: ["/Acessórios/Mochilas/", "/Acessórios/"],
        items: [
          {
            itemId: "11",
            ean: "7890000000011",
            referenceId: [{ Key: "RefId", Value: "MOC-PR" }],
            variations: ["Cor"],
            Cor: ["Preta"],
            images: [{ imageUrl: "https://loja.vteximg.com.br/arquivos/ids/1/preta.jpg" }],
            sellers: [{ commertialOffer: { Price: 199.9, ListPrice: 249.9, AvailableQuantity: 3 } }],
          },
          {
            itemId: "12",
            variations: ["Cor"],
            Cor: ["Cinza"],
            images: [{ imageUrl: "https://loja.vteximg.com.br/arquivos/ids/2/cinza.jpg" }],
            sellers: [{ commertialOffer: { Price: 209.9, ListPrice: 209.9 } }],
          },
        ],
      },
    ],
    LOJA
  );
  assert.equal(mochila.preco, 199.9);
  assert.equal(mochila.precoAntigo, 249.9);
  assert.deepEqual(mochila.categorias, ["Acessórios", "Mochilas"]);
  assert.equal(mochila.fotos.length, 2);
  assert.deepEqual(mochila.atributos, [{ nome: "Cor", valores: ["Preta", "Cinza"] }]);
  assert.equal(apelidoDaVtex(`${LOJA}/mochila-aurora/p`), "mochila-aurora");
  assert.equal(totalDaVtex("0-49/1234"), 1234);
});

/* ---------------------------------------------------------------- páginas */

const PAGINA_JSON_LD = `<html><head>
<script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Início","item":"${LOJA}/"},{"@type":"ListItem","position":2,"name":"Brindes","item":"${LOJA}/brindes"},{"@type":"ListItem","position":3,"name":"Canetas","item":"${LOJA}/canetas"}]}</script>
<script type="application/ld+json">
{"@context":"https://schema.org/","@type":"Product","name":"Caneta Metal Aurora","image":["/img/caneta-1.jpg","https://cdn.exemplo.com/caneta-2.jpg"],
 "description":"Caneta de metal com gravação.","sku":"CAN-MET","gtin13":"7891234567895","brand":{"@type":"Brand","name":"Aurora"},
 "offers":{"@type":"AggregateOffer","lowPrice":"4.90","highPrice":"6.90","priceCurrency":"BRL"}}
</script></head><body><h1>Caneta</h1></body></html>`;

test("página com JSON-LD: produto, fotos absolutas e categoria pela trilha", () => {
  const lido = lerPagina(PAGINA_JSON_LD, `${LOJA}/produto/caneta-metal`);
  assert.ok(lido);
  assert.equal(lido.nome, "Caneta Metal Aurora");
  assert.equal(lido.preco, 4.9);
  assert.equal(lido.moeda, "BRL");
  assert.deepEqual(lido.fotos, [`${LOJA}/img/caneta-1.jpg`, "https://cdn.exemplo.com/caneta-2.jpg"]);
  assert.deepEqual(lido.categorias, ["Brindes", "Canetas"]);
  assert.equal(lido.codigo, "CAN-MET");
  assert.equal(lido.codigoDeBarras, "7891234567895");
  assert.equal(lido.marca, "Aurora");
});

test("página com ProductGroup: cada variação com o seu preço", () => {
  const variante = (sku: string, tamanho: string, cor: string, preco: string) =>
    `{"@type":"Product","sku":"${sku}","size":"${tamanho}","color":"${cor}","image":"https://cdn.exemplo.com/mol-${cor.toLowerCase()}.jpg","offers":{"@type":"Offer","price":"${preco}","priceCurrency":"BRL"}}`;
  const html = `<script type="application/ld+json">{"@context":"https://schema.org","@graph":[{"@type":"ProductGroup","name":"Moletom Aurora","productGroupID":"MOL","variesBy":["https://schema.org/size","https://schema.org/color"],"hasVariant":[${[
    variante("MOL-P-AZ", "P", "Azul", "120.00"),
    variante("MOL-G-AZ", "G", "Azul", "130.00"),
    variante("MOL-P-VE", "P", "Verde", "120.00"),
    variante("MOL-G-VE", "G", "Verde", "130.00"),
  ].join(",")}]}]}</script>`;
  const lido = lerPagina(html, `${LOJA}/produto/moletom`);
  assert.ok(lido);
  assert.equal(lido.preco, 120);
  assert.deepEqual(lido.atributos, [
    { nome: "Tamanho", valores: ["P", "G"] },
    { nome: "Cor", valores: ["Azul", "Verde"] },
  ]);
  const variacoes = montarVariacoes(lido, novoId);
  assert.equal(variacoes.modo, "somada");
  assert.equal(resumoDasVariacoes(variacoes.atributos), "Tamanho: P, G (+ R$ 10,00); Cor: Azul, Verde");
  /* A cor decide a foto. */
  const cores = variacoes.atributos[1].options;
  assert.equal(variacoes.fotosDasOpcoes.get(cores[0].id), "https://cdn.exemplo.com/mol-azul.jpg");
  assert.equal(variacoes.fotosDasOpcoes.get(cores[1].id), "https://cdn.exemplo.com/mol-verde.jpg");
});

test("página da Nuvemshop: variações do data-variants com os nomes dos rótulos", () => {
  const variantes = [
    { price_number: 29.9, compare_at_price_number: 39.9, option0: "Cru", option1: "M", option2: null, image_url: "//acdn.mitiendanube.com/stores/1/products/eco-cru.jpg", sku: "ECO-CRU-M" },
    { price_number: 34.9, option0: "Preta", option1: "M", image_url: "//acdn.mitiendanube.com/stores/1/products/eco-preta.jpg" },
  ];
  const html = `<script type="application/ld+json">{"@context":"https://schema.org","@type":"Product","name":"Ecobag Aurora","image":"https://acdn.mitiendanube.com/stores/1/products/eco.jpg","offers":{"@type":"Offer","price":"29.90","priceCurrency":"BRL"}}</script>
<label class="form-label" for="variation_1">Cor:</label><select id="variation_1"></select>
<label class="form-label" for="variation_2">Tamanho</label>
<form id="product_form" data-variants="${JSON.stringify(variantes).replace(/"/g, "&quot;")}"></form>`;
  const lido = lerPagina(html, `${LOJA}/produtos/ecobag-aurora/`);
  assert.ok(lido);
  assert.equal(lido.preco, 29.9);
  assert.equal(lido.precoAntigo, 39.9);
  assert.deepEqual(lido.variantes.map((variante) => variante.opcoes), [{ Cor: "Cru", Tamanho: "M" }, { Cor: "Preta", Tamanho: "M" }]);
  assert.ok(lido.fotos.includes("https://acdn.mitiendanube.com/stores/1/products/eco-preta.jpg"));
  /* O tamanho tem um valor só: não é escolha, fica de fora. */
  const variacoes = montarVariacoes(lido, novoId);
  assert.deepEqual(variacoes.atributos.map((atributo) => atributo.name), ["Cor"]);
  assert.deepEqual(variacoes.atributos[0].options.map((opcao) => [opcao.label, opcao.price]), [["Cru", 0], ["Preta", 5]]);
});

test("página com produtos relacionados: vale o desta página, com a trilha de dentro do bloco", () => {
  const produto = (nome: string, url: string, preco: string, descricao = "") =>
    `{"@type":"Product","name":"${nome}","url":"${url}","description":"${descricao}","offers":{"@type":"Offer","price":"${preco}","priceCurrency":"BRL","url":"${url}"}}`;
  const html = `<script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","breadcrumb":{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Início"},{"@type":"ListItem","position":2,"name":"CAMISAS GRIFES"}]},"mainEntity":[${[
    produto("Camiseta Relacionada", `${LOJA}/produtos/outra/`, "59.90"),
    produto("Camiseta Pima", `${LOJA}/produtos/camiseta-pima/`, "109.99", "MALHA PIMA &amp;#128175;% IMPORTADO"),
    produto("Mais Uma", `${LOJA}/produtos/mais-uma/`, "79.90"),
  ].join(",")}]}</script>`;
  const lido = lerPagina(html, `${LOJA}/produtos/camiseta-pima/`);
  assert.ok(lido);
  assert.equal(lido.nome, "Camiseta Pima");
  assert.equal(lido.preco, 109.99);
  assert.equal(lido.descricao, "MALHA PIMA 💯% IMPORTADO");
  assert.deepEqual(lido.categorias, ["CAMISAS GRIFES"]);
});

test("página só com og: e página que não é de produto", () => {
  const og = `<meta property="og:type" content="product"><meta property="og:title" content="Chaveiro Aurora"><meta content="https://cdn.exemplo.com/chaveiro.jpg" property="og:image"><meta property="product:price:amount" content="12.50"><meta property="product:price:currency" content="BRL">`;
  const lido = lerPagina(og, `${LOJA}/chaveiro`);
  assert.ok(lido);
  assert.equal(lido.nome, "Chaveiro Aurora");
  assert.equal(lido.preco, 12.5);
  assert.deepEqual(lido.fotos, ["https://cdn.exemplo.com/chaveiro.jpg"]);
  assert.equal(lerPagina(`<html><head><meta property="og:type" content="article"><title>Blog</title></head></html>`, `${LOJA}/blog/x`), null);
});

test("página de lista: os produtos do ItemList e os links com cara de produto", () => {
  const html = `<script type="application/ld+json">{"@type":"ItemList","itemListElement":[{"@type":"ListItem","position":1,"url":"${LOJA}/produto/a"},{"@type":"ListItem","position":2,"item":{"@id":"${LOJA}/produto/b"}}]}</script>
<a href="/produto/c?cor=azul">C</a><a href="/blog/post">Blog</a><a href="https://outro.com/produto/d">Fora</a><a href="/categoria/canecas">Canecas</a>`;
  assert.deepEqual(produtosDaLista(html, LOJA), [`${LOJA}/produto/a`, `${LOJA}/produto/b`]);
  assert.deepEqual(linksDeProduto(html, LOJA), [`${LOJA}/produto/c`]);
});

test("mapa do site: índice com o de produtos primeiro, e as páginas", () => {
  const indice = lerSitemap(
    `<sitemapindex><sitemap><loc>${LOJA}/sitemap-paginas.xml</loc></sitemap><sitemap><loc>${LOJA}/sitemap-produtos-1.xml</loc></sitemap><sitemap><loc>${LOJA}/sitemap-blog.xml</loc></sitemap></sitemapindex>`,
    LOJA
  );
  assert.equal(indice.mapas[0], `${LOJA}/sitemap-produtos-1.xml`);
  assert.equal(indice.paginas.length, 0);
  const paginas = lerSitemap(`<urlset><url><loc><![CDATA[${LOJA}/produto/caneca]]></loc></url><url><loc>${LOJA}/sobre</loc></url></urlset>`, LOJA);
  assert.deepEqual(paginas.paginas, [`${LOJA}/produto/caneca`, `${LOJA}/sobre`]);
  assert.equal(pareceProduto(`${LOJA}/produto/caneca`), true);
  assert.equal(pareceProduto(`${LOJA}/products/caneca`), true);
  assert.equal(pareceProduto(`${LOJA}/caneca-magica/p`), true);
  assert.equal(pareceProduto(`${LOJA}/sobre`), false);
  assert.equal(pareceProduto(`${LOJA}/categoria/canecas`), false);
});

test("a plataforma pelas marcas da página", () => {
  assert.equal(detectarPlataforma('<script src="https://cdn.shopify.com/x.js"></script>'), "shopify");
  assert.equal(detectarPlataforma('<link href="/wp-content/plugins/woocommerce/x.css">'), "woocommerce");
  assert.equal(detectarPlataforma("<html></html>", { "x-vtex-cache-status": "HIT" }), "vtex");
  assert.equal(detectarPlataforma('<img src="https://acdn.mitiendanube.com/stores/1/x.jpg">'), "nuvemshop");
  assert.equal(detectarPlataforma('<link href="/wp-content/themes/x.css">'), "wordpress");
  assert.equal(detectarPlataforma("<html></html>"), "outra");
  /* Nuvemshop com uma foto hospedada na Shopify continua Nuvemshop. */
  assert.equal(detectarPlataforma('<img src="https://acdn-us.mitiendanube.com/x.jpg"><img src="https://cdn.shopify.com/y.jpg">'), "nuvemshop");
  assert.equal(detectarPlataforma("<script>window.Shopify = {}; </script>"), "shopify");
});

/* ---------------------------------------------------------------- variações */

function produtoComVariantes(precos: Array<[string, string, number]>) {
  const lido = produtoVazio(`${LOJA}/produto/x`, "X");
  lido.variantes = precos.map(([tamanho, cor, preco]) => ({ opcoes: { Tamanho: tamanho, Cor: cor }, preco }));
  return lido;
}

test("variações: todas com o mesmo preço", () => {
  const variacoes = montarVariacoes(produtoComVariantes([["P", "Azul", 30], ["G", "Azul", 30], ["P", "Rosa", 30], ["G", "Rosa", 30]]), novoId);
  assert.equal(variacoes.modo, "sem_diferenca");
  assert.equal(variacoes.base, 30);
  assert.ok(variacoes.atributos.every((atributo) => atributo.options.every((opcao) => opcao.price === 0 && opcao.image_id === null)));
});

test("variações: preço que se explica por soma", () => {
  const camiseta = lerShopify(SHOPIFY, LOJA)[0];
  const variacoes = montarVariacoes(camiseta, novoId);
  assert.equal(variacoes.modo, "somada");
  assert.equal(variacoes.base, 50);
  assert.deepEqual(variacoes.atributos.map((atributo) => atributo.options.map((opcao) => [opcao.label, opcao.price])), [
    [["P", 0], ["M", 5], ["G", 10]],
    [["Azul", 0], ["Preto", 2]],
  ]);
  assert.ok(variacoes.atributos.every((atributo) => atributo.is_required && !atributo.allow_multiple));
});

test("variações: preço que não se explica por soma vira lista única, sem preço errado", () => {
  const variacoes = montarVariacoes(produtoComVariantes([["P", "Azul", 50], ["M", "Azul", 55], ["P", "Preto", 52], ["M", "Preto", 70]]), novoId);
  assert.equal(variacoes.modo, "lista_unica");
  assert.equal(variacoes.atributos.length, 1);
  assert.equal(variacoes.atributos[0].name, "Tamanho / Cor");
  assert.deepEqual(variacoes.atributos[0].options.map((opcao) => [opcao.label, opcao.price]), [
    ["P / Azul", 0],
    ["P / Preto", 2],
    ["M / Azul", 5],
    ["M / Preto", 20],
  ]);
  assert.equal(variacoes.avisos.length, 1);
});

test("variações: combinações demais ficam para o painel", () => {
  const precos: Array<[string, string, number]> = [];
  for (let tamanho = 0; tamanho < 11; tamanho += 1) for (let cor = 0; cor < 11; cor += 1) precos.push([`T${tamanho}`, `C${cor}`, 10 + tamanho * cor]);
  const variacoes = montarVariacoes(produtoComVariantes(precos), novoId);
  assert.equal(variacoes.modo, "demais");
  assert.equal(variacoes.atributos.length, 0);
});

/* ---------------------------------------------------------------- montar */

test("montar: descrição sem link e sem contato, preço por, fotos ligadas às cores", () => {
  const camiseta = lerShopify(SHOPIFY, LOJA)[0];
  const montagem = montarProduto(camiseta, undefined, novoId);
  assert.ok(montagem.ok);
  const produto = montagem.produto;
  assert.doesNotMatch(produto.descricao, /<a\b|98765|wa\.me/);
  assert.match(produto.descricao, /clique aqui/);
  assert.ok(produto.contatosTirados.length >= 1);
  assert.equal(produto.preco, 50);
  assert.equal(produto.sobConsulta, false);
  assert.equal(produto.chave, "loja.exemplo.com.br/products/camiseta-aurora");
  assert.ok(produto.avisos.some((aviso) => /não mostra preço riscado/.test(aviso)));
  assert.equal(produto.fotos.length, 2);
  const [azul, preto] = produto.atributos[1].options;
  assert.equal(azul.image_id, produto.fotos.find((foto) => foto.endereco.endsWith("azul.jpg"))?.id);
  assert.equal(preto.image_id, produto.fotos.find((foto) => foto.endereco.endsWith("preto.jpg"))?.id);
  assert.equal(produto.atributos[0].options[0].image_id, null);
});

test("montar: nome comprido, fotos demais, moeda de fora e ajustes do assistente", () => {
  const lido = produtoVazio(`${LOJA}/produto/kit`, `Kit ${"muito ".repeat(40)}grande`);
  lido.preco = 10;
  lido.moeda = "USD";
  lido.fotos = Array.from({ length: 15 }, (_, indice) => `https://cdn.exemplo.com/kit-${indice}.jpg`);
  lido.variantes = ["Azul", "Rosa", "Verde"].map((cor) => ({ opcoes: { Cor: cor }, preco: 10, foto: `https://cdn.exemplo.com/kit-${cor}.jpg` }));
  const montagem = montarProduto(
    lido,
    { categoria: "Brindes > Kits > Escritório", vendido_por: "metro quadrado", quantidade_minima: 10, faixas_de_preco: [{ a_partir_de: 10, preco_unitario: 9 }, { a_partir_de: 50, preco_unitario: 8 }] },
    novoId
  );
  assert.ok(montagem.ok);
  const produto = montagem.produto;
  assert.ok(produto.nome.length <= 150);
  assert.equal(produto.fotos.length, MAXIMO_DE_FOTOS);
  assert.equal(produto.fotos[0].endereco, "https://cdn.exemplo.com/kit-0.jpg");
  assert.ok(["Azul", "Rosa", "Verde"].every((cor) => produto.fotos.some((foto) => foto.endereco.endsWith(`kit-${cor}.jpg`))));
  assert.ok(produto.atributos[0].options.every((opcao) => opcao.image_id));
  assert.deepEqual(produto.categorias, ["Brindes", "Escritório"]);
  assert.equal(produto.unidade, "square_meter");
  assert.deepEqual(produto.faixas.map((faixa) => [faixa.min_qty, faixa.max_qty, faixa.unit_price]), [[10, 49, 9], [50, null, 8]]);
  assert.equal(produto.quantidadeMinima, 10);
  assert.ok(produto.avisos.some((aviso) => /USD/.test(aviso)));
  assert.ok(produto.avisos.some((aviso) => /encurtado/.test(aviso)));
});

test("montar: variações escritas pelo assistente e o que ele não pode", () => {
  const lido = produtoVazio(`${LOJA}/produto/placa`, "Placa de PVC");
  lido.preco = 80;
  const montagem = montarProduto(
    lido,
    {
      variacoes: [
        { nome: "Espessura", opcoes: [{ nome: "2 mm" }, { nome: "3 mm", acrescimo: 15 }] },
        { nome: "Material", opcoes: [{ nome: "PVC" }] },
      ],
    },
    novoId
  );
  assert.ok(montagem.ok);
  assert.deepEqual(montagem.produto.atributos.map((atributo) => atributo.options.map((opcao) => [opcao.label, opcao.price])), [[["2 mm", 0], ["3 mm", 15]]]);
  assert.ok(montagem.produto.avisos.some((aviso) => /Material/.test(aviso)));

  const consulta = montarProduto(lido, { sob_consulta: true, faixas_de_preco: [{ a_partir_de: 1, preco_unitario: 5 }] }, novoId);
  assert.equal(consulta.ok, false);
  const semPreco = montarProduto(produtoVazio(`${LOJA}/produto/y`, "Sem preço"), undefined, novoId);
  assert.ok(semPreco.ok && semPreco.produto.sobConsulta);
});

/* ---------------------------------------------------------------- ataques */

/* Cada leitura de conteúdo montado para travar tem de terminar rápido: o
   tempo cresce com o tamanho, nunca ao quadrado. */
function rapido(oQue: string, fazer: () => unknown, limiteMs = 2000) {
  const inicio = performance.now();
  fazer();
  const gasto = performance.now() - inicio;
  assert.ok(gasto < limiteMs, `${oQue} levou ${Math.round(gasto)} ms`);
}

test("ataque: páginas montadas para travar a leitura", () => {
  const MEGAS = 6 * 1024 * 1024;
  const repetir = (pedaco: string) => pedaco.repeat(Math.ceil(MEGAS / pedaco.length));
  rapido("meta sem fechar", () => lerPagina(repetir("<meta "), LOJA));
  rapido("script de dados sem fechar", () => lerPagina(repetir('<script type="application/ld+json">{'), LOJA));
  rapido("rótulo sem fechar", () => lerPagina(repetir('<label for="variation_1"'), LOJA));
  rapido("data-variants sem fechar", () => lerPagina(`<meta property="og:type" content="product">${repetir(' data-variants="[')}`, LOJA));
  rapido("título sem fechar", () => lerPagina(repetir("<title>"), LOJA));
  rapido("links sem fechar", () => linksDeProduto(repetir("<a href="), LOJA));
  rapido("lista sem fechar", () => produtosDaLista(repetir('<script type="application/ld+json">'), LOJA));
});

test("ataque: mapa do site montado para travar", () => {
  /* O caso do revisor: 233 bytes levavam 13 s na expressão antiga. */
  rapido("loc com espaços", () => lerSitemap(`<urlset><url><loc>${" ".repeat(200_000)}</url></urlset>`, LOJA));
  rapido("loc sem fechar", () => lerSitemap("<loc>".repeat(1_000_000), LOJA));
  rapido("url sem fechar", () => lerSitemap(`<urlset>${"<url><loc>x".repeat(300_000)}`, LOJA));
});

test("ataque: textos montados para travar a peneira e o resumo", () => {
  rapido("letras sem fim", () => tirarContatos("a".repeat(2_000_000)));
  rapido("pontos sem fim", () => tirarContatos("a.".repeat(1_000_000)));
  rapido("arroba sem fim", () => tirarContatos("a@".repeat(1_000_000)));
  rapido("parênteses e dígitos", () => tirarContatos("1(".repeat(1_000_000)));
  rapido("muitos e-mails", () => tirarContatos("a@b.com ".repeat(200_000)));
  rapido("resumo com script sem fechar", () => textoCorrido("<script".repeat(400_000)));
  rapido("descrição gigante", () => limparDescricao(`<p>${"x<b>1</b>".repeat(100_000)}</p>`));
});

test("ataque: variações do WooCommerce que se multiplicam", () => {
  const atributos = Array.from({ length: 6 }, (_, indice) => ({
    name: `A${indice}`,
    has_variations: true,
    terms: Array.from({ length: 20 }, (_, termo) => ({ name: `T${termo}`, slug: `t${termo}` })),
  }));
  const produto = {
    id: 1,
    name: "Bomba",
    permalink: `${LOJA}/produto/bomba/`,
    prices: { price: "100", currency_minor_unit: 2 },
    attributes: atributos,
    variations: [{ id: 2, attributes: atributos.map((atributo) => ({ name: atributo.name, value: "" })) }],
  };
  let lidos: ReturnType<typeof lerWooCommerce> = [];
  rapido("64 milhões de combinações", () => {
    lidos = lerWooCommerce([produto], LOJA);
  });
  assert.ok(lidos[0].variantes.length <= 1000, `${lidos[0].variantes.length} variantes`);
});

test("ataque: dribles da peneira de contatos", () => {
  const partido = limparDescricao("<p>Ligue 12345<b></b>67890 já!</p>");
  assert.doesNotMatch(partido.html.replace(/<[^>]+>/g, ""), /12345\s*67890/);
  assert.ok(partido.tirados.includes("1234567890"), JSON.stringify(partido.tirados));
  assert.doesNotMatch(limparDescricao("<p>WhatsApp (11)​ 98765​-4321</p>").html, /98765/);
  assert.doesNotMatch(limparDescricao("<p>Ligue １１９８７６５４３２１</p>").html, /[１９]/);
  assert.doesNotMatch(limparDescricao("<p>Ligue ١٢٣٤٥٦٧٨٩٠</p>").html, /١|0000000000/);
  /* O que não é contato fica como está. */
  assert.equal(limparDescricao("<p>Banner de <strong>2 m²</strong> em lona</p>").html, "<p>Banner de <strong>2 m²</strong> em lona</p>");
});

test("limpar descrição: texto puro vira parágrafo, script some", () => {
  assert.equal(limparDescricao("Linha um\n\nLinha dois").html, "<p>Linha um</p><p>Linha dois</p>");
  const { html } = limparDescricao("<p>Linha dois</p><script>alert(1)</script>");
  assert.equal(html, "<p>Linha dois</p>");
  const rica = limparDescricao('<h2>Medidas</h2><p onclick="x()">10 x 20 cm</p><img src="https://x/y.jpg">');
  assert.equal(rica.html, "<h2>Medidas</h2><p>10 x 20 cm</p>");
});
