/**
 * Milestone 1 (R1): Modern Drag-and-Drop Ingestion & Batch Upload Tests
 * Verifies:
 * 1. Batch file ingestion & queue management
 * 2. Instant image thumbnail generation & PDF document preview badge
 * 3. Error boundaries & isolation for corrupted/invalid files
 * 4. Per-file Quick Actions (Retry failed file, Remove item)
 * 5. Concurrency & progress state transitions (queued -> preprocessing -> uploading -> extracting -> success)
 * 6. Batch statistics calculations (Total, In Progress, Success, Failed)
 */

import { describe, test, it, expect, beforeEach } from "../../../../tests/e2e/runner";
import { ProcessedReceipt } from "../../../lib/schema/receipt";
import {
  BatchQueueItem,
  UploadStatus,
} from "../BatchUploadDrawer";

// Mock helper to create mock File objects in Node/Test environment
function createMockFile(name: string, sizeBytes: number, mimeType: string): File {
  const buffer = new Uint8Array(sizeBytes);
  const blob = new Blob([buffer], { type: mimeType });
  return new File([blob], name, { type: mimeType, lastModified: Date.now() });
}

// Queue State Machine Logic Helper for Testing Verification
class TestBatchQueueManager {
  items: BatchQueueItem[] = [];
  activeWorkers = new Set<string>();
  maxConcurrent = 2;
  processedReceipts: ProcessedReceipt[] = [];
  trackedBlobUrls = new Set<string>();

  addFiles(files: File[]) {
    for (const file of files) {
      const isPdf =
        file.type === "application/pdf" || file.name.toLowerCase().endsWith(".pdf");
      const previewThumbnailUrl = !isPdf
        ? `blob:http://localhost/mock-thumb-${file.name}`
        : undefined;

      if (previewThumbnailUrl) {
        this.trackedBlobUrls.add(previewThumbnailUrl);
      }

      // Duplicate protection
      const isDuplicate = this.items.some(
        (i) => i.fileName === file.name && i.fileSizeBytes === file.size
      );
      if (isDuplicate) continue;

      this.items.push({
        id: `upload_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`,
        file,
        fileName: file.name,
        fileSizeBytes: file.size,
        mimeType: file.type || (isPdf ? "application/pdf" : "image/jpeg"),
        previewThumbnailUrl,
        isPdf,
        status: "queued",
        progressPercent: 0,
        stageMessage: "In Warteschlange...",
      });
    }
  }

  getStats() {
    const total = this.items.length;
    const inProgress = this.items.filter((i) =>
      ["queued", "preprocessing", "uploading", "extracting"].includes(i.status)
    ).length;
    const success = this.items.filter(
      (i) => i.status === "success" || i.status === "duplicate_suspected"
    ).length;
    const failed = this.items.filter((i) => i.status === "error").length;
    const aggregatePercent =
      total === 0
        ? 0
        : Math.round(
            this.items.reduce((acc, curr) => acc + curr.progressPercent, 0) / total
          );

    return { total, inProgress, success, failed, aggregatePercent };
  }

  async processItem(
    itemId: string,
    mockServerHandler?: (file: File) => Promise<{ success: boolean; receipt?: any; error?: string; pageCount?: number }>
  ) {
    const item = this.items.find((i) => i.id === itemId);
    if (!item) return;

    this.activeWorkers.add(itemId);

    // 1. Pre-flight Validation
    if (item.fileSizeBytes > 10 * 1024 * 1024) {
      item.status = "error";
      item.progressPercent = 100;
      item.stageMessage = "Datei zu groß";
      item.error = "Die Datei überschreitet das Limit von 10 MB.";
      this.activeWorkers.delete(itemId);
      return;
    }

    // 2. Preprocessing
    item.status = "preprocessing";
    item.progressPercent = 25;
    item.stageMessage = "Vorbereitung & Prüfung...";

    // 3. Uploading
    item.status = "uploading";
    item.progressPercent = 50;
    item.stageMessage = "Wird hochgeladen...";

    // 4. AI Vision Extraction
    item.status = "extracting";
    item.progressPercent = 75;
    item.stageMessage = "KI-Vision & Mathe-Prüfung...";

    try {
      if (mockServerHandler) {
        const res = await mockServerHandler(item.file);
        if (!res.success) {
          throw new Error(res.error || "Serverfehler bei der Extraktion");
        }
        item.status = "success";
        item.progressPercent = 100;
        item.stageMessage = res.pageCount && res.pageCount > 1 ? `${res.pageCount} Seiten erfasst` : "Erfolgreich erfasst";
        item.receiptResult = res.receipt;
        if (res.receipt) this.processedReceipts.push(res.receipt);
      } else {
        item.status = "success";
        item.progressPercent = 100;
        item.stageMessage = "Erfolgreich erfasst";
      }
    } catch (err: any) {
      item.status = "error";
      item.progressPercent = 100;
      item.stageMessage = "Fehlgeschlagen";
      item.error = err.message || "Fehler beim Verarbeiten";
    } finally {
      this.activeWorkers.delete(itemId);
    }
  }

