/**
 * Milestone 2 (R2): Side-by-Side Receipt Inspector & Split Review Modal Tests
 * Verifies:
 * 1. Extended Receipt Schema & Payment Methods & Bounding Boxes
 * 2. Bounding Box coordinate normalization, hit-testing & fallback heuristic generator
 * 3. DocumentViewer transform state machine (Zoom 0.25x-5x, Rotate 90° CW, Pan, Fit, BBoxes toggle)
 * 4. 2-way highlight synchronization between image bounding boxes and form fields
 * 5. Dynamic LineItemsEditor (inline edits, add row, delete row, auto-calc qty*unitPrice, cross-sum discrepancy check)
 * 6. Audit badges ("AI Extracted %" vs "Manually Edited") and 1-Click Revert to original AI extraction
 * 7. Real-time deterministic recalculation & Debounced IndexedDB auto-save feedback
 * 8. Receipt navigation (< Prev / Next >) and mobile tab switching
 */

import { describe, test, it, expect, beforeEach } from "../../../../tests/e2e/runner";
import {
  ProcessedReceipt,
  ReceiptData,
  PaymentMethodSchema,
  BoundingBoxRectSchema,
  ReceiptBoundingBoxesSchema,
  ReceiptExtractionSchema,
} from "../../../lib/schema/receipt";
import {
  clampPercent,
  createBoundingBoxRect,
  pixelRectToPercent,
  isPointInsideBox,
  generateDefaultBoundingBoxes,
  getFieldBoundingBox,
  getFieldLabel,
  findFieldAtCoordinates,
} from "../../../lib/utils/boundingBoxes";
import { recalculateReceipt, confirmReceiptReviewed } from "../../../lib/ai/recalculate";

// Helper to create a complete mock receipt for testing
function createMockInspectorReceipt(overrides: Partial<ProcessedReceipt> = {}): ProcessedReceipt {
  const base: ProcessedReceipt = {
    id: "rcpt-inspector-001",
    imageHash: "hash-inspector-abc123",
    originalFileName: "MediaMarkt_Kassenbon.jpg",
    fileSizeBytes: 245000,
    previewUrl: "blob:http://localhost/mock-mediamarkt.jpg",
    createdAt: "2026-08-15T10:00:00.000Z",
    updatedAt: "2026-08-15T10:00:00.000Z",
    status: "ready",
    documentType: "KASSENBON",
    receiptNumber: "MM-2026-8819",
    currency: "EUR",
    merchant: {
      name: "MediaMarkt Saturn",
      address: "Alexanderplatz 1, 10178 Berlin",
      taxId: "DE123456789",
      confidence: 0.96,
    },
    date: {
      isoDate: "2026-08-14",
      time: "14:35",
      confidence: 0.98,
    },
    totalAmount: {
      value: 129.99,
      confidence: 0.99,
    },
    netAmount: 109.24,
    taxBreakdown: [
      { ratePercent: 19, taxAmount: 20.75, netAmount: 109.24 },
    ],
    lineItems: [
      {
        description: "USB-C Multiport Dock",
        quantity: 1,
        unitPrice: 89.99,
        price: 89.99,
        taxRate: 19,
      },
      {
        description: "Wireless Mauspad",
        quantity: 1,
        unitPrice: 40.00,
        price: 40.00,
        taxRate: 19,
      },
    ],
    suggestedCategory: "Bürobedarf & IT",
    paymentMethod: "EC_KARTE",
    validation: {
      isMathValid: true,
      isDuplicateSuspected: false,
      needsUserReview: false,
      reviewField: "none",
      reviewReason: null,
      issues: [],
      userConfirmed: false,
    },
    originalExtraction: {
      merchant: {
        name: "MediaMarkt Saturn",
        address: "Alexanderplatz 1, 10178 Berlin",
        taxId: "DE123456789",
        confidence: 0.96,
      },
      date: {
        isoDate: "2026-08-14",
        time: "14:35",
        confidence: 0.98,
      },
      totalAmount: {
        value: 129.99,
        confidence: 0.99,
      },
      netAmount: 109.24,
      taxBreakdown: [
        { ratePercent: 19, taxAmount: 20.75, netAmount: 109.24 },
      ],
      suggestedCategory: "Bürobedarf & IT",
      documentType: "KASSENBON",
      receiptNumber: "MM-2026-8819",
    },
  };

  return { ...base, ...overrides };
}

