import { describe, expect, it } from "vitest";
import {
  census,
  defaultChoice,
  launchPlan,
  MAX_TARGETS_PER_SITE,
  type CensusCountry,
  type CountryChoice,
} from "./import-countries";
import type { CountryCount } from "./imports";
import type { Target } from "./sites";

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

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

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

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

describe("census", () => {
  it("garde l'ordre de l'api et sépare les libellés non résolus", () => {
    const { countries, unresolved } = census([
      entry({ raw: "United States", code: "US", label: "États-Unis", rows: 74 }),
      entry({ raw: "United Kingdom", code: "GB", label: "Royaume-Uni", rows: 55 }),
      entry({ raw: "New York", rows: 3 }),
    ]);
    // L'ordre rendu par l'api (lignes décroissantes) EST l'ordre d'affichage : rien n'est retrié.
    expect(countries.map((country) => country.code)).toEqual(["US", "GB"]);
    expect(unresolved).toEqual([{ raw: "New York", label: "New York", rows: 3 }]);
  });

  it("fusionne deux libellés bruts qui rendent le même code", () => {
    // Le contrat de l'api est « une entrée par libellé BRUT » : sans fusion, cocher les deux
    // créerait la cible une fois puis prendrait un 409 sur la seconde.
    const { countries } = census([
      entry({ raw: "United States", code: "US", label: "États-Unis", rows: 74 }),
      entry({ raw: "USA", code: "us", label: "États-Unis", rows: 6, suggested_language: "EN" }),
    ]);
    expect(countries).toHaveLength(1);
    expect(countries[0]?.rows).toBe(80);
    expect(countries[0]?.rawLabels).toEqual(["United States", "USA"]);
    // Casse forcée dans les deux sens : `^[A-Z]{2}$` pour le pays, `^[a-z]{2}$` pour la langue.
    expect(countries[0]?.code).toBe("US");
    expect(countries[0]?.suggestedLanguage).toBe("en");
  });

  it("rend un recensement vide sans lever quand l'api n'en sert aucun", () => {
    expect(census([])).toEqual({ countries: [], unresolved: [] });
  });
});

describe("defaultChoice", () => {
  const us: CensusCountry = {
    code: "US",
    label: "États-Unis",
    rawLabels: ["United States"],
    rows: 74,
    suggestedLanguage: "en",
  };
  const ph: CensusCountry = { ...us, code: "PH", label: "Philippines", suggestedLanguage: null };

  it("pré-coche le pays de la cible courante, dans SA langue", () => {
    const current = target("US", "es");
    expect(defaultChoice(us, [current], current)).toEqual({ selected: true, language: "es" });
  });

  it("propose la langue d'une cible existante du site plutôt que celle de CLDR", () => {
    const current = target("FR", "fr");
    const other = target("US", "es");
    expect(defaultChoice(us, [current, other], current)).toEqual({
      selected: false,
      language: "es",
    });
  });

  it("laisse la langue VIDE quand CLDR n'en propose aucune", () => {
    // 8 territoires ISO sur 249 (dont PH, que CLDR nomme `fil` — trois lettres, refusées par
    // `ck_site_targets_language`) : le Select doit accepter une proposition vide.
    expect(defaultChoice(ph, [], undefined)).toEqual({ selected: false, language: "" });
  });
});

