import assert from "node:assert/strict";
import { test } from "node:test";

import { EnvioEmPartes, erroDefinitivo } from "./cliente/gravador.ts";

/* Junta as partes enviadas na ordem dos números (como o depósito faz ao fechar). */
function juntar(enviadas: Map<number, Uint8Array>): Uint8Array {
  const numeros = [...enviadas.keys()].sort((a, b) => a - b);
  const total = numeros.reduce((s, n) => s + enviadas.get(n)!.length, 0);
  const saida = new Uint8Array(total);
  let pos = 0;
  for (const n of numeros) {
    saida.set(enviadas.get(n)!, pos);
    pos += enviadas.get(n)!.length;
  }
  return saida;
}

function arquivoDeTeste(tamanho: number): Uint8Array {
  const dados = new Uint8Array(tamanho);
  for (let i = 0; i < tamanho; i += 1) dados[i] = (i * 7 + 3) % 251;
  return dados;
}

test("partes do mesmo tamanho, a última menor, e o arquivo sai igual (com o começo corrigido no fim)", async () => {
  const TAM = 1000;
  const enviadas = new Map<number, Uint8Array>();
  const ordem: number[] = [];
  const envio = new EnvioEmPartes(async (numero, dados) => {
    ordem.push(numero);
    enviadas.set(numero, new Uint8Array(dados));
  }, TAM);
  const arquivo = arquivoDeTeste(5 * TAM + 321);
  /* Como o MP4: escreve em pedaços de tamanhos variados, em sequência... */
  const pedacos = [1, 999, 37, 2048, 500, 1, 700];
  let pos = 0;
  let i = 0;
  while (pos < arquivo.length) {
    const n = Math.min(pedacos[i++ % pedacos.length], arquivo.length - pos);
    envio.escrever(arquivo.subarray(pos, pos + n), pos);
    pos += n;
  }
  /* ...e no fim volta ao começo para corrigir o tamanho da caixa de dados. */
  const correcao = new Uint8Array([9, 9, 9, 9]);
  envio.escrever(correcao, 24);
  arquivo.set(correcao, 24);
  await envio.terminar();

  assert.equal(envio.erro, null);
  assert.deepEqual([...enviadas.keys()].sort((a, b) => a - b), [1, 2, 3, 4, 5, 6]);
  for (let n = 1; n <= 5; n += 1) assert.equal(enviadas.get(n)!.length, TAM, `parte ${n} tem o tamanho cheio`);
  assert.equal(enviadas.get(6)!.length, 321);
  assert.equal(ordem[ordem.length - 1], 1, "o começo sobe por último");
  assert.deepEqual(juntar(enviadas), arquivo);
});

test("arquivo menor que uma parte sobe numa parte só, do tamanho certo", async () => {
  const enviadas = new Map<number, Uint8Array>();
  const envio = new EnvioEmPartes(async (n, d) => void enviadas.set(n, new Uint8Array(d)), 1000);
  envio.escrever(arquivoDeTeste(10), 0);
  await envio.terminar();
  assert.deepEqual([...enviadas.keys()], [1]);
  assert.equal(enviadas.get(1)!.length, 10);
});

test("tamanho exato de duas partes: nenhuma sobra vazia", async () => {
  const enviadas = new Map<number, Uint8Array>();
  const envio = new EnvioEmPartes(async (n, d) => void enviadas.set(n, new Uint8Array(d)), 1000);
  envio.escrever(arquivoDeTeste(2000), 0);
  await envio.terminar();
  assert.deepEqual([...enviadas.keys()].sort(), [1, 2]);
  assert.equal(enviadas.get(1)!.length, 1000);
  assert.equal(enviadas.get(2)!.length, 1000);
});

test("uma parte que não subiu para as seguintes e o fim avisa", async () => {
  const tentadas: number[] = [];
  const envio = new EnvioEmPartes(async (n) => {
    tentadas.push(n);
    if (n === 2) throw new Error("recusada");
  }, 100);
  envio.escrever(arquivoDeTeste(1000), 0);
  await assert.rejects(envio.terminar(), /recusada/);
  assert.deepEqual(tentadas, [2], "depois da falha nenhuma outra parte sobe");
});

test("só recusa de verdade (4xx) para as tentativas", () => {
  const com = (status: number) => Object.assign(new Error("x"), { status });
  assert.equal(erroDefinitivo(com(409)), true);
  assert.equal(erroDefinitivo(com(413)), true);
  assert.equal(erroDefinitivo(com(408)), false);
  assert.equal(erroDefinitivo(com(429)), false);
  assert.equal(erroDefinitivo(com(503)), false);
  assert.equal(erroDefinitivo(new Error("rede")), false);
});