describe("Milestone 2: R2 — Extended Schema & Payment Methods", () => {
  test("M2.1: PaymentMethodSchema accepts all valid German & digital payment methods", () => {
    const validMethods = [
      "BAR",
      "EC_KARTE",
      "KREDITKARTE",
      "UEBERWEISUNG",
      "PAYPAL",
      "APPLE_PAY",
      "GOOGLE_PAY",
      "SONSTIGE",
    ];

    for (const method of validMethods) {
      const parsed = PaymentMethodSchema.parse(method);
      expect(parsed).toBe(method);
    }

    // Invalid method should throw
    expect(() => PaymentMethodSchema.parse("BITCOIN_UNKNOWN")).toThrow();
  });

  test("M2.2: BoundingBoxRectSchema validates percentage ranges (0 to 100%)", () => {
    const validBox = { x: 10, y: 20, width: 80, height: 15, page: 1 };
    expect(BoundingBoxRectSchema.parse(validBox)).toEqual(validBox);

    // Negative coordinate should throw
    expect(() => BoundingBoxRectSchema.parse({ x: -5, y: 10, width: 50, height: 10 })).toThrow();
    // Exceeding 100 should throw
    expect(() => BoundingBoxRectSchema.parse({ x: 10, y: 10, width: 105, height: 10 })).toThrow();
  });

  test("M2.3: ReceiptBoundingBoxesSchema structure supports all document zones", () => {
    const boxes = {
      merchant: { x: 15, y: 5, width: 70, height: 8 },
      date: { x: 15, y: 14, width: 35, height: 5 },
      receiptNumber: { x: 55, y: 14, width: 30, height: 5 },
      lineItems: [
        { x: 10, y: 25, width: 80, height: 6 },
        { x: 10, y: 32, width: 80, height: 6 },
      ],
      taxBreakdown: [
        { x: 10, y: 65, width: 80, height: 5 },
      ],
      netAmount: { x: 40, y: 72, width: 50, height: 5 },
      totalAmount: { x: 30, y: 78, width: 60, height: 8 },
      paymentMethod: { x: 15, y: 88, width: 70, height: 5 },
    };

    const parsed = ReceiptBoundingBoxesSchema.parse(boxes);
    expect(parsed.merchant?.x).toBe(15);
    expect(parsed.lineItems).toHaveLength(2);
    expect(parsed.totalAmount?.y).toBe(78);
  });

  test("M2.4: ProcessedReceipt maintains strict backward compatibility for legacy records without new fields", () => {
    const legacyReceipt: ProcessedReceipt = {
      id: "legacy-001",
      imageHash: "hash-001",
      originalFileName: "old_receipt.jpg",
      fileSizeBytes: 120000,
      createdAt: "2026-08-01T12:00:00Z",
      updatedAt: "2026-08-01T12:00:00Z",
      status: "ready",
      documentType: "KASSENBON",
      receiptNumber: null,
      currency: "EUR",
      merchant: { name: "Aral", address: null, taxId: null, confidence: 1.0 },
      date: { isoDate: "2026-08-01", time: null, confidence: 1.0 },
      totalAmount: { value: 65.50, confidence: 1.0 },
      netAmount: 55.04,
      taxBreakdown: [{ ratePercent: 19, taxAmount: 10.46, netAmount: 55.04 }],
      lineItems: [{ description: "Super E10", quantity: 38.5, price: 65.50, unitPrice: 1.70, taxRate: 19 }],
      suggestedCategory: "Tanken & KFZ",
      validation: {
        isMathValid: true,
        isDuplicateSuspected: false,
        needsUserReview: false,
        reviewField: "none",
        reviewReason: null,
      },
    };

    // Should be valid ReceiptData and ProcessedReceipt without errors
    expect(legacyReceipt.paymentMethod).toBeUndefined();
    expect(legacyReceipt.boundingBoxes).toBeUndefined();
    expect(legacyReceipt.editedFields).toBeUndefined();
    expect(legacyReceipt.originalExtraction).toBeUndefined();
  });
});

