import { type ElectronApplication, type Page } from '@playwright/test';

import { cspRichtlinie } from '../src/shared/csp';
import {
  angewandteRichtlinien,
  appStarten,
  bauPruefen,
  konsoleBeimLaden,
  metaRichtlinie,
} from './electron-hilfe';
import { expect, test } from './konsolenwache';

// `undefined`, solange der beforeAll-Hook übersprungen wurde.
let app: ElectronApplication | undefined;
let fenster: Page;

test.beforeAll(async () => {
  // Fehlender oder veralteter Bau ist ein Fehler, kein Grund zum Schweigen.
  bauPruefen();
  ({ app, fenster } = await appStarten());
});

test.afterAll(async () => {
  await app?.close();
});

test.describe('Startbildschirm', () => {
  test('zeigt Titel und Überschriftenhierarchie auf Deutsch', async () => {
    await expect(fenster).toHaveTitle('Waffensachkunde – Lernsoftware');

    await expect(fenster.getByRole('heading', { level: 1 })).toHaveText(
      'Waffensachkunde – Lernsoftware',
    );

    // Genau eine h1, darunter h2-Abschnitte, darunter h3 – keine
    // übersprungenen Ebenen. Die Anzahl der Abschnitte wächst mit dem
    // Funktionsumfang; geprüft wird deshalb die Hierarchie, nicht die Menge.
    await expect(fenster.getByRole('heading', { level: 1 })).toHaveCount(1);
    expect(await fenster.getByRole('heading', { level: 2 }).count()).toBeGreaterThan(0);

    const ebenen = await fenster.evaluate(() =>
      [...document.querySelectorAll('h1, h2, h3, h4, h5, h6')].map((h) =>
        Number(h.tagName.substring(1)),
      ),
    );
    expect(ebenen.length).toBeGreaterThan(0);
    expect(ebenen[0]).toBe(1);

    // Eine Überschrift darf höchstens eine Ebene tiefer sein als die vorige.
    let vorige = 1;
    for (const ebene of ebenen) {
      expect(ebene).toBeLessThanOrEqual(vorige + 1);
      vorige = ebene;
    }
  });

  test('setzt lang="de" auf dem Wurzelelement', async () => {
    await expect(fenster.locator('html')).toHaveAttribute('lang', 'de');
  });

  test('meldet einen einsatzbereiten SQLite-Rauchtest', async () => {
    const abzeichen = fenster.locator('.statusabzeichen--ok');
    await expect(abzeichen).toBeVisible();
    await expect(fenster.getByText(/SQLite \d+\.\d+/u)).toBeVisible();
  });
});

