import { beforeEach, describe, expect, it, vi } from "vitest";
import {
  ADDRESS_CANDIDATES_CACHE_TTL_SECONDS,
  L1_TTL_SECONDS,
  L2_TTL_BASE_SECONDS,
  L2_TTL_JITTER_MAX_SECONDS,
  L3_TTL_SECONDS,
  LOCK_TTL_SECONDS,
} from "@/lib/constants/cache";

type RedisMock = {
  store: Map<string, { value: string; expireAt?: number }>;
  get: ReturnType<typeof vi.fn>;
  set: ReturnType<typeof vi.fn>;
  del: ReturnType<typeof vi.fn>;
  exists: ReturnType<typeof vi.fn>;
  incr: ReturnType<typeof vi.fn>;
  expire: ReturnType<typeof vi.fn>;
};

function createRedisMock(): RedisMock {
  const store = new Map<string, { value: string; expireAt?: number }>();

  const isExpired = (key: string): boolean => {
    const entry = store.get(key);
    if (!entry?.expireAt) {
      return false;
    }
    return Date.now() > entry.expireAt;
  };

  return {
    store,
    get: vi.fn(async (key: string) => {
      if (isExpired(key)) {
        store.delete(key);
        return null;
      }
      return store.get(key)?.value ?? null;
    }),
    set: vi.fn(
      async (
        key: string,
        value: string,
        ...args: (string | number)[]
      ): Promise<string | null> => {
        const nx = args.includes("NX");
        if (nx && store.has(key) && !isExpired(key)) {
          return null;
        }

        let ttlSeconds: number | undefined;
        const exIndex = args.indexOf("EX");
        if (exIndex >= 0) {
          ttlSeconds = Number(args[exIndex + 1]);
        }

        store.set(key, {
          value,
          expireAt: ttlSeconds ? Date.now() + ttlSeconds * 1000 : undefined,
        });
        return "OK";
      },
    ),
    del: vi.fn(async (key: string) => {
      store.delete(key);
      return 1;
    }),
    exists: vi.fn(async (key: string) => {
      if (isExpired(key)) {
        store.delete(key);
        return 0;
      }
      return store.has(key) ? 1 : 0;
    }),
    incr: vi.fn(async (key: string) => {
      const current = store.get(key);
      const next = current ? Number(current.value) + 1 : 1;
      store.set(key, { value: String(next) });
      return next;
    }),
    expire: vi.fn(async (key: string, seconds: number) => {
      const entry = store.get(key);
      if (!entry) {
        return 0;
      }
      entry.expireAt = Date.now() + seconds * 1000;
      return 1;
    }),
  };
}

let redisMock: RedisMock;

vi.mock("@/lib/redis", () => ({
  getRedis: () => redisMock,
}));

describe("modules/cache/redis-cache", () => {
  beforeEach(async () => {
    redisMock = createRedisMock();
    vi.resetModules();
  });

  async function loadCache() {
    return import("@/modules/cache/redis-cache");
  }

  it("L1 setL1 SETNX 不覆盖 + TTL 86400", async () => {
    const cache = await loadCache();
    const payload = { quote_id: "Q1", status: "done" };

    expect(await cache.setL1("req-1", payload)).toBe(true);
    expect(await cache.setL1("req-1", { quote_id: "Q2" })).toBe(false);
    expect(await cache.getL1("req-1")).toEqual(payload);

    const [, , , ttl] = redisMock.set.mock.calls[0];
    expect(ttl).toBe(L1_TTL_SECONDS);
  });

  it("L2 TTL 在 180~210s 之间", async () => {
    const cache = await loadCache();
    const ttls = new Set<number>();

    for (let i = 0; i < 20; i++) {
      redisMock.store.clear();
      const ttl = await cache.setL2("hash-1", { quotes: [] });
      ttls.add(ttl);
    }

    for (const ttl of ttls) {
      expect(ttl).toBeGreaterThanOrEqual(L2_TTL_BASE_SECONDS);
      expect(ttl).toBeLessThanOrEqual(
        L2_TTL_BASE_SECONDS + L2_TTL_JITTER_MAX_SECONDS,
      );
    }
  });

  it("L3 TTL 1800s", async () => {
    const cache = await loadCache();
    await cache.setL3("hash-1", { quotes: [] });

    const [, , , ttl] = redisMock.set.mock.calls[0];
    expect(ttl).toBe(L3_TTL_SECONDS);
  });

  it("acquireLock SETNX TTL 5s", async () => {
    const cache = await loadCache();

    expect(await cache.acquireLock("hash-1")).toBe(true);
    expect(await cache.acquireLock("hash-1")).toBe(false);

    const [, , , ttl] = redisMock.set.mock.calls[0];
    expect(ttl).toBe(LOCK_TTL_SECONDS);
  });

  it("addr-cand 缓存 TTL 120s", async () => {
    const cache = await loadCache();
    await cache.setAddressCandidatesCache("hash-addr", {
      pickup_candidates: [],
      delivery_candidates: [],
    });

    const [key, , , ttl] = redisMock.set.mock.calls[0];
    expect(key).toBe("addr-cand:hash-addr");
    expect(ttl).toBe(ADDRESS_CANDIDATES_CACHE_TTL_SECONDS);
  });
});

describe("modules/cache/rate-limiter", () => {
  beforeEach(async () => {
    redisMock = createRedisMock();
    vi.resetModules();
  });

  it("60 次通过，第 61 次 RATE_LIMITED", async () => {
    const { checkRateLimit } = await import("@/modules/cache/rate-limiter");

    for (let i = 0; i < 60; i++) {
      expect(await checkRateLimit("CUST_001")).toEqual({ allowed: true });
    }

    expect(await checkRateLimit("CUST_001")).toEqual({
      allowed: false,
      errorCode: "RATE_LIMITED",
    });
  });
});

describe("modules/cache/ip-rate-limiter", () => {
  beforeEach(async () => {
    redisMock = createRedisMock();
    vi.resetModules();
  });

  it("超 1000 次封禁 IP", async () => {
    const { checkIpRateLimit } = await import(
      "@/modules/cache/ip-rate-limiter"
    );

    for (let i = 0; i < 1000; i++) {
      expect(await checkIpRateLimit("1.2.3.4")).toEqual({ allowed: true });
    }

    const blocked = await checkIpRateLimit("1.2.3.4");
    expect(blocked).toEqual({
      allowed: false,
      errorCode: "RATE_LIMITED",
      banned: true,
    });

    expect(await checkIpRateLimit("1.2.3.4")).toEqual({
      allowed: false,
      errorCode: "RATE_LIMITED",
      banned: true,
    });
  });
});

describe("modules/cache/circuit-breaker", () => {
  beforeEach(async () => {
    redisMock = createRedisMock();
    vi.resetModules();
  });

  it("连续 3 次失败熔断", async () => {
    const breaker = await import("@/modules/cache/circuit-breaker");

    expect(await breaker.isCircuitOpen()).toBe(false);
    await breaker.recordRpaFailure();
    await breaker.recordRpaFailure();
    expect(await breaker.isCircuitOpen()).toBe(false);
    await breaker.recordRpaFailure();
    expect(await breaker.isCircuitOpen()).toBe(true);
  });

  it("closeCircuit 清除熔断状态", async () => {
    const breaker = await import("@/modules/cache/circuit-breaker");

    await breaker.openCircuit();
    expect(await breaker.isCircuitOpen()).toBe(true);
    await breaker.closeCircuit();
    expect(await breaker.isCircuitOpen()).toBe(false);
  });
});
