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

/**
 * Parcours du dashboard admin.
 *
 * Ces tests décrivent ce qu'un administrateur vient faire, dans l'ordre où il
 * le fait — c'est le critère qui a guidé le redécoupage des onglets.
 */

const USER = "nico";
const PASSWORD = "motdepasse-e2e";

/** Connexion explicite — utilisée uniquement par le bloc Authentification. */
async function login(page) {
  await page.goto("/");
  await page.fill("#login-username", USER);
  await page.fill("#login-password", PASSWORD);
  await page.click('#login-form button[type="submit"]');
  await expect(page.locator("#app")).toBeVisible();
}

// ------------------------------------------------------------------
test.describe("Authentification", () => {
  // Seul bloc à repartir sans session : c'est le parcours de connexion qu'il teste.
  test.use({ storageState: { cookies: [], origins: [] } });

  test("l'écran de connexion s'affiche tant qu'on n'est pas identifié", async ({ page }) => {
    await page.goto("/");
    await expect(page.locator("#login-screen")).toBeVisible();
    await expect(page.locator("#app")).toBeHidden();
  });

  test("un mauvais mot de passe affiche une erreur et ne laisse pas entrer", async ({ page }) => {
    await page.goto("/");
    await page.fill("#login-username", USER);
    await page.fill("#login-password", "mauvais");
    await page.click('#login-form button[type="submit"]');

    await expect(page.locator("#login-error")).toHaveText(/identifiants invalides/i);
    await expect(page.locator("#app")).toBeHidden();
  });

  test("la connexion donne accès au dashboard et affiche l'utilisateur", async ({ page }) => {
    await login(page);
    await expect(page.locator("#whoami")).toHaveText(USER);
  });

  test("le champ mot de passe est vidé après connexion", async ({ page }) => {
    await login(page);
    await expect(page.locator("#login-password")).toHaveValue("");
  });

  test("la déconnexion ramène à l'écran de connexion", async ({ page }) => {
    // Ce bloc n'a pas de session partagée : il faut se connecter d'abord.
    await login(page);
    await page.click("#logout-btn");
    await expect(page.locator("#login-screen")).toBeVisible();
    await expect(page.locator("#app")).toBeHidden();
  });
});

// ------------------------------------------------------------------
test.describe("Navigation", () => {
  test.beforeEach(async ({ page }) => { await page.goto("/"); });

  test("les cinq sections sont accessibles et marquent l'onglet actif", async ({ page }) => {
    const sections = [
      ["pilotage", "Pilotage"],
      ["bands", "Groupes"],
      ["locations", "Localisations"],
      ["activity", "Journal"],
      ["llm", "LLM & coûts"],
    ];
    for (const [view, title] of sections) {
      await page.click(`.nav a[data-view="${view}"]`);
      await expect(page.locator(".view-head h1")).toHaveText(title);
      await expect(page.locator(`.nav a[data-view="${view}"]`)).toHaveAttribute("aria-current", "page");
    }
  });

  test("l'ancienne URL #/actions redirige vers Pilotage (favoris préservés)", async ({ page }) => {
    await page.goto("/#/actions");
    await expect(page.locator(".view-head h1")).toHaveText("Pilotage");
  });

  test("une URL inconnue retombe sur Pilotage", async ({ page }) => {
    await page.goto("/#/nimportequoi");
    await expect(page.locator(".view-head h1")).toHaveText("Pilotage");
  });
});

