// Regressionssperre für das Portabilitäts-Gate des fachlichen QAF-Kerns.
//
// Der Test ruft `check-qaf-core-portability.mjs` als echten Prozess auf und
// legt dafür einen vollständigen Mini-Baum unter einer eigenen Wurzel an
// (`QAF_PORTABILITY_ROOT`). Er baut die Mustererkennung bewusst NICHT nach:
// eine Prüfung, die dieselbe Regex zweimal schreibt, erbt jeden blinden Fleck
// des Originals und wäre grün, obwohl das Gate nichts sieht.
//
// Jede Probe verlangt zusätzlich, dass die Meldung GENAU ihre Inhaltsklasse
// nennt. Ohne diese Zusicherung würde eine Probe schon dadurch bestehen, dass
// irgendein anderer Teil des Gates rot wird — rot aus dem falschen Grund ist
// kein Nachweis.

import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { execFileSync } from 'node:child_process'
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'

const GATE = path.resolve(__dirname, '..', 'check-qaf-core-portability.mjs')

let wurzel: string

/** Legt einen Mini-Baum an: Kern-Verzeichnisse plus optional eine Baseline. */
function baueWurzel(baseline: Record<string, number> | null = {}) {
  wurzel = mkdtempSync(path.join(tmpdir(), 'qaf-portability-'))
  mkdirSync(path.join(wurzel, 'lib/qaf-differences/internal'), { recursive: true })
  mkdirSync(path.join(wurzel, 'scripts'), { recursive: true })
  // Eine unbedenkliche Datei, damit der Scan nie ins Leere greift und die
  // Proben sich am Verstoß entscheiden, nicht an einem leeren Verzeichnis.
  schreibe('lib/qaf-differences/internal/rein.ts', `export const takt = (a: number) => a * 2\n`)
  if (baseline !== null) {
    writeFileSync(
      path.join(wurzel, 'scripts/qaf-core-portability-baseline.json'),
      `${JSON.stringify(baseline, null, 2)}\n`,
    )
  }
}

function schreibe(relativ: string, inhalt: string) {
  const ziel = path.join(wurzel, relativ)
  mkdirSync(path.dirname(ziel), { recursive: true })
  writeFileSync(ziel, inhalt)
}

/** Führt das Gate aus und gibt Exitcode plus gesammelte Ausgabe zurück. */
function laufe(args: string[] = []): { code: number; ausgabe: string } {
  try {
    const out = execFileSync('node', [GATE, ...args], {
      env: { ...process.env, QAF_PORTABILITY_ROOT: wurzel },
      encoding: 'utf8',
      stdio: ['ignore', 'pipe', 'pipe'],
    })
    return { code: 0, ausgabe: out }
  } catch (fehler) {
    const e = fehler as { status?: number; stdout?: string; stderr?: string }
    return { code: e.status ?? -1, ausgabe: `${e.stdout ?? ''}${e.stderr ?? ''}` }
  }
}

afterEach(() => {
  if (wurzel) rmSync(wurzel, { recursive: true, force: true })
})

