// @vitest-environment node
// Reiner Dateisystem-Test – jsdom wird hier nicht gebraucht und würde
// `import.meta.url` auf eine http-URL setzen.

import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';

import { describe, expect, it } from 'vitest';

import { istGleicheDatei } from '../src/main/oberflaeche';
import { cspRichtlinie } from '../src/shared/csp';

/**
 * Schutzgitter für die nicht verhandelbaren Sicherheitsschalter.
 *
 * Electron 43 bietet keine öffentliche API mehr, um die tatsächlich
 * angewandten `webPreferences` zur Laufzeit auszulesen. Dieser Test liest
 * deshalb die Quelle und schlägt fehl, sobald jemand einen der drei harten
 * Schalter umstellt oder entfernt. Das beobachtbare Verhalten prüfen
 * zusätzlich die E2E-Tests in `e2e/anwendung.spec.ts`.
 */

const wurzel = fileURLToPath(new URL('..', import.meta.url));

function quelle(relativerPfad: string): string {
  return readFileSync(join(wurzel, relativerPfad), 'utf8');
}

describe('webPreferences des Hauptfensters', () => {
  const fensterQuelle = quelle('src/main/fenster.ts');

  it.each([
    ['contextIsolation', 'true'],
    ['nodeIntegration', 'false'],
    ['sandbox', 'true'],
    ['nodeIntegrationInWorker', 'false'],
    ['nodeIntegrationInSubFrames', 'false'],
    ['webSecurity', 'true'],
    ['allowRunningInsecureContent', 'false'],
    ['webviewTag', 'false'],
  ])('setzt %s auf %s', (schalter, wert) => {
    expect(fensterQuelle).toMatch(new RegExp(`\\b${schalter}\\s*:\\s*${wert}\\s*,`, 'u'));
  });

  it('lädt das Preload-Skript aus dem gebauten Ausgabeverzeichnis', () => {
    expect(fensterQuelle).toContain("join(__dirname, '../preload/index.js')");
  });
});

describe('Content-Security-Policy', () => {
  /*
   * Was hier gemessen wird – und was ausdrücklich nicht.
   *
   * In dieser Datei läuft kein Browser. Geprüft wird der **Wortlaut** der
   * Richtlinie, nicht ihre Wirkung. Genau daran hat dieser Block schon einmal
   * grün gemeldet, während nichts galt: `toContain("frame-ancestors 'none'")`
   * war erfüllt, obwohl Chromium die Direktive im `<meta>`-Element verwirft
   * und bei jedem Start einen Konsolenfehler darüber schrieb. Eine Zusicherung,
   * die mehr verspricht als sie hält, ist schlimmer als keine – deshalb steht
   * der Satz hier und nicht im Nachhinein.
   *
   * Die Wirkung misst `e2e/anwendung.spec.ts` im echten Fenster: dort werden
   * die tatsächlich angewandten Richtlinien über `securitypolicyviolation`
   * ausgelesen und die Konsole beim Laden mitgehört.
   *
   * Hier bleibt, was ohne Browser beantwortbar ist: ob der Wortlaut stimmt und
   * ob die beiden Fassungen (`<meta>` und HTTP-Kopf) zueinander passen.
   */
  const produktionKopf = cspRichtlinie('production', 'kopf');
  const produktionMeta = cspRichtlinie('production', 'meta');
  const entwicklungKopf = cspRichtlinie('development', 'kopf');
  const entwicklungMeta = cspRichtlinie('development', 'meta');

  /** Beide Fassungen des Produktionsbuilds, für die gemeinsamen Zusicherungen. */
  const produktion: readonly [string, string][] = [
    ['HTTP-Kopf', produktionKopf],
    ['<meta>', produktionMeta],
  ];

  it.each(produktion)('sperrt im Produktionsbuild alles per Vorgabe (%s)', (_weg, richtlinie) => {
    expect(richtlinie).toContain("default-src 'none'");
  });

  it.each(produktion)(
    'erlaubt im Produktionsbuild weder unsafe-inline noch unsafe-eval (%s)',
    (_weg, richtlinie) => {
      expect(richtlinie).not.toContain('unsafe-inline');
      expect(richtlinie).not.toContain('unsafe-eval');
    },
  );

  it.each(produktion)(
    'verbietet Formularversand, fremde Basis-URLs, Objekte und eigene Rahmen (%s)',
    (_weg, richtlinie) => {
      expect(richtlinie).toContain("form-action 'none'");
      expect(richtlinie).toContain("base-uri 'none'");
      expect(richtlinie).toContain("object-src 'none'");
      // `frame-src` wirkt auch im `<meta>`: Die eigene Seite darf nichts einbetten.
      expect(richtlinie).toContain("frame-src 'none'");
    },
  );

  it.each(produktion)(
    'beschränkt Netzwerkverbindungen auf die eigene Herkunft (%s)',
    (_weg, richtlinie) => {
      expect(richtlinie).toContain("connect-src 'self'");
      expect(richtlinie).not.toMatch(/connect-src[^;]*https?:/u);
    },
  );

  /**
   * Direktiven, die jeder Browser im `<meta>`-Element ignoriert (CSP Level 3).
   * Sie gehören ausschließlich in den HTTP-Kopf.
   */
  const NUR_IM_KOPF = ['frame-ancestors', 'report-uri', 'sandbox'] as const;

  /** Namen der gesetzten Direktiven, in der Reihenfolge der Richtlinie. */
  function direktivnamen(richtlinie: string): string[] {
    return richtlinie.split('; ').map((teil) => teil.split(' ')[0] ?? '');
  }

  it('setzt frame-ancestors dort, wo die Direktive gilt: im HTTP-Kopf', () => {
    expect(produktionKopf).toContain("frame-ancestors 'none'");
    expect(entwicklungKopf).toContain("frame-ancestors 'none'");
  });

  it.each([
    ['Produktion', produktionMeta],
    ['Entwicklung', entwicklungMeta],
  ])(
    'lässt aus der <meta>-Fassung jede Direktive weg, die dort ignoriert würde (%s)',
    (_modus, richtlinie) => {
      /* Nicht nur `frame-ancestors`: Wer später `sandbox` oder `report-uri`
         ergänzt, soll hier auffallen und nicht erst am Konsolenfehler beim
         Start. Ein `<meta>`, das eine ignorierte Direktive nennt, kostet
         nichts – behauptet aber Schutz, den es nicht gibt. */
      for (const direktive of NUR_IM_KOPF) {
        expect(direktivnamen(richtlinie)).not.toContain(direktive);
      }
    },
  );

  it.each([
    ['Produktion', produktionKopf, produktionMeta],
    ['Entwicklung', entwicklungKopf, entwicklungMeta],
  ])(
    'hält beide Fassungen deckungsgleich – bis auf die Nur-Kopf-Direktiven (%s)',
    (_modus, kopf, meta) => {
      /* Wäre die Kopf-Fassung an anderer Stelle strenger als die
         `<meta>`-Fassung, bräche der Dev-Modus unbemerkt an der Schnittmenge
         beider Richtlinien. Verglichen wird deshalb Zeichen für Zeichen. */
      const erwartet = kopf
        .split('; ')
        .filter((teil) => !NUR_IM_KOPF.some((d) => teil.startsWith(`${d} `)))
        .join('; ');
      expect(meta).toBe(erwartet);
    },
  );

  it('lockert im Dev-Modus nur genau das, was Vite braucht', () => {
    expect(entwicklungKopf).toContain("script-src 'self' 'unsafe-inline'");
    expect(entwicklungKopf).toContain('ws:');
    // Auch im Dev-Modus bleibt eval verboten.
    expect(entwicklungKopf).not.toContain('unsafe-eval');
  });
});