// ------------------------------------------------------------------
test.describe("Pilotage — est-ce que ça tourne ?", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/");
    await page.click('.nav a[data-view="pilotage"]');
  });

  test("le bandeau de santé résume l'état du pipeline", async ({ page }) => {
    const strip = page.locator("#p-health");
    await expect(strip.locator(".health-card")).toHaveCount(6);
    for (const label of ["Groupes", "Géocodage", "En file", "Bloqués", "Coût LLM", "Services"]) {
      await expect(strip).toContainText(label);
    }
  });

  // Le cœur de la refonte : plus de « je constate ici, j'agis ailleurs ».
  test("chaque problème est affiché avec le bouton qui le résout", async ({ page }) => {
    const alert = page.locator(".alert-err").filter({ hasText: "erreur de géocodage" });
    await expect(alert).toBeVisible();
    await expect(alert).toContainText("12");
    await expect(alert.getByRole("button", { name: "Tout remettre en file" })).toBeVisible();
    await expect(alert.getByRole("link", { name: "Examiner" })).toBeVisible();
  });

  test("« Examiner » amène sur les localisations filtrées sur le bon statut", async ({ page }) => {
    await page.locator(".alert").filter({ hasText: "erreur de géocodage" })
      .getByRole("link", { name: "Examiner" }).click();

    await expect(page.locator(".view-head h1")).toHaveText("Localisations");
    await expect(page.locator('.chip[data-status="error"]')).toHaveAttribute("aria-pressed", "true");
  });

  test("les traitements peuvent être lancés depuis le pilotage", async ({ page }) => {
    await page.click('[data-job="enrich"]');
    await expect(page.locator("#p-job-feedback")).toContainText("Demande #77 enregistrée");
    await expect(page.locator("#p-job-feedback")).toHaveClass(/ok/);
  });

  test("les opérations destructives sont repliées et prévenues", async ({ page }) => {
    const danger = page.locator("#p-danger");
    await expect(danger.locator(".btn-danger").first()).toBeHidden();
    await danger.locator("summary").click();
    await expect(danger.locator('[data-danger="reset-all"]')).toBeVisible();
    await expect(danger).toContainText("facturés");
  });

  test("une opération destructive demande confirmation et respecte le refus", async ({ page }) => {
    page.on("dialog", (d) => d.dismiss());
    await page.locator("#p-danger summary").click();
    await page.click('[data-danger="reset-all"]');
    await expect(page.locator("#p-danger-feedback")).toBeEmpty();
  });
});

