import { describe, expect, it } from "bun:test";
import { planListImport, resolveListCountry } from "./list-import";
import type { MangoolsKeywordItem, MangoolsListDetail } from "./types";

const NOW = "2026-07-28T00:00:00.000Z";
const SITE = {
  id: "site_1",
  primaryLanguage: "fr",
  targetCountries: ["FR"],
};

/** Série `msv` de 13 points bornée sur le MÊME mois calendaire (janvier 2025 →
 *  janvier 2026) : c'est la seule forme que `growthFromMsv` accepte, sinon il
 *  rend `null` (garde YoY). Croissance = (last − first) / first × 100. */
function msv13(first: number, last: number): [number, number, number][] {
  const points: [number, number, number][] = [[2025, 0, first]];
  for (let month = 1; month <= 11; month++) points.push([2025, month, first]);
  points.push([2026, 0, last]);
  return points;
}

function kw(
  kwText: string,
  over: Partial<MangoolsKeywordItem> = {},
): MangoolsKeywordItem {
  // `Object.assign` plutôt qu'un spread : sous `exactOptionalPropertyTypes`, un
  // spread de `Partial<T>` réintroduit `undefined` sur chaque champ.
  return Object.assign(
    {
      _id: `kw_${kwText}`,
      kw: kwText,
      lid: 2840,
      country: "FR" as string | null,
      sv: 1000 as number | null,
      seo: 32 as number | null,
      cpc: 1.5 as number | null,
      ppc: null,
      msv: msv13(100, 150) as [number, number, number][] | null,
    },
    over,
  );
}

function detail(
  keywords: MangoolsKeywordItem[],
  countries: string[],
): MangoolsListDetail {
  return {
    list: {
      id: "list_1",
      name: "Bagues",
      keywordCount: keywords.length,
      createdAt: NOW,
      updatedAt: NOW,
    },
    keywords,
    countries,
  };
}

describe("resolveListCountry", () => {
  it("accepte un pays unique présent dans les cibles du site", () => {
    expect(resolveListCountry(["US"], ["US", "FR"])).toEqual({
      kind: "ok",
      country: "US",
    });
  });

  it("signale un pays hors des cibles du site plutôt que de le taire", () => {
    expect(resolveListCountry(["US"], ["FR"])).toEqual({
      kind: "off-target",
      country: "US",
      siteCountries: ["FR"],
    });
  });

  it("refuse de choisir quand la liste mélange plusieurs pays", () => {
    expect(resolveListCountry(["US", "FR"], ["FR"])).toEqual({
      kind: "mixed",
      countries: ["FR", "US"],
    });
  });

  it("rend unknown quand aucun pays n'est lisible", () => {
    expect(resolveListCountry([], ["FR"])).toEqual({ kind: "unknown" });
  });

  it("normalise la casse et les doublons avant de trancher", () => {
    expect(resolveListCountry([" us ", "US"], ["us"])).toEqual({
      kind: "ok",
      country: "US",
    });
  });

  it("écarte les codes vides — une liste qui n'en porte que rend unknown", () => {
    expect(resolveListCountry(["", "  "], ["FR"])).toEqual({ kind: "unknown" });
  });

  it("dit off-target quand le site n'a AUCUN pays cible", () => {
    expect(resolveListCountry(["FR"], [])).toEqual({
      kind: "off-target",
      country: "FR",
      siteCountries: [],
    });
  });
});

describe("planListImport — le pays vient des mots-clés", () => {
  it("importe sur le pays de la LISTE, pas sur celui du site", () => {
    const plan = planListImport({
      detail: detail([kw("bague or")], ["US"]),
      site: SITE,
      themeSlug: null,
      now: NOW,
    });
    expect(plan.verdict).toEqual({
      kind: "off-target",
      country: "US",
      siteCountries: ["FR"],
    });
    // Importé quand même — mais sur "US", jamais sur le repli "FR" du site.
    expect(plan.rows).toHaveLength(1);
    expect(plan.rows[0]?.country).toBe("US");
    expect(plan.rows[0]?.lang).toBe("fr");
  });

  it("ne tranche pas une liste multi-pays : aucune ligne", () => {
    const plan = planListImport({
      detail: detail([kw("bague or"), kw("gold ring")], ["FR", "US"]),
      site: SITE,
      themeSlug: null,
      now: NOW,
    });
    expect(plan.verdict).toEqual({ kind: "mixed", countries: ["FR", "US"] });
    expect(plan.rows).toEqual([]);
    expect(plan.skipped).toBe(2);
  });

  it("ne devine pas non plus quand aucun pays n'est lisible", () => {
    const plan = planListImport({
      detail: detail([kw("bague or")], []),
      site: SITE,
      themeSlug: null,
      now: NOW,
    });
    expect(plan.verdict).toEqual({ kind: "unknown" });
    expect(plan.rows).toEqual([]);
    expect(plan.skipped).toBe(1);
  });
});