test.describe('Sicherheitsgrundlagen', () => {
  /* Anmerkung: Electron bietet ab Version 43 keine öffentliche API mehr, um
     die angewandten `webPreferences` auszulesen. Deshalb wird hier das
     beobachtbare Verhalten geprüft (das ist ohnehin die härtere Zusage);
     die Konfigurationswerte selbst sichert `tests/sicherheit.test.ts` ab. */

  test('stellt dem Renderer keine Node-Globals bereit', async () => {
    const globals = await fenster.evaluate(() =>
      ['require', 'process', 'module', 'exports', 'Buffer', 'global'].map(
        (name) => `${name}=${typeof (globalThis as Record<string, unknown>)[name]}`,
      ),
    );

    expect(globals).toEqual([
      'require=undefined',
      'process=undefined',
      'module=undefined',
      'exports=undefined',
      'Buffer=undefined',
      'global=undefined',
    ]);
  });

  test('isoliert den Kontext: kein Zugriff auf Electron-Interna', async () => {
    const interna = await fenster.evaluate(() =>
      ['electron', 'ipcRenderer', 'contextBridge', '__electronApi'].map(
        (name) => typeof (globalThis as Record<string, unknown>)[name],
      ),
    );

    expect(interna).toEqual(['undefined', 'undefined', 'undefined', 'undefined']);
  });

  test('legt genau die vereinbarte Bridge frei – und nichts weiter', async () => {
    const bruecke = await fenster.evaluate(() => {
      const api = (globalThis as Record<string, unknown>)['lernApp'];
      if (typeof api !== 'object' || api === null) {
        return null;
      }
      return Object.keys(api).sort();
    });

    // Bewusst als vollständige Liste gepflegt: Der Test soll auffallen, sobald
    // die Bridge etwas freilegt, das hier nicht bewusst eingetragen wurde.
    expect(bruecke).toEqual([
      'anwendungsInfoLesen',
      'anzeigegroesseBeobachten',
      'anzeigegroesseSetzen',
      'dateiZeigen',
      'datenschutzLesen',
      'einstellungenLesen',
      'einstellungenSchreiben',
      'energiesparenBeobachten',
      'erklaerungenLaden',
      'fehlerprotokollDrucken',
      'fragenlisteDrucken',
      'glossarLaden',
      'hilfsmittelAktiv',
      'hilfsmittelBeobachten',
      'katalogBild',
      'katalogLaden',
      'lernAntworten',
      'lernFragestand',
      'lernHartnaeckige',
      'lernMerken',
      'lernPlan',
      'lernSitzung',
      'lernUebersicht',
      'lernZuruecksetzen',
      'lernberichtDrucken',
      'lizenzenLesen',
      'menuebefehlBeobachten',
      'normtexteLaden',
      'profilAktualisieren',
      'profilAnlegen',
      'profilListe',
      'profilLoeschen',
      'pruefungAuswerten',
      'pruefungLaeuft',
      'pruefungOffen',
      'pruefungSichern',
      'pruefungStarten',
      'pruefungVerlauf',
      'pruefungVerwerfen',
      'sicherungAnlegen',
      'sicherungEinspielen',
      'sicherungPruefen',
      'sicherungUebernehmen',
      'themenLaden',
      'unterstuetzungOeffnen',
      'zwischenablageSchreiben',
    ]);
  });

  test('erzwingt eine strenge Content-Security-Policy', async () => {
    const richtlinie = await metaRichtlinie(fenster);

    // Der Bau setzt die `<meta>`-Fassung ein; abweichen darf davon nichts.
    expect(richtlinie).toBe(cspRichtlinie('production', 'meta'));
    expect(richtlinie).toContain("default-src 'none'");
    expect(richtlinie).toContain("object-src 'none'");
    // Im Produktionsbuild darf es kein 'unsafe-inline' und kein 'unsafe-eval' geben.
    expect(richtlinie).not.toContain('unsafe-inline');
    expect(richtlinie).not.toContain('unsafe-eval');
  });

  test('lässt frame-ancestors über den HTTP-Kopf gelten, nicht über das <meta>-Element', async () => {
    /*
      Der Kern dieser Prüfung – und der Grund, warum sie nicht mehr am Wortlaut
      des Attributs hängt.

      Vorher stand hier `expect(richtlinie).toContain("frame-ancestors 'none'")`.
      Das war grün, während die Direktive gar nicht galt: Chromium verwirft
      `frame-ancestors` im `<meta>`-Element und schrieb bei jedem Start einen
      Konsolenfehler darüber. Gemessen wird jetzt, welche Richtlinien der
      Renderer tatsächlich anwendet.
    */
    const meta = await metaRichtlinie(fenster);
    expect(meta).not.toContain('frame-ancestors');

    const angewandt = await angewandteRichtlinien(fenster);

    // Zwei Wege, zwei Richtlinien: das `<meta>`-Element und der HTTP-Kopf.
    expect(angewandt).toContain(meta);
    const ausDemKopf = angewandt.filter((r) => r !== meta);
    expect(ausDemKopf, `Angewandte Richtlinien:\n${angewandt.join('\n')}`).toHaveLength(1);

    /*
      Nicht gegen einen festen Wortlaut geprüft: Dieser Lauf startet den Bau aus
      `out/` unmittelbar, also ist `app.isPackaged` falsch und der Hauptprozess
      liefert die Dev-Fassung in den Kopf (nachgemessen). Für die Frage, um die
      es hier geht, ist das gleichgültig – entscheidend ist, dass die Direktive
      über einen Weg ankommt, auf dem sie gilt.
    */
    expect(ausDemKopf[0]).toContain("frame-ancestors 'none'");

    /*
      Und die Schnittmenge bleibt streng: Chromium wertet beide Richtlinien
      einzeln aus, erlaubt ist nur, was jede erlaubt. Dass die Kopf-Fassung im
      ungepackten Lauf `'unsafe-inline'` enthält, lockert deshalb nichts – die
      `<meta>`-Fassung verbietet es weiterhin (oben geprüft).
    */
    for (const einzelne of angewandt) {
      expect(einzelne).toContain("default-src 'none'");
    }
  });

  test('schreibt beim Laden keinen Konsolenfehler über die Richtlinie', async () => {
    /*
      Die Gegenprobe zur Direktive, die nicht gilt: Chromium meldet jede
      ignorierte `<meta>`-Direktive als Konsolenfehler, bei jedem einzelnen
      Start. Der Fehler ist hier die Zusage, nicht nur ein Schönheitsfehler –
      eine Wache, die auf Konsolenfehler anspringt, hätte sonst dauerhaft eine
      Ausnahme für diese eine Meldung tragen müssen.

      Gemessen wird beim Neuladen – warum, steht bei `konsoleBeimLaden`.
    */
    const meldungen = await konsoleBeimLaden(fenster);

    const zurRichtlinie = meldungen.filter((m) => /content security policy/iu.test(m));
    expect(
      zurRichtlinie,
      `Konsolenausgabe beim Laden:\n${meldungen.join('\n') || '(keine)'}`,
    ).toEqual([]);
  });
});

test.describe('Tastaturbedienung', () => {
  test('erreicht den Skip-Link als ersten Tabstopp und springt zum Inhalt', async () => {
    await fenster.locator('body').press('Tab');

    const fokussiert = fenster.locator(':focus');
    await expect(fokussiert).toHaveText('Direkt zum Hauptinhalt springen');

    await fokussiert.press('Enter');
    await expect(fenster.locator('#inhalt')).toBeFocused();
  });

  test('schaltet das Theme allein per Tastatur um', async () => {
    const gruppe = fenster.getByRole('radiogroup', { name: 'Farbschema' });
    await gruppe.getByRole('radio', { name: 'Systemvorgabe', exact: true }).focus();

    await fenster.keyboard.press('End');
    await expect(gruppe.getByRole('radio', { name: 'Hoher Kontrast', exact: true })).toBeChecked();

    await expect(fenster.locator('html')).toHaveAttribute('data-thema', 'hochkontrast');
  });

  test('sagt die Umschaltung über aria-live an', async () => {
    /* Gezielt die Ansage der Anwendung, nicht irgendeine Live-Region: Der
       Startbildschirm hat inzwischen eine zweite für den Lernbericht. Zwei
       höfliche Regionen nebeneinander sind zulässig – jede meldet nur, was in
       ihr steht –, aber ein Test muss sagen, welche er meint. */
    const meldung = fenster.locator('.nur-screenreader[role="status"][aria-live="polite"]');
    await expect(meldung).toBeAttached();

    // Der Auswahlglyph ist `aria-hidden`, der zugängliche Name ist deshalb
    // exakt die Beschriftung.
    await fenster.getByRole('radio', { name: 'Dunkel', exact: true }).click();
    await expect(meldung).toHaveText('Darstellung umgestellt auf Dunkel.');
  });
});
