// @vitest-environment jsdom
/**
 * Fehlerprotokoll und Fragenliste – Aufbau, Wahrheitsgehalt, Umfang.
 *
 * Geprüft wird an einem kleinen, aber echten Fragenzuschnitt: eine
 * Auswahlfrage mit einer richtigen Antwort, eine mit zweien, eine mit
 * Abbildung an der Option, eine offene Frage. Genau diese vier Fälle sind es,
 * an denen die Dokumente auseinandergingen.
 */

import axe from 'axe-core';
import { describe, expect, it } from 'vitest';

import { jsdomOptionen } from '../src/shared/wcag';

import {
  fehlerprotokollBauen,
  fehlerprotokollDateiname,
} from '../src/shared/druck/fehlerprotokoll';
import { fragenlisteBauen, fragenlisteDateiname } from '../src/shared/druck/fragenliste';
import type { Papierfrage } from '../src/shared/druck/frageblock';
import { frageKurzbezeichnung } from '../src/shared/druck/frageblock';
import type { Quellenangabe } from '../src/shared/druck/dokument';
import { seitenFehlerprotokoll, seitenFragenliste, umfangssatz } from '../src/shared/druck/umfang';
import type { Frage, Kapitel } from '../src/shared/katalog';

const BILD_URL = 'data:image/png;base64,iVBORw0KGgo=';

function option(label: string, text: string, korrekt: boolean, bilder: string[] = []) {
  return { label, inhalt: { text, segmente: [{ t: text }] }, korrekt, bilder };
}

const EINE_RICHTIGE: Frage = {
  id: 'I.1-01',
  amtliche_nummer: '1.01',
  kapitel: 'I',
  abschnitt: 'I.1',
  typ: 'mc',
  seite: 1,
  frage: {
    text: 'Was regelt das Waffengesetz?',
    segmente: [{ t: 'Was regelt das Waffengesetz?' }],
  },
  bilder: [],
  optionen: [
    option('a', 'Den Umgang mit Waffen.', true),
    option('b', 'Die Jagd.', false),
    option('c', 'Den Straßenverkehr.', false),
  ],
};

const ZWEI_RICHTIGE: Frage = {
  ...EINE_RICHTIGE,
  id: 'I.1-02',
  amtliche_nummer: '1.02',
  frage: { text: 'Welche Aussagen treffen zu?', segmente: [{ t: 'Welche Aussagen treffen zu?' }] },
  optionen: [
    option('a', 'Erste zutreffende Aussage.', true),
    option('b', 'Falsche Aussage.', false),
    option('c', 'Zweite zutreffende Aussage.', true),
  ],
};

/* Der Härtefall aus dem echten Katalog: Bei Frage 3.05 haben zwei Optionen
   überhaupt keinen Text, dort steht nur das Prüfzeichen. */
const BILDOPTIONEN: Frage = {
  ...EINE_RICHTIGE,
  id: 'I.3-05',
  amtliche_nummer: '3.05',
  abschnitt: 'I.3',
  frage: {
    text: 'Welches Zeichen ist richtig?',
    segmente: [{ t: 'Welches Zeichen ist richtig?' }],
  },
  optionen: [option('a', '', true, ['zeichen-1']), option('b', '', false, ['zeichen-2'])],
};

const OFFEN: Frage = {
  id: 'II-01',
  amtliche_nummer: '2.01',
  kapitel: 'II',
  abschnitt: null,
  typ: 'freitext',
  seite: 2,
  frage: {
    text: 'Erklären Sie den Begriff Führen.',
    segmente: [{ t: 'Erklären Sie den Begriff Führen.' }],
  },
  bilder: [],
  musterantwort: {
    text: 'Das Ausüben der tatsächlichen Gewalt außerhalb der eigenen Wohnung.',
    segmente: [
      { t: 'Das ' },
      { t: 'Ausüben der tatsächlichen Gewalt', h: true },
      { t: ' außerhalb der eigenen Wohnung.' },
    ],
  },
};