describe("planListImport — mapping", () => {
  it("rend une ligne complète, croissance comprise (la série msv est là)", () => {
    const plan = planListImport({
      detail: detail([kw("Bague Or")], ["FR"]),
      site: SITE,
      themeSlug: "bagues",
      now: NOW,
    });
    expect(plan.verdict).toEqual({ kind: "ok", country: "FR" });
    const row = plan.rows[0];
    expect(row).toBeDefined();
    expect(row?.id).toBe("site_1-bague-or");
    expect(row?.siteId).toBe("site_1");
    expect(row?.term).toBe("Bague Or");
    // `theme` porte le slug libre du site ; la colonne est typée sur l'union
    // héritée `KeywordTheme`, d'où l'élargissement pour l'assertion.
    expect(row?.theme as string | null).toBe("bagues");
    expect(row?.status).toBe("idea");
    expect(row?.primarySource).toBe("mangools");
    expect(row?.sources).toEqual(["mangools"]);
    expect(row?.stages).toEqual([]);
    expect(row?.metrics).toEqual({
      mangools: {
        volume: 1000,
        kd: 32,
        cpc: 1.5,
        // C'est tout l'intérêt de passer par `metricsFromMangools` : le chemin
        // du dialog pose `growthPct: null` faute de série msv, pas celui-ci.
        growthPct: 50,
        competitiveDensity: null,
        numberOfResults: null,
        updatedAt: NOW,
      },
    });
    expect(row?.trend).toHaveLength(13);
    expect(row?.trend?.[12]).toEqual({ month: "2026-01", volume: 150 });
    expect(row?.trendBySource).toEqual({ mangools: row?.trend ?? [] });
  });

  it("pose theme à null quand aucun thème n'est demandé", () => {
    const plan = planListImport({
      detail: detail([kw("bague or")], ["FR"]),
      site: SITE,
      themeSlug: null,
      now: NOW,
    });
    expect(plan.rows[0]?.theme).toBeNull();
  });

  it("range dans la Recherche quand la destination le demande", () => {
    const plan = planListImport({
      detail: detail([kw("bague or")], ["FR"]),
      site: SITE,
      themeSlug: null,
      now: NOW,
      destination: "recherche",
    });
    expect(plan.rows[0]?.stages).toEqual(["research"]);
  });

  it("écarte un terme vide", () => {
    const plan = planListImport({
      detail: detail([kw("   "), kw("bague or")], ["FR"]),
      site: SITE,
      themeSlug: null,
      now: NOW,
    });
    expect(plan.rows).toHaveLength(1);
    expect(plan.rows[0]?.term).toBe("bague or");
    expect(plan.skipped).toBe(1);
  });

  it("écarte un terme de plus de 512 caractères", () => {
    const plan = planListImport({
      detail: detail([kw("a".repeat(513)), kw("bague or")], ["FR"]),
      site: SITE,
      themeSlug: null,
      now: NOW,
    });
    expect(plan.rows).toHaveLength(1);
    expect(plan.rows[0]?.term).toBe("bague or");
    expect(plan.skipped).toBe(1);
  });

  it("écarte un terme qui ne produit aucun slug", () => {
    const plan = planListImport({
      detail: detail([kw("!!!"), kw("bague or")], ["FR"]),
      site: SITE,
      themeSlug: null,
      now: NOW,
    });
    expect(plan.rows).toHaveLength(1);
    expect(plan.skipped).toBe(1);
  });

  it("écarte une métrique non finie ou négative", () => {
    const plan = planListImport({
      detail: detail(
        [
          kw("volume absurde", { sv: Number.NaN }),
          kw("cpc negatif", { cpc: -1 }),
          kw("kd infini", { seo: Number.POSITIVE_INFINITY }),
          kw("bague or"),
        ],
        ["FR"],
      ),
      site: SITE,
      themeSlug: null,
      now: NOW,
    });
    expect(plan.rows).toHaveLength(1);
    expect(plan.rows[0]?.term).toBe("bague or");
    expect(plan.skipped).toBe(3);
  });

  it("tolère des métriques absentes (null → 0 au stockage)", () => {
    const plan = planListImport({
      detail: detail(
        [kw("bague or", { sv: null, seo: null, cpc: null })],
        ["FR"],
      ),
      site: SITE,
      themeSlug: null,
      now: NOW,
    });
    expect(plan.rows[0]?.metrics).toEqual({
      mangools: {
        volume: 0,
        kd: 0,
        cpc: 0,
        growthPct: 50,
        competitiveDensity: null,
        numberOfResults: null,
        updatedAt: NOW,
      },
    });
  });

  it("déduplique deux termes qui donnent le même identifiant", () => {
    const plan = planListImport({
      detail: detail([kw("Bague Or"), kw("bague  or")], ["FR"]),
      site: SITE,
      themeSlug: null,
      now: NOW,
    });
    expect(plan.rows).toHaveLength(1);
    // Le PREMIER gagne, comme dans `addRelatedKeywordsAction`.
    expect(plan.rows[0]?.term).toBe("Bague Or");
    expect(plan.skipped).toBe(1);
  });

  it("rend une liste vide sans rien inventer", () => {
    const plan = planListImport({
      detail: detail([], ["FR"]),
      site: SITE,
      themeSlug: null,
      now: NOW,
    });
    expect(plan.verdict).toEqual({ kind: "ok", country: "FR" });
    expect(plan.rows).toEqual([]);
    expect(plan.skipped).toBe(0);
  });

  it("replie la langue sur « en » quand le site n'en déclare aucune", () => {
    const plan = planListImport({
      detail: detail([kw("gold ring")], ["US"]),
      site: { id: "site_2", primaryLanguage: null, targetCountries: ["US"] },
      themeSlug: null,
      now: NOW,
    });
    expect(plan.rows[0]?.lang).toBe("en");
  });
});
