import { describe, expect, it } from "vitest";
import { MAX_TARGETS_PER_SITE, type CensusCountry, type CountryChoice } from "./import-countries";
import {
  addImportFile,
  effectiveFormat,
  listSuggestions,
  mappingDirty,
  mappingReady,
  mergedCensus,
  multiLaunchPlan,
  needsMapping,
  newImportFile,
  patchImportFile,
  readyFiles,
  removeImportFile,
  resolveDestination,
  type ImportFile,
} from "./import-files";
import type { CountryCount, ImportPreview, UploadResult } from "./imports";
import type { KeywordList } from "./keywords";
import type { Target } from "./sites";

// Fabriques minimales : chaque test ne redéclare que ce qu'il éprouve.

function upload(overrides: Partial<UploadResult> & { sha256: string }): UploadResult {
  return {
    filename: `${overrides.sha256}.csv`,
    size: 1024,
    duplicate_of: null,
    ...overrides,
  };
}

function country(overrides: Partial<CountryCount> & { raw: string }): CountryCount {
  return {
    code: null,
    label: overrides.raw,
    rows: 1,
    suggested_language: null,
    target_ids: [],
    ...overrides,
  };
}

function preview(overrides: Partial<ImportPreview> = {}): ImportPreview {
  return {
    format: "mangools",
    confidence: 0.9,
    language: "fr",
    headers: ["Keyword", "Search", "Country"],
    columns: {},
    countries: [],
    candidates: [],
    sample: [],
    warnings: [],
    ...overrides,
  };
}

function file(sha256: string, overrides: Partial<ImportFile> = {}): ImportFile {
  return { ...newImportFile(upload({ sha256 })), preview: preview(), ...overrides };
}

let seq = 0;
function target(countryCode: string, language: string): Target {
  seq += 1;
  return {
    id: `00000000-0000-4000-8000-${String(seq).padStart(12, "0")}`,
    country: countryCode,
    language,
    is_default: false,
    created_at: "2026-09-02T00:00:00Z",
  };
}

function list(id: string, name: string, itemCount = 12): KeywordList {
  return {
    id,
    site_target_id: "00000000-0000-4000-8000-00000000ffff",
    name,
    source: null,
    created_from_import_id: null,
    created_at: "2026-09-02T00:00:00Z",
    item_count: itemCount,
  };
}

/** Coche les codes donnés, dans la langue proposée par le recensement. */
function checking(codes: readonly string[]): (item: CensusCountry) => CountryChoice {
  return (item) => ({
    selected: codes.includes(item.code),
    language: item.suggestedLanguage ?? "",
  });
}

const US = country({
  raw: "United States",
  code: "US",
  label: "États-Unis",
  rows: 40,
  suggested_language: "en",
});
const GB = country({
  raw: "United Kingdom",
  code: "GB",
  label: "Royaume-Uni",
  rows: 25,
  suggested_language: "en",
});

describe("collection de fichiers", () => {
  it("n'ajoute qu'une ligne pour un même contenu déposé deux fois", () => {
    // Deux imports du même `sha256` sur la même cible se solderaient par un 409 : l'écran ne
    // doit pas promettre un travail qui n'aura pas lieu.
    const once = addImportFile([], upload({ sha256: "aa", filename: "export.csv" }));
    const twice = addImportFile(once, upload({ sha256: "aa", filename: "export (1).csv" }));
    expect(twice).toHaveLength(1);
    expect(twice[0]?.filename).toBe("export (1).csv");
  });

  it("empile les contenus distincts dans l'ordre de dépôt", () => {
    const files = addImportFile(
      addImportFile([], upload({ sha256: "aa" })),
      upload({ sha256: "bb" }),
    );
    expect(files.map((item) => item.sha256)).toEqual(["aa", "bb"]);
  });

  it("modifie et retire un fichier par son empreinte", () => {
    const files = [file("aa"), file("bb")];
    expect(patchImportFile(files, "bb", { error: "illisible" })[1]?.error).toBe("illisible");
    expect(removeImportFile(files, "aa").map((item) => item.sha256)).toEqual(["bb"]);
  });

  it("n'expose que les fichiers dont l'aperçu a abouti", () => {
    const broken = file("bb", { preview: null, error: "Format inconnu" });
    expect(readyFiles([file("aa"), broken]).map((item) => item.sha256)).toEqual(["aa"]);
  });
});

