import { test, expect } from "@playwright/test";
import { patchJson } from "./helpers.js";

/**
 * Couverture de chaque OUTIL du dashboard, de bout en bout.
 *
 * admin.spec.js décrit les parcours principaux ; ce fichier complète les
 * commandes qui n'y étaient pas exercées — tri, pagination, arbitrage de
 * conflit, annulations, filtres LLM — pour qu'aucun bouton ne reste sans test.
 */

test.describe("Cycle de vie des vues", () => {
  // Régression : le minuteur d'auto-refresh était armé APRÈS le chargement.
  // Quitter la vue pendant celui-ci laissait le Pilotage interroger quatre
  // endpoints toutes les 15 s depuis un autre onglet, indéfiniment.
  test("quitter une vue pendant son chargement n'arme pas de minuteur orphelin", async ({ page }) => {
    await page.goto("/");
    await page.route("**/admin/api/live", async (route) => {
      await new Promise((r) => setTimeout(r, 800));
      await route.continue();
    });

    await page.click('.nav a[data-view="pilotage"]');
    await page.waitForTimeout(150);          // le chargement est en cours
    await page.click('.nav a[data-view="bands"]');
    await page.waitForTimeout(1200);         // le chargement du Pilotage se termine

    await expect(page.locator(".view-head h1")).toHaveText("Groupes");
    // La vue Groupes n'a pas d'auto-refresh : aucun minuteur ne doit subsister.
    expect(await page.evaluate("state.timer")).toBeNull();
  });

  test("changer de vue arrête l'auto-refresh de la précédente", async ({ page }) => {
    await page.goto("/#/activity");
    await expect(page.locator(".view-head h1")).toHaveText("Journal");
    expect(await page.evaluate("state.timer")).not.toBeNull();

    await page.click('.nav a[data-view="bands"]');
    await expect(page.locator(".view-head h1")).toHaveText("Groupes");
    expect(await page.evaluate("state.timer")).toBeNull();
  });
});

test.describe("Groupes — tri et pagination", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/");
    await page.click('.nav a[data-view="bands"]');
  });

  test("cliquer un en-tête trie, recliquer inverse le sens", async ({ page }) => {
    const asc = page.waitForRequest((r) => r.url().includes("sort=name") && r.url().includes("dir=asc"));
    await page.locator('.th-sort[data-sort="name"]').click();
    await asc;

    const desc = page.waitForRequest((r) => r.url().includes("sort=name") && r.url().includes("dir=desc"));
    await page.locator('.th-sort[data-sort="name"]').click();
    await desc;
  });

  test("changer de colonne repart en ordre croissant", async ({ page }) => {
    await page.locator('.th-sort[data-sort="name"]').click();
    await page.locator('.th-sort[data-sort="name"]').click();   // desc
    const req = page.waitForRequest((r) => r.url().includes("sort=country") && r.url().includes("dir=asc"));
    await page.locator('.th-sort[data-sort="country"]').click();
    await req;
  });

  test("la pagination est désactivée quand il n'y a qu'une page", async ({ page }) => {
    await expect(page.locator('#b-pager [data-page="prev"]')).toBeDisabled();
    await expect(page.locator('#b-pager [data-page="next"]')).toBeDisabled();
  });

  test("la page suivante est demandée avec le bon numéro", async ({ page }) => {
    await patchJson(page, "**/admin/api/bands?*", (body) => {
      body.total = 500;                       // force plusieurs pages
    });
    await page.click('.nav a[data-view="pilotage"]');
    await page.click('.nav a[data-view="bands"]');

    const req = page.waitForRequest((r) => r.url().includes("page=2"));
    await page.locator('#b-pager [data-page="next"]').click();
    await req;
  });

  test("« Tout effacer » remet la recherche et les filtres à zéro", async ({ page }) => {
    await page.fill("#b-q", "mayhem");
    await page.locator('.chip[data-chip="has_lat"]').click();
    await expect(page.locator('.chip[data-chip="has_lat"]')).toHaveAttribute("aria-pressed", "true");

    await page.click("#b-clear");
    await expect(page.locator("#b-q")).toHaveValue("");
    await expect(page.locator('.chip[data-chip="has_lat"]')).toHaveAttribute("aria-pressed", "false");
  });

  test("les filtres avancés sont transmis à l'API", async ({ page }) => {
    await page.click("#b-advanced-toggle");
    await page.fill("#b-country", "NO");
    await page.fill("#b-genre", "black");
    const req = page.waitForRequest((r) => r.url().includes("country=NO") && r.url().includes("genre=black"));
    await page.click("#b-apply");
    await req;
  });
});

