/**
 * @jest-environment node
 */
import { Cache, clearAllCaches, getAllCacheStats } from "../cache";

describe("Cache", () => {
  let cache: Cache<string>;

  beforeEach(() => {
    cache = new Cache<string>(1000); // 1 second TTL
  });

  describe("set and get", () => {
    it("should store and retrieve a value", () => {
      cache.set("key1", "value1");
      const value = cache.get("key1");

      expect(value).toBe("value1");
    });

    it("should return null for non-existent key", () => {
      const value = cache.get("nonexistent");
      expect(value).toBeNull();
    });

    it("should return null for expired key", async () => {
      cache.set("key1", "value1", 50); // 50ms TTL

      // Wait for expiration
      await new Promise((resolve) => setTimeout(resolve, 100));

      const value = cache.get("key1");
      expect(value).toBeNull();
    });

    it("should use default TTL when not specified", () => {
      const shortCache = new Cache<string>(100); // 100ms default
      shortCache.set("key1", "value1");

      const value = shortCache.get("key1");
      expect(value).toBe("value1");
    });

    it("should override default TTL when specified", async () => {
      const longCache = new Cache<string>(5000); // 5s default
      longCache.set("key1", "value1", 50); // 50ms override

      await new Promise((resolve) => setTimeout(resolve, 100));

      const value = longCache.get("key1");
      expect(value).toBeNull();
    });
  });

  describe("getOrSet", () => {
    it("should return cached value if exists", async () => {
      let callCount = 0;
      const fn = async () => {
        callCount++;
        return "computed-value";
      };

      const value1 = await cache.getOrSet("key1", fn);
      const value2 = await cache.getOrSet("key1", fn);

      expect(value1).toBe("computed-value");
      expect(value2).toBe("computed-value");
      expect(callCount).toBe(1); // Function should only be called once
    });

    it("should compute value if not cached", async () => {
      const value = await cache.getOrSet("key1", async () => {
        return "new-value";
      });

      expect(value).toBe("new-value");
    });

    it("should recompute value after expiration", async () => {
      let callCount = 0;
      const fn = async () => {
        callCount++;
        return `value-${callCount}`;
      };

      const value1 = await cache.getOrSet("key1", fn, 50); // 50ms TTL
      expect(value1).toBe("value-1");

      await new Promise((resolve) => setTimeout(resolve, 100));

      const value2 = await cache.getOrSet("key1", fn, 50);
      expect(value2).toBe("value-2");
      expect(callCount).toBe(2);
    });
  });

  describe("delete", () => {
    it("should remove a key from cache", () => {
      cache.set("key1", "value1");
      expect(cache.get("key1")).toBe("value1");

      const deleted = cache.delete("key1");
      expect(deleted).toBe(true);
      expect(cache.get("key1")).toBeNull();
    });

    it("should return false when deleting non-existent key", () => {
      const deleted = cache.delete("nonexistent");
      expect(deleted).toBe(false);
    });
  });

  describe("clear", () => {
    it("should remove all entries", () => {
      cache.set("key1", "value1");
      cache.set("key2", "value2");
      cache.set("key3", "value3");

      expect(cache.size()).toBe(3);

      cache.clear();

      expect(cache.size()).toBe(0);
      expect(cache.get("key1")).toBeNull();
      expect(cache.get("key2")).toBeNull();
      expect(cache.get("key3")).toBeNull();
    });

    it("should reset hit/miss statistics", () => {
      cache.set("key1", "value1");
      cache.get("key1"); // hit
      cache.get("nonexistent"); // miss

      let stats = cache.getStats();
      expect(stats.hits).toBe(1);
      expect(stats.misses).toBe(1);

      cache.clear();

      stats = cache.getStats();
      expect(stats.hits).toBe(0);
      expect(stats.misses).toBe(0);
    });
  });

  describe("getStats", () => {
    it("should track hits and misses", () => {
      cache.set("key1", "value1");

      cache.get("key1"); // hit
      cache.get("key1"); // hit
      cache.get("nonexistent"); // miss

      const stats = cache.getStats();

      expect(stats.hits).toBe(2);
      expect(stats.misses).toBe(1);
      expect(stats.hitRate).toBeCloseTo(2 / 3, 2);
      expect(stats.size).toBe(1);
    });

    it("should return 0 hit rate when no requests", () => {
      const stats = cache.getStats();
      expect(stats.hitRate).toBe(0);
      expect(stats.hits).toBe(0);
      expect(stats.misses).toBe(0);
    });

    it("should count expired entries as misses", async () => {
      cache.set("key1", "value1", 50);

      await new Promise((resolve) => setTimeout(resolve, 100));

      cache.get("key1"); // miss (expired)

      const stats = cache.getStats();
      expect(stats.misses).toBe(1);
      expect(stats.hits).toBe(0);
    });
  });

  describe("cleanup", () => {
    it("should remove expired entries", async () => {
      cache.set("key1", "value1", 50); // 50ms TTL
      cache.set("key2", "value2", 1000); // 1s TTL

      await new Promise((resolve) => setTimeout(resolve, 100));

      const removed = cache.cleanup();

      expect(removed).toBe(1);
      expect(cache.get("key1")).toBeNull();
      expect(cache.get("key2")).toBe("value2");
    });

    it("should return 0 when no expired entries", () => {
      cache.set("key1", "value1", 1000);
      const removed = cache.cleanup();

      expect(removed).toBe(0);
      expect(cache.size()).toBe(1);
    });
  });

  describe("has", () => {
    it("should return true for existing non-expired key", () => {
      cache.set("key1", "value1");
      expect(cache.has("key1")).toBe(true);
    });

    it("should return false for non-existent key", () => {
      expect(cache.has("nonexistent")).toBe(false);
    });

    it("should return false for expired key", async () => {
      cache.set("key1", "value1", 50);

      await new Promise((resolve) => setTimeout(resolve, 100));

      expect(cache.has("key1")).toBe(false);
    });
  });

  describe("keys", () => {
    it("should return all cache keys", () => {
      cache.set("key1", "value1");
      cache.set("key2", "value2");
      cache.set("key3", "value3");

      const keys = cache.keys();

      expect(keys).toHaveLength(3);
      expect(keys).toContain("key1");
      expect(keys).toContain("key2");
      expect(keys).toContain("key3");
    });

    it("should return empty array for empty cache", () => {
      const keys = cache.keys();
      expect(keys).toEqual([]);
    });
  });

  describe("size", () => {
    it("should return number of entries", () => {
      expect(cache.size()).toBe(0);

      cache.set("key1", "value1");
      expect(cache.size()).toBe(1);

      cache.set("key2", "value2");
      expect(cache.size()).toBe(2);

      cache.delete("key1");
      expect(cache.size()).toBe(1);
    });
  });

  describe("Complex data types", () => {
    it("should cache objects", () => {
      const objCache = new Cache<{ name: string; age: number }>();
      const obj = { name: "John", age: 30 };

      objCache.set("user1", obj);
      const cached = objCache.get("user1");

      expect(cached).toEqual(obj);
    });

    it("should cache arrays", () => {
      const arrayCache = new Cache<number[]>();
      const arr = [1, 2, 3, 4, 5];

      arrayCache.set("numbers", arr);
      const cached = arrayCache.get("numbers");

      expect(cached).toEqual(arr);
    });
  });

  describe("Global cache utilities", () => {
    it("clearAllCaches should clear all global caches", () => {
      clearAllCaches();
      // Should not throw
      expect(true).toBe(true);
    });

    it("getAllCacheStats should return stats for all global caches", () => {
      const stats = getAllCacheStats();

      expect(stats).toHaveProperty("users");
      expect(stats).toHaveProperty("fitnessGoals");
      expect(stats).toHaveProperty("recommendations");
      expect(stats).toHaveProperty("dashboardStats");

      expect(stats.users).toHaveProperty("hits");
      expect(stats.users).toHaveProperty("misses");
      expect(stats.users).toHaveProperty("size");
      expect(stats.users).toHaveProperty("hitRate");
    });
  });
});
