// @vitest-environment node
/**
 * Die Datenschutzerklärung in der Anwendung.
 *
 * ## Warum diese Datei die wichtigste Wache dieses Bereichs trägt
 *
 * Der Fehler, der hier droht, ist nicht „die Erklärung fehlt“. Er ist: **es
 * gibt sie zweimal, und die zweite ist die falsche.** Der Fall ist bereits
 * eingetreten, ohne dass jemand es bemerkt hätte: Am 30.08.2026 stand die
 * veröffentlichte Seite auf Fassung 1.1, während `docs/datenschutz.md` bei
 * 1.5 war — vier Revisionen Abstand in vier Tagen, bei vorhandenem Erzeuger
 * und vorhandener Prüfliste in `docs/veroeffentlichen.md`. Der Erzeuger hatte
 * gehalten, was er sollte; ausgelaufen ist der Schritt, den nur ein Mensch
 * tut.
 *
 * Daraus folgt die Regel, die dieser Datei zugrunde liegt und die
 * `tests/dokumentation.test.ts` für den Updatebericht schon einmal
 * aufgeschrieben hat: **Ein Erzeuger allein reicht nicht. Was zählt, ist der
 * Test, der rot wird, wenn er nicht gelaufen ist.**
 *
 * Deshalb steht die Prüfsumme des Veröffentlichungsteils in der erzeugten
 * Datei, und deshalb wird sie hier nachgerechnet.
 */

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

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

import { abschnitte, type Datenschutzerklaerung } from '../src/shared/datenschutz';

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

const TRENNER = '---\n---\n';

function lies(pfad: string): string {
  /* Zeilenenden vereinheitlicht: `.gitattributes` erzwingt LF, aber ein
     Arbeitsbaum, der einmal mit CRLF ausgecheckt wird, machte diese Wache
     sonst dauerhaft und grundlos rot — und eine grundlos rote Wache wird
     abgeschaltet. */
  return readFileSync(join(wurzel, pfad), 'utf8').replace(/\r\n/gu, '\n');
}

/**
 * Derselbe Schnitt wie in `tools/datenschutz_html.py`.
 *
 * Einschliesslich des abschliessenden Abschneidens der Leerzeilen dort
 * (`strip`) – ohne das käme eine andere Prüfsumme heraus, und die Wache wäre
 * dauerhaft rot, ohne dass etwas falsch wäre.
 */
function veroeffentlichungsteil(markdown: string): string {
  const stelle = markdown.indexOf(TRENNER);
  const teil = stelle === -1 ? markdown : markdown.slice(stelle + TRENNER.length);
  return teil.replace(/^\n+/u, '').replace(/\n+$/u, '');
}

const quelle = lies('docs/datenschutz.md');
const erzeugt = JSON.parse(lies('content/datenschutz.json')) as Datenschutzerklaerung;