test.describe("Groupes — arbitrage des conflits crawler", () => {
  /** Un groupe dont le crawler propose d'autres valeurs. */
  async function withConflicts(page) {
    // Seul le GET est modifié : le PATCH de sauvegarde doit continuer à
    // atteindre la vraie API.
    await patchJson(page, "**/admin/api/bands/1", (body) => {
      if (body.item) body.item.crawler_pending = { genre: "True Black Metal", country: "SE" };
    }, (route) => route.request().method() === "GET");
    await page.goto("/");
    await page.click('.nav a[data-view="bands"]');
    await page.locator('tr[data-ma-id="1"]').click();
    await expect(page.locator(".modal-backdrop .modal")).toBeVisible();
  }

  test("les conflits sont mis en avant avec les deux valeurs", async ({ page }) => {
    await withConflicts(page);
    const conflicts = page.locator(".modal-section.conflicts");
    await expect(conflicts).toBeVisible();
    await expect(conflicts).toContainText("genre");
    await expect(conflicts).toContainText("Black Metal");        // valeur actuelle
    await expect(conflicts).toContainText("True Black Metal");   // proposition MA
  });

  test("« Garder » envoie keep_mine pour le bon champ", async ({ page }) => {
    await withConflicts(page);
    const req = page.waitForRequest((r) => r.url().includes("resolve-conflict") && r.method() === "POST");
    await page.locator('[data-conflict-action="keep_mine"][data-field="genre"]').click();
    expect(JSON.parse((await req).postData())).toEqual({ field: "genre", action: "keep_mine" });
  });

  test("« Accepter MA » envoie accept_crawler", async ({ page }) => {
    await withConflicts(page);
    const req = page.waitForRequest((r) => r.url().includes("resolve-conflict") && r.method() === "POST");
    await page.locator('[data-conflict-action="accept_crawler"][data-field="country"]').click();
    expect(JSON.parse((await req).postData())).toEqual({ field: "country", action: "accept_crawler" });
  });

  test("un groupe sans conflit n'affiche pas la section", async ({ page }) => {
    await page.goto("/");
    await page.click('.nav a[data-view="bands"]');
    await page.locator('tr[data-ma-id="1"]').click();
    await expect(page.locator(".modal-section.conflicts")).toHaveCount(0);
  });
});

test.describe("Pilotage — actions restantes", () => {
  test("l'action d'une alerte appelle bien son endpoint", async ({ page }) => {
    await page.goto("/#/pilotage");
    const req = page.waitForRequest(
      (r) => r.url().includes("/admin/api/locations/reset-errors") && r.method() === "POST");
    await page.locator(".alert").filter({ hasText: "erreur de géocodage" })
      .getByRole("button", { name: "Tout remettre en file" }).click();
    await req;
  });

  test("l'alerte d'enrichissement déclenche le job correspondant", async ({ page }) => {
    await page.goto("/#/pilotage");
    const req = page.waitForRequest(
      (r) => r.url().includes("/admin/api/job-triggers") && r.method() === "POST");
    await page.locator(".alert").filter({ hasText: "jamais enrichis" })
      .getByRole("button", { name: "Lancer l'enrichissement" }).click();
    expect(JSON.parse((await req).postData())).toEqual({ job_type: "enrich" });
  });

  test("le crawl complet demande confirmation avant d'être lancé", async ({ page }) => {
    await page.goto("/#/pilotage");
    const dialog = new Promise((resolve) => page.once("dialog", (d) => { resolve(d.message()); d.dismiss(); }));
    await page.click('[data-job="full_crawl"]');
    expect(await dialog).toMatch(/plusieurs heures/i);
    // Refus : aucune demande ne doit partir.
    await expect(page.locator("#p-job-feedback")).toBeEmpty();
  });

  test("une demande en attente peut être retirée de la file", async ({ page }) => {
    await patchJson(page, "**/admin/api/live", (body) => {
      body.pending_jobs = [{ id: 7, job_type: "enrich", status: "pending",
        requested_by: "nico", created_at: new Date().toISOString() }];
    });
    await page.goto("/#/pilotage");
    await expect(page.locator("[data-cancel-job]")).toBeVisible();

    page.once("dialog", (d) => d.accept());
    const req = page.waitForRequest(
      (r) => r.url().includes("/admin/api/job-triggers/7/cancel") && r.method() === "POST");
    await page.click("[data-cancel-job]");
    await req;
  });

  test("l'échec d'une action est affiché sans casser la vue", async ({ page }) => {
    await page.goto("/#/pilotage");
    await page.route("**/admin/api/job-triggers", (route) =>
      route.fulfill({ status: 500, json: { ok: false, error: "Erreur création job" } }));
    await page.click('[data-job="enrich"]');
    await expect(page.locator("#p-job-feedback")).toContainText("Erreur création job");
    await expect(page.locator("#p-job-feedback")).toHaveClass(/err/);
    await expect(page.locator("#p-health")).toBeVisible();
  });
});

