/**
 * Extraktions-Qualität: Regressionen aus dem Wechsel auf openai/gpt-5.6-luna.
 *
 * Deckt drei Befunde ab, die live an den Demo-Belegen reproduziert wurden:
 * 1. Platzhalter-Händlernamen ("Nicht lesbar") kamen mit hoher Confidence durch
 *    und landeten ungeprüft als Händlername in der Excel.
 * 2. Trinkgeld auf Bewirtungsbelegen darf nicht gegen den Bruttobetrag gerechnet
 *    werden — sonst meldet Check 5 bei jedem Beleg mit Tip eine Abweichung.
 * 3. Das Modell-Schema darf `validation` nicht enthalten (wird lokal berechnet)
 *    und muss für OpenAI-strict jedes Feld in `required` führen.
 */

import { describe, test, expect } from "./runner";
import {
  validateReceiptMath,
  isPlaceholderMerchantName,
  isTipLineItem,
  CONFIDENCE_THRESHOLDS,
} from "../../src/lib/ai/mathValidator";
import ExcelJS from "exceljs";
import {
  ProcessedReceipt,
  ReceiptData,
  ReceiptExtractionModelSchema,
  LineItemViewBatchModelSchema,
  LineItemVerificationModelSchema,
  PENDING_VALIDATION,
  grossWithTip,
} from "../../src/lib/schema/receipt";
import { generateDualSheetExcel } from "../../src/lib/export/excelGenerator";
import { generateAccountingCsv } from "../../src/lib/export/csvGenerator";
import {
  chooseBetterExtraction,
  extractionQualityScore,
  mergeLineItemViews,
} from "../../src/lib/ai/extractor";

/** Extraktionsergebnis zu einem gespeicherten Beleg aufwerten. */
function stored(data: ReceiptData): ProcessedReceipt {
  return {
    ...data,
    id: "rcpt_test",
    imageHash: "hash_test",
    originalFileName: "test.jpg",
    fileSizeBytes: 1024,
    createdAt: "2026-08-12T10:00:00.000Z",
    updatedAt: "2026-08-12T10:00:00.000Z",
    status: "ready",
  };
}

function receipt(overrides: Partial<ReceiptData> = {}): ReceiptData {
  return {
    merchant: { name: "REWE", address: null, taxId: null, confidence: 0.97 },
    date: { isoDate: "2026-08-12", time: null, confidence: 0.95 },
    documentType: "KASSENBON",
    receiptNumber: null,
    currency: "EUR",
    totalAmount: { value: 11.9, confidence: 0.98 },
    netAmount: 10.0,
    taxBreakdown: [{ ratePercent: 19, taxAmount: 1.9, netAmount: 10.0 }],
    lineItems: [
      { description: "Testartikel", quantity: 1, price: 11.9, unitPrice: null, taxRate: 19 },
    ],
    suggestedCategory: "Sonstiges",
    validation: { ...PENDING_VALIDATION },
    ...overrides,
  } as ReceiptData;
}

describe("Extraktion: Platzhalter-Händlernamen", () => {
  const placeholders = [
    "Nicht lesbar",
    "nicht lesbar",
    "Taxiunternehmen (Name unleserlich)",
    "Unbekannter Händler",
    "Unknown",
    "n/a",
    "N/A",
    "—",
    "-",
    "   ",
    "",
  ];

  for (const name of placeholders) {
    test(`EQ-1: "${name || "(leer)"}" gilt als Platzhalter`, () => {
      expect(isPlaceholderMerchantName(name)).toBe(true);
    });
  }

  const realNames = [
    "REWE",
    "Aral Tankstelle Station",
    "BISTRO AM MARKT",
    "Apotheke am Stadtpark",
    "Trattoria Bella Vista",
    "MediaMarkt",
    "Deutsche Bahn AG",
  ];

  for (const name of realNames) {
    test(`EQ-2: "${name}" gilt NICHT als Platzhalter`, () => {
      expect(isPlaceholderMerchantName(name)).toBe(false);
    });
  }

  test("EQ-3: Platzhalter wird trotz hoher Confidence zur Prüfung markiert", () => {
    const r = validateReceiptMath(
      receipt({
        merchant: { name: "Nicht lesbar", address: null, taxId: null, confidence: 0.99 },
      })
    );
    expect(r.needsUserReview).toBe(true);
    expect(r.reviewField).toBe("merchant");
  });

  test("EQ-4: Echter Händlername mit hoher Confidence bleibt ungeflaggt", () => {
    const r = validateReceiptMath(receipt());
    expect(r.needsUserReview).toBe(false);
  });

  test("EQ-5: Niedrige Confidence flaggt weiterhin unabhängig vom Namen", () => {
    const r = validateReceiptMath(
      receipt({
        merchant: {
          name: "REWE",
          address: null,
          taxId: null,
          confidence: CONFIDENCE_THRESHOLDS.merchant - 0.01,
        },
      })
    );
    expect(r.needsUserReview).toBe(true);
    expect(r.reviewField).toBe("merchant");
  });
});