// ------------------------------------------------------------------
test.describe("Pilotage — annulation coopérative", () => {
  test("un run actif peut être arrêté, et l'état intermédiaire est explicite", async ({ page }) => {
    // Un run actif depuis 5 min, non encore annulé.
    await patchJson(page, "**/admin/api/live", (body) => {
      body.active_runs = [{
        id: 5, run_type: "enrich", status: "running",
        started_at: new Date(Date.now() - 5 * 60000).toISOString(),
        bands_seen: 120, bands_new: 3, bands_updated: 0, bands_enriched: 118,
        error: null, cancel_requested: false,
      }];
    });

    await page.goto("/");
    await page.click('.nav a[data-view="pilotage"]');
    await expect(page.locator(".run-row")).toContainText("enrich");

    // Le libellé doit dire que l'arrêt est demandé, pas immédiat : le crawler
    // ne s'interrompt qu'à son prochain point de contrôle.
    const dialog = new Promise((resolve) => page.once("dialog", (d) => { resolve(d.message()); d.accept(); }));
    await page.click("[data-cancel-run]");
    expect(await dialog).toMatch(/point de contrôle/i);
  });

  test("un run dont l'arrêt est déjà demandé affiche l'attente et désactive le bouton", async ({ page }) => {
    await patchJson(page, "**/admin/api/live", (body) => {
      body.active_runs = [{
        id: 5, run_type: "full_europe", status: "running",
        started_at: new Date(Date.now() - 60000).toISOString(),
        bands_seen: 10, bands_new: 0, bands_updated: 0, bands_enriched: 0,
        error: null, cancel_requested: true, cancel_requested_by: "nico",
      }];
    });

    await page.goto("/");
    await page.click('.nav a[data-view="pilotage"]');
    await expect(page.locator(".run-cancel-note")).toContainText("Arrêt demandé");
    await expect(page.locator(".run-cancel-note")).toContainText("nico");
    await expect(page.locator(".run-row button")).toBeDisabled();
  });
});

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

  test("la liste s'affiche avec le nombre total", async ({ page }) => {
    await expect(page.locator("#b-table tbody tr")).toHaveCount(3);
    await expect(page.locator("#b-count")).toContainText("3");
  });

  test("la recherche part sur Entrée et transmet le terme à l'API", async ({ page }) => {
    const request = page.waitForRequest((r) => r.url().includes("q=mayhem"));
    await page.fill("#b-q", "mayhem");
    await page.press("#b-q", "Enter");
    await request;
  });

  test("les filtres rapides sont des bascules", async ({ page }) => {
    const chip = page.locator('.chip[data-chip="has_conflict"]');
    await expect(chip).toHaveAttribute("aria-pressed", "false");
    await chip.click();
    await expect(page.locator('.chip[data-chip="has_conflict"]')).toHaveAttribute("aria-pressed", "true");
    await page.locator('.chip[data-chip="has_conflict"]').click();
    await expect(page.locator('.chip[data-chip="has_conflict"]')).toHaveAttribute("aria-pressed", "false");
  });

  test("les filtres avancés sont masqués par défaut", async ({ page }) => {
    await expect(page.locator("#b-advanced")).toBeHidden();
    await page.click("#b-advanced-toggle");
    await expect(page.locator("#b-advanced")).toBeVisible();
    await expect(page.locator("#b-advanced-toggle")).toHaveAttribute("aria-expanded", "true");
  });

  test("un groupe sans coordonnées est signalé dans la liste", async ({ page }) => {
    const row = page.locator('tr[data-ma-id="2"]');
    await expect(row.locator('[title="Pas de coordonnées"]')).toBeVisible();
  });

  test("un groupe avec des champs verrouillés porte un cadenas", async ({ page }) => {
    await expect(page.locator('tr[data-ma-id="2"] .lock')).toBeVisible();
  });

  test("cliquer une ligne ouvre la fiche du groupe", async ({ page }) => {
    await page.locator('tr[data-ma-id="1"]').click();
    const modal = page.locator(".modal-backdrop .modal");
    await expect(modal).toBeVisible();
    await expect(modal.locator("h3")).toContainText("Mayhem");
    await expect(modal.locator("#m-name")).toHaveValue("Mayhem");
  });

  // Les lignes de tableau sont souvent inaccessibles au clavier : ici elles
  // portent role=button et tabindex, et répondent à Entrée.
  test("une ligne s'ouvre aussi au clavier", async ({ page }) => {
    await page.locator('tr[data-ma-id="1"]').focus();
    await page.keyboard.press("Enter");
    await expect(page.locator(".modal-backdrop .modal")).toBeVisible();
  });

  test("la fiche se ferme avec Échap et rend le focus", async ({ page }) => {
    await page.locator('tr[data-ma-id="1"]').click();
    await expect(page.locator(".modal-backdrop")).toBeVisible();
    await page.keyboard.press("Escape");
    await expect(page.locator(".modal-backdrop")).toHaveCount(0);
    await expect(page.locator('tr[data-ma-id="1"]')).toBeFocused();
  });

  test("l'édition envoie bien les champs modifiés", async ({ page }) => {
    await page.locator('tr[data-ma-id="1"]').click();
    await page.fill("#m-name", "Mayhem (corrigé)");

    const request = page.waitForRequest(
      (r) => r.url().includes("/admin/api/bands/1") && r.method() === "PATCH");
    await page.click("#m-save");
    const body = JSON.parse((await request).postData());
    expect(body.name).toBe("Mayhem (corrigé)");
    await expect(page.locator(".modal-backdrop")).toHaveCount(0);
  });

  test("une erreur serveur reste dans la fiche au lieu de la fermer", async ({ page }) => {
    await page.route("**/admin/api/bands/1", async (route) => {
      if (route.request().method() === "PATCH") {
        return route.fulfill({ status: 400, json: { ok: false, error: "formed_year invalide" } });
      }
      return route.continue();
    });
    await page.locator('tr[data-ma-id="1"]').click();
    await page.click("#m-save");
    await expect(page.locator("#m-error")).toHaveText("formed_year invalide");
    await expect(page.locator(".modal-backdrop")).toBeVisible();
  });
});

