/**
 * Der Block „Diese Arbeit unterstützen“ in „Über diese Software“.
 *
 * Geprüft wird beides, und das ist der Punkt: der Zustand von heute (keine
 * Adresse eingetragen – das Angebot fehlt vollständig) und der von morgen
 * (eine Adresse eingetragen – der Knopf öffnet genau sie). Die Komponente
 * bekommt das Ziel deshalb als Eigenschaft; die eine Stelle, an der die
 * Adresse steht, ist `shared/unterstuetzung.ts` und wird in
 * `tests/unterstuetzung.test.ts` geprüft.
 *
 * Das Angebot ist eine Bitte und kein Handel: Es schaltet nichts frei, es
 * nennt keinen Betrag, und es kommt von selbst nie wieder. Die letzten beiden
 * Zusagen stehen unten als eigene Prüfungen – sie sind der Grund, warum ein
 * Spendenknopf in dieser Anwendung überhaupt vertretbar ist.
 */

import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import axe from 'axe-core';
import { afterEach, describe, expect, it, vi } from 'vitest';

import { Unterstuetzung } from '../src/renderer/src/components/ueber/Unterstuetzung';
import { KONTAKT } from '../src/shared/kontakt';
import { jsdomOptionen } from '../src/shared/wcag';
import { UNTERSTUETZUNG_KNOPF, UNTERSTUETZUNG_TITEL } from '../src/shared/unterstuetzung';

/** Eine erfundene, aber formgerechte Adresse – die echte steht noch nicht fest. */
const ZIEL = 'https://beispiel.example/unterstuetzen';

/** Attrappe des einzigen Kanals, den dieser Block braucht. */
function bruecke(oeffnen: (url: string) => Promise<boolean>): void {
  vi.stubGlobal('lernApp', { unterstuetzungOeffnen: vi.fn(oeffnen) });
}

function knopf(): HTMLElement {
  return screen.getByRole('button', { name: UNTERSTUETZUNG_KNOPF });
}

afterEach(() => {
  vi.unstubAllGlobals();
});

describe('Unterstützungsangebot – wann es erscheint', () => {
  it('erscheint, sobald eine Adresse eingetragen ist', () => {
    bruecke(() => Promise.resolve(true));
    render(<Unterstuetzung ziel={ZIEL} />);

    expect(
      screen.getByRole('heading', { name: UNTERSTUETZUNG_TITEL, level: 2 }),
    ).toBeInTheDocument();
    expect(knopf()).toBeInTheDocument();
    // Wer gleich den Browser verlässt, soll vorher lesen können, wohin.
    expect(screen.getByText(ZIEL)).toBeInTheDocument();
  });

  it('fehlt vollständig, wenn keine Adresse eingetragen ist', () => {
    /*
      Vollständig heißt: kein Knopf, keine Überschrift, kein leerer Block und
      auch keine Zeile „zurzeit nicht verfügbar“. Ein toter Knopf sagt zu, dass
      es dort weitergeht; eine Erklärung, warum gerade nichts geht, ist eine
      Auskunft über den Betreiber und keine über die Anwendung.
    */
    bruecke(() => Promise.resolve(true));
    const { container } = render(<Unterstuetzung ziel={null} />);

    expect(container).toBeEmptyDOMElement();
    expect(screen.queryByRole('button')).toBeNull();
    expect(screen.queryByRole('heading')).toBeNull();
  });

  it('fehlt vollständig, wenn der Anwendungskern den Kanal nicht anbietet', () => {
    /* Der Anwendungskern kann älter sein als die Oberfläche. Ein Knopf, der
       auf Druck erklären müsste, dass er nicht kann, wäre eine Zusage ohne
       Deckung – dieselbe Regel wie beim Kopierknopf im Meldeblock. */
    vi.stubGlobal('lernApp', {});
    const { container } = render(<Unterstuetzung ziel={ZIEL} />);

    expect(container).toBeEmptyDOMElement();
  });

  it('fehlt vollständig, wenn gar keine Brücke da ist', () => {
    /* Im Browser ohne Preload – etwa in diesen Tests, wenn niemand eine
       Attrappe setzt. */
    const { container } = render(<Unterstuetzung ziel={ZIEL} />);

    expect(container).toBeEmptyDOMElement();
  });
});

