// @vitest-environment node
/**
 * Das Programmsymbol muss zur Zeichnung passen.
 *
 * `build-resources/symbol.svg` ist das Original, `icon.ico` und `icon.png`
 * sind daraus erzeugt (`npm run symbol`). Erzeugte Dateien haben in diesem
 * Projekt eine eigene Fehlerart: Jemand ändert die Quelle, vergisst den
 * Aufruf, und alles bleibt grün – die Anwendung zeigt nur weiter das alte
 * Bild. Genau das fängt die Prüfsumme in `symbol.json` ab.
 *
 * Geprüft wird außerdem die Form der ICO-Datei. Sie wird hier von Hand
 * zusammengesetzt, ohne Fremdprogramm; ein Fehler im Behälter fiele sonst
 * erst auf, wenn Windows das Symbol nicht anzeigt – und das sieht niemand,
 * der die Anwendung nur baut.
 */

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

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

const app = join(fileURLToPath(new URL('..', import.meta.url)));
const ordner = join(app, 'build-resources');

const QUELLE = join(ordner, 'symbol.svg');
const BUCH = join(ordner, 'symbol.json');
const ICO = join(ordner, 'icon.ico');
const PNG = join(ordner, 'icon.png');
/** Die Store-Grafiken; electron-builder sucht sie genau hier. */
const APPX = join(ordner, 'appx');

interface Buchung {
  quelle: string;
  pruefsumme: string;
  groessen: number[];
  pngGroesse: number;
  kacheln: { datei: string; breite: number; hoehe: number }[];
}

/** Ein Eintrag im Verzeichnis einer ICO-Datei. */
interface Symboleintrag {
  breite: number;
  hoehe: number;
  bitPunkt: number;
  laenge: number;
  versatz: number;
  /** `true`, wenn die Bilddaten ein PNG sind statt eines BMP. */
  istPng: boolean;
}

/**
 * Liest das Verzeichnis einer ICO-Datei.
 *
 * Bewusst von Hand statt mit einer Bibliothek: Das Format ist ein Kopf und
 * eine Tabelle, und eine Abhängigkeit, die nur ein Testfall braucht, müsste
 * im Gate mitgeprüft und in der Lizenzübersicht geführt werden.
 */
function icoLesen(daten: Buffer): Symboleintrag[] {
  expect(daten.readUInt16LE(0), 'ICO: erstes Feld muss 0 sein').toBe(0);
  expect(daten.readUInt16LE(2), 'ICO: Typ 1 steht für ein Symbol').toBe(1);

  const anzahl = daten.readUInt16LE(4);
  const eintraege: Symboleintrag[] = [];
  for (let i = 0; i < anzahl; i += 1) {
    const p = 6 + i * 16;
    const versatz = daten.readUInt32LE(p + 12);
    const laenge = daten.readUInt32LE(p + 8);
    eintraege.push({
      // 0 steht für 256 – ein Byte fasst die Zahl nicht.
      breite: daten.readUInt8(p) === 0 ? 256 : daten.readUInt8(p),
      hoehe: daten.readUInt8(p + 1) === 0 ? 256 : daten.readUInt8(p + 1),
      bitPunkt: daten.readUInt16LE(p + 6),
      laenge,
      versatz,
      istPng: daten.readUInt32BE(versatz) === 0x89504e47,
    });
  }
  return eintraege;
}

