/**
 * Milestone 3 (R3) Empirical Challenger Verification Harness
 * Comprehensive Adversarial Stress Testing of Table, Filter & Selection Engines
 */

import { describe, test, it, expect } from "./runner";
import { ProcessedReceipt, ReceiptCategory, DocumentType, PaymentMethod } from "../../src/lib/schema/receipt";
import { resolveReceiptStatusTier, getStatusTierMeta, ReceiptStatusTier } from "../../src/components/dashboard/StatusBadge";
import { recalculateReceipt, confirmReceiptReviewed } from "../../src/lib/ai/recalculate";
import { generateDualSheetExcel } from "../../src/lib/export/excelGenerator";
import { generateAccountingCsv } from "../../src/lib/export/csvGenerator";
import { grossOf, netOf } from "../../src/components/dashboard/receiptFormat";

// Mock Receipt Factory
function mockReceipt(id: string, overrides: Partial<ProcessedReceipt> = {}): ProcessedReceipt {
  return {
    id,
    imageHash: `hash-${id}`,
    originalFileName: `${id}.pdf`,
    fileSizeBytes: 50000,
    previewUrl: `blob:http://localhost/${id}.pdf`,
    createdAt: "2026-08-15T10:00:00.000Z",
    updatedAt: "2026-08-15T10:00:00.000Z",
    status: "ready",
    documentType: "KASSENBON",
    receiptNumber: `REC-${id}`,
    currency: "EUR",
    merchant: {
      name: `Merchant ${id}`,
      address: "Musterstr. 1, Berlin",
      taxId: "DE123456789",
      confidence: 0.99,
    },
    date: {
      isoDate: "2026-08-15",
      time: "10:30",
      confidence: 0.99,
    },
    totalAmount: {
      value: 100.0,
      confidence: 0.99,
    },
    netAmount: 84.03,
    taxBreakdown: [{ ratePercent: 19, taxAmount: 15.97, netAmount: 84.03 }],
    lineItems: [
      { description: `Item for ${id}`, quantity: 1, price: 100.0, taxRate: 19 },
    ],
    suggestedCategory: "Bürobedarf & IT",
    paymentMethod: "EC_KARTE",
    validation: {
      isMathValid: true,
      isDuplicateSuspected: false,
      needsUserReview: false,
      userConfirmed: false,
      reviewField: "none",
      reviewReason: null,
      issues: [],
    },
    ...overrides,
  };
}

// Pure Filter Predicate implementation matching useReceiptFilters
function applyFilters(
  receipts: ProcessedReceipt[],
  filters: {
    period?: string;
    status?: string;
    category?: string;
    searchQuery?: string;
    amountRange?: { min?: number | null; max?: number | null };
  }
): ProcessedReceipt[] {
  const {
    period = "all",
    status = "all",
    category = "all",
    searchQuery = "",
    amountRange = { min: null, max: null },
  } = filters;

  const query = searchQuery.trim().toLowerCase();

  return receipts.filter((receipt) => {
    // 1. Period filter
    if (period !== "all") {
      if (!receipt.date?.isoDate) return false;
      const date = new Date(receipt.date.isoDate);
      if (isNaN(date.getTime())) return false;

      const now = new Date();
      const dateYear = date.getFullYear();
      const dateMonth = date.getMonth();
      const dateDay = date.getDate();

      const nowYear = now.getFullYear();
      const nowMonth = now.getMonth();
      const nowDay = now.getDate();

      if (period === "today") {
        if (!(dateYear === nowYear && dateMonth === nowMonth && dateDay === nowDay)) return false;
      } else if (period === "month") {
        if (!(dateYear === nowYear && dateMonth === nowMonth)) return false;
      } else if (period === "year") {
        if (dateYear !== nowYear) return false;
      } else if (typeof period === "string") {
        if (!receipt.date.isoDate.startsWith(period)) return false;
      }
    }

    // 2. Status filter
    if (status !== "all") {
      const tier = resolveReceiptStatusTier(receipt);
      const norm = status.toLowerCase();
      if (norm === "pending" || norm === "pending_review" || norm === "pruefen") {
        if (tier !== "pending_review") return false;
      } else if (norm === "confirmed" || norm === "bestaetigt") {
        if (tier !== "confirmed") return false;
      } else if (norm === "scanned" || norm === "erfasst" || norm === "ready") {
        if (tier !== "scanned") return false;
      }
    }

    // 3. Category filter
    if (category !== "all") {
      if (receipt.suggestedCategory !== category) return false;
    }

    // 4. Amount Range filter
    const gross = grossOf(receipt);
    if (amountRange.min != null && !isNaN(amountRange.min)) {
      if (gross < amountRange.min) return false;
    }
    if (amountRange.max != null && !isNaN(amountRange.max)) {
      if (gross > amountRange.max) return false;
    }

    // 5. Search query
    if (query) {
      const merchantName = (receipt.merchant?.name ?? "").toLowerCase();
      const merchantAddress = (receipt.merchant?.address ?? "").toLowerCase();
      const receiptNo = (receipt.receiptNumber ?? "").toLowerCase();
      const isoDate = (receipt.date?.isoDate ?? "").toLowerCase();
      const cat = (receipt.suggestedCategory ?? "").toLowerCase();
      const docType = (receipt.documentType ?? "").toLowerCase();
      const paymentMethod = (receipt.paymentMethod ?? "").toLowerCase();
      const grossStr = gross.toFixed(2);
      const grossStrDe = grossStr.replace(".", ",");

      const lineItemsMatch = receipt.lineItems?.some((item) =>
        item?.description?.toLowerCase().includes(query)
      );

      const matches =
        merchantName.includes(query) ||
        merchantAddress.includes(query) ||
        receiptNo.includes(query) ||
        isoDate.includes(query) ||
        cat.includes(query) ||
        docType.includes(query) ||
        paymentMethod.includes(query) ||
        grossStr.includes(query) ||
        grossStrDe.includes(query) ||
        lineItemsMatch;

      if (!matches) return false;
    }

    return true;
  });
}

