/**
 * Daily AI-Usage Cap (src/lib/ai/usage.ts) — rein logische Tests.
 *
 * Kein Netzwerk, keine Datenbank: geprüft wird ausschließlich der in-memory
 * 24h-Fenster-Limiter (Pre-Check + Record-nach-Extraktion). Die Scan-Route
 * selbst wird hier NICHT aufgerufen (CSRF-/DB-/Extraction-Abhängigkeit) — die
 * pure Limiter-Logik ist die vertragliche Einheit.
 *
 * Determinismus: Alle Fenster-Aussagen laufen über das injizierbare `now`,
 * echte Uhrzeit wird in keinem Test verwendet. Jeder Test nutzt eigene Keys,
 * damit die modul-globale Map keine Suite-übergreifenden Nebeneffekte hat.
 */

import { describe, test, expect, runAllTests } from "../e2e/runner";
import {
  DAILY_SCAN_LIMIT,
  DAILY_GUEST_SCAN_LIMIT,
  DAILY_PRO_SCAN_LIMIT,
  HOURLY_SCAN_LIMIT,
  checkDailyUsage,
  dailyScanLimitFor,
  recordUsage,
  usageKeyForUser,
  usageKeyForGuest,
} from "../../src/lib/ai/usage";

const HOUR = 60 * 60 * 1000;
const DAY = 24 * HOUR;

/** Eindeutiger Key pro Testaufruf — isoliert Fenster über die Testgrenzen hinweg. */
let keySeq = 0;
const nextKey = (prefix = "usage") => `${prefix}_${Date.now()}_${keySeq++}`;

// ---------------------------------------------------------------------------
// 1. Pre-Check-Semantik (checkDailyUsage)
// ---------------------------------------------------------------------------

describe("checkDailyUsage — Pre-Check-Semantik", () => {
  test("C-1: ohne Verbrauch erlaubt, used=0, remaining=Limit", () => {
    const key = nextKey();
    const r = checkDailyUsage(key, DAILY_SCAN_LIMIT);
    expect(r.allowed).toBe(true);
    expect(r.used).toBe(0);
    expect(r.remaining).toBe(DAILY_SCAN_LIMIT);
    // resetsAt liegt in der Zukunft (neues Fenster würde erst beim Record starten)
    expect(r.resetsAt).toBeGreaterThan(Date.now());
  });

  test("C-2: erlaubt bis zum Limit; Limit erreicht → blocked (Limit+1-Einheit)", () => {
    const key = nextKey();
    for (let i = 1; i <= DAILY_SCAN_LIMIT; i++) {
      recordUsage(key, 1);
    }
    const atLimit = checkDailyUsage(key, DAILY_SCAN_LIMIT);
    expect(atLimit.used).toBe(DAILY_SCAN_LIMIT);
    expect(atLimit.remaining).toBe(0);
    expect(atLimit.allowed).toBe(false);
    // Und eine Einheit davor war noch erlaubt:
    const key2 = nextKey();
    for (let i = 1; i <= DAILY_SCAN_LIMIT - 1; i++) {
      recordUsage(key2, 1);
    }
    expect(checkDailyUsage(key2, DAILY_SCAN_LIMIT).allowed).toBe(true);
    expect(checkDailyUsage(key2, DAILY_SCAN_LIMIT).remaining).toBe(1);
  });

  test("C-3: allowed hängt am explizit übergebenen Limit", () => {
    const key = nextKey();
    for (let i = 0; i < DAILY_GUEST_SCAN_LIMIT; i++) {
      recordUsage(key, 1);
    }
    // Gegen das User-Limit (30) noch frei, gegen das Guest-Limit (10) blockiert:
    expect(checkDailyUsage(key, DAILY_SCAN_LIMIT).allowed).toBe(true);
    expect(checkDailyUsage(key, DAILY_GUEST_SCAN_LIMIT).allowed).toBe(false);
  });

  test("C-3b: Pro-Cap (150) erlaubt weiter, wo Free (30) schon blockiert", () => {
    const key = nextKey();
    for (let i = 0; i < DAILY_SCAN_LIMIT; i++) {
      recordUsage(key, 1);
    }
    expect(checkDailyUsage(key, DAILY_SCAN_LIMIT).allowed).toBe(false);
    expect(checkDailyUsage(key, DAILY_PRO_SCAN_LIMIT).allowed).toBe(true);
    expect(checkDailyUsage(key, DAILY_PRO_SCAN_LIMIT).remaining).toBe(
      DAILY_PRO_SCAN_LIMIT - DAILY_SCAN_LIMIT
    );
    for (let i = DAILY_SCAN_LIMIT; i < DAILY_PRO_SCAN_LIMIT; i++) {
      recordUsage(key, 1);
    }
    expect(checkDailyUsage(key, DAILY_PRO_SCAN_LIMIT).allowed).toBe(false);
    expect(checkDailyUsage(key, DAILY_PRO_SCAN_LIMIT).used).toBe(DAILY_PRO_SCAN_LIMIT);
  });

  test("C-4: ohne explizites Limit gilt der schlüssel-abgeleitete Default", () => {
    expect(checkDailyUsage(usageKeyForUser("u_a")).remaining).toBe(DAILY_SCAN_LIMIT);
    expect(checkDailyUsage(usageKeyForGuest("guest_a")).remaining).toBe(DAILY_GUEST_SCAN_LIMIT);
  });

  test("C-5: leerer Key oder ungültiges Limit throttelt nie (Misconfiguration-Sicherheit)", () => {
    const r = checkDailyUsage("", DAILY_SCAN_LIMIT);
    expect(r.allowed).toBe(true);
    const r2 = checkDailyUsage("some_key", 0);
    expect(r2.allowed).toBe(true);
  });
});