  async runQueue(
    mockServerHandler?: (file: File) => Promise<{ success: boolean; receipt?: any; error?: string; pageCount?: number }>
  ) {
    while (true) {
      const queued = this.items.filter((i) => i.status === "queued");
      if (queued.length === 0 && this.activeWorkers.size === 0) break;

      const availableSlots = this.maxConcurrent - this.activeWorkers.size;
      if (availableSlots <= 0 || queued.length === 0) break;

      const batch = queued.slice(0, availableSlots);
      await Promise.all(batch.map((item) => this.processItem(item.id, mockServerHandler)));
    }
  }

  retryItem(itemId: string) {
    const item = this.items.find((i) => i.id === itemId);
    if (item) {
      item.status = "queued";
      item.progressPercent = 0;
      item.error = undefined;
      item.stageMessage = "In Warteschlange...";
    }
  }

  removeItem(itemId: string) {
    this.activeWorkers.delete(itemId);
    const item = this.items.find((i) => i.id === itemId);
    if (item?.previewThumbnailUrl) {
      this.trackedBlobUrls.delete(item.previewThumbnailUrl);
    }
    this.items = this.items.filter((i) => i.id !== itemId);
  }

  clearCompleted() {
    this.items.forEach((item) => {
      if (item.status === "success" && item.previewThumbnailUrl) {
        this.trackedBlobUrls.delete(item.previewThumbnailUrl);
      }
    });
    this.items = this.items.filter((i) => i.status !== "success");
  }

  unmount() {
    this.trackedBlobUrls.clear();
  }
}