// Pure Selection Engine state machine
class SelectionStateMachine {
  selectedIds: string[] = [];

  constructor(initial: string[] = []) {
    this.selectedIds = [...initial];
  }

  get set(): Set<string> {
    return new Set(this.selectedIds);
  }

  isSelected(id: string): boolean {
    return this.set.has(id);
  }

  toggleSelect(id: string) {
    if (!id) return;
    this.selectedIds = this.selectedIds.includes(id)
      ? this.selectedIds.filter((item) => item !== id)
      : [...this.selectedIds, id];
  }

  selectAll(allIds: string[]) {
    this.selectedIds = Array.from(new Set(allIds.filter(Boolean)));
  }

  toggleSelectAll(allIds: string[]) {
    if (!allIds || allIds.length === 0) {
      this.selectedIds = [];
      return;
    }
    const allSelected = allIds.every((id) => this.set.has(id));
    if (allSelected) {
      this.selectedIds = this.selectedIds.filter((id) => !allIds.includes(id));
    } else {
      this.selectedIds = Array.from(new Set([...this.selectedIds, ...allIds]));
    }
  }

  selectRange(fromId: string, toId: string, allOrderedIds: string[]) {
    const fromIdx = allOrderedIds.indexOf(fromId);
    const toIdx = allOrderedIds.indexOf(toId);
    if (fromIdx === -1 || toIdx === -1) return;
    const start = Math.min(fromIdx, toIdx);
    const end = Math.max(fromIdx, toIdx);
    const rangeIds = allOrderedIds.slice(start, end + 1);
    this.selectedIds = Array.from(new Set([...this.selectedIds, ...rangeIds]));
  }

  isAllSelected(allIds: string[]): boolean {
    if (!allIds || allIds.length === 0) return false;
    return allIds.every((id) => this.set.has(id));
  }

  isPartiallySelected(allIds: string[]): boolean {
    if (!allIds || allIds.length === 0) return false;
    const some = allIds.some((id) => this.set.has(id));
    const all = allIds.every((id) => this.set.has(id));
    return some && !all;
  }

  clear() {
    this.selectedIds = [];
  }
}