// ---------------------------------------------------------------------------
// 2. Record-Semantik (recordUsage)
// ---------------------------------------------------------------------------

describe("recordUsage — Zähler & Fensterstart", () => {
  test("R-1: erhöht den Zähler, used/remaining konsistent", () => {
    const key = nextKey();
    const r1 = recordUsage(key, 1);
    expect(r1.used).toBe(1);
    expect(r1.remaining).toBe(DAILY_SCAN_LIMIT - 1);
    const r2 = recordUsage(key, 5);
    expect(r2.used).toBe(6);
    expect(r2.remaining).toBe(DAILY_SCAN_LIMIT - 6);
  });

  test("R-2: remaining wird bei 0 geklemmt", () => {
    const key = nextKey();
    const r = recordUsage(key, DAILY_SCAN_LIMIT + 50);
    expect(r.used).toBe(DAILY_SCAN_LIMIT + 50);
    expect(r.remaining).toBe(0);
  });

  test("R-3: Multi-Unit-Record zählt Seiten (20-Seiten-PDF = 20 Einheiten)", () => {
    const key = nextKey();
    const r = recordUsage(key, 20);
    expect(r.used).toBe(20);
    expect(checkDailyUsage(key, DAILY_SCAN_LIMIT).used).toBe(20);
  });

  test("R-4: ungültige/negative/0 Einheiten werden defensiv zu 1 normalisiert", () => {
    const key = nextKey();
    expect(recordUsage(key, 2.5).used).toBe(3); // aufgerundet
    expect(recordUsage(key, -3).used).toBe(4); // negativ → 1
    expect(recordUsage(key, 0).used).toBe(5);
    expect(recordUsage(key, NaN).used).toBe(6);
  });

  test("R-5: erster Record startet das 24h-Fenster (resetsAt = now + 24h)", () => {
    const key = nextKey();
    const t0 = new Date("2026-03-01T12:00:00.000Z");
    recordUsage(key, 1, t0);
    const check = checkDailyUsage(key, DAILY_SCAN_LIMIT, new Date(t0.getTime() + 1000));
    expect(check.resetsAt).toBe(t0.getTime() + DAY);
  });
});

// ---------------------------------------------------------------------------
// 3. Fenster-Reset (injizierbares `now`)
// ---------------------------------------------------------------------------

describe("Fenster-Reset über injiziertes `now`", () => {
  test("W-1: nach 24h+1ms ist das Limit wieder frei", () => {
    const key = nextKey();
    const day1 = new Date("2026-01-01T00:00:00.000Z");
    for (let i = 0; i < DAILY_SCAN_LIMIT; i++) {
      recordUsage(key, 1, day1);
    }
    // Am Tag 1 aufgebraucht:
    expect(checkDailyUsage(key, DAILY_SCAN_LIMIT, day1).allowed).toBe(false);
    // Kurz vor Ablauf weiterhin blockiert:
    const almostDay2 = new Date(day1.getTime() + DAY - 1000);
    expect(checkDailyUsage(key, DAILY_SCAN_LIMIT, almostDay2).allowed).toBe(false);
    // 24h + 1ms später: frisches Fenster:
    const day2 = new Date(day1.getTime() + DAY + 1);
    const fresh = checkDailyUsage(key, DAILY_SCAN_LIMIT, day2);
    expect(fresh.allowed).toBe(true);
    expect(fresh.used).toBe(0);
    expect(fresh.remaining).toBe(DAILY_SCAN_LIMIT);
    // Record im neuen Fenster startet es neu:
    const rec = recordUsage(key, 3, day2);
    expect(rec.used).toBe(3);
    expect(rec.remaining).toBe(DAILY_SCAN_LIMIT - 3);
  });

  test("W-2: checkDailyUsage mutiert nicht — mehrere Reads liefern gleiche Werte", () => {
    const key = nextKey();
    const t = new Date("2026-02-02T08:00:00.000Z");
    recordUsage(key, 4, t);
    const a = checkDailyUsage(key, DAILY_SCAN_LIMIT, t);
    const b = checkDailyUsage(key, DAILY_SCAN_LIMIT, t);
    expect(b).toEqual(a);
    expect(a.used).toBe(4);
  });
});