describe("format et mapping, fichier par fichier", () => {
  it("préfère le format forcé à celui de la détection", () => {
    expect(effectiveFormat(file("aa", { format: "generic_csv" }))).toBe("generic_csv");
    expect(effectiveFormat(file("aa"))).toBe("mangools");
  });

  it("n'exige un mapping que du CSV générique, et seulement le mot-clé", () => {
    const generic = file("aa", { format: "generic_csv" });
    expect(needsMapping(generic)).toBe(true);
    expect(mappingReady(generic)).toBe(false);
    expect(mappingReady({ ...generic, mapping: { keyword: "Keyword" } })).toBe(true);
    expect(mappingReady(file("aa"))).toBe(true);
  });

  it("rallume « Appliquer » sur la colonne pays autant que sur le mapping", () => {
    // La colonne pays ne change pas le tableau affiché, mais elle change le recensement : sans
    // ce réveil, l'étape 3 proposerait des marchés qui ne viennent pas de ce que l'écran montre.
    const applied = file("aa", {
      format: "generic_csv",
      mapping: { keyword: "Keyword" },
      preview: preview({ columns: { keyword: "Keyword" } }),
      countryColumn: null,
      appliedCountryColumn: null,
    });
    expect(mappingDirty(applied)).toBe(false);
    expect(mappingDirty({ ...applied, countryColumn: "Country" })).toBe(true);
    expect(mappingDirty({ ...applied, mapping: { keyword: "Autre" } })).toBe(true);
  });
});

describe("mergedCensus", () => {
  it("additionne les lignes d'un même pays trouvé dans plusieurs fichiers", () => {
    const { countries } = mergedCensus([
      file("aa", { preview: preview({ countries: [US, GB] }) }),
      file("bb", { preview: preview({ countries: [US] }) }),
    ]);
    expect(countries.map((item) => [item.code, item.rows])).toEqual([
      ["US", 80],
      ["GB", 25],
    ]);
  });

  it("fusionne un libellé non reconnu que trois exports portent tous", () => {
    const newYork = country({ raw: "New York", rows: 3 });
    const { unresolved } = mergedCensus([
      file("aa", { preview: preview({ countries: [newYork] }) }),
      file("bb", { preview: preview({ countries: [newYork] }) }),
    ]);
    expect(unresolved).toEqual([{ raw: "New York", label: "New York", rows: 6 }]);
  });

  it("ignore les fichiers dont l'aperçu a échoué", () => {
    expect(mergedCensus([file("aa", { preview: null })])).toEqual({
      countries: [],
      unresolved: [],
    });
  });
});