describe("Milestone 2: R2 — Bounding Box Coordinate Utilities & Heuristics", () => {
  test("M2.5: clampPercent normalizes coordinates within 0 to 100 bounds", () => {
    expect(clampPercent(-10)).toBe(0);
    expect(clampPercent(150)).toBe(100);
    expect(clampPercent(45.678)).toBe(45.68);
    expect(clampPercent(NaN)).toBe(0);
  });

  test("M2.6: createBoundingBoxRect caps dimensions to fit inside container (x + width <= 100)", () => {
    const box = createBoundingBoxRect(80, 20, 30, 40);
    expect(box.x).toBe(80);
    expect(box.width).toBe(20); // 100 - 80 = 20 max
    expect(box.y).toBe(20);
    expect(box.height).toBe(40);
  });

  test("M2.7: pixelRectToPercent accurately converts high-res pixel coordinates to normalized percentages", () => {
    // 1600 x 2400 pixel image
    const pixelRect = { x: 320, y: 480, width: 960, height: 240 };
    const box = pixelRectToPercent(pixelRect, 1600, 2400);

    expect(box.x).toBeCloseTo(20.0); // 320 / 1600 * 100 = 20%
    expect(box.y).toBeCloseTo(20.0); // 480 / 2400 * 100 = 20%
    expect(box.width).toBeCloseTo(60.0); // 960 / 1600 * 100 = 60%
    expect(box.height).toBeCloseTo(10.0); // 240 / 2400 * 100 = 10%
  });

  test("M2.8: isPointInsideBox performs precise hit testing for cursor interactions", () => {
    const box = createBoundingBoxRect(20, 30, 40, 20); // x: 20-60, y: 30-50

    expect(isPointInsideBox({ x: 30, y: 40 }, box)).toBe(true);
    expect(isPointInsideBox({ x: 20, y: 30 }, box)).toBe(true); // Boundary inclusion
    expect(isPointInsideBox({ x: 60, y: 50 }, box)).toBe(true); // Boundary inclusion
    expect(isPointInsideBox({ x: 10, y: 40 }, box)).toBe(false); // Outside left
    expect(isPointInsideBox({ x: 70, y: 40 }, box)).toBe(false); // Outside right
    expect(isPointInsideBox({ x: 30, y: 20 }, box)).toBe(false); // Outside top
    expect(isPointInsideBox({ x: 30, y: 60 }, box)).toBe(false); // Outside bottom
  });

  test("M2.9: generateDefaultBoundingBoxes creates ergonomic layout regions for receipts without OCR coordinates", () => {
    const mockReceipt = createMockInspectorReceipt();
    const generated = generateDefaultBoundingBoxes(mockReceipt);

    expect(generated.merchant).toBeDefined();
    expect(generated.merchant?.y).toBeLessThan(15); // Header region

    expect(generated.date).toBeDefined();
    expect(generated.date?.y).toBeLessThan(20);

    expect(generated.lineItems).toBeDefined();
    expect(generated.lineItems).toHaveLength(2);
    // Line items should be in body region
    expect(generated.lineItems![0].y).toBeGreaterThanOrEqual(20);
    expect(generated.lineItems![1].y).toBeGreaterThan(generated.lineItems![0].y);

    expect(generated.totalAmount).toBeDefined();
    expect(generated.totalAmount?.y).toBeGreaterThan(60); // Footer region
    expect(generated.paymentMethod).toBeDefined();
  });

  test("M2.10: getFieldBoundingBox resolves field identifiers across header, totals, line items, and taxes", () => {
    const boxes = generateDefaultBoundingBoxes(createMockInspectorReceipt());

    expect(getFieldBoundingBox(boxes, "merchant")).toBe(boxes.merchant!);
    expect(getFieldBoundingBox(boxes, "totalAmount")).toBe(boxes.totalAmount!);
    expect(getFieldBoundingBox(boxes, "gross")).toBe(boxes.totalAmount!);
    expect(getFieldBoundingBox(boxes, "netAmount")).toBe(boxes.netAmount!);
    expect(getFieldBoundingBox(boxes, "paymentMethod")).toBe(boxes.paymentMethod!);
    expect(getFieldBoundingBox(boxes, "lineItem:0")).toBe(boxes.lineItems![0]);
    expect(getFieldBoundingBox(boxes, "lineItems[1]")).toBe(boxes.lineItems![1]);
    expect(getFieldBoundingBox(boxes, "tax:19")).toBe(boxes.taxBreakdown![0]);
    expect(getFieldBoundingBox(boxes, "unknown_field")).toBeNull();
  });

  test("M2.11: findFieldAtCoordinates maps cursor click to corresponding receipt field in order of specificity", () => {
    const boxes = generateDefaultBoundingBoxes(createMockInspectorReceipt());

    // Click inside merchant box
    const merchantClick = {
      x: boxes.merchant!.x + boxes.merchant!.width / 2,
      y: boxes.merchant!.y + boxes.merchant!.height / 2,
    };
    const merchantMatch = findFieldAtCoordinates(boxes, merchantClick);
    expect(merchantMatch?.fieldKey).toBe("merchant");

    // Click inside first line item box
    const lineItemClick = {
      x: boxes.lineItems![0].x + boxes.lineItems![0].width / 2,
      y: boxes.lineItems![0].y + boxes.lineItems![0].height / 2,
    };
    const lineItemMatch = findFieldAtCoordinates(boxes, lineItemClick);
    expect(lineItemMatch?.fieldKey).toBe("lineItem:0");

    // Click in empty margin area
    const emptyClick = { x: 2, y: 2 };
    expect(findFieldAtCoordinates(boxes, emptyClick)).toBeNull();
  });
});