describe("Empirical Challenger M3: Adversarial Filter Engine Stress", () => {
  const corpus: ProcessedReceipt[] = [
    mockReceipt("rcpt-special-chars", {
      merchant: { name: 'Café "Kranzler" (GmbH & Co. KG) [Berlin]', address: "Kurfürstendamm 18/20", taxId: "DE999", confidence: 1.0 },
      receiptNumber: "INV-2026/08+99$#1",
      date: { isoDate: "2026-08-15", time: "09:00", confidence: 1.0 },
      totalAmount: { value: 12.50, confidence: 1.0 },
      suggestedCategory: "Bewirtung",
      paymentMethod: "BAR",
      lineItems: [{ description: "Kaffee & Croissant (Set *Special*)", quantity: 1, price: 12.50, taxRate: 19 }],
    }),
    mockReceipt("rcpt-zero-gross", {
      merchant: { name: "Gratis Probe Store", address: "Alexanderplatz 1", taxId: "DE000", confidence: 1.0 },
      receiptNumber: "ZERO-000",
      date: { isoDate: "2026-08-01", time: "10:00", confidence: 1.0 },
      totalAmount: { value: 0.00, confidence: 1.0 },
      netAmount: 0.00,
      taxBreakdown: [{ ratePercent: 19, taxAmount: 0.00, netAmount: 0.00 }],
      suggestedCategory: "Sonstiges",
    }),
    mockReceipt("rcpt-high-gross", {
      merchant: { name: "Apple Store Kurfürstendamm", address: "Berlin", taxId: "DE888", confidence: 1.0 },
      receiptNumber: "APPL-9988",
      date: { isoDate: "2026-07-25", time: "14:00", confidence: 1.0 },
      totalAmount: { value: 3499.00, confidence: 1.0 },
      suggestedCategory: "Bürobedarf & IT",
      paymentMethod: "APPLE_PAY",
    }),
    mockReceipt("rcpt-pending-review", {
      merchant: { name: "Unbekannter Beleg", address: "", taxId: "", confidence: 0.4 },
      receiptNumber: null,
      date: { isoDate: "2026-08-10", time: "12:00", confidence: 0.5 },
      totalAmount: { value: 50.00, confidence: 0.4 },
      suggestedCategory: "Sonstiges",
      validation: {
        isMathValid: false,
        isDuplicateSuspected: false,
        needsUserReview: true,
        userConfirmed: false,
        reviewField: "merchant",
        reviewReason: "Geringe Erkennungsgenauigkeit",
        issues: [{ field: "merchant", severity: "error", message: "Händler unsicher" }],
      },
    }),
    mockReceipt("rcpt-confirmed", {
      merchant: { name: "Tankstelle Jet", address: "Hamburg", taxId: "DE777", confidence: 0.9 },
      receiptNumber: "JET-4421",
      date: { isoDate: "2026-08-12", time: "18:00", confidence: 0.9 },
      totalAmount: { value: 85.40, confidence: 0.9 },
      suggestedCategory: "Tanken & KFZ",
      validation: {
        isMathValid: true,
        isDuplicateSuspected: false,
        needsUserReview: false,
        userConfirmed: true,
        reviewField: "none",
        reviewReason: null,
        issues: [],
      },
    }),
  ];

  test("CHALLENGE-1.1: Complex regex meta-characters in search query do not throw", () => {
    const maliciousPatterns = [
      ".*",
      "+",
      "?",
      "^",
      "$",
      "{1,3}",
      "()",
      "[]",
      "|",
      "\\d+",
      "[Berlin]",
      "*Special*",
      "$#1",
      '(GmbH & Co. KG)',
      '\\',
      '/.*+?^${}()|[]\\',
    ];

    for (const pat of maliciousPatterns) {
      const results = applyFilters(corpus, { searchQuery: pat });
      expect(Array.isArray(results)).toBe(true);
    }
  });

  test("CHALLENGE-1.2: German comma vs English dot search resolves correctly", () => {
    // 12,50 and 12.50 should both find rcpt-special-chars
    const resDe = applyFilters(corpus, { searchQuery: "12,50" });
    const resEn = applyFilters(corpus, { searchQuery: "12.50" });
    expect(resDe.map((r) => r.id)).toEqual(["rcpt-special-chars"]);
    expect(resEn.map((r) => r.id)).toEqual(["rcpt-special-chars"]);

    // 85,40 and 85.40
    const resJetDe = applyFilters(corpus, { searchQuery: "85,40" });
    const resJetEn = applyFilters(corpus, { searchQuery: "85.40" });
    expect(resJetDe.map((r) => r.id)).toEqual(["rcpt-confirmed"]);
    expect(resJetEn.map((r) => r.id)).toEqual(["rcpt-confirmed"]);
  });

  test("CHALLENGE-1.3: Substring search in line items descriptions", () => {
    const resLineItem = applyFilters(corpus, { searchQuery: "Croissant" });
    expect(resLineItem).toHaveLength(1);
    expect(resLineItem[0].id).toBe("rcpt-special-chars");
  });

  test("CHALLENGE-1.4: Inverted amount range (min > max) returns clean empty array without error", () => {
    const inverted = applyFilters(corpus, {
      amountRange: { min: 500, max: 100 },
    });
    expect(inverted).toHaveLength(0);
  });

  test("CHALLENGE-1.5: Zero gross receipt filtering with min:0 max:0", () => {
    const zeroExact = applyFilters(corpus, {
      amountRange: { min: 0, max: 0 },
    });
    expect(zeroExact).toHaveLength(1);
    expect(zeroExact[0].id).toBe("rcpt-zero-gross");
  });

  test("CHALLENGE-1.6: Boundary amount filtering (< 50, 50-200, > 200)", () => {
    const under50 = applyFilters(corpus, { amountRange: { min: 0, max: 50 } });
    // rcpt-special-chars (12.50), rcpt-zero-gross (0), rcpt-pending-review (50)
    expect(under50.map((r) => r.id)).toEqual(["rcpt-special-chars", "rcpt-zero-gross", "rcpt-pending-review"]);

    const between50and200 = applyFilters(corpus, { amountRange: { min: 50, max: 200 } });
    // rcpt-pending-review (50), rcpt-confirmed (85.40)
    expect(between50and200.map((r) => r.id)).toEqual(["rcpt-pending-review", "rcpt-confirmed"]);

    const over200 = applyFilters(corpus, { amountRange: { min: 200, max: null } });
    // rcpt-high-gross (3499)
    expect(over200.map((r) => r.id)).toEqual(["rcpt-high-gross"]);
  });

  test("CHALLENGE-1.7: Status tier filtering accurately partitions dataset", () => {
    const scanned = applyFilters(corpus, { status: "scanned" });
    const pending = applyFilters(corpus, { status: "pending" });
    const confirmed = applyFilters(corpus, { status: "confirmed" });

    expect(scanned.map((r) => r.id)).toEqual(["rcpt-special-chars", "rcpt-zero-gross", "rcpt-high-gross"]);
    expect(pending.map((r) => r.id)).toEqual(["rcpt-pending-review"]);
    expect(confirmed.map((r) => r.id)).toEqual(["rcpt-confirmed"]);
    expect(scanned.length + pending.length + confirmed.length).toBe(corpus.length);
  });

  test("CHALLENGE-1.8: Category filtering with multi-criteria conjunction", () => {
    const bewirtung = applyFilters(corpus, {
      category: "Bewirtung",
      amountRange: { min: 10, max: 20 },
      searchQuery: "Berlin",
    });
    expect(bewirtung).toHaveLength(1);
    expect(bewirtung[0].id).toBe("rcpt-special-chars");

    // Non-matching conjunction
    const emptyResult = applyFilters(corpus, {
      category: "Bewirtung",
      amountRange: { min: 50, max: 100 }, // rcpt-special-chars is 12.50
    });
    expect(emptyResult).toHaveLength(0);
  });

  test("CHALLENGE-1.9: Whitespace-only and empty search query does not filter out valid records", () => {
    expect(applyFilters(corpus, { searchQuery: "   " })).toHaveLength(corpus.length);
    expect(applyFilters(corpus, { searchQuery: "" })).toHaveLength(corpus.length);
  });
});