describe('Unterstützungsangebot – was der Knopf tut', () => {
  it('öffnet genau die angezeigte Adresse und keine andere', async () => {
    const oeffnen = vi.fn<(url: string) => Promise<boolean>>().mockResolvedValue(true);
    vi.stubGlobal('lernApp', { unterstuetzungOeffnen: oeffnen });
    const nutzer = userEvent.setup();
    render(<Unterstuetzung ziel={ZIEL} />);

    await nutzer.click(knopf());

    /* Auf die Aufrufliste und nicht auf `toHaveBeenCalledWith`: So fällt auch
       ein zweiter Aufruf mit einer anderen Adresse auf. */
    expect(oeffnen.mock.calls).toEqual([[ZIEL]]);
  });

  it('geht ausschließlich über den Kanal des Hauptprozesses', async () => {
    /*
      Die Anwendung öffnet von sich aus keine Verbindung nach außen – kein
      `fetch`, kein `net.request`, und auch kein `window.open`, das an der
      Prüfung im Hauptprozess vorbeiliefe. Der Knopf ist ein `<button>` und
      kein `<a href="https://…">`: Diese Anwendung navigiert nicht, sie stößt
      den Systembrowser an.
    */
    const geoeffnet = vi.fn();
    const geladen = vi.fn();
    vi.stubGlobal('open', geoeffnet);
    vi.stubGlobal('fetch', geladen);
    const oeffnen = vi.fn<(url: string) => Promise<boolean>>().mockResolvedValue(true);
    vi.stubGlobal('lernApp', { unterstuetzungOeffnen: oeffnen });
    const nutzer = userEvent.setup();
    render(<Unterstuetzung ziel={ZIEL} />);

    expect(screen.queryAllByRole('link')).toEqual([]);
    await nutzer.click(knopf());

    expect(geoeffnet).not.toHaveBeenCalled();
    expect(geladen).not.toHaveBeenCalled();
    expect(oeffnen).toHaveBeenCalledTimes(1);
  });

  it('zeigt die Adresse zum Abschreiben, wenn der Browser sich nicht öffnen lässt', async () => {
    const nutzer = userEvent.setup();
    bruecke(() => Promise.resolve(false));
    render(<Unterstuetzung ziel={ZIEL} />);

    await nutzer.click(knopf());

    const meldung = await screen.findByRole('alert');
    expect(meldung).toHaveTextContent(/Standardbrowser/u);
    // Die Adresse bleibt stehen – die Meldung verweist auf sie.
    expect(screen.getByText(ZIEL)).toBeInTheDocument();
  });

  it('meldet auch einen Fehler des Kanals, statt stumm zu bleiben', async () => {
    const nutzer = userEvent.setup();
    bruecke(() => Promise.reject(new Error('Kanal weg')));
    render(<Unterstuetzung ziel={ZIEL} />);

    await nutzer.click(knopf());

    await waitFor(() => {
      expect(screen.getByRole('alert')).toHaveTextContent(/Kanal weg/u);
    });
  });
});

describe('Unterstützungsangebot – kein Dark Pattern', () => {
  it('nennt keinen Betrag und keinen Vorschlag', () => {
    /* Kein „schon ab 3 Euro“, kein Schieberegler, keine Stufen. Wer geben
       will, entscheidet auf der Seite des Dienstes, wie viel. */
    bruecke(() => Promise.resolve(true));
    const { container } = render(<Unterstuetzung ziel={ZIEL} />);
    const text = container.textContent;

    expect(text).not.toMatch(/€|EUR\b|Euro|Dollar|\$/u);
    expect(text).not.toMatch(/\bab \d/u);
  });

  it('sagt ausdrücklich, dass nichts freigeschaltet wird', () => {
    /*
      Store-Richtlinie und Haltung zugleich: Hinter dem Verweis wird kein
      digitales Gut und keine Funktion verkauft. Das steht nicht nur im
      Quelltext, sondern für den Lernenden lesbar auf dem Bildschirm – sonst
      wäre es eine Zusage, die nur der Entwickler kennt.
    */
    bruecke(() => Promise.resolve(true));
    const { container } = render(<Unterstuetzung ziel={ZIEL} />);
    const text = container.textContent;

    expect(text).toMatch(/kostenlos/u);
    expect(text).toMatch(/Funktionsumfang ändert das nichts/u);
    expect(text).toMatch(/freizuschalten/u);
  });

  it('sagt zu, dass die Bitte beim Lernen nicht wiederkommt', () => {
    bruecke(() => Promise.resolve(true));
    const { container } = render(<Unterstuetzung ziel={ZIEL} />);

    expect(container.textContent).toMatch(/beim Lernen kommt diese Bitte nicht wieder/u);
  });

  /*
    Bis Fassung 0.24.1 stand hier „ihr Quelltext ist offen“. Ein öffentliches
    Quelltextarchiv gibt es nicht, und die Anwendung nannte weder eine Adresse
    noch den Weg über die Anfrage – wer danach suchte, fand nichts.
    Updatebericht und Datenschutzerklärung sagen seit jeher „auf Anfrage“.
  */
  it('verspricht kein Quelltextarchiv, sondern nennt den Weg dorthin', () => {
    bruecke(() => Promise.resolve(true));
    const { container } = render(<Unterstuetzung ziel={ZIEL} />);
    const text = container.textContent;

    expect(text).not.toMatch(/Quelltext ist offen/u);
    expect(text).toMatch(/Quelltext gibt es auf Anfrage/u);
    expect(text).toContain(KONTAKT.epost);
  });
});