// ------------------------------------------------------------------
test.describe("Localisations — débloquer le géocodage", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/");
    await page.click('.nav a[data-view="locations"]');
  });

  test("la vue s'ouvre sur ce qui est à débloquer", async ({ page }) => {
    await expect(page.locator('.chip[data-status="error,llm_needed"]')).toHaveAttribute("aria-pressed", "true");
    await expect(page.locator("#l-table tbody tr")).toHaveCount(2);
  });

  test("chaque ligne montre le diagnostic sans avoir à cliquer", async ({ page }) => {
    const row = page.locator("tr[data-loc-id='11']");
    await expect(row).toContainText("Kolbotn");
    await expect(row).toContainText("erreur");
    await expect(row).toContainText("3 géo · 0 LLM");     // essais géocodeur / LLM
    await expect(row).toContainText("seuil de confiance"); // message d'erreur réel
  });

  // Régression : le paramètre de statut de l'URL était relu à CHAQUE rendu.
  // Arrivé depuis une alerte du Pilotage, cliquer un autre chip n'avait donc
  // aucun effet — le filtre revenait immédiatement à celui de l'URL.
  test("après être venu d'une alerte, les chips restent utilisables", async ({ page }) => {
    await page.goto("/#/locations?status=error");
    await expect(page.locator('.chip[data-status="error"]')).toHaveAttribute("aria-pressed", "true");

    await page.locator('.chip[data-status="llm_needed"]').click();
    await expect(page.locator('.chip[data-status="llm_needed"]')).toHaveAttribute("aria-pressed", "true");
    await expect(page.locator('.chip[data-status="error"]')).toHaveAttribute("aria-pressed", "false");
  });

  test("le paramètre de statut est retiré de l'URL une fois appliqué", async ({ page }) => {
    await page.goto("/#/locations?status=error");
    await expect(page.locator('.chip[data-status="error"]')).toHaveAttribute("aria-pressed", "true");
    expect(await page.evaluate(() => location.hash)).toBe("#/locations");
  });

  test("un filtre de statut recharge la liste avec le bon paramètre", async ({ page }) => {
    const request = page.waitForRequest((r) => r.url().includes("status=llm_needed"));
    await page.locator('.chip[data-status="llm_needed"]').click();
    await request;
  });

  // Auparavant il fallait relancer des milliers d'appels facturés pour
  // débloquer un seul lieu.
  test("une localisation peut être relancée seule", async ({ page }) => {
    const request = page.waitForRequest(
      (r) => r.url().includes("/admin/api/locations/11/requeue") && r.method() === "POST");
    await page.locator("tr[data-loc-id='11'] [data-requeue]").click();
    await request;
  });

  test("les coordonnées peuvent être saisies à la main", async ({ page }) => {
    await page.locator("tr[data-loc-id='11'] [data-setcoords]").click();
    const modal = page.locator(".modal-backdrop .modal");
    await expect(modal).toContainText("Kolbotn");

    await modal.locator("#c-lat").fill("59.7955");
    await modal.locator("#c-lon").fill("10.8");

    const request = page.waitForRequest(
      (r) => r.url().includes("/admin/api/locations/11") && r.method() === "PATCH");
    await modal.locator("#c-save").click();
    const body = JSON.parse((await request).postData());
    expect(body).toEqual({ lat: "59.7955", lon: "10.8" });
  });

  test("des coordonnées invalides sont refusées et le message reste affiché", async ({ page }) => {
    await page.locator("tr[data-loc-id='11'] [data-setcoords]").click();
    await page.locator("#c-lat").fill("999");
    await page.locator("#c-lon").fill("10");
    await page.locator("#c-save").click();
    await expect(page.locator("#c-error")).toHaveText(/lat invalide/);
    await expect(page.locator(".modal-backdrop")).toBeVisible();
  });

  test("le nom du groupe renvoie vers sa fiche", async ({ page }) => {
    await page.locator("tr[data-loc-id='11'] [data-open-band]").click();
    await expect(page.locator(".modal-backdrop .modal h3")).toContainText("Darkthrone");
  });
});