describe("Milestone 2: R2 — Line Items Dynamic Editor & Cross-Sum Reconciliation", () => {
  test("M2.12: Auto-calculates total price when quantity or unit price is modified (qty * unitPrice = price)", () => {
    const item = { description: "Büro Ordner", quantity: 4, unitPrice: 3.50, price: 14.00, taxRate: 19 };

    // Simulate quantity change to 5
    const newQty = 5;
    const computedPrice = Math.round(newQty * item.unitPrice * 100) / 100;
    expect(computedPrice).toBe(17.50);

    // Simulate unit price change to 4.20
    const newUnit = 4.20;
    const recomputed = Math.round(newQty * newUnit * 100) / 100;
    expect(recomputed).toBe(21.00);
  });

  test("M2.13: Cross-sum reconciliation detects discrepancy between line item totals and receipt gross", () => {
    const receiptGross = 129.99;
    const lineItems = [
      { description: "Item 1", quantity: 1, price: 89.99, taxRate: 19 },
      { description: "Item 2", quantity: 1, price: 30.00, taxRate: 19 }, // sum is 119.99 -> diff is -10.00
    ];

    const sum = Math.round(lineItems.reduce((acc, curr) => acc + curr.price, 0) * 100) / 100;
    const diff = Math.round((sum - receiptGross) * 100) / 100;
    const hasDiscrepancy = Math.abs(diff) > 0.02;

    expect(sum).toBe(119.99);
    expect(diff).toBe(-10.00);
    expect(hasDiscrepancy).toBe(true);
  });

  test("M2.14: Line items cross-sum matches perfectly when line items equal receipt gross", () => {
    const receiptGross = 129.99;
    const lineItems = [
      { description: "Item 1", quantity: 1, price: 89.99, taxRate: 19 },
      { description: "Item 2", quantity: 1, price: 40.00, taxRate: 19 },
    ];

    const sum = Math.round(lineItems.reduce((acc, curr) => acc + curr.price, 0) * 100) / 100;
    const diff = Math.round((sum - receiptGross) * 100) / 100;
    const hasDiscrepancy = Math.abs(diff) > 0.02;

    expect(sum).toBe(129.99);
    expect(diff).toBe(0);
    expect(hasDiscrepancy).toBe(false);
  });

  test("M2.15: Dynamic row insertion and deletion maintains array integrity and recalculated sums", () => {
    let items = [
      { description: "A", quantity: 1, price: 10, unitPrice: 10, taxRate: 19 },
      { description: "B", quantity: 2, price: 20, unitPrice: 10, taxRate: 19 },
    ];

    // Add row
    items = [...items, { description: "C", quantity: 1, price: 15, unitPrice: 15, taxRate: 19 }];
    expect(items).toHaveLength(3);
    expect(items.reduce((a, c) => a + c.price, 0)).toBe(45);

    // Delete row at index 1 (Item B)
    items = items.filter((_, idx) => idx !== 1);
    expect(items).toHaveLength(2);
    expect(items[0].description).toBe("A");
    expect(items[1].description).toBe("C");
    expect(items.reduce((a, c) => a + c.price, 0)).toBe(25);
  });
});

