/**
 * Prompt-Injection-Schutz der AI/LLM-Extraktion — rein logische Tests.
 *
 * Kein Netzwerk, keine Datenbank: Geprüft werden ausschließlich der komponierte
 * System-Prompt (Guard-Präsenz) und die Output-Sanitisierung
 * (sanitizeExtractionOutput) gegen ihre harten Grenzen.
 */

import { describe, test, expect, runAllTests } from "../e2e/runner";
import {
  INJECTION_GUARD,
  buildExtractionSystemPrompt,
  sanitizeExtractionOutput,
} from "../../src/lib/ai/promptInjection";
import { SYSTEM_PROMPT } from "../../src/lib/ai/extractor";
import { ReceiptData } from "../../src/lib/schema/receipt";

/**
 * Minimales, vollständig gültiges ReceiptData-Fixture. Alle Schlüssel sind
 * gesetzt, damit der Round-Trip-Vergleich (Key-Gleichheit) eindeutig ist.
 */
function validReceipt(overrides: Partial<ReceiptData> = {}): ReceiptData {
  return {
    merchant: {
      name: "REWE City",
      address: "Friedrichstraße 190, 10117 Berlin",
      taxId: "DE811122334",
      confidence: 0.97,
    },
    date: { isoDate: "2026-08-12", time: "17:45", confidence: 0.96 },
    documentType: "KASSENBON",
    receiptNumber: "RW-77821",
    currency: "EUR",
    totalAmount: { value: 31.8, confidence: 0.98 },
    netAmount: 26.72,
    tipAmount: null,
    taxBreakdown: [{ ratePercent: 19, taxAmount: 5.08, netAmount: 26.72 }],
    lineItems: [
      { description: "Bio Milch 3.8%", quantity: 2, price: 3.18, unitPrice: 1.59, taxRate: 7 },
    ],
    suggestedCategory: "Verpflegungsmehraufwand",
    paymentMethod: null,
    hospitality: { occasion: "Geschäftsessen", participants: "Herr Müller, Frau Schmidt" },
    validation: {
      isMathValid: true,
      isDuplicateSuspected: false,
      needsUserReview: false,
      reviewField: "none",
      reviewReason: null,
    },
    ...overrides,
  };
}

// ---------------------------------------------------------------------------
// 1. System-Prompt-Komposition
// ---------------------------------------------------------------------------

describe("Prompt-Injection-Guard: System-Prompt-Komposition", () => {
  test("P-1: INJECTION_GUARD ist ein nicht-leerer deutschsprachiger Sicherheitstext mit Kernpunkten", () => {
    expect(typeof INJECTION_GUARD).toBe("string");
    expect(INJECTION_GUARD.length).toBeGreaterThan(200);
    expect(INJECTION_GUARD).toContain("UNVERTRAUTE DATEN");
    expect(INJECTION_GUARD).toContain("ignorier");
    expect(INJECTION_GUARD).toContain("Systemanweisungen");
    expect(INJECTION_GUARD).toContain("vorgegebene Schema");
  });

  test("P-2: buildExtractionSystemPrompt hängt den Guard an die Basis an", () => {
    const base = "Du bist ein hochpräziser Beleg-Scanner.";
    const composed = buildExtractionSystemPrompt(base);
    expect(composed.startsWith(base)).toBe(true);
    expect(composed).toContain(INJECTION_GUARD);
    expect(composed.length).toBeGreaterThan(base.length);
  });

  test("P-3: Der exportierte finale SYSTEM_PROMPT des Extractors enthält den Guard", () => {
    expect(SYSTEM_PROMPT).toContain(INJECTION_GUARD);
    expect(SYSTEM_PROMPT).toContain("UNVERTRAUTE DATEN");
    expect(SYSTEM_PROMPT).toContain("ignorier");
    expect(SYSTEM_PROMPT).toContain("Systemanweisungen");
    // Basis-Inhalt bleibt vollständig erhalten.
    expect(SYSTEM_PROMPT).toContain("KI-Beleg-Scanner");
    expect(SYSTEM_PROMPT).toContain("EXTRAKTIONS-REGELN");
  });
});