describe('Unterstützungsangebot – Barrierefreiheit', () => {
  it('nennt im zugänglichen Namen, dass der Browser geöffnet wird', () => {
    /* WCAG 3.2: keine Überraschung. Und WCAG 2.5.3: Die sichtbare
       Beschriftung ist der ganze zugängliche Name – es gibt kein
       `aria-label`, das mit der Zeit danebenliefe. */
    bruecke(() => Promise.resolve(true));
    render(<Unterstuetzung ziel={ZIEL} />);

    const schaltflaeche = knopf();
    expect(schaltflaeche).toHaveAccessibleName(UNTERSTUETZUNG_KNOPF);
    expect(schaltflaeche.textContent).toBe(UNTERSTUETZUNG_KNOPF);
    expect(schaltflaeche).not.toHaveAttribute('aria-label');
    expect(schaltflaeche).toHaveTextContent(/Browser/u);
  });

  it('trägt die Klasse, die Klickfläche und Fokusring mitbringt', () => {
    /* Die 44 Pixel (`--klickflaeche-min`, WCAG 2.5.8) und der Fokusring
       hängen an `.knopf` bzw. an der zentralen Regel in `global.css`. Unter
       jsdom lässt sich keine Größe messen – axe misst `target-size`
       ausdrücklich nur im echten Fenster (siehe `shared/wcag.ts`). Geprüft
       wird deshalb, dass der Knopf an dieser Regel hängt und sich nicht sein
       eigenes Aussehen gibt. */
    bruecke(() => Promise.resolve(true));
    render(<Unterstuetzung ziel={ZIEL} />);

    expect(knopf()).toHaveClass('knopf');
  });

  it('meldet keine Verstöße gegen WCAG 2.1 A/AA (axe-core)', async () => {
    const nutzer = userEvent.setup();
    bruecke(() => Promise.resolve(false));
    /* In einem `main`, damit die Überschrift nicht als landmarkenlos
       auffällt – so steht der Block auch in der Anwendung. */
    render(
      <main>
        <h1>Über diese Software</h1>
        <Unterstuetzung ziel={ZIEL} />
      </main>,
    );

    // Auch mit sichtbarer Fehlermeldung – der Kasten gehört mit geprüft.
    await nutzer.click(knopf());
    await screen.findByRole('alert');

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

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

  it('steht als eigener Block mit einer Überschrift zweiter Ebene', () => {
    /* In „Über diese Software“ trägt jeder Block eine `h2`. Eine `h1` wäre
       eine zweite Ansichtsüberschrift, eine `h3` machte den Block zum Teil des
       Blocks darüber – hier: der Fremdkomponenten. */
    bruecke(() => Promise.resolve(true));
    const { container } = render(<Unterstuetzung ziel={ZIEL} />);

    const block = container.firstElementChild as HTMLElement;
    expect(block).toHaveClass('ueber__block');
    expect(within(block).getByRole('heading', { level: 2 })).toHaveTextContent(
      UNTERSTUETZUNG_TITEL,
    );
  });
});
