import { describe, expect, it, vi } from "vitest";
import {
  RequestScopedCache,
  ClientMemoryCache,
  ServerLruCache,
  cacheKey,
} from "./cache.js";

/**
 * lru-cache captures the global `performance` object at module-load time.
 * `vi.useFakeTimers()` replaces `globalThis.performance` with a new object,
 * so the faked `performance.now()` never reaches lru-cache's internal `perf`.
 *
 * We work around this by patching `performance.now` in-place via `vi.hoisted`
 * (which runs before any imports) and controlling time manually.
 */
const mockNow = vi.hoisted(() => {
  let time = 1000;
  const original = performance.now.bind(performance);
  return {
    install: () => {
      time = 1000;
      performance.now = () => time;
    },
    advance: (ms: number) => {
      time += ms;
    },
    restore: () => {
      performance.now = original;
    },
  };
});

describe("cacheKey", () => {
  it("creates a deterministic key from endpoint, query, and locale", () => {
    const k1 = cacheKey("/flights", { date: "2025-01-15", dep: "SVO" }, "ru");
    const k2 = cacheKey("/flights", { date: "2025-01-15", dep: "SVO" }, "ru");
    expect(k1).toBe(k2);
  });

  it("produces different keys for different locales", () => {
    const k1 = cacheKey("/flights", { date: "2025-01-15" }, "ru");
    const k2 = cacheKey("/flights", { date: "2025-01-15" }, "en");
    expect(k1).not.toBe(k2);
  });

  it("produces different keys for different queries", () => {
    const k1 = cacheKey("/flights", { a: "1" }, "ru");
    const k2 = cacheKey("/flights", { b: "2" }, "ru");
    expect(k1).not.toBe(k2);
  });

  it("sorts query keys for stability", () => {
    const k1 = cacheKey("/flights", { b: "2", a: "1" }, "ru");
    const k2 = cacheKey("/flights", { a: "1", b: "2" }, "ru");
    expect(k1).toBe(k2);
  });
});

describe("RequestScopedCache", () => {
  it("deduplicates concurrent promises for the same key", async () => {
    const cache = new RequestScopedCache();
    let callCount = 0;
    const factory = () => {
      callCount++;
      return Promise.resolve("value");
    };

    const p1 = cache.get<string>("k") ?? (() => { const p = factory(); cache.set("k", p); return p; })();
    const p2 = cache.get<string>("k") ?? (() => { const p = factory(); cache.set("k", p); return p; })();

    expect(await p1).toBe("value");
    expect(await p2).toBe("value");
    expect(callCount).toBe(1);
  });

  it("returns undefined for unknown keys", () => {
    const cache = new RequestScopedCache();
    expect(cache.get("missing")).toBeUndefined();
  });
});

describe("ClientMemoryCache", () => {
  it("stores and retrieves values", () => {
    const cache = new ClientMemoryCache<string>({ max: 10, defaultTtlMs: 5000 });
    cache.set("k", "v");
    expect(cache.get("k")).toBe("v");
  });

  it("evicts after TTL expires", () => {
    mockNow.install();
    const cache = new ClientMemoryCache<string>({ max: 10, defaultTtlMs: 100 });
    cache.set("k", "v");
    expect(cache.get("k")).toBe("v");

    mockNow.advance(101);
    expect(cache.get("k")).toBeUndefined();
    mockNow.restore();
  });

  it("supports per-entry TTL override", () => {
    mockNow.install();
    const cache = new ClientMemoryCache<string>({ max: 10, defaultTtlMs: 5000 });
    cache.set("k", "v", 50);
    mockNow.advance(51);
    expect(cache.get("k")).toBeUndefined();
    mockNow.restore();
  });

  it("evicts LRU when max count reached", () => {
    const cache = new ClientMemoryCache<string>({ max: 2, defaultTtlMs: 60000 });
    cache.set("a", "1");
    cache.set("b", "2");
    cache.set("c", "3");
    expect(cache.get("a")).toBeUndefined();
    expect(cache.get("b")).toBe("2");
    expect(cache.get("c")).toBe("3");
  });

  it("tracks size", () => {
    const cache = new ClientMemoryCache<string>({ max: 10, defaultTtlMs: 5000 });
    expect(cache.size).toBe(0);
    cache.set("k", "v");
    expect(cache.size).toBe(1);
    cache.delete("k");
    expect(cache.size).toBe(0);
  });

  it("clear() removes all entries", () => {
    const cache = new ClientMemoryCache<string>({ max: 10, defaultTtlMs: 5000 });
    cache.set("a", "1");
    cache.set("b", "2");
    cache.clear();
    expect(cache.size).toBe(0);
  });
});

describe("ServerLruCache", () => {
  it("stores and retrieves values", () => {
    const cache = new ServerLruCache<string>({
      maxBytes: 1024,
      defaultTtlMs: 5000,
    });
    cache.set("k", "hello");
    expect(cache.get("k")).toBe("hello");
  });

  it("evicts when byte cap is exceeded", () => {
    const cache = new ServerLruCache<string>({
      maxBytes: 50,
      defaultTtlMs: 60000,
    });
    // Each string is ~5-10 bytes when JSON.stringify'd, but the key is also counted
    cache.set("a", "x".repeat(20));
    cache.set("b", "y".repeat(20));
    cache.set("c", "z".repeat(20));
    // At least one of the earlier entries should be evicted
    const remaining = [cache.get("a"), cache.get("b"), cache.get("c")].filter(Boolean);
    expect(remaining.length).toBeLessThan(3);
  });

  it("tracks calculatedSize", () => {
    const cache = new ServerLruCache<string>({
      maxBytes: 10000,
      defaultTtlMs: 5000,
    });
    expect(cache.calculatedSize).toBe(0);
    cache.set("k", "hello");
    expect(cache.calculatedSize).toBeGreaterThan(0);
  });

  it("evicts after TTL expires", () => {
    mockNow.install();
    const cache = new ServerLruCache<string>({
      maxBytes: 10000,
      defaultTtlMs: 100,
    });
    cache.set("k", "v");
    expect(cache.get("k")).toBe("v");
    mockNow.advance(101);
    expect(cache.get("k")).toBeUndefined();
    mockNow.restore();
  });

  it("supports custom sizeCalculation", () => {
    const cache = new ServerLruCache<{ data: string }>({
      maxBytes: 100,
      defaultTtlMs: 5000,
      sizeCalculation: (value) => value.data.length,
    });
    cache.set("k", { data: "x".repeat(50) });
    expect(cache.calculatedSize).toBe(50);
  });
});