// ---------------------------------------------------------------------------
// 2. Strings & Längengrenzen
// ---------------------------------------------------------------------------

describe("sanitizeExtractionOutput: Strings & Längengrenzen", () => {
  test("S-1: Händlername > 160 Zeichen wird auf 160 gekappt", () => {
    const long = "X".repeat(201);
    const out = sanitizeExtractionOutput(validReceipt({ merchant: { ...validReceipt().merchant, name: long } }));
    expect(out.merchant.name).toHaveLength(160);
    expect(out.merchant.name).toBe(long.slice(0, 160));
  });

  test("S-2: Kontrollzeichen werden entfernt, Whitespace-Runs kollabieren", () => {
    const out = sanitizeExtractionOutput(
      validReceipt({
        merchant: { ...validReceipt().merchant, name: "REWE\u0000\u0007\u001B\t\tCity  \n  GmbH" },
      })
    );
    expect(out.merchant.name).toBe("REWE City GmbH");
    // Kein Kontrollzeichen und kein Doppel-Space darf übrig bleiben.
    expect(/[\u0000-\u001F\u007F-\u009F]/.test(out.merchant.name)).toBe(false);
    expect(out.merchant.name.includes("  ")).toBe(false);
  });

  test("S-3: Adresse, Steuernummer und Belegnummer werden auf ihre Caps gekappt", () => {
    const out = sanitizeExtractionOutput(
      validReceipt({
        merchant: { ...validReceipt().merchant, address: "A".repeat(500), taxId: "T".repeat(100) },
        receiptNumber: "N".repeat(200),
      })
    );
    expect(out.merchant.address).toHaveLength(300);
    expect(out.merchant.taxId).toHaveLength(64);
    expect(out.receiptNumber).toHaveLength(128);
  });

  test("S-4: lineItems-Beschreibung wird auf 200 Zeichen gekappt", () => {
    const out = sanitizeExtractionOutput(
      validReceipt({
        lineItems: [{ description: "D".repeat(250), quantity: 1, price: 1, unitPrice: null, taxRate: null }],
      })
    );
    expect(out.lineItems[0].description).toHaveLength(200);
  });

  test("S-5: hospitality-Strings (occasion, participants) werden auf 200 Zeichen gekappt", () => {
    const out = sanitizeExtractionOutput(
      validReceipt({
        hospitality: { occasion: "O".repeat(300), participants: "P".repeat(250) },
      })
    );
    expect(out.hospitality?.occasion).toHaveLength(200);
    expect(out.hospitality?.participants).toHaveLength(200);
  });
});

// ---------------------------------------------------------------------------
// 3. Zahlen-Grenzen
// ---------------------------------------------------------------------------