// ------------------------------------------------------------------
test.describe("Journal", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/");
    await page.click('.nav a[data-view="activity"]');
  });

  test("traitements et actions admin apparaissent dans une même chronologie", async ({ page }) => {
    await expect(page.locator("#a-table tbody tr")).toHaveCount(2);
    await expect(page.locator("#a-table")).toContainText("Traitement");
    await expect(page.locator("#a-table")).toContainText("Admin");
  });

  test("ouvrir un traitement affiche son journal détaillé", async ({ page }) => {
    await page.locator("#a-table tbody tr").first().click();
    const modal = page.locator(".modal-backdrop .modal");
    await expect(modal.locator("h3")).toContainText("Traitement #5");
    await expect(modal.locator("#act-logs")).toContainText("enrich done");
  });

  test("ouvrir une action admin montre l'avant et l'après", async ({ page }) => {
    await page.locator("#a-table tbody tr").nth(1).click();
    const modal = page.locator(".modal-backdrop .modal");
    await expect(modal.locator("h3")).toContainText("Action admin");
    await expect(modal).toContainText("Avant");
    await expect(modal).toContainText("Après");
  });

  test("le filtre par type recharge la liste", async ({ page }) => {
    const request = page.waitForRequest((r) => r.url().includes("type=run"));
    await page.locator('.chip[data-atype="run"]').click();
    await request;
  });
});

// ------------------------------------------------------------------
test.describe("LLM & coûts", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/");
    await page.click('.nav a[data-view="llm"]');
  });

  test("le coût est mis en avant, pas enterré", async ({ page }) => {
    await expect(page.locator("#llm-summary")).toContainText("Coût total");
    await expect(page.locator("#llm-summary")).toContainText("$0.0412");
  });

  test("chaque appel expose son prompt et sa réponse brute", async ({ page }) => {
    await expect(page.locator("#llm-list")).toContainText("Bayonne");
    await page.locator("#llm-list details summary").first().click();
    await expect(page.locator(".llm-detail").first()).toContainText("PROMPT:");
  });
});

// ------------------------------------------------------------------
test.describe("Robustesse", () => {
  test("une session expirée renvoie à l'écran de connexion", async ({ page }) => {
    await page.goto("/");
    await page.route("**/admin/api/**", (route) =>
      route.fulfill({ status: 401, json: { ok: false, error: "unauthorized" } }));
    await page.click('.nav a[data-view="bands"]');
    await expect(page.locator("#login-screen")).toBeVisible();
  });

  test("une erreur serveur est affichée, pas avalée en silence", async ({ page }) => {
    await page.goto("/");
    await page.route("**/admin/api/bands**", (route) =>
      route.fulfill({ status: 500, json: { ok: false, error: "Erreur recherche bands" } }));
    await page.click('.nav a[data-view="bands"]');
    await expect(page.locator("#b-table .err-box")).toContainText("Erreur recherche bands");
  });

  test("une liste vide affiche un message, pas un tableau vide", async ({ page }) => {
    await page.goto("/");
    await page.route("**/admin/api/locations?*", (route) =>
      route.fulfill({ status: 200, json: { ok: true, items: [], total: 0, page: 1, pageSize: 50 } }));
    await page.click('.nav a[data-view="locations"]');
    await expect(page.locator("#l-table")).toContainText("Aucune localisation");
  });
});
