// @vitest-environment node
/**
 * Der Rückmeldeweg: eine Adresse, keine Rufnummer, keine Anschrift.
 *
 * Anlass ist die Auslieferung über den Microsoft Store. Bis dahin standen
 * Telefonnummer und Wohnort des Betreibers in der Anwendung; beide sind
 * private Angaben und für die Prüfschritte 9.2.2 und 9.2.3 des
 * Barrierefreiheits-Prüfplans nicht gefordert. 9.2.3 verlangt einen Kanal,
 * der „mindestens E-Mail oder ein barrierefreies Formular, nicht
 * ausschließlich Telefon“ ist – E-Mail allein erfüllt ihn also.
 *
 * Ein Test, der bloß die neue Adresse prüft, wäre zu schwach: Er bliebe grün,
 * wenn jemand die Rufnummer irgendwo wieder ergänzt. Deshalb prüft diese
 * Datei beide Richtungen – dass die eine Adresse dasteht **und** dass keine
 * Rufnummer und keine Anschrift zurückgekehrt ist. Gesucht wird nach Mustern,
 * nicht nach den entfernten Zeichenketten: Eine andere Nummer wäre derselbe
 * Rückschritt.
 *
 * Gegen den Quelltext, nicht nur gegen die Konstanten: Die Angaben ließen
 * sich auch als Festtext an einer Ansicht vorbeischmuggeln, ohne `KONTAKT` je
 * anzufassen. Gelesen wird, was `git ls-files` unter `app/src` führt – nur
 * das wird ausgeliefert.
 *
 * Und seit dem 28.08.2026 gegen den **ganzen** Arbeitsbaum. Der Grund ist ein
 * anderer Verbreitungsweg: Für den Store genügt `app/src`, denn nur das geht
 * an die Anwender. Für ein öffentliches Quelltextarchiv nicht – dort wandert
 * alles mit, und die privaten Angaben stünden nicht in der Anwendung, sondern
 * in den Unterlagen: `docs/stand.md`, `PLAN.md`, der Änderungsverlauf. Genau
 * dorthin hat vorher niemand gesehen.
 *
 * Was die Muster nicht erreichen, steht bei `TEXTENDUNGEN` und `AUSGENOMMEN`
 * ausgeschrieben. Und eines erreichen sie grundsätzlich nicht: die
 * Autorenangabe der Commits. Sie steht nicht im Text, sondern in den
 * Kopfdaten – dagegen hilft kein Test, sondern nur `git config user.email`
 * vor dem ersten Commit eines öffentlichen Archivs (docs/stand.md,
 * Abschnitt 8).
 */

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

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

import { HILFE_KAPITEL, type Hilfeblock } from '../src/shared/hilfe';
import { KONTAKT, KONTAKT_ANLIEGEN } from '../src/shared/kontakt';
import { meldetextBauen } from '../src/shared/meldetext';

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

/** Die einzige Anschrift, über die zurückgemeldet wird. */
const ADRESSE = 'Olaf@olaf-willerding.de';

/**
 * Eine deutsche Rufnummer im Fließtext.
 *
 * Verlangt Vorwahl, ein Trennzeichen und mindestens fünf weitere Ziffern –
 * genau die Form, in der die entfernte Nummer dastand. Enger gefasst als
 * „irgendeine Ziffernfolge“, damit Fassungsnummern, Prüfsummen und CSS-Werte
 * nicht mitgefangen werden; am Stand des Umbaus lief das Muster über alle 155
 * Quelldateien unter `app/src` und fand nichts.
 */
const RUFNUMMER = /(?:\b0\d{3,4}[ /-]\d{5,9}\b|\+49[\d\s/-]{6,}\d)/u;

/** Postleitzahl und Ortsname – die Form der entfernten Ortsangabe. */
const ANSCHRIFT = /\b\d{5}\s+[A-ZÄÖÜ][a-zäöüß]/u;

/** Die aufgegebene Adresse des Betreibers, in jeder Schreibweise. */
const ALTE_ADRESSE = /t-online\.de/iu;