describe("Empirical Challenger M3: Selection Engine State & Invariant Stress", () => {
  const ids = Array.from({ length: 50 }, (_, i) => `item-${i}`);

  test("CHALLENGE-2.1: 1,000 Rapid toggles maintains strict set consistency", () => {
    const sm = new SelectionStateMachine();
    for (let i = 0; i < 1000; i++) {
      sm.toggleSelect("rapid-id");
    }
    // 1000 toggles = even number = unselected
    expect(sm.selectedIds).toHaveLength(0);
    expect(sm.isSelected("rapid-id")).toBe(false);

    sm.toggleSelect("rapid-id");
    expect(sm.selectedIds).toEqual(["rapid-id"]);
    expect(sm.isSelected("rapid-id")).toBe(true);
  });

  test("CHALLENGE-2.2: selectAll deduplicates and ignores falsy values", () => {
    const sm = new SelectionStateMachine();
    sm.selectAll(["id-1", "id-1", "id-2", "", "id-3", "id-2"]);
    expect(sm.selectedIds).toEqual(["id-1", "id-2", "id-3"]);
    expect(sm.selectedIds.length).toBe(3);
  });

  test("CHALLENGE-2.3: Forward, backward, and single range selections", () => {
    const sm = new SelectionStateMachine();

    // Forward range 5 to 10
    sm.selectRange("item-5", "item-10", ids);
    expect(sm.selectedIds).toHaveLength(6);
    expect(sm.selectedIds).toEqual(["item-5", "item-6", "item-7", "item-8", "item-9", "item-10"]);

    // Backward range 15 down to 12
    sm.selectRange("item-15", "item-12", ids);
    expect(sm.selectedIds).toHaveLength(10); // 6 + 4
    expect(sm.isSelected("item-12")).toBe(true);
    expect(sm.isSelected("item-15")).toBe(true);

    // Single item range
    sm.selectRange("item-20", "item-20", ids);
    expect(sm.isSelected("item-20")).toBe(true);
  });

  test("CHALLENGE-2.4: Range selection with invalid / unlisted boundary IDs fails safely without mutation", () => {
    const sm = new SelectionStateMachine(["item-1"]);
    sm.selectRange("missing-from", "item-5", ids);
    expect(sm.selectedIds).toEqual(["item-1"]);

    sm.selectRange("item-5", "missing-to", ids);
    expect(sm.selectedIds).toEqual(["item-1"]);

    sm.selectRange("missing-1", "missing-2", ids);
    expect(sm.selectedIds).toEqual(["item-1"]);

    sm.selectRange("item-1", "item-2", []); // empty ordered list
    expect(sm.selectedIds).toEqual(["item-1"]);
  });

  test("CHALLENGE-2.5: isAllSelected and isPartiallySelected state predicates", () => {
    const sm = new SelectionStateMachine();
    const testIds = ["a", "b", "c"];

    expect(sm.isAllSelected(testIds)).toBe(false);
    expect(sm.isPartiallySelected(testIds)).toBe(false);

    sm.toggleSelect("a");
    expect(sm.isAllSelected(testIds)).toBe(false);
    expect(sm.isPartiallySelected(testIds)).toBe(true);

    sm.toggleSelect("b");
    sm.toggleSelect("c");
    expect(sm.isAllSelected(testIds)).toBe(true);
    expect(sm.isPartiallySelected(testIds)).toBe(false);

    sm.toggleSelectAll(testIds); // all selected -> should deselect all
    expect(sm.selectedIds).toHaveLength(0);
    expect(sm.isAllSelected(testIds)).toBe(false);
  });

  test("CHALLENGE-2.6: High-scale selection (1,000 items) executes in under 15ms", () => {
    const largeList = Array.from({ length: 1000 }, (_, i) => `large-${i}`);
    const sm = new SelectionStateMachine();

    const start = performance.now();
    sm.selectAll(largeList);
    expect(sm.isAllSelected(largeList)).toBe(true);
    sm.toggleSelectAll(largeList);
    expect(sm.selectedIds).toHaveLength(0);
    const duration = performance.now() - start;

    expect(duration).toBeLessThan(200);
  });
});