describe("Extraktion: Trinkgeld auf Bewirtungsbelegen", () => {
  test("EQ-6: Trinkgeld-Positionen werden erkannt", () => {
    expect(isTipLineItem("Trinkgeld")).toBe(true);
    expect(isTipLineItem("trinkgeld")).toBe(true);
    expect(isTipLineItem("Tip")).toBe(true);
    expect(isTipLineItem("Gratuity")).toBe(true);
  });

  test("EQ-7: Normale Positionen sind kein Trinkgeld", () => {
    expect(isTipLineItem("Pizza Margherita")).toBe(false);
    expect(isTipLineItem("San Pellegrino 0.75l")).toBe(false);
    expect(isTipLineItem("Tiramisu")).toBe(false);
    expect(isTipLineItem(null)).toBe(false);
  });

  test("EQ-8: Trattoria-Fall — Tip zählt nicht gegen den Bruttobetrag", () => {
    // Realer Beleg: Total 31,80 (Netto 26,72 + 19% 5,08), handschriftlich
    // Trinkgeld 5,00 und Gesamtbetrag 36,80.
    const r = validateReceiptMath(
      receipt({
        merchant: { name: "Trattoria Bella Vista", address: null, taxId: null, confidence: 0.98 },
        documentType: "BEWIRTUNGSBELEG",
        totalAmount: { value: 31.8, confidence: 0.98 },
        netAmount: 26.72,
        taxBreakdown: [{ ratePercent: 19, taxAmount: 5.08, netAmount: 26.72 }],
        lineItems: [
          { description: "Pizza Margherita", quantity: 2, price: 25.0, unitPrice: 12.5, taxRate: 19 },
          { description: "San Pellegrino 0.75l", quantity: 1, price: 6.8, unitPrice: null, taxRate: 19 },
          { description: "Trinkgeld", quantity: 1, price: 5.0, unitPrice: null, taxRate: 0 },
        ],
      })
    );
    expect(r.isMathValid).toBe(true);
    expect(r.needsUserReview).toBe(false);
    expect(r.issues.some((i) => i.field === "lineItems")).toBe(false);
  });

  test("EQ-9: Ohne Tip-Ausnahme bliebe eine echte Artikel-Abweichung erkennbar", () => {
    const r = validateReceiptMath(
      receipt({
        totalAmount: { value: 31.8, confidence: 0.98 },
        netAmount: 26.72,
        taxBreakdown: [{ ratePercent: 19, taxAmount: 5.08, netAmount: 26.72 }],
        lineItems: [
          { description: "Pizza Margherita", quantity: 2, price: 25.0, unitPrice: 12.5, taxRate: 19 },
          { description: "Dessert", quantity: 1, price: 11.8, unitPrice: null, taxRate: 19 },
        ],
      })
    );
    expect(r.issues.some((i) => i.field === "lineItems")).toBe(true);
  });
});