describe("sanitizeExtractionOutput: Zahlen-Grenzen", () => {
  test("N-1: totalAmount.value -5e9 → 0 (negative Beträge werden auf 0 geklemmt)", () => {
    const out = sanitizeExtractionOutput(validReceipt({ totalAmount: { value: -5e9, confidence: 0.9 } }));
    expect(out.totalAmount.value).toBe(0);
  });

  test("N-2: totalAmount.value 1e12 → 1e9 (Obergrenze)", () => {
    const out = sanitizeExtractionOutput(validReceipt({ totalAmount: { value: 1e12, confidence: 0.9 } }));
    expect(out.totalAmount.value).toBe(1_000_000_000);
  });

  test("N-3: NaN/Infinity → 0 bzw. null", () => {
    const out = sanitizeExtractionOutput(
      validReceipt({
        totalAmount: { value: NaN, confidence: 0.9 },
        netAmount: NaN,
        tipAmount: Infinity,
      })
    );
    expect(out.totalAmount.value).toBe(0);
    expect(out.netAmount).toBeNull();
    expect(out.tipAmount).toBeNull();
  });

  test("N-4: taxAmount 1e12 → 1e9, ratePercent 150 → 100", () => {
    const out = sanitizeExtractionOutput(
      validReceipt({ taxBreakdown: [{ ratePercent: 150, taxAmount: 1e12, netAmount: 26.72 }] })
    );
    expect(out.taxBreakdown[0].ratePercent).toBe(100);
    expect(out.taxBreakdown[0].taxAmount).toBe(1_000_000_000);
  });

  test("N-5: quantity wird auf 0..1e6 geklemmt", () => {
    const out = sanitizeExtractionOutput(
      validReceipt({
        lineItems: [
          { description: "a", quantity: -3, price: 10, unitPrice: null, taxRate: 19 },
          { description: "b", quantity: 5e7, price: 10, unitPrice: null, taxRate: 19 },
        ],
      })
    );
    expect(out.lineItems[0].quantity).toBe(0);
    expect(out.lineItems[1].quantity).toBe(1_000_000);
  });

  test("N-6: confidence wird auf 0..1 geklemmt, NaN → 0", () => {
    const out = sanitizeExtractionOutput(
      validReceipt({
        merchant: { ...validReceipt().merchant, confidence: 2.5 },
        date: { ...validReceipt().date, confidence: -1 },
        totalAmount: { value: 10, confidence: NaN },
      })
    );
    expect(out.merchant.confidence).toBe(1);
    expect(out.date.confidence).toBe(0);
    expect(out.totalAmount.confidence).toBe(0);
  });
});

// ---------------------------------------------------------------------------
// 4. Datum & Uhrzeit
// ---------------------------------------------------------------------------

describe("sanitizeExtractionOutput: Datum & Uhrzeit", () => {
  test("D-1: Ungültiges Datum (2026-13-45) → leerer String", () => {
    const out = sanitizeExtractionOutput(
      validReceipt({ date: { isoDate: "2026-13-45", time: "17:45", confidence: 0.9 } })
    );
    expect(out.date.isoDate).toBe("");
  });

  test("D-2: Kein reales Kalenderdatum (2026-02-30) → leerer String", () => {
    const out = sanitizeExtractionOutput(
      validReceipt({ date: { isoDate: "2026-02-30", time: "17:45", confidence: 0.9 } })
    );
    expect(out.date.isoDate).toBe("");
  });

  test("D-3: DACH-Datum (12.08.2026) wird nach YYYY-MM-DD normalisiert", () => {
    const out = sanitizeExtractionOutput(
      validReceipt({ date: { isoDate: "12.08.2026", time: "17:45", confidence: 0.9 } })
    );
    expect(out.date.isoDate).toBe("2026-08-12");
  });

  test("D-4: Gültiges Datum bleibt erhalten", () => {
    const out = sanitizeExtractionOutput(validReceipt());
    expect(out.date.isoDate).toBe("2026-08-12");
  });

  test("D-5: Uhrzeit nur als striktes HH:MM (24h)", () => {
    const outBadHour = sanitizeExtractionOutput(
      validReceipt({ date: { isoDate: "2026-08-12", time: "25:99", confidence: 0.9 } })
    );
    expect(outBadHour.date.time).toBeNull();

    const outBadPad = sanitizeExtractionOutput(
      validReceipt({ date: { isoDate: "2026-08-12", time: "9:05", confidence: 0.9 } })
    );
    expect(outBadPad.date.time).toBeNull();

    const outGood = sanitizeExtractionOutput(
      validReceipt({ date: { isoDate: "2026-08-12", time: "08:42", confidence: 0.9 } })
    );
    expect(outGood.date.time).toBe("08:42");
  });
});

// ---------------------------------------------------------------------------
// 5. Enums & Währung
// ---------------------------------------------------------------------------