describe("Milestone 1: R1 — Modern Drag-and-Drop Ingestion & Batch Upload", () => {
  let queueManager: TestBatchQueueManager;

  beforeEach(() => {
    queueManager = new TestBatchQueueManager();
  });

  test("M1.1: Batch file ingestion creates valid queue items with correct metadata", () => {
    const files = [
      createMockFile("aral_tankbeleg.jpg", 150000, "image/jpeg"),
      createMockFile("rewe_bon.png", 220000, "image/png"),
      createMockFile("hotel_rechnung.pdf", 480000, "application/pdf"),
      createMockFile("taxi_quittung.webp", 95000, "image/webp"),
    ];

    queueManager.addFiles(files);

    expect(queueManager.items).toHaveLength(4);

    const jpgItem = queueManager.items.find((i) => i.fileName === "aral_tankbeleg.jpg");
    expect(jpgItem).toBeDefined();
    expect(jpgItem?.fileSizeBytes).toBe(150000);
    expect(jpgItem?.isPdf).toBe(false);
    expect(jpgItem?.status).toBe("queued");
    expect(jpgItem?.progressPercent).toBe(0);

    const pdfItem = queueManager.items.find((i) => i.fileName === "hotel_rechnung.pdf");
    expect(pdfItem).toBeDefined();
    expect(pdfItem?.isPdf).toBe(true);
    expect(pdfItem?.mimeType).toBe("application/pdf");
  });

  test("M1.2: Image thumbnail generation creates preview URLs and PDF uses document preview fallback", () => {
    const files = [
      createMockFile("photo.jpg", 120000, "image/jpeg"),
      createMockFile("document.pdf", 300000, "application/pdf"),
    ];

    queueManager.addFiles(files);

    const imageItem = queueManager.items[0];
    expect(imageItem.previewThumbnailUrl).toBeDefined();
    expect(imageItem.previewThumbnailUrl).toContain("blob:");
    expect(imageItem.isPdf).toBe(false);

    const pdfItem = queueManager.items[1];
    expect(pdfItem.previewThumbnailUrl).toBeUndefined();
    expect(pdfItem.isPdf).toBe(true);
  });

  test("M1.3: Prevents duplicate queue entries when same file is added twice", () => {
    const file = createMockFile("duplicate_check.jpg", 100000, "image/jpeg");
    queueManager.addFiles([file]);
    expect(queueManager.items).toHaveLength(1);

    // Attempt re-adding same file
    queueManager.addFiles([file]);
    expect(queueManager.items).toHaveLength(1);
  });

  test("M1.4: State machine transitions through all upload stages (queued -> preprocessing -> uploading -> extracting -> success)", async () => {
    const file = createMockFile("stage_test.jpg", 140000, "image/jpeg");
    queueManager.addFiles([file]);

    const item = queueManager.items[0];
    expect(item.status).toBe("queued");
    expect(item.progressPercent).toBe(0);

    // Run processing
    await queueManager.processItem(item.id, async () => {
      return {
        success: true,
        receipt: {
          id: "rcpt-stage-1",
          merchant: { name: "MediaMarkt" },
          totalAmount: { value: 129.99 },
          currency: "EUR",
        } as any,
      };
    });

    expect(item.status).toBe("success");
    expect(item.progressPercent).toBe(100);
    expect(item.stageMessage).toBe("Erfolgreich erfasst");
    expect(item.receiptResult?.merchant?.name).toBe("MediaMarkt");
  });

  test("M1.5: Concurrency manager limits active simultaneous workers to maximum of 2", async () => {
    const files = [
      createMockFile("file1.jpg", 100000, "image/jpeg"),
      createMockFile("file2.jpg", 100000, "image/jpeg"),
      createMockFile("file3.jpg", 100000, "image/jpeg"),
      createMockFile("file4.jpg", 100000, "image/jpeg"),
    ];

    queueManager.addFiles(files);
    expect(queueManager.items).toHaveLength(4);

    let maxSimultaneousObserved = 0;

    await queueManager.runQueue(async () => {
      maxSimultaneousObserved = Math.max(maxSimultaneousObserved, queueManager.activeWorkers.size);
      // simulate network delay
      await new Promise((res) => setTimeout(res, 10));
      return { success: true };
    });

    expect(maxSimultaneousObserved).toBeLessThanOrEqual(2);
  });

  test("M1.6: Independent error isolation — corrupt file triggers error without breaking or halting remaining uploads", async () => {
    const files = [
      createMockFile("valid_1.jpg", 100000, "image/jpeg"),
      createMockFile("corrupt_file.bin", 50000, "application/octet-stream"),
      createMockFile("valid_2.png", 120000, "image/png"),
    ];

    queueManager.addFiles(files);

    await queueManager.runQueue(async (file) => {
      if (file.name === "corrupt_file.bin") {
        return {
          success: false,
          error: "Das PDF konnte nicht gelesen werden. Möglicherweise ist es beschädigt.",
        };
      }
      return {
        success: true,
        receipt: { id: `rcpt-${file.name}` } as any,
      };
    });

    const corruptItem = queueManager.items.find((i) => i.fileName === "corrupt_file.bin");
    expect(corruptItem?.status).toBe("error");
    expect(corruptItem?.error).toContain("beschädigt");

    const validItem1 = queueManager.items.find((i) => i.fileName === "valid_1.jpg");
    expect(validItem1?.status).toBe("success");

    const validItem2 = queueManager.items.find((i) => i.fileName === "valid_2.png");
    expect(validItem2?.status).toBe("success");

    const stats = queueManager.getStats();
    expect(stats.total).toBe(3);
    expect(stats.success).toBe(2);
    expect(stats.failed).toBe(1);
    expect(stats.inProgress).toBe(0);
  });

  test("M1.7: Rejects oversized files (> 10 MB) during pre-flight validation", async () => {
    const hugeFile = createMockFile("huge_scan.pdf", 15 * 1024 * 1024, "application/pdf");
    queueManager.addFiles([hugeFile]);

    await queueManager.processItem(queueManager.items[0].id);

    const item = queueManager.items[0];
    expect(item.status).toBe("error");
    expect(item.error).toContain("10 MB");
  });

  test("M1.8: Per-file Quick Action — Retry failed file resets status to queued and allows successful re-execution", async () => {
    const file = createMockFile("retry_target.jpg", 100000, "image/jpeg");
    queueManager.addFiles([file]);

    let shouldFail = true;

    // First attempt fails
    await queueManager.processItem(queueManager.items[0].id, async () => {
      if (shouldFail) return { success: false, error: "Netzwerk-Timeout" };
      return { success: true, receipt: { id: "rcpt-ok" } as any };
    });

    expect(queueManager.items[0].status).toBe("error");
    expect(queueManager.items[0].error).toBe("Netzwerk-Timeout");

    // Retry action triggered
    queueManager.retryItem(queueManager.items[0].id);
    expect(queueManager.items[0].status).toBe("queued");
    expect(queueManager.items[0].error).toBeUndefined();

    // Second attempt succeeds
    shouldFail = false;
    await queueManager.processItem(queueManager.items[0].id, async () => {
      return { success: true, receipt: { id: "rcpt-ok" } as any };
    });

    expect(queueManager.items[0].status).toBe("success");
    expect(queueManager.items[0].error).toBeUndefined();
  });

  test("M1.9: Per-file Quick Action — Remove item from queue removes item and updates batch statistics", () => {
    const files = [
      createMockFile("file_a.jpg", 100000, "image/jpeg"),
      createMockFile("file_b.jpg", 100000, "image/jpeg"),
    ];

    queueManager.addFiles(files);
    expect(queueManager.items).toHaveLength(2);

    const itemToRemove = queueManager.items[0];
    queueManager.removeItem(itemToRemove.id);

    expect(queueManager.items).toHaveLength(1);
    expect(queueManager.items[0].fileName).toBe("file_b.jpg");
    expect(queueManager.getStats().total).toBe(1);
  });

  test("M1.10: Multi-page PDF reports correct page count in stage messaging", async () => {
    const pdfFile = createMockFile("multipage_invoice.pdf", 400000, "application/pdf");
    queueManager.addFiles([pdfFile]);

    await queueManager.processItem(queueManager.items[0].id, async () => {
      return {
        success: true,
        pageCount: 3,
        receipt: { id: "rcpt-p1" } as any,
      };
    });

    const item = queueManager.items[0];
    expect(item.status).toBe("success");
    expect(item.stageMessage).toContain("3 Seiten erfasst");
  });

  test("M1.11: Clear completed action removes only finished items and preserves active/error items", () => {
    const files = [
      createMockFile("success1.jpg", 100000, "image/jpeg"),
      createMockFile("error1.jpg", 100000, "image/jpeg"),
      createMockFile("queued1.jpg", 100000, "image/jpeg"),
    ];

    queueManager.addFiles(files);
    queueManager.items[0].status = "success";
    queueManager.items[1].status = "error";
    queueManager.items[2].status = "queued";

    queueManager.clearCompleted();

    expect(queueManager.items).toHaveLength(2);
    expect(queueManager.items.find((i) => i.fileName === "success1.jpg")).toBeUndefined();
    expect(queueManager.items.find((i) => i.fileName === "error1.jpg")).toBeDefined();
    expect(queueManager.items.find((i) => i.fileName === "queued1.jpg")).toBeDefined();
  });

  test("M1.12: Object URL lifecycle tracking revokes URLs on item removal and unmount", () => {
    const files = [
      createMockFile("receipt1.jpg", 100000, "image/jpeg"),
      createMockFile("receipt2.jpg", 100000, "image/jpeg"),
      createMockFile("invoice.pdf", 200000, "application/pdf"),
    ];

    queueManager.addFiles(files);
    // 2 image thumbnail URLs tracked, PDF uses fallback
    expect(queueManager.trackedBlobUrls.size).toBe(2);

    // Remove one image item -> should remove tracked URL
    const itemToRemove = queueManager.items[0];
    queueManager.removeItem(itemToRemove.id);
    expect(queueManager.trackedBlobUrls.size).toBe(1);

    // Unmount -> should clear all remaining tracked URLs
    queueManager.unmount();
    expect(queueManager.trackedBlobUrls.size).toBe(0);
  });
});