describe("multiLaunchPlan", () => {
  const files = [
    file("aa", { filename: "semrush.csv", preview: preview({ countries: [US, GB] }) }),
    file("bb", { filename: "mangools.csv", preview: preview({ countries: [US] }) }),
  ];
  const countries = mergedCensus(files).countries;

  it("lance un import par fichier et par pays coché, sans compter deux fois la cible", () => {
    const current = target("US", "en");
    const plan = multiLaunchPlan({
      files,
      targets: [current],
      targetId: current.id,
      countries,
      choice: checking(["US", "GB"]),
      listName: "Lot — 2026-09-02",
      existingListId: null,
    });
    expect(plan.blockers).toEqual([]);
    // Trois imports : US et GB pour le premier fichier, US seul pour le second.
    expect(plan.imports.map((item) => [item.filename, item.country])).toEqual([
      ["semrush.csv", "US"],
      ["semrush.csv", "GB"],
      ["mangools.csv", "US"],
    ]);
    // UNE cible à créer, pas deux : le Royaume-Uni n'est visé qu'une fois, et les États-Unis
    // existent déjà. C'est ce compte-là qui vaut au plafond du site.
    expect(plan.creations).toBe(1);
    // Deux cibles visées : chaque liste porte son pays, sinon deux marchés se mélangeraient.
    expect(plan.imports.map((item) => item.list)).toEqual([
      { name: "Lot — 2026-09-02 — US" },
      { name: "Lot — 2026-09-02 — GB" },
      { name: "Lot — 2026-09-02 — US" },
    ]);
  });

  it("donne une clé unique à deux fichiers qui visent le même marché", () => {
    const current = target("US", "en");
    const plan = multiLaunchPlan({
      files,
      targets: [current],
      targetId: current.id,
      countries,
      choice: checking(["US"]),
      listName: "Lot",
      existingListId: null,
    });
    expect(new Set(plan.imports.map((item) => item.key)).size).toBe(2);
  });

  it("garde le nom saisi quand tout le lot vise une seule cible", () => {
    // Les deux imports atterrissent alors dans la MÊME liste — c'est ce que le lancement
    // obtient en réutilisant l'identifiant rendu par le premier d'entre eux.
    const current = target("US", "en");
    const plan = multiLaunchPlan({
      files,
      targets: [current],
      targetId: current.id,
      countries,
      choice: checking(["US"]),
      listName: "Lot",
      existingListId: null,
    });
    expect(plan.imports.map((item) => item.list)).toEqual([{ name: "Lot" }, { name: "Lot" }]);
  });

  it("verse tout le lot dans la liste existante choisie quand elle est atteignable", () => {
    const current = target("US", "en");
    const plan = multiLaunchPlan({
      files,
      targets: [current],
      targetId: current.id,
      countries,
      choice: checking(["US"]),
      listName: "Peu importe",
      existingListId: "11111111-1111-4111-8111-111111111111",
    });
    expect(plan.imports.map((item) => item.list)).toEqual([
      { id: "11111111-1111-4111-8111-111111111111" },
      { id: "11111111-1111-4111-8111-111111111111" },
    ]);
  });

  it("retombe sur des listes nommées dès qu'un second marché est retenu", () => {
    // `lists` n'est chargée que pour la cible du sélecteur : la donner à une autre cible
    // produirait un 404 « Liste de mots-clés inconnue ».
    const current = target("US", "en");
    const plan = multiLaunchPlan({
      files,
      targets: [current],
      targetId: current.id,
      countries,
      choice: checking(["US", "GB"]),
      listName: "Lot",
      existingListId: "11111111-1111-4111-8111-111111111111",
    });
    expect(plan.imports.every((item) => "name" in item.list)).toBe(true);
  });

  it("nomme les fichiers qui ne portent aucun pays coché sans bloquer les autres", () => {
    const current = target("GB", "en");
    const plan = multiLaunchPlan({
      files,
      targets: [current],
      targetId: current.id,
      countries,
      choice: checking(["GB"]),
      listName: "Lot",
      existingListId: null,
    });
    expect(plan.blockers).toEqual([]);
    expect(plan.imports).toHaveLength(1);
    expect(plan.skipped).toEqual(["mangools.csv"]);
  });

  it("bloque une seule fois sur une langue manquante, quel que soit le nombre de fichiers", () => {
    const current = target("FR", "fr");
    const plan = multiLaunchPlan({
      files,
      targets: [current],
      targetId: current.id,
      countries,
      choice: (item) => ({ selected: item.code === "US", language: "" }),
      listName: "Lot",
      existingListId: null,
    });
    expect(plan.blockers.filter((item) => item.includes("États-Unis"))).toHaveLength(1);
    expect(plan.imports).toEqual([]);
  });

  it("bloque sur un nom de liste vide, et seulement quand une liste est à créer", () => {
    const current = target("US", "en");
    const empty = {
      files,
      targets: [current],
      targetId: current.id,
      countries,
      choice: checking(["US"]),
      listName: "   ",
    };
    expect(multiLaunchPlan({ ...empty, existingListId: null }).blockers).toContain(
      "Nommez la liste de destination.",
    );
    expect(
      multiLaunchPlan({ ...empty, existingListId: "11111111-1111-4111-8111-111111111111" })
        .blockers,
    ).toEqual([]);
  });

  it("dit, au pluriel, qu'aucun fichier ne porte le pays de la cible", () => {
    const current = target("FR", "fr");
    const plan = multiLaunchPlan({
      files,
      targets: [current],
      targetId: current.id,
      countries,
      choice: checking([]),
      listName: "Lot",
      existingListId: null,
    });
    expect(plan.droppedTarget).toBe("France · français");
    expect(plan.blockers[0]).toContain("Aucun de ces fichiers ne contient de ligne");
  });

  it("refuse de lancer un lot sans aucun fichier lisible", () => {
    const current = target("US", "en");
    const plan = multiLaunchPlan({
      files: [file("cc", { preview: null, error: "Format inconnu" })],
      targets: [current],
      targetId: current.id,
      countries: [],
      choice: checking([]),
      listName: "Lot",
      existingListId: null,
    });
    expect(plan.blockers[0]).toBe("Ajoutez au moins un fichier lisible.");
    expect(plan.imports).toEqual([]);
  });

  it("compte le plafond sur les couples distincts, pas sur les imports", () => {
    // Site à 19 cibles, deux fichiers visant le MÊME marché à créer : une somme naïve des
    // plans par fichier compterait deux créations et bloquerait à tort.
    const existing = Array.from({ length: MAX_TARGETS_PER_SITE - 1 }, (_, index) =>
      target(
        "FR",
        ["fr", "en", "de", "es", "it", "nl", "pl", "cs", "sv", "no"][index % 10] ?? "fr",
      ),
    );
    const plan = multiLaunchPlan({
      files,
      targets: existing,
      targetId: existing[0]?.id ?? "",
      countries,
      choice: checking(["US"]),
      listName: "Lot",
      existingListId: null,
    });
    expect(plan.imports).toHaveLength(2);
    expect(plan.creations).toBe(1);
    expect(plan.blockers).toEqual([]);
  });

  it("bloque quand même quand les couples distincts dépassent le plafond", () => {
    const existing = Array.from({ length: MAX_TARGETS_PER_SITE - 1 }, (_, index) =>
      target(
        "FR",
        ["fr", "en", "de", "es", "it", "nl", "pl", "cs", "sv", "no"][index % 10] ?? "fr",
      ),
    );
    const plan = multiLaunchPlan({
      files,
      targets: existing,
      targetId: existing[0]?.id ?? "",
      countries,
      choice: checking(["US", "GB"]),
      listName: "Lot",
      existingListId: null,
    });
    expect(plan.creations).toBe(2);
    expect(plan.blockers).toContain(
      `Ce site atteindrait le plafond de ${MAX_TARGETS_PER_SITE} cibles (19 existantes, 2 à créer) : décochez des pays.`,
    );
  });

  it("mêle un export sans recensement et un export recensé sans les faire mentir", () => {
    // Le cas réel le plus probable : un Semrush (aucun pays dans le fichier) déposé avec un
    // KWFinder multi-marchés. Le Semrush vise la cible du sélecteur, le KWFinder ses pays
    // cochés — et c'est le nombre total de cibles qui décide du suffixe, pas chaque fichier.
    const current = target("US", "en");
    const mixed = [
      file("aa", { filename: "semrush.csv", preview: preview({ countries: [] }) }),
      file("bb", { filename: "mangools.csv", preview: preview({ countries: [US, GB] }) }),
    ];
    const merged = mergedCensus(mixed).countries;
    const plan = multiLaunchPlan({
      files: mixed,
      targets: [current],
      targetId: current.id,
      countries: merged,
      choice: checking(["US", "GB"]),
      listName: "Lot",
      existingListId: null,
    });
    expect(plan.imports.map((item) => [item.filename, item.country, item.list])).toEqual([
      ["semrush.csv", "US", { name: "Lot — US" }],
      ["mangools.csv", "US", { name: "Lot — US" }],
      ["mangools.csv", "GB", { name: "Lot — GB" }],
    ]);
    // Le couple « principal » du Semrush est le même que le US du KWFinder : une seule cible
    // existante, une seule à créer.
    expect(plan.creations).toBe(1);
    expect(plan.skipped).toEqual([]);
  });

  it("garde un nom sans suffixe quand le Semrush et le KWFinder visent le même marché", () => {
    const current = target("US", "en");
    const mixed = [
      file("aa", { filename: "semrush.csv", preview: preview({ countries: [] }) }),
      file("bb", { filename: "mangools.csv", preview: preview({ countries: [US] }) }),
    ];
    const plan = multiLaunchPlan({
      files: mixed,
      targets: [current],
      targetId: current.id,
      countries: mergedCensus(mixed).countries,
      choice: checking(["US"]),
      listName: "Lot",
      existingListId: null,
    });
    expect(plan.imports.map((item) => item.list)).toEqual([{ name: "Lot" }, { name: "Lot" }]);
  });

  it("bloque le lot sur un fichier dont la colonne pays ne résout rien, et dit lequel", () => {
    const current = target("US", "en");
    const broken = file("cc", {
      filename: "kwfinder-villes.csv",
      preview: preview({ countries: [country({ raw: "New York", rows: 40 })] }),
    });
    const batch = [files[0] as ImportFile, broken];
    const plan = multiLaunchPlan({
      files: batch,
      targets: [current],
      targetId: current.id,
      countries: mergedCensus(batch).countries,
      choice: checking(["US"]),
      listName: "Lot",
      existingListId: null,
    });
    // L'autre fichier reste planifié — c'est le blocage qui empêche le départ, pas l'oubli.
    expect(plan.imports.map((item) => item.filename)).toEqual(["semrush.csv"]);
    expect(plan.unreadable.map((item) => item.sha256)).toEqual(["cc"]);
    expect(plan.blockers[0]).toContain("kwfinder-villes.csv");
    expect(plan.blockers[0]).toContain("« New York »");
  });

  it("ne préfixe pas du nom de fichier quand il n'y en a qu'un", () => {
    const current = target("US", "en");
    const broken = [
      file("cc", {
        filename: "kwfinder-villes.csv",
        preview: preview({ countries: [country({ raw: "New York", rows: 40 })] }),
      }),
    ];
    const plan = multiLaunchPlan({
      files: broken,
      targets: [current],
      targetId: current.id,
      countries: mergedCensus(broken).countries,
      choice: checking([]),
      listName: "Lot",
      existingListId: null,
    });
    expect(plan.imports).toEqual([]);
    expect(plan.blockers).toHaveLength(1);
    expect(plan.blockers[0]?.startsWith("Aucun pays reconnu")).toBe(true);
  });

  it("laisse un fichier sans recensement viser la cible du sélecteur, comme avant", () => {
    const current = target("FR", "fr");
    const plan = multiLaunchPlan({
      files: [file("aa", { filename: "semrush.csv" })],
      targets: [current],
      targetId: current.id,
      countries: [],
      choice: checking([]),
      listName: "Semrush — 2026-09-02",
      existingListId: null,
    });
    expect(plan.imports).toHaveLength(1);
    expect(plan.imports[0]?.targetId).toBe(current.id);
    expect(plan.imports[0]?.list).toEqual({ name: "Semrush — 2026-09-02" });
  });
});