describe("launchPlan — fichier sans recensement", () => {
  it("lance un seul import sur la cible du sélecteur, comme avant le cloisonnement", () => {
    const current = target("FR", "fr");
    const plan = launchPlan({
      targets: [current],
      targetId: current.id,
      countries: [],
      choice: checking([]),
      listName: "Semrush — 2026-09-01",
      existingListId: null,
    });
    expect(plan.blockers).toEqual([]);
    expect(plan.creations).toBe(0);
    expect(plan.imports).toEqual([
      {
        key: "principal",
        targetId: current.id,
        country: "FR",
        language: "fr",
        label: "France · français",
        // Aucun suffixe : un seul import, le nom saisi est le nom créé.
        list: { name: "Semrush — 2026-09-01" },
      },
    ]);
  });

  it("réutilise la liste existante choisie au lieu d'en créer une", () => {
    const current = target("FR", "fr");
    const plan = launchPlan({
      targets: [current],
      targetId: current.id,
      countries: [],
      choice: checking([]),
      listName: "",
      existingListId: "11111111-1111-4111-8111-111111111111",
    });
    expect(plan.imports[0]?.list).toEqual({ id: "11111111-1111-4111-8111-111111111111" });
    // Le nom vide ne bloque rien quand aucune liste n'est à créer.
    expect(plan.blockers).toEqual([]);
  });

  it("refuse de lancer quand la colonne pays ne résout RIEN", () => {
    // §4.3 : « colonne absente » et « colonne illisible » ne sont pas la même chose. Sans
    // cette distinction, un export dont la colonne Location ne porte que des localités
    // retombait sur l'import principal — un job dont on savait déjà qu'il ignorerait toutes
    // les lignes, pour un rapport à zéro insérée que personne n'aurait su expliquer.
    const current = target("FR", "fr");
    const { unresolved } = census([
      entry({ raw: "New York", rows: 40 }),
      entry({ raw: "Greater London", rows: 12 }),
    ]);
    const plan = launchPlan({
      targets: [current],
      targetId: current.id,
      countries: [],
      unresolved,
      choice: checking([]),
      listName: "Semrush — 2026-09-02",
      existingListId: null,
    });
    expect(plan.imports).toEqual([]);
    expect(plan.blockers).toHaveLength(1);
    expect(plan.blockers[0]).toBe(
      "Aucun pays reconnu dans la colonne pays : 2 valeurs non reconnues (« New York », « Greater London »). Corrigez la colonne, ou retirez ce fichier du lot.",
    );
  });

  it("laisse passer un fichier sans colonne pays, lui", () => {
    // Le discriminant est bien `unresolved`, pas « countries vide » : Semrush n'a pas de
    // colonne pays et doit continuer à s'importer sur la cible du sélecteur.
    const current = target("FR", "fr");
    const plan = launchPlan({
      targets: [current],
      targetId: current.id,
      countries: [],
      unresolved: [],
      choice: checking([]),
      listName: "Semrush — 2026-09-02",
      existingListId: null,
    });
    expect(plan.blockers).toEqual([]);
    expect(plan.imports).toHaveLength(1);
  });

  it("bloque sur un nom de liste vide", () => {
    const current = target("FR", "fr");
    const plan = launchPlan({
      targets: [current],
      targetId: current.id,
      countries: [],
      choice: checking([]),
      listName: "   ",
      existingListId: null,
    });
    expect(plan.blockers).toContain("Nommez la liste de destination.");
  });
});