describe("Trinkgeld: tipAmount-Feld", () => {
  const trattoria = () =>
    receipt({
      merchant: { name: "Trattoria Bella Vista", address: null, taxId: null, confidence: 0.98 },
      documentType: "BEWIRTUNGSBELEG",
      totalAmount: { value: 31.8, confidence: 0.98 },
      netAmount: 26.72,
      tipAmount: 5.0,
      taxBreakdown: [{ ratePercent: 19, taxAmount: 5.08, netAmount: 26.72 }],
      lineItems: [
        { description: "Pizza Margherita", quantity: 2, price: 25.0, unitPrice: 12.5, taxRate: 19 },
        { description: "San Pellegrino 0.75l", quantity: 1, price: 6.8, unitPrice: null, taxRate: 19 },
      ],
    });

  test("EQ-15: grossWithTip addiert das Trinkgeld auf den Rechnungsbetrag", () => {
    expect(grossWithTip(trattoria())).toBe(36.8);
  });

  test("EQ-16: ohne Trinkgeld bleibt grossWithTip der Bruttobetrag", () => {
    expect(grossWithTip(receipt())).toBe(11.9);
    expect(grossWithTip(receipt({ tipAmount: null }))).toBe(11.9);
  });

  test("EQ-17: Trinkgeld verfälscht die Netto/MwSt-Gegenprobe nicht", () => {
    const r = validateReceiptMath(trattoria());
    expect(r.isMathValid).toBe(true);
    expect(r.needsUserReview).toBe(false);
  });

  test("EQ-18: negatives Trinkgeld ist ein Fehler", () => {
    const r = validateReceiptMath(receipt({ tipAmount: -2 }));
    expect(r.isMathValid).toBe(false);
    expect(r.needsUserReview).toBe(true);
  });

  test("EQ-19: Trinkgeld über dem Rechnungsbetrag wird zur Prüfung markiert", () => {
    // Typischer Lesefehler: handschriftlicher Gesamtbetrag als Tip erfasst.
    const r = validateReceiptMath(
      receipt({ totalAmount: { value: 31.8, confidence: 0.98 }, netAmount: 26.72, tipAmount: 36.8, taxBreakdown: [{ ratePercent: 19, taxAmount: 5.08, netAmount: 26.72 }] })
    );
    expect(r.needsUserReview).toBe(true);
  });

  test("EQ-20: Excel führt Trinkgeld- und Gesamt-gezahlt-Spalte nur bei Bedarf", async () => {
    const withTip = await generateDualSheetExcel([stored(trattoria())]);
    const withoutTip = await generateDualSheetExcel([stored(receipt())]);
    const headerOf = async (buf: Buffer) => {
      const wb = new ExcelJS.Workbook();
      await wb.xlsx.load(buf as unknown as ArrayBuffer);
      const row = wb.worksheets[0].getRow(1);
      const out: string[] = [];
      row.eachCell((c) => out.push(String(c.value ?? "")));
      return out;
    };
    const h1 = await headerOf(withTip);
    expect(h1.some((h) => h.startsWith("Trinkgeld"))).toBe(true);
    expect(h1.some((h) => h.startsWith("Gesamt gezahlt"))).toBe(true);

    const h2 = await headerOf(withoutTip);
    expect(h2.some((h) => h.startsWith("Trinkgeld"))).toBe(false);
  });

  test("EQ-21: CSV enthält Trinkgeld und Gesamt gezahlt", () => {
    const csv = generateAccountingCsv([stored(trattoria())]);
    const [header, row] = csv.replace(/^﻿/, "").split("\r\n");
    expect(header.includes("Trinkgeld")).toBe(true);
    expect(header.includes("Gesamt gezahlt")).toBe(true);
    // Rechnungsbetrag bleibt 31,80, gezahlt wurden 36,80.
    expect(row.includes('"31,80"')).toBe(true);
    expect(row.includes('"5,00"')).toBe(true);
    expect(row.includes('"36,80"')).toBe(true);
  });

  test("EQ-22: CSV ohne Trinkgeld führt die Spalten nicht", () => {
    const csv = generateAccountingCsv([stored(receipt())]);
    expect(csv.split("\r\n")[0].includes("Trinkgeld")).toBe(false);
  });
});