test.describe("Journal — annulation depuis le détail", () => {
  test("un traitement en cours peut être arrêté depuis son détail", async ({ page }) => {
    await patchJson(page, "**/admin/api/activity?*", (body) => {
      body.items[0].status = "running";
    });
    await page.goto("/#/activity");
    await page.locator("#a-table tbody tr").first().click();

    const modal = page.locator(".modal-backdrop .modal");
    await expect(modal.locator("#am-cancel")).toBeVisible();

    page.once("dialog", (d) => d.accept());
    const req = page.waitForRequest(
      (r) => r.url().includes("/admin/api/crawl-runs/5/cancel") && r.method() === "POST");
    await modal.locator("#am-cancel").click();
    await req;
  });

  test("un traitement terminé n'offre pas d'annulation", async ({ page }) => {
    await page.goto("/#/activity");
    await page.locator("#a-table tbody tr").first().click();
    await expect(page.locator("#am-cancel")).toHaveCount(0);
  });

  test("le filtre « Erreurs seules » est une bascule", async ({ page }) => {
    await page.goto("/#/activity");
    const req = page.waitForRequest((r) => r.url().includes("status=error"));
    await page.locator('[data-astatus="error"]').click();
    await req;

    const off = page.waitForRequest(
      (r) => r.url().includes("/admin/api/activity") && !r.url().includes("status=error"));
    await page.locator('[data-astatus="error"]').click();
    await off;
  });
});

test.describe("LLM — filtres", () => {
  test.beforeEach(async ({ page }) => { await page.goto("/#/llm"); });

  test("le filtre par modèle est transmis", async ({ page }) => {
    await page.selectOption("#llm-model", "llama-3.1-8b-instant");
    const req = page.waitForRequest((r) => r.url().includes("model=llama-3.1-8b-instant"));
    await page.click("#llm-apply");
    await req;
  });

  test("« sans résultat uniquement » est transmis", async ({ page }) => {
    await page.check("#llm-null");
    const req = page.waitForRequest((r) => r.url().includes("only_null=1"));
    await page.click("#llm-apply");
    await req;
  });

  test("la recherche part sur Entrée", async ({ page }) => {
    const req = page.waitForRequest((r) => r.url().includes("q=bayonne"));
    await page.fill("#llm-q", "bayonne");
    await page.press("#llm-q", "Enter");
    await req;
  });

  test("cliquer un groupe depuis la liste LLM ouvre sa fiche", async ({ page }) => {
    await page.locator("#llm-list [data-open-band]").first().click();
    await expect(page.locator(".modal-backdrop .modal h3")).toContainText("Gojira");
  });
});

test.describe("Localisations — pagination et recherche", () => {
  test.beforeEach(async ({ page }) => { await page.goto("/#/locations"); });

  test("la recherche combine texte et pays", async ({ page }) => {
    await page.fill("#l-q", "kolbotn");
    await page.fill("#l-country", "NO");
    const req = page.waitForRequest((r) => r.url().includes("q=kolbotn") && r.url().includes("country=NO"));
    await page.click("#l-search");
    await req;
  });

  test("le filtre « Toutes » retire le paramètre de statut", async ({ page }) => {
    const req = page.waitForRequest(
      (r) => r.url().includes("/admin/api/locations") && !r.url().includes("status="));
    await page.locator('.chip[data-status=""]').click();
    await req;
  });

  test("une erreur de l'API est affichée dans le tableau", async ({ page }) => {
    await page.route("**/admin/api/locations?*", (route) =>
      route.fulfill({ status: 500, json: { ok: false, error: "Erreur liste localisations" } }));
    await page.locator('.chip[data-status="manual"]').click();
    await expect(page.locator("#l-table .err-box")).toContainText("Erreur liste localisations");
  });
});