/** Alle Textstellen eines Handbuchblocks, gleich welcher Art. */
function texte(block: Hilfeblock): readonly string[] {
  switch (block.art) {
    case 'absatz':
      return [block.text];
    case 'liste':
    case 'schritte':
      return block.punkte;
    case 'tasten':
      return block.zeilen.map((zeile) => zeile.wirkung);
  }
}

/** Erfundene Eingaben für den Meldetext – hier zählt nur sein Kopf. */
const MELDETEXT = meldetextBauen(
  { id: 'I.1-01', amtliche_nummer: '1.01', seite: 6 },
  {
    herausgeber: 'Bundesverwaltungsamt',
    stand: '2024-12-16',
    quelldatei_sha256: '7e5e41faee0a9198b6ba3e2fe3b46ef313448b06753516579596f98212b1d108',
  },
  { fassung: '0.21.1', baukennung: '0218791', baustand: '2026-08-22' },
);

/** Was der Anwendung an Betreiberangaben aus dem Mund kommt. */
const SICHTBARER_TEXT = [
  HILFE_KAPITEL.flatMap((kapitel) => kapitel.bloecke.flatMap(texte)).join('\n'),
  Object.values(KONTAKT).join('\n'),
  KONTAKT_ANLIEGEN.join('\n'),
  MELDETEXT,
].join('\n');

/** Die von Git geführten Dateien unterhalb von `pfad`, oder `null` ohne Git. */
function versionierteDateien(pfad: string): readonly string[] | null {
  try {
    return execFileSync('git', ['ls-files', pfad], {
      cwd: wurzel,
      encoding: 'utf8',
      stdio: ['ignore', 'pipe', 'ignore'],
    })
      .split('\n')
      .map((zeile) => zeile.trim())
      .filter((zeile) => zeile.length > 0);
  } catch {
    return null;
  }
}

/** Der ausgelieferte Quelltext, oder `null` ohne Git. */
function quelltexte(): readonly string[] | null {
  const dateien = versionierteDateien('app/src');
  if (dateien === null) {
    return null;
  }
  return dateien
    .filter((datei) => datei.endsWith('.ts') || datei.endsWith('.tsx'))
    .map((datei) => readFileSync(join(wurzel, datei), 'utf8'));
}

/**
 * Dateiendungen, hinter denen Text steht, den ein Mensch geschrieben hat.
 *
 * Bilder und PDF stehen nicht dabei: In ihnen findet ein Textmuster nichts,
 * auch wenn die Angabe als Pixel darinsteht. Für sie trägt die Reihenfolge
 * den Beweis – alle Aufnahmen entstehen aus einem Bau der Anwendung, und in
 * der Anwendung hält der Test darüber die Angaben draußen.
 */
const TEXTENDUNGEN = [
  '.md',
  '.ts',
  '.tsx',
  '.mjs',
  '.js',
  '.json',
  '.txt',
  '.yml',
  '.yaml',
  '.css',
  '.html',
  '.py',
  '.toml',
] as const;

/**
 * Dateien, die das Muster nicht sinnvoll prüfen kann – jede mit ihrem Grund.
 *
 * Kurz halten. Jeder Eintrag ist ein Stück Arbeitsbaum, über das diese Wache
 * nichts mehr sagt; eine Ausnahme ohne Grund ist eine Lücke mit Absegnung.
 */
const AUSGENOMMEN: readonly { readonly datei: string; readonly grund: string }[] = [
  {
    datei: 'app/tests/kontakt.test.ts',
    grund:
      'Diese Datei selbst. Sie führt erfundene Proben in der Form der ' +
      'entfernten Angaben, damit die Gegenprobe oben nicht ins Leere läuft.',
  },
  {
    datei: 'app/package-lock.json',
    grund:
      'Von npm erzeugt. Die Integritätsprüfsummen sind Base64 und enthalten ' +
      'Schrägstriche, an denen das Rufnummernmuster zufällig anschlägt; ' +
      'einen Wohnort schreibt dort niemand hinein.',
  },
  {
    datei: 'docs/katalog-pruefbericht.html',
    grund:
      'Erzeugter Prüfbericht mit eingebetteten Bildern als Base64 – dieselbe ' +
      'Zufallstrefferlage. Sein Inhalt stammt vollständig aus dem amtlichen ' +
      'Katalog, nicht aus Betreiberangaben.',
  },
];