describe("Extraktion: Modell-Schema für OpenAI strict mode", () => {
  test("EQ-10: validation ist nicht Teil des Modell-Schemas", () => {
    const keys = Object.keys(ReceiptExtractionModelSchema.shape);
    expect(keys.includes("validation")).toBe(false);
    expect(keys.includes("merchant")).toBe(true);
    expect(keys.includes("totalAmount")).toBe(true);
    expect(keys.includes("taxBreakdown")).toBe(true);
  });

  test("EQ-11: kein Feld ist optional (strict verlangt required für jeden Key)", () => {
    // .optional() erzeugt eine Lücke in `required` -> HTTP 400 invalid_json_schema.
    const optional = Object.entries(ReceiptExtractionModelSchema.shape)
      .filter(([, v]) => (v as { isOptional?: () => boolean }).isOptional?.())
      .map(([k]) => k);
    expect(optional.length).toBe(0);
  });

  test("EQ-12: Modell-Ausgabe ohne validation ist gültig", () => {
    const sample = {
      merchant: { name: "REWE", address: null, taxId: null, confidence: 0.97 },
      date: { isoDate: "2026-08-12", time: "14:32", confidence: 0.95 },
      documentType: "KASSENBON",
      receiptNumber: "2026-004871",
      currency: "EUR",
      totalAmount: { value: 9.06, confidence: 0.98 },
      netAmount: 8.18,
      taxBreakdown: [
        { ratePercent: 7, taxAmount: 0.4, netAmount: 5.67 },
        { ratePercent: 19, taxAmount: 0.48, netAmount: 2.51 },
      ],
      lineItems: [
        {
          description: "Vollmilch",
          quantity: 2,
          price: 2.58,
          unitPrice: 1.29,
          taxRate: 7,
          confidence: 0.96,
          sourceView: null,
          rowOrder: null,
        },
      ],
      suggestedCategory: "Verpflegungsmehraufwand",
      hospitality: null,
      tipAmount: null,
      paymentMethod: null,
    };
    expect(ReceiptExtractionModelSchema.safeParse(sample).success).toBe(true);
  });

  test("EQ-14: tipAmount ist gegenüber dem Modell Pflicht (nullable, nicht optional)", () => {
    // Fehlt das Feld, wäre es nicht in `required` -> HTTP 400 im strict mode.
    const shape = ReceiptExtractionModelSchema.shape as Record<string, { isOptional?: () => boolean }>;
    expect("tipAmount" in shape).toBe(true);
    expect(shape.tipAmount.isOptional?.() ?? false).toBe(false);
  });

  test("EQ-13: PENDING_VALIDATION ist neutral (flaggt nichts vor)", () => {
    expect(PENDING_VALIDATION.needsUserReview).toBe(false);
    expect(PENDING_VALIDATION.isMathValid).toBe(true);
    expect(PENDING_VALIDATION.reviewField).toBe("none");
  });

  test("EQ-25: fokussierter OCR-Retry hat ein kleines strict-kompatibles Schema", () => {
    const parsed = LineItemVerificationModelSchema.safeParse({
      lineItems: [
        {
          description: "Bio Gurken",
          quantity: 1,
          price: 1.29,
          unitPrice: null,
          taxRate: 7,
          confidence: 0.94,
          sourceView: null,
          rowOrder: null,
        },
      ],
    });
    expect(parsed.success).toBe(true);
    expect(Object.keys(LineItemVerificationModelSchema.shape)).toEqual(["lineItems"]);
  });

  test("EQ-26: Kachel-Batch verlangt Ausschnitt, Reihenfolge und Confidence", () => {
    const parsed = LineItemViewBatchModelSchema.safeParse({
      views: [
        {
          sourceView: 2,
          lineItems: [
            {
              description: "Bio Gurken",
              quantity: 1,
              price: 1.29,
              unitPrice: null,
              taxRate: 7,
              confidence: 0.93,
              sourceView: 2,
              rowOrder: 1,
            },
          ],
        },
      ],
    });
    expect(parsed.success).toBe(true);
  });
});