test.describe("Santé des services de fond", () => {
  test("le bandeau affiche l'état des services", async ({ page }) => {
    await page.goto("/#/pilotage");
    await expect(page.locator("#p-health")).toContainText("Services");
    await expect(page.locator("#p-health")).toContainText("2/2");
  });

  // Ces services ne sont pas exposés par Traefik : sans ce battement de cœur,
  // une dépendance injoignable reste invisible jusqu'à ce qu'on remarque
  // l'absence de données nouvelles.
  test("un service dégradé remonte en alerte actionnable", async ({ page }) => {
    await patchJson(page, "**/admin/api/live", (body) => {
      body.services = [
        { service: "crawler", ok: false, checks: { flaresolverr: false },
          error: "flaresolverr — HTTP 503", checked_at: new Date().toISOString(), stale: false },
      ];
    });
    await page.goto("/#/pilotage");
    const alert = page.locator(".alert").filter({ hasText: "crawler" });
    await expect(alert).toContainText("dégradé");
    await expect(alert).toContainText("HTTP 503");
    await expect(alert.getByRole("link", { name: "Voir le journal" })).toBeVisible();
  });

  test("un service silencieux est signalé même si son dernier contrôle était bon", async ({ page }) => {
    await patchJson(page, "**/admin/api/live", (body) => {
      body.services = [
        { service: "geocoder", ok: true, checks: {}, error: null,
          checked_at: "2026-08-01T00:00:00Z", stale: true },
      ];
    });
    await page.goto("/#/pilotage");
    await expect(page.locator(".alert").filter({ hasText: "geocoder" })).toContainText("silencieux");
  });
});

test.describe("Boutons de crawl et présence du crawler", () => {
  test("les traitements crawler sont proposés quand le crawler est vivant", async ({ page }) => {
    await page.goto("/#/pilotage");
    await expect(page.locator('[data-job="incremental"]')).toBeEnabled();
    await expect(page.locator('[data-job="full_crawl"]')).toBeEnabled();
  });

  // La production ne déploie pas le service crawler : sans cette garde, le
  // bouton promettait une exécution « sous ~1 min » qui n'arrivait jamais et la
  // demande restait « en attente » indéfiniment.
  test("aucun crawler déployé : les boutons concernés sont désactivés et expliqués", async ({ page }) => {
    await patchJson(page, "**/admin/api/live", (body) => {
      body.services = [{ service: "geocoder", ok: true, checks: {}, error: null,
        checked_at: new Date().toISOString(), stale: false }];
    });
    await page.goto("/#/pilotage");
    await expect(page.locator('[data-job="incremental"]')).toBeDisabled();
    await expect(page.locator('[data-job="enrich"]')).toBeDisabled();
    await expect(page.locator('[data-job="full_crawl"]')).toBeDisabled();
    await expect(page.locator("#p-triggers")).toContainText("Aucun crawler déployé");
  });

  // Le géocodage est alimenté par le geocoder, pas par le crawler.
  test("l'alimentation du géocodage reste possible sans crawler", async ({ page }) => {
    await patchJson(page, "**/admin/api/live", (body) => { body.services = []; });
    await page.goto("/#/pilotage");
    await expect(page.locator('[data-job="geocoder_enqueue"]')).toBeEnabled();
  });

  test("crawler silencieux : les boutons sont désactivés avec la bonne raison", async ({ page }) => {
    await patchJson(page, "**/admin/api/live", (body) => {
      body.services = [{ service: "crawler", ok: true, checks: {}, error: null,
        checked_at: "2026-08-01T00:00:00Z", stale: true }];
    });
    await page.goto("/#/pilotage");
    await expect(page.locator('[data-job="incremental"]')).toBeDisabled();
    await expect(page.locator("#p-triggers")).toContainText("silencieux ou dégradé");
  });
});
