/**
 * Sprint B: scanner speed — client prepare, vision page pick, reasoning
 * retry policy, provider timeout constant, PDF raster target.
 */

import { describe, test, expect } from "./runner";
import {
  AI_IMAGE_LONG_EDGE_PX,
  computeReceiptTileRegions,
  MAX_RECEIPT_DETAIL_TILES,
  MAX_VISION_PAGES,
  selectVisionPages,
} from "../../src/lib/image/processor";
import { prepareUploadFile, CLIENT_UPLOAD_SKIP_BELOW_BYTES } from "../../src/lib/image/prepareUpload";
import {
  PROVIDER_TIMEOUT_MS,
  isRateLimitError,
  shouldRetryWithReasoning,
  shouldVerifyLineItems,
  selectLineItemViewsForVerification,
} from "../../src/lib/ai/extractor";
import { PENDING_VALIDATION, ReceiptData } from "../../src/lib/schema/receipt";

function receipt(mathValid: boolean): 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: [],
    suggestedCategory: "Sonstiges",
    validation: { ...PENDING_VALIDATION, isMathValid: mathValid },
  };
}

describe("Sprint B — Vision-Seiten", () => {
  test("kurze Dokumente behalten alle Seiten", () => {
    expect(selectVisionPages([1, 2, 3])).toEqual([1, 2, 3]);
    expect(selectVisionPages([1, 2, 3, 4, 5, 6, 7, 8])).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
  });

  test("lange Dokumente: erste 6 + letzte 2, ohne Duplikate", () => {
    const pages = Array.from({ length: 20 }, (_, i) => i + 1);
    expect(selectVisionPages(pages)).toEqual([1, 2, 3, 4, 5, 6, 19, 20]);
    expect(selectVisionPages(pages).length).toBeLessThanOrEqual(MAX_VISION_PAGES);
  });

  test("7 Seiten überlappen sich nicht doppelt", () => {
    expect(selectVisionPages([1, 2, 3, 4, 5, 6, 7])).toEqual([1, 2, 3, 4, 5, 6, 7]);
  });

  test("langer Thermobon wird in überlappende Detailansichten geteilt", () => {
    const regions = computeReceiptTileRegions(1000, 3600);
    expect(regions.length).toBeGreaterThan(1);
    expect(regions[0].top).toBe(0);
    expect(regions[regions.length - 1].top + regions[regions.length - 1].height).toBe(3600);
    expect(regions[1].top).toBeLessThan(regions[0].top + regions[0].height);
  });

  test("normales Dokument wird nicht unnötig gekachelt", () => {
    expect(computeReceiptTileRegions(1200, 1600)).toEqual([]);
  });

  for (const aspect of [8, 12, 20]) {
    test(`${aspect}:1-Thermobon bleibt vollständig und lückenlos`, () => {
      const width = 1000;
      const height = width * aspect;
      const regions = computeReceiptTileRegions(width, height);
      expect(regions.length).toBeGreaterThan(5);
      expect(regions.length).toBeLessThanOrEqual(MAX_RECEIPT_DETAIL_TILES);
      expect(regions[0].top).toBe(0);
      expect(regions[regions.length - 1].top + regions[regions.length - 1].height).toBe(height);
      for (let index = 1; index < regions.length; index++) {
        expect(regions[index].top).toBeLessThanOrEqual(
          regions[index - 1].top + regions[index - 1].height
        );
      }
    });
  }
});

