// src/lib/mangools/serp-fetch.test.ts — SERP auto via Mangools (Task 9).
// Intégration gated DB_TESTS=1 (*_test DB uniquement) : la lecture du mot-clé
// et le merge jsonb du snapshot passent par la VRAIE DB de test ; le reste
// (token, résolution location, client Mangools) est injecté par DI — zéro
// réseau, `mock.module` proscrit pour la DI (mémoire projet). On vérifie :
// happy path (snapshot mergé sur la BONNE locale sans écraser les autres),
// no_connector (token absent), no_location (résolution nulle), quota
// (quota_exceeded), unauthorized (→ connecteur invalidé + no_connector).
import { beforeEach, describe, expect, mock, test } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";

mock.module("server-only", () => ({}));
mock.module("next/cache", () => ({
  revalidatePath: () => {},
  revalidateTag: () => {},
  updateTag: () => {},
  unstable_cache: (fn: unknown) => fn,
}));

// MangoolsApiError est ré-importé pour de vrai : les tests quota/unauthorized
// lèvent la vraie classe pour que le `instanceof` du module la reconnaisse.
const { MangoolsApiError } = await import("./client");
const { fetchSerpViaMangools } = await import("./serp-fetch");

import type { SerpSnapshot } from "@/lib/keywords/serp-types";
import type { MangoolsSerpResponse } from "./types";

function loadFixture(name: string): MangoolsSerpResponse {
  return JSON.parse(
    readFileSync(join(import.meta.dir, "fixtures", name), "utf8"),
  ) as MangoolsSerpResponse;
}

/** Snapshot SE-Ranking préexistant sur une AUTRE locale (US) — sert à prouver
 *  que le merge ne clobbe pas les locales voisines. */
const US_SNAPSHOT: SerpSnapshot = {
  source: "se-ranking",
  importedAt: "2026-07-01T00:00:00.000Z",
  features: [],
  competitors: [
    {
      position: 1,
      domain: "us-existing.com",
      url: "https://us-existing.com",
      backlinks: 0,
      refDomains: 0,
      pageAuthority: 0,
      searchTraffic: 0,
      urlKeywords: 0,
      title: null,
      metaDescription: null,
      lang: null,
      images: null,
      imagesWithoutAlt: null,
      imageAlts: null,
      missingAltSrcs: null,
      hasJsonLd: null,
      jsonLdTypes: null,
      crawledAt: null,
    },
  ],
};

const RUN_DB = process.env.DB_TESTS === "1";

describe.skipIf(!RUN_DB)("fetchSerpViaMangools (postgres)", () => {
  let db: typeof import("@/lib/db/client").db;
  let schema: typeof import("@/lib/db/domain-schema");
  let eq: typeof import("drizzle-orm").eq;

  /** Deps par défaut d'un test : token présent, location résolue, client qui
   *  renvoie la fixture SERP. Chaque test peut surcharger un champ. */
  function makeDeps(
    overrides?: Partial<Parameters<typeof fetchSerpViaMangools>[1]>,
  ) {
    const invalidated: string[] = [];
    const deps = {
      getToken: async () => "tok-mangools",
      markInvalid: async (orgId: string) => {
        invalidated.push(orgId);
      },
      createClient: () =>
        ({
          serps: async () => loadFixture("fixture-serps.json"),
        }) as never,
      resolveLocation: async () => 2840,
      now: () => "2026-07-25T00:00:00.000Z",
      ...overrides,
    };
    return { deps, invalidated };
  }

  async function readSerp(id: string): Promise<Record<string, SerpSnapshot>> {
    const [row] = await db
      .select({ serp: schema.keywords.serp })
      .from(schema.keywords)
      .where(eq(schema.keywords.id, id));
    return (row?.serp ?? {}) as Record<string, SerpSnapshot>;
  }

  beforeEach(async () => {
    ({ db } = await import("@/lib/db/client"));
    schema = await import("@/lib/db/domain-schema");
    ({ eq } = await import("drizzle-orm"));
    const { assertTestDb } = await import("@/lib/db/assert-test-db");
    await assertTestDb();

    await db.delete(schema.keywords);
    await db.delete(schema.sites);
    await db.insert(schema.sites).values({
      id: "site-a",
      orgId: "demo-org",
      ownerUserId: "seed-owner",
      name: "Site A",
      primaryLanguage: "fr",
      languages: [],
      targetCountries: ["FR"],
    });
    await db.insert(schema.keywords).values({
      id: "kw-1",
      siteId: "site-a",
      term: "bague de fiancailles",
      lang: "fr",
      country: "FR",
      status: "active",
      intent: ["commercial"],
      primarySource: "mangools",
      sources: ["mangools"],
      metrics: {},
      trend: [],
      // Snapshot US préexistant : le merge FR ne doit PAS l'effacer.
      serp: { US: US_SNAPSHOT },
    });
  });

  test("happy path : snapshot mergé sur la locale FR, US préservé", async () => {
    const { deps } = makeDeps();
    const res = await fetchSerpViaMangools("kw-1", deps);
    expect(res).toEqual({ ok: true });

    const serp = await readSerp("kw-1");
    // La locale du mot-clé (FR) porte le nouveau snapshot Mangools.
    expect(serp.FR?.source).toBe("mangools");
    expect(serp.FR?.importedAt).toBe("2026-07-25T00:00:00.000Z");
    expect(Array.isArray(serp.FR?.competitors)).toBe(true);
    // La locale voisine (US) n'a pas été clobbée par le merge jsonb.
    expect(serp.US?.source).toBe("se-ranking");
    expect(serp.US?.competitors[0]?.domain).toBe("us-existing.com");
  });

  test("no_connector : token absent → aucune écriture", async () => {
    const { deps } = makeDeps({ getToken: async () => null });
    const res = await fetchSerpViaMangools("kw-1", deps);
    expect(res).toEqual({ ok: false, error: "no_connector" });
    const serp = await readSerp("kw-1");
    expect(serp.FR).toBeUndefined();
  });

  test("no_location : résolution nulle → aucune écriture", async () => {
    const { deps } = makeDeps({ resolveLocation: async () => null });
    const res = await fetchSerpViaMangools("kw-1", deps);
    expect(res).toEqual({ ok: false, error: "no_location" });
    const serp = await readSerp("kw-1");
    expect(serp.FR).toBeUndefined();
  });

  test("quota : quota_exceeded → error:quota, aucune écriture", async () => {
    const { deps } = makeDeps({
      createClient: () =>
        ({
          serps: async () => {
            throw new MangoolsApiError("quota_exceeded", 402);
          },
        }) as never,
    });
    const res = await fetchSerpViaMangools("kw-1", deps);
    expect(res).toEqual({ ok: false, error: "quota" });
    const serp = await readSerp("kw-1");
    expect(serp.FR).toBeUndefined();
  });

  test("unauthorized : connecteur invalidé + no_connector, aucune écriture", async () => {
    const { deps, invalidated } = makeDeps({
      createClient: () =>
        ({
          serps: async () => {
            throw new MangoolsApiError("unauthorized", 401);
          },
        }) as never,
    });
    const res = await fetchSerpViaMangools("kw-1", deps);
    expect(res).toEqual({ ok: false, error: "no_connector" });
    // Le connecteur de l'org du mot-clé a été marqué invalide.
    expect(invalidated).toEqual(["demo-org"]);
    const serp = await readSerp("kw-1");
    expect(serp.FR).toBeUndefined();
  });
});