const KAPITEL: readonly Kapitel[] = [
  {
    id: 'I',
    titel: 'Waffenrecht',
    abschnitte: [
      { id: 'I.1', titel: 'Begriffe' },
      { id: 'I.3', titel: 'Kennzeichnung' },
    ],
  },
  { id: 'II', titel: 'Technik', abschnitte: [] },
];

const QUELLE = (
  fragen: number,
  loesungen: 'alle' | 'keine',
  optionen: 'alle' | 'nur die richtigen' = 'alle',
): Quellenangabe => ({
  amtlich: 'Amtlicher Fragenkatalog, Stand 16.12.2024.',
  herausgeber: 'Bundesverwaltungsamt',
  stand: '2024-12-16',
  quelleUrl: 'https://www.bva.bund.de/',
  umfang: { art: 'wiedergabe', fragen, fragenGesamt: 575, loesungen, optionen },
});

function papier(frage: Frage, mitErklaerung = false): Papierfrage {
  return {
    frage,
    kapitelTitel: frage.kapitel === 'I' ? 'Waffenrecht' : 'Technik',
    bilder: new Map([
      ['zeichen-1', BILD_URL],
      ['zeichen-2', BILD_URL],
    ]),
    alttexte: new Map([
      ['zeichen-1', 'Adler über dem Buchstaben N'],
      ['zeichen-2', 'Krone über den Buchstaben PTB'],
    ]),
    erklaerung: mitErklaerung
      ? {
          kurz: 'Es regelt den Umgang.',
          text: 'Ausführlich: Antwort b ist falsch, weil die Jagd im Bundesjagdgesetz steht.',
          merksatz: 'Umgang, nicht Gegenstand.',
          fundstellen: ['§ 1 Abs. 1 WaffG'],
        }
      : undefined,
  };
}

/* Wie in `druck-struktur.test.ts`: Das Dokument ist eine vollständige
   HTML-Datei, kein Fragment – geparst wird es deshalb, statt es in den
   Testbaum zu hängen. */
function baum(html: string): Document {
  return new DOMParser().parseFromString(html, 'text/html');
}

// ─── Fehlerprotokoll ────────────────────────────────────────────────────