describe("sanitizeExtractionOutput: Enums & Währung", () => {
  test("E-1: documentType außerhalb des Enums → SONSTIGES", () => {
    const out = sanitizeExtractionOutput(validReceipt({ documentType: "QUITTUNG" as any }));
    expect(out.documentType).toBe("SONSTIGES");

    const outUndefined = sanitizeExtractionOutput(validReceipt({ documentType: undefined as any }));
    expect(outUndefined.documentType).toBe("SONSTIGES");
  });

  test("E-2: suggestedCategory außerhalb des Enums → Sonstiges", () => {
    const out = sanitizeExtractionOutput(validReceipt({ suggestedCategory: "Hobby" as any }));
    expect(out.suggestedCategory).toBe("Sonstiges");
  });

  test("E-3: currency nur A–Z, ≤ 8 Zeichen, sonst EUR", () => {
    expect(sanitizeExtractionOutput(validReceipt({ currency: "usd" })).currency).toBe("USD");
    expect(sanitizeExtractionOutput(validReceipt({ currency: " chf " })).currency).toBe("CHF");
    expect(sanitizeExtractionOutput(validReceipt({ currency: "€" })).currency).toBe("EUR");
    expect(sanitizeExtractionOutput(validReceipt({ currency: "SUPERLANGE_WAEHRUNG" })).currency).toBe("EUR");
  });
});

// ---------------------------------------------------------------------------
// 6. Arrays & Integrität
// ---------------------------------------------------------------------------

describe("sanitizeExtractionOutput: Arrays & Integrität", () => {
  test("A-1: > 200 lineItems werden auf 200 gekappt", () => {
    const items = Array.from({ length: 250 }, (_, i) => ({
      description: `Artikel ${i}`,
      quantity: 1,
      price: 1,
      unitPrice: null,
      taxRate: null,
    }));
    const out = sanitizeExtractionOutput(validReceipt({ lineItems: items }));
    expect(out.lineItems).toHaveLength(200);
    expect(out.lineItems[199].description).toBe("Artikel 199");
  });

  test("A-2: > 10 taxBreakdown-Einträge werden auf 10 gekappt", () => {
    const taxes = Array.from({ length: 15 }, (_, i) => ({ ratePercent: i, taxAmount: 1, netAmount: 10 }));
    const out = sanitizeExtractionOutput(validReceipt({ taxBreakdown: taxes }));
    expect(out.taxBreakdown).toHaveLength(10);
  });

  test("A-3: Gültige Eingabe bleibt unverändert (Round-Trip)", () => {
    const input = validReceipt();
    const out = sanitizeExtractionOutput(input);
    expect(out).toEqual(input);
  });

  test("A-4: Eingabe-Objekt wird NICHT mutiert (JSON-Vergleich)", () => {
    const input = validReceipt({
      merchant: { ...validReceipt().merchant, name: "REWE\u0000City" },
      totalAmount: { value: -5e9, confidence: 0.9 },
    });
    const before = JSON.stringify(input);
    sanitizeExtractionOutput(input);
    expect(JSON.stringify(input)).toBe(before);
  });

  test("A-5: Nicht-modellierte Felder (id, imageHash, previewUrl, createdAt) bleiben unangetastet", () => {
    const withMeta = {
      ...validReceipt(),
      id: "rec-1",
      imageHash: "abc123",
      previewUrl: "https://example.com/preview.jpg",
      createdAt: "2026-08-12T10:00:00.000Z",
    };
    const out = sanitizeExtractionOutput(withMeta);
    expect(out.id).toBe("rec-1");
    expect(out.imageHash).toBe("abc123");
    expect(out.previewUrl).toBe("https://example.com/preview.jpg");
    expect(out.createdAt).toBe("2026-08-12T10:00:00.000Z");
  });
});

if (typeof require !== "undefined" && require.main === module) {
  runAllTests().then((ok) => process.exit(ok ? 0 : 1));
}