describe("Sprint B — Reasoning on-demand", () => {
  test("Math-Fehler löst Reasoning-Retry aus", () => {
    expect(shouldRetryWithReasoning(receipt(false))).toBe(true);
  });

  test("Positionswarnung löst trotz valider Steuerrechnung einen Retry aus", () => {
    const value = receipt(true);
    value.validation = {
      ...value.validation,
      needsUserReview: true,
      issues: [{ field: "lineItems", severity: "warning", message: "Sum mismatch" }],
    };
    expect(shouldRetryWithReasoning(value)).toBe(true);
    expect(shouldVerifyLineItems(value)).toBe(true);
  });

  test("echter Netto-/Steuerfehler bleibt ein Reasoning-Fall", () => {
    const value = receipt(false);
    value.validation = {
      ...value.validation,
      needsUserReview: true,
      issues: [{ field: "taxBreakdown", severity: "error", message: "Tax mismatch" }],
    };
    expect(shouldVerifyLineItems(value)).toBe(false);
  });

  test("valide Math braucht kein Reasoning", () => {
    expect(shouldRetryWithReasoning(receipt(true))).toBe(false);
  });

  test("extremer oder schwieriger Bon erzwingt fokussierte Zeilen-OCR", () => {
    expect(shouldVerifyLineItems(receipt(true), true)).toBe(true);
  });

  test("niedrige Zeilen-Confidence löst gezielte Prüfung aus", () => {
    const value = receipt(true);
    value.lineItems = [
      {
        description: "Schwer lesbar",
        quantity: 1,
        price: 11.9,
        unitPrice: null,
        taxRate: 19,
        confidence: 0.55,
      },
    ];
    expect(shouldVerifyLineItems(value)).toBe(true);
  });

  test("nur unsicherer Ausschnitt plus direkte Nachbarn werden erneut gelesen", () => {
    const value = receipt(true);
    value.lineItems = [
      {
        description: "Artikel",
        quantity: 1,
        price: 11.9,
        unitPrice: null,
        taxRate: 19,
        confidence: 0.92,
        sourceView: 3,
        rowOrder: 1,
      },
    ];
    const views = Array.from({ length: 6 }, (_, index) => ({
      buffer: Buffer.from([index]),
      sourceView: index + 1,
      qualityScore: index === 3 ? 0.2 : 0.9,
      difficult: index === 3,
    }));
    expect(
      selectLineItemViewsForVerification(value, views, "detail_views").map(
        (view) => view.sourceView
      )
    ).toEqual([3, 4, 5]);
  });

  test("Extrembon prüft trotz lokaler Auswahl alle lückenlosen Ausschnitte", () => {
    const value = receipt(true);
    value.lineItems = [
      {
        description: "Artikel",
        quantity: 1,
        price: 11.9,
        unitPrice: null,
        taxRate: 19,
        confidence: 0.9,
        sourceView: 1,
        rowOrder: 1,
      },
    ];
    const views = Array.from({ length: 8 }, (_, index) => ({
      buffer: Buffer.from([index]),
      sourceView: index + 1,
      qualityScore: 0.9,
      difficult: false,
    }));
    expect(selectLineItemViewsForVerification(value, views, "long_receipt_summary")).toHaveLength(8);
  });
});

describe("Sprint B — Rate-Limit & Timeout", () => {
  test("429 wird als retryable erkannt", () => {
    expect(isRateLimitError({ status: 429, message: "Too Many Requests" })).toBe(true);
    expect(isRateLimitError(new Error("HTTP 429 rate_limit"))).toBe(true);
    expect(isRateLimitError(new Error("timeout"))).toBe(false);
  });

  test("Provider-Timeout lässt Detailansichten genug Zeit", () => {
    expect(PROVIDER_TIMEOUT_MS).toBe(20_000);
  });
});

describe("Sprint B — Bildpipeline", () => {
  test("PDF-Raster und AI-JPEG teilen die 1536px-Kante", () => {
    expect(AI_IMAGE_LONG_EDGE_PX).toBe(1536);
  });
});

describe("Sprint B — Client-Prepare", () => {
  test("PDF bleibt unverändert", async () => {
    const pdf = new File([new Uint8Array([0x25, 0x50, 0x44, 0x46])], "invoice.pdf", {
      type: "application/pdf",
    });
    const out = await prepareUploadFile(pdf);
    expect(out).toBe(pdf);
  });

  test("kleine JPEGs unter dem Skip-Limit bleiben unverändert", async () => {
    const bytes = new Uint8Array(Math.min(1024, CLIENT_UPLOAD_SKIP_BELOW_BYTES));
    const file = new File([bytes], "tiny.jpg", { type: "image/jpeg" });
    Object.defineProperty(file, "size", { value: 12_000 });
    const out = await prepareUploadFile(file);
    expect(out).toBe(file);
  });
});