describe("Empirical Challenger M3: Inline Recalculation & Financial Math Stress", () => {
  test("CHALLENGE-3.1: Gross amount change on 19% single-rate receipt", () => {
    const r = mockReceipt("single-rate-19", {
      totalAmount: { value: 119.0, confidence: 1.0 },
      netAmount: 100.0,
      taxBreakdown: [{ ratePercent: 19, taxAmount: 19.0, netAmount: 100.0 }],
    });

    const updated = {
      ...r,
      totalAmount: { ...r.totalAmount, value: 238.0 },
      editedFields: { totalAmount: true },
    };

    const recalculated = recalculateReceipt(updated, { editedField: "totalAmount" });
    expect(recalculated.totalAmount.value).toBe(238.0);
    expect(recalculated.netAmount).toBe(200.0);
    expect(recalculated.taxBreakdown[0].taxAmount).toBe(38.0);
    expect(recalculated.validation.isMathValid).toBe(true);
  });

  test("CHALLENGE-3.2: Gross amount set to 0.00 € maintains zero math without NaN or Division by Zero", () => {
    const r = mockReceipt("zero-gross-test");
    const updated = {
      ...r,
      totalAmount: { ...r.totalAmount, value: 0.0 },
      editedFields: { totalAmount: true },
    };

    const recalculated = recalculateReceipt(updated, { editedField: "totalAmount" });
    expect(recalculated.totalAmount.value).toBe(0.0);
    expect(recalculated.netAmount).toBe(0.0);
    expect(recalculated.taxBreakdown[0].taxAmount).toBe(0.0);
    expect(recalculated.validation.isMathValid).toBe(true);
  });

  test("CHALLENGE-3.3: Gross change on mixed 7% and 19% VAT rates redistributes proportionately", () => {
    const r = mockReceipt("mixed-rate-test", {
      totalAmount: { value: 100.0, confidence: 1.0 },
      netAmount: 88.0,
      taxBreakdown: [
        { ratePercent: 7, taxAmount: 3.5, netAmount: 50.0 },
        { ratePercent: 19, taxAmount: 8.5, netAmount: 38.0 },
      ],
    });

    const updated = {
      ...r,
      totalAmount: { ...r.totalAmount, value: 200.0 },
      editedFields: { totalAmount: true },
    };

    const recalculated = recalculateReceipt(updated, { editedField: "totalAmount" });
    expect(recalculated.totalAmount.value).toBe(200.0);
    expect(recalculated.validation.isMathValid).toBe(true);
    // Net + total tax equals gross
    const totalTax = recalculated.taxBreakdown.reduce((sum, item) => sum + (item.taxAmount || 0), 0);
    expect(Math.round(((recalculated.netAmount || 0) + totalTax) * 100) / 100).toBe(200.0);
  });

  test("CHALLENGE-3.4: Editing non-financial fields (date, merchant) updates audit flag without corrupting math", () => {
    const r = mockReceipt("audit-flag-test");
    const updated = {
      ...r,
      merchant: { ...r.merchant, name: "Neuer Bäcker" },
      date: { ...r.date, isoDate: "2026-08-01" },
      editedFields: { merchant: true, date: true },
    };

    const recalculated = recalculateReceipt(updated, { editedField: "merchant" });
    expect(recalculated.merchant.name).toBe("Neuer Bäcker");
    expect(recalculated.date.isoDate).toBe("2026-08-01");
    expect(recalculated.totalAmount.value).toBe(100.0);
    expect(recalculated.netAmount).toBe(84.03);
    expect(recalculated.editedFields?.merchant).toBe(true);
    expect(recalculated.editedFields?.date).toBe(true);
  });
});