describe('Fehlerprotokoll', () => {
  const dokument = fehlerprotokollBauen({
    fragen: [papier(EINE_RICHTIGE, true), papier(ZWEI_RICHTIGE, true)],
    profilName: 'Max',
    erstelltAm: '2026-08-23T09:00:00.000Z',
    tiefe: 'kurz',
    schriftgroesse: 'normal',
    quelle: QUELLE(2, 'alle'),
  });

  /*
    Der entscheidende Punkt. Nachgemessen nennen 302 der 471 Erklärungen zu
    Auswahlfragen eine FALSCHE Option beim Buchstaben („Antwort b ist
    falsch …“). Ein Dokument mit nur der richtigen Antwort machte seine
    eigenen Begründungen unlesbar.
  */
  it('druckt alle Antwortmöglichkeiten, nicht nur die richtige', () => {
    expect(dokument).toContain('Den Umgang mit Waffen.');
    expect(dokument).toContain('Die Jagd.');
    expect(dokument).toContain('Den Straßenverkehr.');
  });

  it('kennzeichnet die richtige Antwort und sagt es bei mehreren', () => {
    const doc = baum(dokument);
    const punkte = [...doc.querySelectorAll('.optionen li')].map((li) => li.textContent);

    expect(punkte.filter((t) => t.includes('☑'))).toHaveLength(3);
    expect(dokument).toContain('Bei dieser Frage sind mehrere Antworten richtig.');
  });

  it('nennt in jeder Überschrift Kapitel und Nummer', () => {
    /* 575 Fragen tragen nur 437 verschiedene amtliche Nummern; 227 Fragen
       teilen sich eine mit einer anderen. Ohne das Kapitel ist die Frage im
       amtlichen Werk nicht nachschlagbar. */
    const doc = baum(dokument);
    for (const h3 of doc.querySelectorAll('h3')) {
      expect(h3.textContent).toMatch(/^Kapitel [IVX]+, Frage /u);
    }
  });

  it('kennzeichnet die Erklärung als eigenen Inhalt – vollständig', () => {
    expect(dokument).toContain(
      'Diese Erklärung ist eine Ergänzung dieser Software und nicht Teil des amtlichen Fragenkatalogs.',
    );
  });

  it('lässt die ausführliche Begründung bei „kurz“ weg', () => {
    expect(dokument).toContain('Es regelt den Umgang.');
    expect(dokument).not.toContain('Bundesjagdgesetz');
    expect(dokument).not.toContain('§ 1 Abs. 1 WaffG');
  });

  it('nimmt bei „voll“ Begründung, Merksatz und Fundstellen mit', () => {
    const voll = fehlerprotokollBauen({
      fragen: [papier(EINE_RICHTIGE, true)],
      profilName: 'Max',
      erstelltAm: '2026-08-23T09:00:00.000Z',
      tiefe: 'voll',
      schriftgroesse: 'normal',
      quelle: QUELLE(1, 'alle'),
    });

    expect(voll).toContain('Bundesjagdgesetz');
    expect(voll).toContain('Umgang, nicht Gegenstand.');
    expect(voll).toContain('§ 1 Abs. 1 WaffG');
  });

  it('sagt bei null Fehlern, dass das Dokument leer bleibt', () => {
    const leer = fehlerprotokollBauen({
      fragen: [],
      profilName: 'Max',
      erstelltAm: '2026-08-23T09:00:00.000Z',
      tiefe: 'kurz',
      schriftgroesse: 'normal',
      quelle: QUELLE(0, 'alle'),
    });

    expect(leer).toContain('keine Frage als zuletzt falsch beantwortet vermerkt');
  });

  it('spricht im Singular, wenn es eine einzige Frage ist', () => {
    const eine = fehlerprotokollBauen({
      fragen: [papier(EINE_RICHTIGE)],
      profilName: 'Max',
      erstelltAm: '2026-08-23T09:00:00.000Z',
      tiefe: 'kurz',
      schriftgroesse: 'normal',
      quelle: QUELLE(1, 'alle'),
    });

    expect(eine).toContain('die eine Frage, die Sie zuletzt falsch beantwortet haben');
  });

  it('macht aus Name und Datum einen brauchbaren Dateinamen', () => {
    expect(fehlerprotokollDateiname('Max Mustermann', '2026-08-23T09:00:00.000Z')).toBe(
      'Fehlerprotokoll-Max-Mustermann-2026-08-23.pdf',
    );
  });

  /*
    Die Wache, die gefehlt hat.

    `absatz()` maskiert seinen ganzen Parameter. `frageblock.ts` übergab ihm
    bis 0.24.1 Zeichenketten, die bereits maskiert waren UND Auszeichnung
    enthielten. Auf dem Papier stand deshalb an jeder Frage mit Erklärung
    wörtlich „<strong>Kurz:</strong>“, und aus jedem kaufmännischen Und wurde
    „&amp;“. Kein Test hat es gemeldet, weil alle Prüftexte aus Buchstaben und
    Punkten bestanden.

    Deshalb trägt der Prüftext hier jetzt genau die Zeichen, an denen es
    schiefging.
  */
  const HEIKEL: Frage = {
    ...EINE_RICHTIGE,
    id: 'I.2-08',
    amtliche_nummer: '2.08',
    frage: {
      text: 'Welche Erlaubnis braucht eine Pistole .32 S&W?',
      segmente: [{ t: 'Welche Erlaubnis braucht eine Pistole .32 S&W?' }],
    },
    optionen: [option('a', 'Waffenbesitzkarte & Bedürfnis', true), option('b', 'Keine', false)],
  };

  const heikelDokument = fehlerprotokollBauen({
    fragen: [
      {
        ...papier(HEIKEL, true),
        erklaerung: {
          kurz: 'Kurz mit & und "Zitat".',
          text: 'Volltext mit & Zeichen.',
          merksatz: 'Merken: A & B.',
          fundstellen: ['§ 1 Abs. 1 WaffG'],
        },
      },
    ],
    profilName: 'Max & Moritz',
    erstelltAm: '2026-08-23T09:00:00.000Z',
    tiefe: 'voll',
    schriftgroesse: 'normal',
    quelle: QUELLE(1, 'alle'),
  });

  it('setzt die Beschriftungen als Fettschrift und nicht als sichtbaren Quelltext', () => {
    expect(heikelDokument).toContain('<strong>Kurz:</strong>');
    expect(heikelDokument).toContain('<strong>Merksatz:</strong>');
    expect(heikelDokument).not.toContain('&lt;strong&gt;');
  });

  it('maskiert Sonderzeichen genau einmal', () => {
    /* Einmal maskiert ist „&amp;“ – zweimal maskiert wäre „&amp;amp;“, und
       der Leser sähe auf dem Papier „&amp;“ stehen. */
    expect(heikelDokument).not.toContain('&amp;amp;');
    expect(heikelDokument).not.toContain('&amp;quot;');
    expect(heikelDokument).toContain('Pistole .32 S&amp;W?');

    const text = baum(heikelDokument).body.textContent;
    expect(text).toContain('Pistole .32 S&W?');
    expect(text).toContain('Waffenbesitzkarte & Bedürfnis');
    expect(text).toContain('Kurz mit & und "Zitat".');
    expect(text).toContain('Merken: A & B.');
    expect(text).not.toContain('<strong>');
  });

  /*
    Das Fehlerprotokoll hat keinen Lösungsanhang – anders als die Fragenliste.
    Wer es hört, muss die Lösung trotzdem erfahren; das Kästchen ☑ ist
    `aria-hidden` und trägt sie nicht.
  */
  it('nennt die richtige Antwort auch im gelesenen Text, nicht nur im Kästchen', () => {
    const punkte = [...baum(heikelDokument).querySelectorAll('.optionen li')];
    expect(punkte).toHaveLength(2);
    expect(punkte[0]?.textContent).toContain('Richtig:');
    expect(punkte[1]?.textContent).toContain('Falsch:');
  });
});