/**
 * Anschriften, die dastehen **müssen** – jede einzeln und mit Grund.
 *
 * Diese Wache hält die **privaten** Angaben des Betreibers draußen. Fremde
 * Geschäftsanschriften sind etwas anderes: Art. 13 Abs. 1 lit. e DSGVO
 * verlangt, dass Empfänger personenbezogener Daten benannt werden, und wer
 * eine Webseite ausliefert, verarbeitet die IP-Adresse jedes Lesers. Eine
 * Datenschutzerklärung, die ihren Auftragsverarbeiter verschweigt, wäre
 * unvollständig.
 *
 * Erlaubt wird deshalb der **Wortlaut**, nicht die Datei: `docs/datenschutz.md`
 * bleibt vollständig bewacht, und jede andere Anschrift darin schlägt weiter
 * an. Die Alternative – die Datei freistellen – hätte ausgerechnet das
 * Dokument aus der Prüfung genommen, in dem eine Anschrift am ehesten
 * versehentlich landet.
 */
const ERLAUBTE_ANSCHRIFTEN: readonly { readonly text: string; readonly grund: string }[] = [
  {
    text: 'Hauptstraße 68, 02742 Friedersdorf',
    grund:
      'Sitz von ALL-INKL.COM – Neue Medien Münnich, dem Anbieter des ' +
      'Webspace, auf dem die Datenschutzerklärung liegt (Abschnitt 10.5). ' +
      'Öffentliche Geschäftsanschrift aus dem Impressum des Anbieters.',
  },
];

describe('Rückmeldeweg – die eine Adresse', () => {
  it('nennt genau die Adresse, die gelten soll', () => {
    expect(KONTAKT.epost).toBe(ADRESSE);
  });

  it('behält den Namen als Urheberbenennung', () => {
    /* Der Name ist keine private Kontaktangabe, sondern steht so in der
       EUPL-Auszeichnung, in REUSE.toml und als Herausgeber im Store. Er darf
       beim Entfernen der Kontaktdaten nicht mit verschwinden. */
    expect(KONTAKT.name).toBe('Olaf Willerding');
  });

  it('führt kein Feld für Telefon oder Ort mehr', () => {
    /* Ein übrig gebliebenes leeres Feld wäre die stille Einladung, es wieder
       zu füllen. Deshalb hat der Vertrag die Felder gar nicht erst. */
    expect(Object.keys(KONTAKT).sort()).toEqual(['epost', 'frist', 'name']);
  });

  it('zeigt die Adresse im Handbuch und im Meldetext', () => {
    /* Zwei Stellen mit derselben Adresse laufen auseinander; beide lesen
       deshalb aus KONTAKT. Dieser Test merkt, wenn eine davon wegfällt. */
    expect(SICHTBARER_TEXT).toContain(ADRESSE);
    expect(MELDETEXT).toContain(ADRESSE);
  });
});