describe('Datenschutzerklärung – die erzeugte Fassung', () => {
  it('stammt aus der heutigen Quelle', () => {
    const erwartet = createHash('sha256')
      .update(veroeffentlichungsteil(quelle), 'utf8')
      .digest('hex');

    expect(
      erzeugt.quellpruefsumme,
      'docs/datenschutz.md hat sich geändert, content/datenschutz.json nicht. ' +
        'Neu erzeugen mit: python tools/datenschutz_anwendung.py',
    ).toBe(erwartet);
  });

  it('nennt dieselbe Fassung und denselben Stand wie die Quelle', () => {
    /* Beide werden aus der Standtabelle gelesen und nicht eingetragen. Eine
       Fassungsnummer an zwei Orten ist genau die zweite Wahrheit, um die es
       hier geht. */
    const fassung = /^\|\s*\*\*Fassung der Erklärung\*\*\s*\|\s*([^|]+?)\s*\|/mu.exec(quelle);
    const stand = /^\|\s*\*\*Stand\*\*\s*\|\s*([^|]+?)\s*\|/mu.exec(quelle);

    expect(fassung?.[1]).toBeDefined();
    expect(stand?.[1]).toBeDefined();
    expect(erzeugt.fassung).toBe(fassung?.[1]);
    expect(erzeugt.stand).toBe(stand?.[1]);
  });

  it('enthält den ganzen Veröffentlichungsteil und nicht nur den Anfang', () => {
    /* Gegenprobe gegen eine stillschweigend abgeschnittene Umsetzung: Der
       Erzeuger bricht bei unbekannten Auszeichnungen ab, aber ein Fehler in
       der Blockbildung könnte hinten etwas verlieren, ohne dass jemand es
       sähe. Verglichen wird die Zahl der Überschriften. */
    const ueberschriften = veroeffentlichungsteil(quelle)
      .split('\n')
      .filter((zeile) => /^#{1,3} /u.test(zeile)).length;

    const gebaut = erzeugt.bloecke.filter((block) => block.art === 'ueberschrift').length;

    expect(gebaut, 'Es fehlen Überschriften in der erzeugten Fassung.').toBe(ueberschriften);
    expect(gebaut).toBeGreaterThan(20);
  });

  it('führt für jeden Abschnitt ein eindeutiges Sprungziel', () => {
    const ziele = abschnitte(erzeugt);
    expect(ziele.length).toBeGreaterThan(10);
    expect(new Set(ziele.map((ziel) => ziel.kennung)).size).toBe(ziele.length);
    for (const ziel of ziele) {
      expect(ziel.kennung, `Leeres Sprungziel bei „${ziel.text}“`).not.toBe('');
    }
  });

  it('enthält kein Markup in den Texten', () => {
    /* Dieselbe Zusage wie beim Handbuch: Die Blöcke werden als reiner Text
       gerendert. Stünde hier eine spitze Klammer oder ein Sternchenpaar,
       erschiene es wörtlich auf dem Bildschirm. */
    /*
      Kennzeichnungen bleiben ausgenommen. Sie sind der Ort, an dem das
      Dokument über Auszeichnung **spricht**: Abschnitt 8 erklärt, die Adresse
      stehe „technisch in einem `<code>`-Element, nicht in einem Link“. Der
      Satz ist richtig und soll genau so dastehen; ihn als Markup-Rückstand zu
      werten wäre der erste Fehlalarm, nach dem eine Wache abgeschaltet wird.
    */
    const sichtbar = (teile: readonly { art: string; text: string }[]): string[] =>
      teile.filter((teil) => teil.art !== 'kennzeichnung').map((teil) => teil.text);

    const texte: string[] = [];
    for (const block of erzeugt.bloecke) {
      if (block.art === 'ueberschrift' || block.art === 'code') {
        texte.push(block.text);
      } else if (block.art === 'absatz') {
        texte.push(...sichtbar(block.teile));
      } else {
        texte.push(...sichtbar(block.punkte.flat()));
      }
    }
    const gesamt = texte.join('\n');

    /*
      Gesucht werden die Elemente, die der Umsetzer erzeugt – nicht jede
      spitze Klammer. Das Dokument enthält echte Platzhalter in spitzen
      Klammern („C:\\Users\\<Ihr Name>\\AppData“,
      „Lernstand-selbsttaetig-<Zeitstempel>.wsklernstand“), und die sollen
      genau so dastehen. Eine Wache, die daran anschlägt, wird nach dem
      zweiten Fehlalarm abgeschaltet.
    */
    const tags = /<\/?(?:p|ul|li|h[1-6]|strong|code|pre|a|em|br|div|span|table)\b/iu;

    expect(gesamt, 'Ein Element ist als Text in die Blöcke geraten.').not.toMatch(tags);
    expect(gesamt).not.toMatch(/\*\*[^*]+\*\*/u);
    /* Gegenprobe: Der Text ist tatsächlich da, und die Wache trifft, wenn
       etwas dasteht. */
    expect(gesamt.length).toBeGreaterThan(10_000);
    expect(tags.test('Ein <strong>fetter</strong> Text.')).toBe(true);
    expect(tags.test('Der Pfad C:\\Users\\<Ihr Name>\\AppData.')).toBe(false);
  });

  it('sagt in der Sache dasselbe wie die Oberfläche', () => {
    /* Der eigentliche Anlass dieses ganzen Bereichs: „Über diese Software“
       sagte bis 0.24.2 „sie sammelt keine Daten“, während Abschnitt 3 der
       Erklärung ausdrücklich das Gegenteil sagt. Diese Zusicherung hält
       fest, dass die Erklärung bei ihrer Aussage bleibt. */
    const gesamt = erzeugt.bloecke
      .flatMap((block) =>
        block.art === 'absatz'
          ? block.teile.map((teil) => teil.text)
          : block.art === 'liste'
            ? block.punkte.flat().map((teil) => teil.text)
            : [block.text],
      )
      .join(' ');

    expect(gesamt).toContain('entstehen Daten');
    expect(gesamt).not.toMatch(/sammelt\s+kein/u);
  });
});