// ─── Fragenliste ────────────────────────────────────────────────────────

describe('Fragenliste', () => {
  const daten = {
    fragen: [papier(EINE_RICHTIGE), papier(BILDOPTIONEN), papier(OFFEN)],
    auswahl: 'gesamter Fragenkatalog',
    erstelltAm: '2026-08-23T09:00:00.000Z',
    kapitel: KAPITEL,
    schriftgroesse: 'normal' as const,
  };

  const ohneLoesung = fragenlisteBauen({
    ...daten,
    mitLoesungen: false,
    quelle: QUELLE(3, 'keine'),
  });

  const mitLoesung = fragenlisteBauen({
    ...daten,
    mitLoesungen: true,
    quelle: QUELLE(3, 'alle'),
  });

  it('verrät im Hauptteil nicht, welche Antwort richtig ist', () => {
    const doc = baum(ohneLoesung);
    const punkte = [...doc.querySelectorAll('.optionen li')].map((li) => li.textContent);

    expect(punkte.length).toBeGreaterThan(0);
    expect(punkte.some((t) => t.includes('☑'))).toBe(false);
    expect(punkte.every((t) => t.includes('☐'))).toBe(true);
  });

  it('druckt trotzdem alle Antwortmöglichkeiten im Wortlaut', () => {
    expect(ohneLoesung).toContain('Den Umgang mit Waffen.');
    expect(ohneLoesung).toContain('Die Jagd.');
  });

  it('bettet die Prüfzeichen ein – sonst wäre die Option leer', () => {
    /* Bei Frage 3.05 haben zwei Optionen keinen Text. Ohne Bild stünde dort
       nichts, und ein Bildschirmleser fände einen leeren Listenpunkt. */
    const doc = baum(ohneLoesung);
    const bilder = [...doc.querySelectorAll('.optionen img')];

    expect(bilder).toHaveLength(2);
    for (const b of bilder) {
      expect(b.getAttribute('src')).toMatch(/^data:image\//u);
      expect(b.getAttribute('alt')).toBeTruthy();
    }
  });

  it('gibt offenen Fragen Platz zum Schreiben statt der Musterantwort', () => {
    expect(ohneLoesung).toContain('schreibfeld');
    expect(ohneLoesung).not.toContain('Ausüben der tatsächlichen Gewalt');
  });

  it('führt den Lösungsanhang nur auf Wunsch – und nennt dort Kapitel und Nummer', () => {
    /* Geprüft wird die Überschrift, nicht das Wort: Der Vorspann nennt den
       Anhang auch dann, wenn er fehlt – er sagt dort, dass er sich
       zuschalten lässt. */
    const ueberschriften = (html: string): string[] =>
      [...baum(html).querySelectorAll('h2')].map((e) => e.textContent);

    expect(ueberschriften(ohneLoesung)).not.toContain('Lösungsanhang');
    expect(ueberschriften(mitLoesung)).toContain('Lösungsanhang');
    expect(mitLoesung).toContain(`${frageKurzbezeichnung(EINE_RICHTIGE)}:`);
    expect(mitLoesung).toContain('Richtig ist a');
  });

  it('nennt im Anhang die Musterantwort offener Fragen', () => {
    expect(mitLoesung).toContain('Ausüben der tatsächlichen Gewalt');
  });

  it('fasst die Fragen unter Zwischenüberschriften je Bereich', () => {
    const doc = baum(ohneLoesung);
    const h2 = [...doc.querySelectorAll('h2')].map((e) => e.textContent);

    expect(h2).toContain('I.1 – Begriffe');
    expect(h2).toContain('II – Technik');
  });

  it('macht aus Auswahl und Datum einen brauchbaren Dateinamen', () => {
    expect(fragenlisteDateiname('Kapitel I – Waffenrecht', '2026-08-23T09:00:00.000Z')).toBe(
      'Fragenliste-Kapitel-I-Waffenrecht-2026-08-23.pdf',
    );
  });
});

// ─── Umfangsschätzung ───────────────────────────────────────────────────

describe('Umfangsschätzung', () => {
  /*
    Die Stützstellen stammen aus wirklich gedruckten PDF. Der erste Anlauf
    rechnete mit einer Faustregel und lag um Faktor 1,7 bis 2,8 zu niedrig –
    die Fragenliste über den ganzen Katalog wurde mit 55 Seiten angekündigt
    und hatte 162. Diese Prüfungen halten die gemessenen Werte fest.
  */
  it.each([
    ['Fehlerprotokoll 150 kurz', seitenFehlerprotokoll(150, 'kurz', 'normal'), 70],
    ['Fehlerprotokoll 150 voll', seitenFehlerprotokoll(150, 'voll', 'normal'), 169],
    ['Fehlerprotokoll 486 voll', seitenFehlerprotokoll(486, 'voll', 'normal'), 539],
    ['Fragenliste 575', seitenFragenliste(575, false, 'normal'), 162],
    ['Fragenliste 575 mit Anhang', seitenFragenliste(575, true, 'normal'), 185],
  ])('trifft %s auf 15 Prozent', (_name, geschaetzt, gemessen) => {
    expect(Math.abs(geschaetzt - gemessen) / gemessen).toBeLessThan(0.15);
  });

  it('schätzt den Großdruck eher zu hoch als zu niedrig', () => {
    /* Lieber ein Dokument, das kürzer ausfällt als angekündigt. */
    expect(seitenFragenliste(575, false, 'gross')).toBeGreaterThanOrEqual(279);
    expect(seitenFehlerprotokoll(486, 'voll', 'gross')).toBeGreaterThanOrEqual(982);
  });

  it('nennt Blatt und Seiten, weil beides verschiedene Dinge sind', () => {
    expect(umfangssatz(982)).toContain('982 Seiten');
    expect(umfangssatz(982)).toContain('491 Blatt');
  });

  it('bleibt bei einer Seite im Singular', () => {
    expect(umfangssatz(1)).toBe('Geschätzt eine Seite.');
    expect(seitenFragenliste(0, true, 'normal')).toBe(1);
  });
});

// ─── Struktur des erzeugten Dokuments ───────────────────────────────────

describe('Struktur beider Dokumente', () => {
  const beide = [
    [
      'Fehlerprotokoll',
      fehlerprotokollBauen({
        fragen: [papier(EINE_RICHTIGE, true), papier(BILDOPTIONEN), papier(OFFEN)],
        profilName: 'Max',
        erstelltAm: '2026-08-23T09:00:00.000Z',
        tiefe: 'voll',
        schriftgroesse: 'normal',
        quelle: QUELLE(3, 'alle'),
      }),
    ],
    [
      'Fragenliste',
      fragenlisteBauen({
        fragen: [papier(EINE_RICHTIGE), papier(BILDOPTIONEN), papier(OFFEN)],
        auswahl: 'gesamter Fragenkatalog',
        erstelltAm: '2026-08-23T09:00:00.000Z',
        mitLoesungen: true,
        kapitel: KAPITEL,
        schriftgroesse: 'normal',
        quelle: QUELLE(3, 'alle'),
      }),
    ],
  ] as const;

  it.each(beide)('%s: hält die Überschriftenhierarchie ohne Sprünge ein', (_name, html) => {
    /* Ohne diese Regel wird der Strukturbaum des PDF unbrauchbar – und er ist
       die einzige Orientierung, die ein Bildschirmleser im PDF hat. */
    const ebenen = [...baum(html).querySelectorAll('h1, h2, h3, h4, h5, h6')].map((e) =>
      Number(e.tagName.slice(1)),
    );

    expect(ebenen[0]).toBe(1);
    expect(ebenen.filter((e) => e === 1)).toHaveLength(1);
    for (let stelle = 1; stelle < ebenen.length; stelle += 1) {
      expect(ebenen[stelle]!).toBeLessThanOrEqual(ebenen[stelle - 1]! + 1);
    }
  });

  it.each(beide)('%s: bettet Bilder ein und führt nirgendwohin', (_name, html) => {
    const doc = baum(html);

    expect(doc.querySelectorAll('script')).toHaveLength(0);
    expect(doc.querySelectorAll('a[href], link[href]')).toHaveLength(0);
    for (const b of doc.querySelectorAll('img')) {
      expect(b.getAttribute('src') ?? '').toMatch(/^data:image\//u);
      expect(b.getAttribute('alt')).toBeTruthy();
    }
  });

  it.each(beide)('%s: nennt die Herkunft im sichtbaren Text', (_name, html) => {
    const text = baum(html).body.textContent;

    expect(text).toContain('Bundesverwaltungsamt');
    expect(text).toContain('des amtlichen Fragenkatalogs wieder');
    expect(text).toContain('stammen nicht aus dem amtlichen Fragenkatalog');
  });

  it.each(beide)('%s: besteht axe ohne Verstoß', async (_name, html) => {
    const geparst = baum(html);
    document.documentElement.lang = geparst.documentElement.lang;
    document.title = geparst.title;
    document.body.innerHTML = geparst.body.innerHTML;

    const ergebnis = await axe.run(document.body, jsdomOptionen());

    expect(ergebnis.violations.map((v) => `${v.id}: ${v.help}`)).toEqual([]);
  });
});