describe('Portabilitäts-Gate für den fachlichen QAF-Kern', () => {
  beforeEach(() => baueWurzel())

  it('ein Kern ohne Kopplung ist grün', () => {
    const { code, ausgabe } = laufe()
    expect(code, ausgabe).toBe(0)
    expect(ausgabe).toContain('sauber')
    // Die Zahl der geprüften Dateien muss in der Ausgabe stehen — sonst wäre
    // ein Scope, der nichts erreicht, von einem sauberen Kern nicht zu
    // unterscheiden.
    expect(ausgabe).toMatch(/1 Dateien geprüft/)
  })

  // Jede verbotene Inhaltsklasse einzeln. Ein gemeinsamer Test über alle vier
  // würde von der ersten funktionierenden Klasse gedeckt werden, und die
  // anderen drei könnten unbemerkt kaputt sein.
  const proben = [
    {
      klasse: 'supabase',
      zeile: `import type { SupabaseClient } from '@supabase/supabase-js'`,
    },
    {
      klasse: 'supabase',
      zeile: `import { createClient } from '@/lib/supabase/server'`,
    },
    {
      klasse: 'cloud-sdk',
      zeile: `import { S3Client } from '@aws-sdk/client-s3'`,
    },
    {
      klasse: 'cloud-sdk',
      zeile: `import { BlobServiceClient } from '@azure/storage-blob'`,
    },
    {
      klasse: 'datenbank-treiber',
      zeile: `import { Pool } from 'pg'`,
    },
    {
      klasse: 'datenbank-treiber',
      zeile: `import { drizzle } from 'drizzle-orm/node-postgres'`,
    },
    {
      klasse: 'framework-laufzeit',
      zeile: `import { cookies } from 'next/headers'`,
    },
    {
      klasse: 'framework-laufzeit',
      zeile: `import { Redis } from '@upstash/redis'`,
    },
    // Die drei Umgehungen, die die erste Fassung des Gates durchgelassen hat.
    // Gefunden durch eine Gegenprobe über fünf Schreibweisen, nicht durch das
    // Lesen des Musters — `from '…'` allein sah vollständig aus.
    {
      klasse: 'supabase',
      zeile: `const { createClient } = await import('@supabase/supabase-js')`,
    },
    {
      klasse: 'datenbank-treiber',
      zeile: `const { Pool } = require('pg')`,
    },
    {
      klasse: 'supabase',
      zeile: `import '@supabase/supabase-js'`,
    },
    {
      klasse: 'supabase',
      zeile: `export { createClient } from '@supabase/supabase-js'`,
    },
    // Die sieben Umgehungen, die ein unabhängiges Review gefunden hat. Alle
    // beruhten darauf, dass der Scanner zeilenweise arbeitete: ein
    // Zeilenumbruch zwischen Schlüsselwort und Pfad genügte, und damit waren
    // genau die drei Formen wieder offen, die der Commit davor als geschlossen
    // ausgewiesen hatte. Kein Trick, nur Formatierung — und in CI läuft kein
    // Formatierungszwang, der das vorher normalisieren würde.
    {
      klasse: 'supabase',
      zeile: `import { createClient } from\n  "@supabase/supabase-js"`,
    },
    {
      klasse: 'supabase',
      zeile: `import { createClient } from /* lokalisiert */ "@supabase/supabase-js"`,
    },
    {
      klasse: 'datenbank-treiber',
      zeile: `const { Pool } = require /* db */ ("pg")`,
    },
    {
      klasse: 'supabase',
      zeile: 'await import(`@supabase/supabase-js`)',
    },
    {
      klasse: 'supabase',
      zeile: `const mod = await import(\n  "@supabase/supabase-js"\n)`,
    },
    {
      klasse: 'datenbank-treiber',
      zeile: `const { Pool } = require(\n  "pg"\n)`,
    },
    {
      klasse: 'supabase',
      zeile: `import\n  "@supabase/supabase-js"`,
    },
    // TypeScript-eigene Importform. Ein Review hat sie in der ersten Runde als
    // „wird erfasst" bestätigt, aber es gab keinen Test dafür — die
    // Mutationsprüfung des Parser-Umbaus zeigte dann 0 rote Tests, als der
    // Zweig entfernt wurde.
    {
      klasse: 'datenbank-treiber',
      zeile: `import pg = require("pg")`,
    },
  ]

  for (const { klasse, zeile } of proben) {
    it(`meldet ${klasse}: ${zeile.slice(0, 46)}…`, () => {
      schreibe('lib/qaf-differences/internal/verstoss.ts', `${zeile}\nexport const x = 1\n`)
      const { code, ausgabe } = laufe()
      expect(code, `Gate blieb grün trotz Verstoß:\n${ausgabe}`).toBe(1)
      expect(ausgabe).toContain('verstoss.ts')
      // Der eigentliche Nachweis: die gemeldete Klasse ist die erwartete.
      expect(ausgabe, `Verstoß erkannt, aber als falsche Klasse:\n${ausgabe}`).toContain(klasse)
    })
  }

  // Tests dürfen einen Client bauen — sie werden nicht mitmigriert. Für die
  // Ausnahme gibt es ZWEI unabhängige Mechanismen: das Verzeichnis `__tests__`
  // und die Dateiendung `.test.ts`/`.spec.ts`. Sie brauchen getrennte Proben.
  //
  // Die erste Fassung dieses Tests legte eine `*.test.ts`-Datei INNERHALB von
  // `__tests__` an und prüfte damit beide gleichzeitig. Die Mutationsprüfung
  // hat das aufgedeckt: das Entfernen der Verzeichnis-Ausnahme machte keinen
  // einzigen Test rot, weil der Endungsfilter die Datei ohnehin fing. Zwei
  // Schutzmaßnahmen, die sich gegenseitig decken, ergeben eine Probe, die
  // nichts beweist.
  it('ein Test-Helfer ohne .test-Endung in __tests__ bleibt erlaubt', () => {
    schreibe(
      'lib/qaf-differences/internal/__tests__/hilfen.ts',
      `import { createClient } from '@supabase/supabase-js'\nexport const klient = createClient\n`,
    )
    const { code, ausgabe } = laufe()
    expect(code, ausgabe).toBe(0)
  })

  it('eine .test.ts-Datei außerhalb von __tests__ bleibt erlaubt', () => {
    schreibe(
      'lib/qaf-differences/internal/adapter.test.ts',
      `import { createClient } from '@supabase/supabase-js'\nexport const x = createClient\n`,
    )
    const { code, ausgabe } = laufe()
    expect(code, ausgabe).toBe(0)
  })

  it('eine Bestandsstelle in der Baseline blockiert nicht', () => {
    schreibe(
      'lib/qaf-differences/internal/bestand.ts',
      `import type { SupabaseClient } from '@supabase/supabase-js'\nexport const x = 1\n`,
    )
    // Erst ohne Baseline-Eintrag: muss rot sein, sonst beweist der zweite
    // Teil nichts.
    expect(laufe().code).toBe(1)

    writeFileSync(
      path.join(wurzel, 'scripts/qaf-core-portability-baseline.json'),
      `${JSON.stringify({ 'lib/qaf-differences/internal/bestand.ts::supabase': 1 }, null, 2)}\n`,
    )
    const { code, ausgabe } = laufe()
    expect(code, ausgabe).toBe(0)
    expect(ausgabe).toContain('Bestandsimport')
  })

  it('eine zweite Kopplung in einer Bestandsdatei wird gemeldet', () => {
    // Der Zähler je Stelle muss wirken: eine Datei mit erlaubtem Bestand darf
    // nicht zur Freikarte für weitere Importe derselben Klasse werden.
    schreibe(
      'lib/qaf-differences/internal/bestand.ts',
      `import type { SupabaseClient } from '@supabase/supabase-js'\n` +
        `import { createClient } from '@supabase/supabase-js'\nexport const x = 1\n`,
    )
    writeFileSync(
      path.join(wurzel, 'scripts/qaf-core-portability-baseline.json'),
      `${JSON.stringify({ 'lib/qaf-differences/internal/bestand.ts::supabase': 1 }, null, 2)}\n`,
    )
    const { code, ausgabe } = laufe()
    expect(code, ausgabe).toBe(1)
    expect(ausgabe).toContain('2 > Baseline 1')
  })

  it('weniger Kopplungen als in der Baseline melden Fortschritt', () => {
    writeFileSync(
      path.join(wurzel, 'scripts/qaf-core-portability-baseline.json'),
      `${JSON.stringify({ 'lib/qaf-differences/internal/weg.ts::supabase': 1 }, null, 2)}\n`,
    )
    const { code, ausgabe } = laufe()
    expect(code, ausgabe).toBe(0)
    expect(ausgabe).toContain('1 Kopplung(en) weniger')
  })

  it('ein Scope, der ins Leere greift, ist ein Fehler und nicht grün', () => {
    // Der wichtigste Test des Gates. Ein umbenanntes Verzeichnis oder ein
    // Tippfehler im Scope würde sonst dauerhaft „sauber" melden, ohne je eine
    // Datei gelesen zu haben.
    rmSync(path.join(wurzel, 'lib'), { recursive: true, force: true })
    const { code, ausgabe } = laufe()
    expect(code, ausgabe).toBe(2)
    expect(ausgabe).toContain('greift ins Leere')
  })

  it('eine fehlende Baseline bricht ab statt stillschweigend zu erlauben', () => {
    rmSync(path.join(wurzel, 'scripts/qaf-core-portability-baseline.json'), { force: true })
    const { code, ausgabe } = laufe()
    expect(code, ausgabe).toBe(2)
    expect(ausgabe).toContain('fehlt')
  })

  it('--update schreibt den Ist-Stand als Baseline', () => {
    schreibe(
      'lib/qaf-differences/internal/neu.ts',
      `import type { SupabaseClient } from '@supabase/supabase-js'\nexport const x = 1\n`,
    )
    expect(laufe().code).toBe(1)
    const { code, ausgabe } = laufe(['--update'])
    expect(code, ausgabe).toBe(0)
    expect(ausgabe).toContain('Baseline geschrieben')
    // Nach dem Nachziehen ist derselbe Stand grün.
    expect(laufe().code).toBe(0)
  })

  it('ein Import in einer Kommentarzeile ist kein Verstoß', () => {
    schreibe(
      'lib/qaf-differences/internal/kommentar.ts',
      `// import type { SupabaseClient } from '@supabase/supabase-js'\nexport const x = 1\n`,
    )
    const { code, ausgabe } = laufe()
    expect(code, ausgabe).toBe(0)
  })

  it('ein Import in einem Blockkommentar ist kein Verstoß', () => {
    // Die erste Fassung übersprang nur Zeilen, die mit `//` oder `*` beginnen.
    // Eine Zeile, die mit `/*` beginnt, tat das nicht — echter Fehlalarm, von
    // einem Review gefunden. Ein Fachkern-Kommentar, der das verbotene Muster
    // zitiert (etwa in einer ADR-Notiz), hätte CI blockiert.
    schreibe(
      'lib/qaf-differences/internal/block.ts',
      `/* import { createClient } from "@supabase/supabase-js"; siehe ADR-020 */\nexport const x = 1\n`,
    )
    const { code, ausgabe } = laufe()
    expect(code, ausgabe).toBe(0)
  })

  it('eine URL in einem String übersteht die Kommentar-Entfernung', () => {
    // Der Grund für den Zustandsautomaten statt eines Regex: `//` in
    // `'https://…'` würde einen naiven Kommentar-Entferner mitten im String
    // auslösen. Alles danach wäre unsichtbar — ein falsches Negativ genau in
    // der Richtung, die zählt. Deshalb steht der echte Verstoß hier NACH der
    // URL: er muss trotzdem gefunden werden.
    schreibe(
      'lib/qaf-differences/internal/mit-url.ts',
      `const doku = 'https://example.invalid/adr'\n` +
        `import type { SupabaseClient } from '@supabase/supabase-js'\n` +
        `export const x = doku\n`,
    )
    const { code, ausgabe } = laufe()
    expect(code, `Verstoß nach einer URL im String wurde übersehen:\n${ausgabe}`).toBe(1)
    expect(ausgabe).toContain('mit-url.ts')
  })

  it('ein Modulname als Objekt-Wert ist kein Verstoß', () => {
    schreibe(
      'lib/qaf-differences/internal/karte.ts',
      `export const karte = { from: '@supabase/supabase-js', to: 'zielsystem' }\n`,
    )
    const { code, ausgabe } = laufe()
    expect(code, ausgabe).toBe(0)
  })

  // Der schwerste Fund der zweiten Review-Runde und der einzige, der ein
  // falsches NEGATIV war: ein gewöhnliches Pfad-Regex mit `\/*` ließ den
  // Automaten in den Blockkommentar-Zustand kippen. Ab da verschwand der
  // komplette Rest der Datei — inklusive eines unverschleierten Imports. Exit 0,
  // „sauber", keine Absicht nötig. Genau die Richtung, die ein Gate nie nehmen
  // darf.
  it('ein Regex mit \\/* verschluckt keinen nachfolgenden Import', () => {
    schreibe(
      'lib/qaf-differences/internal/pfad-regex.ts',
      'const trailingSlashes = /^\\/api\\/*$/\n' +
        `import { createClient } from "@supabase/supabase-js"\n` +
        'export const x = createClient\n',
    )
    const { code, ausgabe } = laufe()
    expect(code, `Import nach einem Pfad-Regex verschluckt:\n${ausgabe}`).toBe(1)
    expect(ausgabe).toContain('pfad-regex.ts')
  })

  it('ein Regex mit /* in einer Zeichenklasse verschluckt keinen Import', () => {
    schreibe(
      'lib/qaf-differences/internal/klasse-regex.ts',
      'const zeichen = /[/*]/\n' +
        `import { createClient } from "@supabase/supabase-js"\n` +
        'export const x = createClient\n',
    )
    const { code, ausgabe } = laufe()
    // Entweder als Verstoß erkannt (1) oder als nicht lesbar abgebrochen (2) —
    // nur „sauber" (0) wäre falsch.
    expect([1, 2], `stillschweigend durchgelassen:\n${ausgabe}`).toContain(code)
  })

  it('eine Division wird nicht als Regex gelesen', () => {
    schreibe(
      'lib/qaf-differences/internal/division.ts',
      'export const quote = (a: number, b: number) => a / b / 2\n' +
        `import { createClient } from "@supabase/supabase-js"\n`,
    )
    const { code, ausgabe } = laufe()
    expect(code, `Division brachte den Automaten aus dem Tritt:\n${ausgabe}`).toBe(1)
  })

  it('eine Datei, die der Automat nicht sauber liest, bricht ab statt zu passieren', () => {
    // Das Netz unter allen verbleibenden Fehlermodi: eine gültige TS-Datei endet
    // im Code-Zustand. Ein unbalancierter Blockkommentar tut das nicht.
    schreibe(
      'lib/qaf-differences/internal/offen.ts',
      `/* Kommentar ohne Ende\nimport { createClient } from "@supabase/supabase-js"\n`,
    )
    const { code, ausgabe } = laufe()
    expect(code, `nicht lesbare Datei wurde still übersprungen:\n${ausgabe}`).toBe(2)
    expect(ausgabe).toContain('nicht eindeutig gelesen')
    expect(ausgabe).toContain('offen.ts')
  })

  it('ein Regex nach schließender Klammer plus späterer JSDoc verdeckt nichts', () => {
    // Der Fund der dritten Review-Runde, und der Grund für den Wechsel auf den
    // TypeScript-Parser. Bei der Heuristik-Fassung galt ein `/` nach `)` als
    // Division, der Regex-Zustand wurde nicht betreten, `\/*` öffnete einen
    // Blockkommentar — und ein beliebiger späterer JSDoc-Kommentar schloss ihn
    // wieder. Damit endete der Automat regulär und auch das Endzustand-Netz sah
    // nichts. Der Import dazwischen war spurlos verschwunden, exit 0.
    schreibe(
      'lib/qaf-differences/internal/nach-klammer.ts',
      'declare const cond: boolean\n' +
        "if (cond) /^\\/api\\/*$/.test('/api/')\n" +
        `import { createClient } from "@supabase/supabase-js"\n` +
        'export const x = createClient\n\n' +
        '/** JSDoc einer ganz anderen Funktion */\n' +
        'export function unrelated() {\n  return 1\n}\n',
    )
    const { code, ausgabe } = laufe()
    expect(code, `Import zwischen Regex und JSDoc verschluckt:\n${ausgabe}`).toBe(1)
    expect(ausgabe).toContain('nach-klammer.ts')
  })

  it('eine Import-Anweisung als Text in einem String ist kein Verstoß', () => {
    // Mit dem Parser kein Fehlalarm mehr: gelesen werden nur Modulangaben
    // echter Import-Knoten, nicht beliebige Zeichenketten. Die Textfassungen
    // davor mussten diesen Fehlalarm bewusst in Kauf nehmen.
    schreibe(
      'lib/qaf-differences/internal/hinweis.ts',
      'export const regel =\n' +
        '  "Bitte nicht: import { createClient } from \'@supabase/supabase-js\' im Fachkern."\n',
    )
    const { code, ausgabe } = laufe()
    expect(code, ausgabe).toBe(0)
  })

  it('ein node_modules-Verzeichnis im Scope wird nicht gescannt', () => {
    // Diese Ausnahme war die einzige Schutzmaßnahme ohne Test — die
    // Mutationsprüfung eines Reviews hat gezeigt, dass ihr Entfernen keinen
    // einzigen Test rot macht. Praktisch selten, aber „ungetestet" ist kein
    // Zustand, den eine Mutationsliste behaupten darf, abgedeckt zu haben.
    schreibe(
      'lib/qaf-differences/internal/node_modules/fremd/index.ts',
      `import { createClient } from '@supabase/supabase-js'\nexport const x = createClient\n`,
    )
    const { code, ausgabe } = laufe()
    expect(code, ausgabe).toBe(0)
  })

  it('eine einzelne Datei im Scope wird gescannt, nicht nur Verzeichnisse', () => {
    // `lib/qaf-parser.ts` liegt direkt in `lib/` und war deshalb ungescannt,
    // obwohl sie Fachlogik trägt. Der Scope nimmt jetzt auch Dateipfade.
    schreibe('lib/qaf-parser.ts', `import { createClient } from '@supabase/supabase-js'\n`)
    const { code, ausgabe } = laufe()
    expect(code, `Datei-Scope greift nicht:\n${ausgabe}`).toBe(1)
    // Auf die GATE-Meldung prüfen, nicht nur auf den Dateinamen: ohne die
    // Datei-Behandlung läuft `readdirSync` auf eine Datei und stirbt mit
    // `ENOTDIR` — dieselbe Exitrichtung und der Pfad steht auch in der
    // Absturzmeldung. Die erste Fassung dieses Tests war deshalb grün, obwohl
    // die Schutzmaßnahme entfernt war. Eine Mutationsprüfung hat es gefunden.
    expect(ausgabe, `kein Gate-Befund, sondern womöglich ein Absturz:\n${ausgabe}`).toContain(
      'NEUE Kopplung',
    )
    expect(ausgabe).toContain('qaf-parser.ts')
  })
})