describe("Empirical Challenger M3: Batch Export Generator Stress", () => {
  test("CHALLENGE-4.1: Dual-sheet Excel generation handles 50 receipts with varying tax configurations", async () => {
    const receipts = Array.from({ length: 50 }, (_, i) =>
      mockReceipt(`batch-xl-${i}`, {
        totalAmount: { value: 10.0 * (i + 1), confidence: 1.0 },
        suggestedCategory: i % 2 === 0 ? "Bewirtung" : "Reisekosten & Hotel",
      })
    );

    const buffer = await generateDualSheetExcel(receipts);
    expect(buffer).toBeDefined();
    expect(buffer.length).toBeGreaterThan(10000);
  });

  test("CHALLENGE-4.2: Accounting CSV generation escapes special CSV characters and enforces UTF-8 BOM", () => {
    const receipt = mockReceipt("csv-escape-test", {
      merchant: { name: 'Firma "Test;Semikolon & Neuer\nZeilenumbruch" GmbH', address: "Köln", taxId: "DE1", confidence: 1.0 },
      totalAmount: { value: 1234.56, confidence: 1.0 },
      suggestedCategory: "Bewirtung",
    });

    const csv = generateAccountingCsv([receipt]);
    expect(csv).toBeDefined();
    expect(csv.charCodeAt(0)).toBe(0xfeff); // UTF-8 BOM
    expect(csv).toContain("1234,56");
    expect(csv).toContain("Firma");
  });
});