describe('Rückmeldeweg – was nicht zurückkehren darf', () => {
  it('erkennt Angaben dieser Art überhaupt', () => {
    /* Gegenprobe. Ohne sie wären die Tests unten auch dann grün, wenn die
       Muster an einer echten Rufnummer vorbeiliefen – und damit wertlos.

       Die Proben sind erfunden, haben aber genau die Form der entfernten
       Angaben. Die echten hier einzusetzen hieße, sie im Arbeitsbaum zu
       behalten – ausgerechnet in der Datei, die sie draußen halten soll. */
    expect(RUFNUMMER.test('Telefon: 09999 111222')).toBe(true);
    expect(RUFNUMMER.test('+49 9999 111222')).toBe(true);
    expect(ANSCHRIFT.test('99999 Musterstadt')).toBe(true);
    expect(ALTE_ADRESSE.test('irgendwer@t-online.de')).toBe(true);
    /* Und die Gegenrichtung: Was dastehen darf, darf nicht anschlagen. */
    expect(RUFNUMMER.test('Fassung 0.21.1, Bau 0218791 vom 22.08.2026')).toBe(false);
    expect(ANSCHRIFT.test(ADRESSE)).toBe(false);
  });

  it('nennt in keinem sichtbaren Text eine Rufnummer oder eine Anschrift', () => {
    expect(SICHTBARER_TEXT).not.toMatch(RUFNUMMER);
    expect(SICHTBARER_TEXT).not.toMatch(ANSCHRIFT);
    expect(SICHTBARER_TEXT).not.toMatch(ALTE_ADRESSE);
  });

  it('nennt auch im Handbuch das Wort „Telefon“ nicht mehr', () => {
    /* Das Handbuch führte den Kanal als Aufzählung „Verantwortlich –
       E-Mail – Telefon“. Fällt die Zeile weg, aber das Wort bleibt, steht
       dort eine Beschriftung ohne Inhalt. */
    expect(SICHTBARER_TEXT).not.toContain('Telefon');
  });

  it('nennt im ausgelieferten Quelltext keine Rufnummer und keine Anschrift', () => {
    const quellen = quelltexte();
    if (quellen === null) {
      /* Kein Git – dann ist nicht feststellbar, was ausgeliefert würde. */
      return;
    }

    expect(quellen.length).toBeGreaterThan(50);
    for (const quelle of quellen) {
      expect(quelle).not.toMatch(RUFNUMMER);
      expect(quelle).not.toMatch(ANSCHRIFT);
      expect(quelle).not.toMatch(ALTE_ADRESSE);
    }
  });

  it('nennt sie auch in keinem anderen versionierten Text – Unterlagen eingeschlossen', () => {
    /* Der Test darüber sieht `app/src`, weil nur das ausgeliefert wird. Für
       den Store genügt das. Für ein öffentliches Archiv nicht: Dort wandert
       der ganze Arbeitsbaum mit, und die privaten Angaben stünden nicht in
       der Anwendung, sondern in den Unterlagen – `docs/stand.md`, `PLAN.md`,
       der Änderungsverlauf. Genau dort hat bisher niemand hingesehen.

       Deshalb dieselben Muster über alles, was Git führt und ein Mensch
       geschrieben hat. Ausgenommen ist, was oben mit Grund dasteht. */
    const dateien = versionierteDateien('.');
    if (dateien === null) {
      /* Kein Git – dann ist nicht feststellbar, was mitwanderte. */
      return;
    }

    const ausgenommen = new Set(AUSGENOMMEN.map((eintrag) => eintrag.datei));
    const zuPruefen = dateien.filter(
      (datei) => TEXTENDUNGEN.some((endung) => datei.endsWith(endung)) && !ausgenommen.has(datei),
    );

    /* Ohne diese Schranke wäre der Test auch dann grün, wenn `git ls-files`
       nichts mehr liefert oder die Endungsliste ins Leere greift. */
    expect(zuPruefen.length).toBeGreaterThan(200);

    const beanstandet: string[] = [];
    for (const datei of zuPruefen) {
      let inhalt = readFileSync(join(wurzel, datei), 'utf8');
      /* Die begründeten Anschriften vor dem Prüfen herausnehmen – und zwar
         als Wortlaut, damit jede andere Anschrift in derselben Datei weiter
         anschlägt. Ein `replaceAll` statt einer Ausnahmeliste für Dateien. */
      for (const erlaubt of ERLAUBTE_ANSCHRIFTEN) {
        inhalt = inhalt.split(erlaubt.text).join('');
      }
      for (const [was, muster] of [
        ['Rufnummer', RUFNUMMER],
        ['Anschrift', ANSCHRIFT],
        ['aufgegebene Adresse', ALTE_ADRESSE],
      ] as const) {
        const treffer = muster.exec(inhalt);
        if (treffer !== null) {
          /* Die Fundstelle wird benannt, der Fund nicht abgedruckt: Eine
             Fehlermeldung landet in Protokollen und Berichten, und dort soll
             die Angabe genauso wenig stehen wie in der Datei. */
          const zeile = inhalt.slice(0, treffer.index).split('\n').length;
          beanstandet.push(`${datei}:${String(zeile)} – ${was}`);
        }
      }
    }

    expect(beanstandet).toEqual([]);
  });
});