describe('Navigationsziel', () => {
  /*
   * Das Protokoll `file:` allein darf nicht genügen. Sonst käme jede Datei auf
   * der Platte in Frage – und das Preload-Skript hängt am `webContents`, wird
   * also auch für die neue Seite wieder eingespielt. Eine heruntergeladene
   * HTML-Datei hätte damit die volle Bridge in der Hand.
   */
  const eigene = 'file:///C:/Programme/Waffensachkunde/resources/app/out/renderer/index.html';

  it('lässt genau die eigene Oberfläche zu', () => {
    expect(istGleicheDatei(eigene, eigene)).toBe(true);
  });

  it('lässt Sprungziele und Parameter innerhalb der eigenen Seite zu', () => {
    expect(istGleicheDatei(`${eigene}#inhalt`, eigene)).toBe(true);
    expect(istGleicheDatei(`${eigene}?x=1`, eigene)).toBe(true);
  });

  it('weist jede andere Datei ab', () => {
    for (const fremd of [
      'file:///C:/Users/Test/Downloads/beliebig.html',
      'file:///C:/Windows/win.ini',
      'file:///C:/Programme/Waffensachkunde/resources/app/out/renderer/andere.html',
      'file:///C:/Programme/Waffensachkunde/resources/app/out/index.html',
    ]) {
      expect(istGleicheDatei(fremd, eigene)).toBe(false);
    }
  });

  it('nimmt Groß- und Kleinschreibung des Pfades hin', () => {
    // Windows liefert den Laufwerksbuchstaben nicht immer gleich geschrieben.
    expect(istGleicheDatei(eigene.replace('C:', 'c:'), eigene)).toBe(true);
  });

  it('behandelt prozentkodierte Pfade wie ihre Klartextform', () => {
    const mitLeerzeichen = 'file:///C:/Mein%20Ordner/index.html';
    expect(istGleicheDatei('file:///C:/Mein Ordner/index.html', mitLeerzeichen)).toBe(true);
  });

  it('weist andere Protokolle und Unsinn ab', () => {
    for (const fremd of ['http://example.com/', 'javascript:alert(1)', 'nichts', '']) {
      expect(istGleicheDatei(fremd, eigene)).toBe(false);
    }
  });
});