describe("Extraktion: Qualitätsauswahl beim Reasoning-Retry", () => {
  test("EQ-23: rechnerisch vollständige Positionen gewinnen gegen eine Warnung", () => {
    const incomplete = receipt({
      totalAmount: { value: 3.72, confidence: 0.99 },
      lineItems: [
        { description: "Artikel A", quantity: 1, price: 1.99, unitPrice: null, taxRate: 7 },
        { description: "Artikel B", quantity: 1, price: 1.13, unitPrice: null, taxRate: 7 },
      ],
      validation: {
        ...PENDING_VALIDATION,
        needsUserReview: true,
        issues: [{ field: "lineItems", severity: "warning", message: "Sum mismatch" }],
      },
    });
    const complete = receipt({
      totalAmount: { value: 3.72, confidence: 0.99 },
      lineItems: [
        { description: "Artikel A", quantity: 1, price: 1.99, unitPrice: null, taxRate: 7 },
        { description: "Artikel B", quantity: 1, price: 1.73, unitPrice: null, taxRate: 7 },
      ],
      validation: { ...PENDING_VALIDATION },
    });
    expect(extractionQualityScore(complete)).toBeLessThan(extractionQualityScore(incomplete));
    expect(chooseBetterExtraction(incomplete, complete)).toBe(complete);
  });

  test("EQ-24: ein schlechterer Retry überschreibt die Erstextraktion nicht", () => {
    const first = receipt({
      totalAmount: { value: 1.99, confidence: 0.99 },
      lineItems: [
        { description: "Bio Artikel", quantity: 1, price: 1.99, unitPrice: null, taxRate: 7 },
      ],
      validation: { ...PENDING_VALIDATION },
    });
    const worse = receipt({
      totalAmount: { value: 1.99, confidence: 0.99 },
      lineItems: [],
      validation: {
        ...PENDING_VALIDATION,
        needsUserReview: true,
        issues: [{ field: "lineItems", severity: "warning", message: "Missing" }],
      },
    });
    expect(chooseBetterExtraction(first, worse)).toBe(first);
  });
});

describe("Extraktion: deterministische Kachel-Zusammenführung", () => {
  const item = (
    description: string,
    price: number,
    sourceView: number,
    rowOrder: number,
    confidence: number = 0.9
  ) => ({
    description,
    quantity: 1,
    price,
    unitPrice: null,
    taxRate: 7,
    confidence,
    sourceView,
    rowOrder,
  });

  test("EQ-27: überlappende Zeilen erscheinen nur einmal", () => {
    const merged = mergeLineItemViews([
      {
        sourceView: 1,
        lineItems: [item("Artikel A", 1.29, 1, 1), item("Bio Heidelbeeren", 1.99, 1, 2)],
      },
      {
        sourceView: 2,
        lineItems: [
          item("Bio Heidelbeer.", 1.99, 2, 1, 0.96),
          item("Bio Gurken", 1.29, 2, 2),
        ],
      },
    ]);
    expect(merged.map((line) => line.price)).toEqual([1.29, 1.99, 1.29]);
    expect(merged[1].description).toBe("Bio Heidelbeer.");
  });

  test("EQ-28: identische Produkte auf getrennten Zeilen bleiben erhalten", () => {
    const merged = mergeLineItemViews([
      {
        sourceView: 1,
        lineItems: [item("Wasser", 0.99, 1, 1), item("Wasser", 0.99, 1, 2)],
      },
      {
        sourceView: 2,
        lineItems: [item("Wasser", 0.99, 2, 1), item("Brot", 2.49, 2, 2)],
      },
    ]);
    expect(merged.map((line) => line.description)).toEqual(["Wasser", "Wasser", "Brot"]);
  });

  test("EQ-29: sehr lange Artikellisten erhalten keine wachsende Euro-Toleranz", () => {
    const value = receipt({
      totalAmount: { value: 100, confidence: 0.99 },
      lineItems: Array.from({ length: 100 }, (_, index) => ({
        description: `Artikel ${index + 1}`,
        quantity: 1,
        price: index === 99 ? 0.9 : 1,
        unitPrice: null,
        taxRate: 7,
      })),
    });
    const validation = validateReceiptMath(value);
    expect(validation.issues.some((issue) => issue.field === "lineItems")).toBe(true);
  });

  test("EQ-30: Gesamtbetrag ohne erkannte Artikel wird markiert", () => {
    const value = receipt({ totalAmount: { value: 12.34, confidence: 0.99 }, lineItems: [] });
    const validation = validateReceiptMath(value);
    expect(validation.issues.some((issue) => issue.field === "lineItems")).toBe(true);
  });
});