describe("destination : chercher, ou créer ce qu'on tape", () => {
  const lists = [list("l1", "Mangools — 2026-09-01"), list("l2", "Été 2026", 3)];

  it("propose tout le catalogue sur une saisie vide", () => {
    expect(listSuggestions(lists, "  ").map((item) => item.id)).toEqual(["l1", "l2"]);
  });

  it("filtre par sous-chaîne, sans accents ni casse", () => {
    expect(listSuggestions(lists, "ETE").map((item) => item.id)).toEqual(["l2"]);
    expect(listSuggestions(lists, "mangools").map((item) => item.id)).toEqual(["l1"]);
    expect(listSuggestions(lists, "rien")).toEqual([]);
  });

  it("rend la liste explicitement choisie", () => {
    expect(resolveDestination(lists, "l1", "Mangools — 2026-09-01")?.id).toBe("l1");
  });

  it("rend la liste dont le nom a été tapé à l'identique, sans l'avoir choisie", () => {
    // Sans cela, écrire ce qu'on voit à l'écran vaudrait un 409 « une liste « X » existe déjà ».
    expect(resolveDestination(lists, null, "  été 2026 ")?.id).toBe("l2");
  });

  it("rend null dès que la saisie s'écarte : c'est une liste à créer", () => {
    expect(resolveDestination(lists, null, "Été 2027")).toBeNull();
    expect(resolveDestination(lists, null, "")).toBeNull();
    // Un choix explicite que la frappe a dépassé ne survit pas non plus.
    expect(resolveDestination(lists, "l2", "Été 2026 bis")).toBeNull();
  });
});