describe('Programmsymbol', () => {
  it('hat eine Zeichnung als Original', () => {
    expect(existsSync(QUELLE), 'build-resources/symbol.svg fehlt.').toBe(true);
    const svg = readFileSync(QUELLE, 'utf8');
    expect(svg).toContain('<svg');
    // Zwei Kurven ergeben das Zeichen; bliebe eine übrig, stünde dort ein S.
    expect(
      (svg.match(/<path/gu) ?? []).length,
      'Das Zeichen besteht aus zwei Kurven – bei einer wäre es ein S.',
    ).toBe(2);
  });

  it('trägt die Akzentfarbe der Anwendung, keine eigene', () => {
    /* Ein Symbol in einer Farbe, die sonst nirgends vorkommt, fällt beim
       nächsten Wechsel des Farbschemas aus dem Rahmen – und niemand sucht
       danach in einer SVG-Datei. */
    const tokens = readFileSync(
      join(app, 'src', 'renderer', 'src', 'styles', 'tokens.css'),
      'utf8',
    );
    const akzent = /--farbe-akzent:\s*(#[0-9a-f]{6})/iu.exec(tokens)?.[1];

    expect(akzent, 'tokens.css: --farbe-akzent nicht gefunden.').toBeDefined();
    expect(
      readFileSync(QUELLE, 'utf8').toLowerCase(),
      `symbol.svg führt nicht die Akzentfarbe ${String(akzent)}.`,
    ).toContain((akzent ?? '').toLowerCase());
  });

  it('ist so erzeugt worden, wie die Zeichnung jetzt aussieht', () => {
    /* Die eigentliche Zusage. Ohne sie zeigt die Anwendung nach einer
       Änderung an der Zeichnung weiter das alte Bild, und alles bleibt grün. */
    expect(existsSync(BUCH), 'build-resources/symbol.json fehlt.').toBe(true);
    const buch = JSON.parse(readFileSync(BUCH, 'utf8')) as Buchung;
    const jetzt = createHash('sha256').update(readFileSync(QUELLE, 'utf8'), 'utf8').digest('hex');

    expect(
      buch.pruefsumme,
      'build-resources/symbol.svg wurde geändert, aber icon.ico und icon.png ' +
        'stammen noch von der vorigen Fassung. Neu erzeugen mit: npm run symbol',
    ).toBe(jetzt);
  });

  it('liefert alle Größen, die Windows abholt', () => {
    /* Fehlt eine Größe, rechnet Windows selbst um – bei 16 und 24 Punkten
       sichtbar schlechter als eine eigens gerechnete. */
    const buch = JSON.parse(readFileSync(BUCH, 'utf8')) as Buchung;
    const eintraege = icoLesen(readFileSync(ICO));

    expect(eintraege.map((e) => e.breite)).toEqual(buch.groessen);
    expect(eintraege.map((e) => e.hoehe)).toEqual(buch.groessen);
    for (const e of eintraege) {
      expect(e.bitPunkt, `Größe ${String(e.breite)}: nicht 32 Bit je Bildpunkt`).toBe(32);
      expect(e.laenge, `Größe ${String(e.breite)}: leere Bilddaten`).toBeGreaterThan(0);
    }
  });

  it('legt kleine Größen als BMP ab und große als PNG', () => {
    /* PNG in kleinen Größen versteht jedes heutige Windows, BMP auch jedes
       ältere – und bei 16 x 16 macht die Ersparnis nichts aus. */
    const eintraege = icoLesen(readFileSync(ICO));
    for (const e of eintraege) {
      expect(e.istPng, `Größe ${String(e.breite)}: falsche Ablageform`).toBe(e.breite >= 128);
    }
  });

  it('zeigt bei 16 Bildpunkten wirklich etwas', () => {
    /* Eine ICO-Datei mit richtiger Form und leeren Bildern bestünde alle
       Prüfungen darüber. Deshalb einmal in die Bildpunkte gesehen: Bei einem
       Symbol auf farbigem Feld muss die Fläche nahezu vollständig deckend
       sein – und beide Farben müssen vorkommen. */
    const daten = readFileSync(ICO);
    const klein = icoLesen(daten).find((e) => e.breite === 16);
    expect(klein, 'Keine Größe 16 in der Datei.').toBeDefined();

    const anfang = (klein?.versatz ?? 0) + 40; // hinter dem BMP-Kopf
    let deckend = 0;
    let hell = 0;
    let dunkel = 0;
    for (let i = 0; i < 16 * 16; i += 1) {
      const p = anfang + i * 4;
      const [blau, gruen, rot, deckung] = [
        daten.readUInt8(p),
        daten.readUInt8(p + 1),
        daten.readUInt8(p + 2),
        daten.readUInt8(p + 3),
      ];
      if (deckung > 200) {
        deckend += 1;
        if (rot + gruen + blau > 600) {
          hell += 1;
        } else if (rot + gruen + blau < 300) {
          dunkel += 1;
        }
      }
    }

    expect(deckend, 'Das 16er-Bild ist überwiegend durchsichtig.').toBeGreaterThan(200);
    expect(hell, 'Im 16er-Bild fehlt das helle Zeichen.').toBeGreaterThan(10);
    expect(dunkel, 'Im 16er-Bild fehlt das farbige Feld.').toBeGreaterThan(60);
  });

  it('legt eine große Fassung als PNG daneben', () => {
    /* macOS nimmt kein ICO. */
    const buch = JSON.parse(readFileSync(BUCH, 'utf8')) as Buchung;
    const png = readFileSync(PNG);

    expect(png.readUInt32BE(0), 'icon.png ist keine PNG-Datei.').toBe(0x89504e47);
    expect(png.readUInt32BE(16), 'icon.png: falsche Breite').toBe(buch.pngGroesse);
    expect(png.readUInt32BE(20), 'icon.png: falsche Höhe').toBe(buch.pngGroesse);
    expect(buch.pngGroesse, 'Für macOS mindestens 512.').toBeGreaterThanOrEqual(512);
  });

  /*
    Die Wache, die gefehlt hat.

    electron-builder sucht die Grafiken des Store-Pakets ausschließlich unter
    `build-resources/appx/` und **schweigt**, wenn sie fehlen: Es greift dann
    zu `SampleAppx.*` aus seinem eigenen Werkzeugpaket. Am Paket 0.22.0
    nachgemessen lagen genau diese Beispielbilder darin – Store-Eintrag,
    Startmenü-Kachel und Taskleiste zeigten ein fremdes Platzhalterbild.
    Weder ein Test noch eine Zeile Dokumentation erwähnte das; `icon.ico` und
    `icon.png` fasst das AppX-Ziel gar nicht an.
  */
  it('bringt die sechs Store-Grafiken mit', () => {
    const buch = JSON.parse(readFileSync(BUCH, 'utf8')) as Buchung;

    expect(buch.kacheln.map((k) => k.datei).sort()).toEqual([
      'LargeTile.png',
      'SmallTile.png',
      'Square150x150Logo.png',
      'Square44x44Logo.png',
      'StoreLogo.png',
      'Wide310x150Logo.png',
    ]);

    for (const kachel of buch.kacheln) {
      const bild = readFileSync(join(APPX, kachel.datei));
      expect(bild.readUInt32BE(0), `${kachel.datei} ist keine PNG-Datei.`).toBe(0x89504e47);
      expect(bild.readUInt32BE(16), `${kachel.datei}: falsche Breite`).toBe(kachel.breite);
      expect(bild.readUInt32BE(20), `${kachel.datei}: falsche Höhe`).toBe(kachel.hoehe);
      /* Eine leere oder abgeschnittene Datei fällt hier durch. */
      expect(bild.length, `${kachel.datei} ist verdächtig klein.`).toBeGreaterThan(500);
    }
  });

  /*
    Die Wache hinter der Wache – und der Grund, warum es die erste allein
    nicht tut.

    Die Liste oben ist von Hand gepflegt. Sie sagt, was dieses Projekt
    mitliefern will, nicht, was electron-builder verlangt. Genau dort lag der
    Fehler: `AppxTarget.js` führt eine Liste `vendorAssetsForDefaultAssets`,
    und für jeden Eintrag, den `build-resources/appx/` **nicht** liefert,
    kopiert es sein eigenes `SampleAppx.*` ins Paket – ohne Warnung, ohne
    Eintrag im Bauprotokoll. Am Paket 0.22.0 traf das alle vier: Die Kacheln
    darin waren byteweise identisch mit den Beispielbildern. Der Store hat es
    am 31.08.2026 unter Richtlinie 10.1.1.11 beanstandet.

    Deshalb wird die Liste hier aus electron-builder selbst gelesen statt
    danebengeschrieben. Kommt dort ein fünfter Eintrag hinzu, wird dieser
    Test rot – statt dass ein neues Platzhalterbild still mitreist.
  */
  it('lässt electron-builder kein einziges Beispielbild einsetzen', () => {
    const quelle = readFileSync(
      join(app, 'node_modules', 'app-builder-lib', 'out', 'targets', 'AppxTarget.js'),
      'utf8',
    );
    const block = /vendorAssetsForDefaultAssets\s*=\s*\{([^}]*)\}/u.exec(quelle);

    expect(
      block,
      'Die Liste der Beispielbilder steht nicht mehr in AppxTarget.js. Von Hand nachsehen, ' +
        'wie electron-builder fehlende Store-Grafiken heute ersetzt.',
    ).not.toBeNull();

    /* Die Liste steht dort in doppelten Anführungszeichen; einfache sind
       mitgefasst, damit eine Umformatierung von electron-builder den
       Ausdruck nicht bricht. */
    const verlangt = [...(block?.[1] ?? '').matchAll(/["']([^"']+\.png)["']\s*:/gu)]
      .map((treffer) => treffer[1])
      .filter((name): name is string => name !== undefined);

    expect(verlangt.length, 'Kein einziger Eintrag gelesen – der Ausdruck passt nicht mehr.').toBe(
      4,
    );

    for (const name of verlangt) {
      expect(
        existsSync(join(APPX, name)),
        `${name} fehlt in build-resources/appx/. electron-builder legte sonst sein ` +
          'Beispielbild ins Store-Paket.',
      ).toBe(true);
    }
  });
});