// ---------------------------------------------------------------------------
// 4. Key-Isolation & Key-Format
// ---------------------------------------------------------------------------

describe("Key-Isolation & Key-Format", () => {
  test("K-1: User- und Guest-Key sind unabhängige Fenster", () => {
    const userKey = usageKeyForUser("user-42");
    const guestKey = usageKeyForGuest("guest_bucket_1");
    for (let i = 0; i < DAILY_SCAN_LIMIT; i++) {
      recordUsage(userKey, 1);
    }
    expect(checkDailyUsage(userKey, DAILY_SCAN_LIMIT).allowed).toBe(false);
    // Guest unberührt:
    expect(checkDailyUsage(guestKey, DAILY_GUEST_SCAN_LIMIT).allowed).toBe(true);
    expect(checkDailyUsage(guestKey, DAILY_GUEST_SCAN_LIMIT).used).toBe(0);
    // Und umgekehrt:
    for (let i = 0; i < DAILY_GUEST_SCAN_LIMIT; i++) {
      recordUsage(guestKey, 1);
    }
    expect(checkDailyUsage(guestKey, DAILY_GUEST_SCAN_LIMIT).allowed).toBe(false);
    expect(checkDailyUsage(userKey, DAILY_SCAN_LIMIT).used).toBe(DAILY_SCAN_LIMIT);
  });

  test("K-2: usageKeyForUser/usageKeyForGuest liefern die dokumentierten Formate", () => {
    expect(usageKeyForUser("u_42")).toBe("ai:user:u_42");
    expect(usageKeyForGuest("guest_abc")).toBe("ai:guest:guest_abc");
    // Kein Prefix-Collision-Risiko zwischen beiden Räumen:
    expect(usageKeyForUser("guest_x")).toBe("ai:user:guest_x");
    expect(usageKeyForGuest("u_1")).toBe("ai:guest:u_1");
  });
});

// ---------------------------------------------------------------------------
// 5. Overshoot-Caveat (Seiten = AI-Calls) & Konfiguration
// ---------------------------------------------------------------------------

describe("Overshoot & Konfiguration", () => {
  test("O-1: Pre-Check erlaubt, Multi-Seiten-Record sprengt das Budget — dokumentierter Overshoot", () => {
    const key = nextKey();
    for (let i = 0; i < 5; i++) {
      recordUsage(key, 1);
    }
    // Pre-Check vor dem Upload: noch 25 frei → erlaubt.
    expect(checkDailyUsage(key, DAILY_SCAN_LIMIT).allowed).toBe(true);
    // 10-Seiten-PDF wird korrekt gebucht:
    expect(recordUsage(key, 10).used).toBe(15);
    // 20-Seiten-PDF: Pre-Check hätte erlaubt (15 frei), Record überschreitet das
    // Budget — das ist der dokumentierte, durch Konkurrenz begrenzte Overshoot.
    const overshoot = recordUsage(key, 20);
    expect(overshoot.used).toBe(35);
    expect(overshoot.remaining).toBe(0);
    expect(checkDailyUsage(key, DAILY_SCAN_LIMIT).allowed).toBe(false);
  });

  test("KONF-1: Limits sind positiv; Guest < Free < Pro; HOURLY dokumentiert", () => {
    expect(DAILY_SCAN_LIMIT).toBe(30);
    expect(DAILY_GUEST_SCAN_LIMIT).toBe(10);
    expect(DAILY_PRO_SCAN_LIMIT).toBe(150);
    expect(DAILY_GUEST_SCAN_LIMIT).toBeLessThan(DAILY_SCAN_LIMIT);
    expect(DAILY_SCAN_LIMIT).toBeLessThan(DAILY_PRO_SCAN_LIMIT);
    expect(HOURLY_SCAN_LIMIT).toBeGreaterThanOrEqual(DAILY_SCAN_LIMIT);
  });

  test("KONF-2: dailyScanLimitFor — Free hat kein Daily-Cap, Pro hat 150", () => {
    expect(dailyScanLimitFor({ isPro: false })).toBeNull();
    expect(dailyScanLimitFor({ isPro: true })).toBe(DAILY_PRO_SCAN_LIMIT);
  });
});

if (typeof require !== "undefined" && require.main === module) {
  runAllTests()
    .then((ok) => process.exit(ok ? 0 : 1))
    .catch((err) => {
      console.error("Fatal runner crash:", err);
      process.exit(1);
    });
}