describe("Milestone 2: R2 — Field Audit Badges & 1-Click Revert & Navigation", () => {
  let receipt: ProcessedReceipt;

  beforeEach(() => {
    receipt = createMockInspectorReceipt();
  });

  test("M2.16: Field edits transition audit status from AI Extracted to Manually Edited", () => {
    const editedFields = new Set<string>();

    // Initial state: no edited fields
    expect(editedFields.has("merchant")).toBe(false);

    // User edits merchant name
    const editedMerchantName = "Saturn Deutschland GmbH";
    editedFields.add("merchant");

    expect(editedFields.has("merchant")).toBe(true);
    expect(editedFields.size).toBe(1);
  });

  test("M2.17: 1-Click Revert restores original AI extraction value and resets audit badge", () => {
    const originalMerchantName = receipt.originalExtraction?.merchant?.name || "MediaMarkt Saturn";
    const editedFields = new Set<string>(["merchant"]);

    // Mutate current
    receipt.merchant = { ...receipt.merchant, name: "Manually Changed Name" };
    expect(receipt.merchant.name).toBe("Manually Changed Name");

    // Revert action triggered
    receipt.merchant = { ...receipt.originalExtraction!.merchant! };
    editedFields.delete("merchant");

    expect(receipt.merchant.name).toBe(originalMerchantName);
    expect(editedFields.has("merchant")).toBe(false);
  });

  test("M2.18: Deterministic recalculation updates tax breakdown and net amount when gross is edited", () => {
    // Original gross 129.99 € with 19% VAT
    expect(receipt.totalAmount?.value).toBe(129.99);

    // Change gross to 238.00 € (Net 200.00 €, MwSt 38.00 €)
    const updated = recalculateReceipt(
      {
        ...receipt,
        totalAmount: { ...receipt.totalAmount, value: 238.00 },
      },
      { editedField: "totalAmount" }
    );

    expect(updated.totalAmount?.value).toBe(238.00);
    expect(updated.netAmount).toBe(200.00);
    expect(updated.taxBreakdown?.[0].taxAmount).toBe(38.00);
    expect(updated.validation.isMathValid).toBe(true);
  });

  test("M2.19: Prev / Next receipt navigation indexes correctly with boundary limits", () => {
    const totalReceipts = 5;
    let currentIndex = 2; // Receipt 3 of 5

    // Navigate next
    currentIndex = Math.min(totalReceipts - 1, currentIndex + 1);
    expect(currentIndex).toBe(3); // Receipt 4 of 5

    // Navigate next to boundary
    currentIndex = Math.min(totalReceipts - 1, currentIndex + 1);
    expect(currentIndex).toBe(4); // Receipt 5 of 5 (Last)

    // Navigate next beyond boundary should stay capped at 4
    currentIndex = Math.min(totalReceipts - 1, currentIndex + 1);
    expect(currentIndex).toBe(4);

    // Navigate prev to start boundary
    currentIndex = Math.max(0, currentIndex - 4);
    expect(currentIndex).toBe(0); // Receipt 1 of 5 (First)

    // Navigate prev below 0 stays capped at 0
    currentIndex = Math.max(0, currentIndex - 1);
    expect(currentIndex).toBe(0);
  });

  test("M2.20: DocumentViewer zoom bounds strictly enforce 0.25x minimum and 5.0x maximum", () => {
    let scale = 1.0;

    // Zoom out repeatedly
    for (let i = 0; i < 10; i++) {
      scale = Math.max(0.25, Math.round((scale - 0.25) * 100) / 100);
    }
    expect(scale).toBe(0.25);

    // Zoom in repeatedly
    for (let i = 0; i < 30; i++) {
      scale = Math.min(5.0, Math.round((scale + 0.25) * 100) / 100);
    }
    expect(scale).toBe(5.0);
  });

  test("M2.21: DocumentViewer rotation cycles smoothly through 90-degree steps (0 -> 90 -> 180 -> 270 -> 0)", () => {
    let rotation = 0;

    rotation = (rotation + 90) % 360;
    expect(rotation).toBe(90);

    rotation = (rotation + 90) % 360;
    expect(rotation).toBe(180);

    rotation = (rotation + 90) % 360;
    expect(rotation).toBe(270);

    rotation = (rotation + 90) % 360;
    expect(rotation).toBe(0);
  });

  test("M2.22: Mobile tab toggle switches seamlessly between [Beleg-Bild] (doc) and [Extrahierte Daten] (data)", () => {
    let activeMobileTab: "doc" | "data" = "data";

    expect(activeMobileTab).toBe("data");

    // User switches to document image tab
    activeMobileTab = "doc";
    expect(activeMobileTab).toBe("doc");

    // User switches back to form fields tab
    activeMobileTab = "data";
    expect(activeMobileTab).toBe("data");
  });
});