describe("launchPlan — fichier recensé", () => {
  const countries = census([
    entry({
      raw: "United States",
      code: "US",
      label: "États-Unis",
      rows: 74,
      suggested_language: "en",
    }),
    entry({
      raw: "United Kingdom",
      code: "GB",
      label: "Royaume-Uni",
      rows: 55,
      suggested_language: "en",
    }),
    entry({ raw: "Australia", code: "AU", label: "Australie", rows: 32, suggested_language: "en" }),
    entry({ raw: "New York", rows: 3 }),
  ]).countries;

  it("lance un job par pays coché et suffixe chaque liste de son pays", () => {
    const current = target("US", "en");
    const plan = launchPlan({
      targets: [current],
      targetId: current.id,
      countries,
      choice: (country) => ({
        selected: ["US", "GB", "AU"].includes(country.code),
        language: country.suggestedLanguage ?? "",
      }),
      listName: "Mangools — 2026-09-01",
      existingListId: null,
    });
    expect(plan.blockers).toEqual([]);
    expect(plan.imports.map((item) => [item.key, item.targetId])).toEqual([
      ["US", current.id],
      ["GB", null],
      ["AU", null],
    ]);
    // Sans suffixe, les trois listes porteraient le même nom et seraient indiscernables.
    expect(plan.imports.map((item) => item.list)).toEqual([
      { name: "Mangools — 2026-09-01 — US" },
      { name: "Mangools — 2026-09-01 — GB" },
      { name: "Mangools — 2026-09-01 — AU" },
    ]);
    // Deux cibles à créer, la troisième réutilisée : c'est ce nombre-là qui compte au plafond.
    expect(plan.creations).toBe(2);
  });

  it("réutilise une cible existante plutôt que d'aller chercher un 409", () => {
    // Le 409 de `POST /sites/{id}/targets` NE PORTE PAS l'identifiant de la cible existante :
    // l'encaisser n'apprendrait rien, la cible doit être trouvée avant.
    const current = target("FR", "fr");
    const british = target("GB", "en");
    const plan = launchPlan({
      targets: [current, british],
      targetId: current.id,
      countries,
      choice: checking(["GB"]),
      listName: "Mangools",
      existingListId: null,
    });
    expect(plan.imports).toHaveLength(1);
    expect(plan.imports[0]?.targetId).toBe(british.id);
    expect(plan.creations).toBe(0);
    // Un seul import : le nom reste celui qui a été saisi.
    expect(plan.imports[0]?.list).toEqual({ name: "Mangools" });
  });

  it("distingue en-GB de fr-GB : même pays, autre langue, autre cible", () => {
    const british = target("GB", "en");
    const plan = launchPlan({
      targets: [british],
      targetId: british.id,
      countries,
      choice: (country) => ({ selected: country.code === "GB", language: "fr" }),
      listName: "Mangools",
      existingListId: null,
    });
    expect(plan.imports).toHaveLength(1);
    expect(plan.imports[0]?.targetId).toBeNull();
    expect(plan.imports[0]?.label).toBe("Royaume-Uni · français");
  });

  it("bloque tant qu'un pays coché n'a pas de langue", () => {
    const current = target("FR", "fr");
    const plan = launchPlan({
      targets: [current],
      targetId: current.id,
      countries,
      choice: (country) => ({ selected: country.code === "GB", language: "" }),
      listName: "Mangools",
      existingListId: null,
    });
    expect(plan.blockers).toContain("Choisissez une langue pour Royaume-Uni.");
    expect(plan.imports).toEqual([]);
  });

  it("bloque, en le disant, quand le fichier ne porte aucune ligne du pays de la cible", () => {
    // Q12 : l'avertissement bloquant vit ICI, sur la foi de l'aperçu — l'api ne refuse rien et
    // rendrait un job à zéro ligne, ce qui n'apprend rien à personne.
    const current = target("FR", "fr");
    const plan = launchPlan({
      targets: [current],
      targetId: current.id,
      countries,
      choice: checking([]),
      listName: "Mangools",
      existingListId: null,
    });
    expect(plan.droppedTarget).toBe("France · français");
    expect(plan.blockers[0]).toBe(
      "Le fichier ne contient aucune ligne pour France · français : cet import serait vide. Cochez un pays présent dans le fichier.",
    );
    expect(plan.imports).toEqual([]);
  });

  it("laisse lancer les pays cochés même quand la cible du sélecteur est absente du fichier", () => {
    const current = target("FR", "fr");
    const plan = launchPlan({
      targets: [current],
      targetId: current.id,
      countries,
      choice: checking(["GB"]),
      listName: "Mangools",
      existingListId: null,
    });
    // La cible est écartée — dit à l'écran —, pas la session : sans cette sortie, l'utilisateur
    // dont la cible par défaut est la France ne pourrait jamais importer un fichier britannique.
    expect(plan.droppedTarget).toBe("France · français");
    expect(plan.blockers).toEqual([]);
    expect(plan.imports.map((item) => item.key)).toEqual(["GB"]);
  });

  it("compte le plafond de cibles AVANT de boucler", () => {
    // Sinon le 422 tombe au milieu de la boucle, cibles à moitié créées.
    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 = launchPlan({
      targets: existing,
      targetId: existing[0]?.id ?? "",
      countries,
      choice: checking(["US", "GB"]),
      listName: "Mangools",
      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("ne planifie jamais deux fois le même couple (pays, langue)", () => {
    const current = target("US", "en");
    const doubled = census([
      entry({
        raw: "United States",
        code: "US",
        label: "États-Unis",
        rows: 74,
        suggested_language: "en",
      }),
    ]).countries;
    const plan = launchPlan({
      targets: [current],
      targetId: current.id,
      countries: [...doubled, ...doubled],
      choice: checking(["US"]),
      listName: "Mangools",
      existingListId: null,
    });
    expect(plan.imports).toHaveLength(1);
  });

  it("réutilise la liste existante quand un seul import vise la cible du sélecteur", () => {
    // `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 = launchPlan({
      targets: [current],
      targetId: current.id,
      countries,
      choice: checking(["US"]),
      listName: "Mangools",
      existingListId: "11111111-1111-4111-8111-111111111111",
    });
    expect(plan.imports[0]?.list).toEqual({ id: "11111111-1111-4111-8111-111111111111" });
  });

  it("retombe sur des listes nommées dès qu'un second pays est retenu", () => {
    // L'écran cache alors le sélecteur de liste existante et annonce « une liste par cible » :
    // le plan ne doit pas le démentir en glissant quand même la liste choisie sur l'une d'elles.
    const current = target("US", "en");
    const plan = launchPlan({
      targets: [current],
      targetId: current.id,
      countries,
      choice: checking(["US", "GB"]),
      listName: "Mangools",
      existingListId: "11111111-1111-4111-8111-111111111111",
    });
    expect(plan.imports.map((item) => item.list)).toEqual([
      { name: "Mangools — US" },
      { name: "Mangools — GB" },
    ]);
  });
});
