import { describe, it, expect, beforeEach, vi, afterEach, Mock } from "vitest";
import { ThrottlerValkeyStorageService } from "./throttler-storage.service";

// Create a mock Redis class
const createMockRedis = (
  options: {
    shouldFailConnect?: boolean;
    error?: Error;
  } = {}
): Record<string, Mock> => ({
  connect: vi.fn().mockImplementation(() => {
    if (options.shouldFailConnect) {
      return Promise.reject(options.error ?? new Error("Connection refused"));
    }
    return Promise.resolve();
  }),
  ping: vi.fn().mockResolvedValue("PONG"),
  quit: vi.fn().mockResolvedValue("OK"),
  multi: vi.fn().mockReturnThis(),
  incr: vi.fn().mockReturnThis(),
  pexpire: vi.fn().mockReturnThis(),
  exec: vi.fn().mockResolvedValue([
    [null, 1],
    [null, 1],
  ]),
  get: vi.fn().mockResolvedValue("5"),
});

// Mock ioredis module
vi.mock("ioredis", () => {
  return {
    default: vi.fn().mockImplementation(() => createMockRedis({ shouldFailConnect: true })),
  };
});

describe("ThrottlerValkeyStorageService", () => {
  let service: ThrottlerValkeyStorageService;
  let loggerErrorSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(() => {
    vi.clearAllMocks();
    service = new ThrottlerValkeyStorageService();

    // Spy on logger methods - access the private logger
    const logger = (
      service as unknown as { logger: { error: () => void; log: () => void; warn: () => void } }
    ).logger;
    loggerErrorSpy = vi.spyOn(logger, "error").mockImplementation(() => undefined);
    vi.spyOn(logger, "log").mockImplementation(() => undefined);
    vi.spyOn(logger, "warn").mockImplementation(() => undefined);
  });

  afterEach(() => {
    vi.clearAllMocks();
  });

  describe("initialization and fallback behavior", () => {
    it("should start in fallback mode before initialization", () => {
      // Before onModuleInit is called, useRedis is false by default
      expect(service.isUsingFallback()).toBe(true);
    });

    it("should log ERROR when Redis connection fails", async () => {
      const newService = new ThrottlerValkeyStorageService();
      const newLogger = (
        newService as unknown as { logger: { error: () => void; log: () => void } }
      ).logger;
      const newErrorSpy = vi.spyOn(newLogger, "error").mockImplementation(() => undefined);
      vi.spyOn(newLogger, "log").mockImplementation(() => undefined);

      await newService.onModuleInit();

      // Verify ERROR was logged (not WARN)
      expect(newErrorSpy).toHaveBeenCalledWith(
        expect.stringContaining("Failed to connect to Valkey for rate limiting")
      );
      expect(newErrorSpy).toHaveBeenCalledWith(
        expect.stringContaining("DEGRADED MODE: Falling back to in-memory rate limiting storage")
      );
    });

    it("should log message indicating rate limits will not be shared", async () => {
      const newService = new ThrottlerValkeyStorageService();
      const newLogger = (
        newService as unknown as { logger: { error: () => void; log: () => void } }
      ).logger;
      const newErrorSpy = vi.spyOn(newLogger, "error").mockImplementation(() => undefined);
      vi.spyOn(newLogger, "log").mockImplementation(() => undefined);

      await newService.onModuleInit();

      expect(newErrorSpy).toHaveBeenCalledWith(
        expect.stringContaining("Rate limits will not be shared across API instances")
      );
    });

    it("should be in fallback mode when Redis connection fails", async () => {
      const newService = new ThrottlerValkeyStorageService();
      const newLogger = (
        newService as unknown as { logger: { error: () => void; log: () => void } }
      ).logger;
      vi.spyOn(newLogger, "error").mockImplementation(() => undefined);
      vi.spyOn(newLogger, "log").mockImplementation(() => undefined);

      await newService.onModuleInit();

      expect(newService.isUsingFallback()).toBe(true);
    });
  });

  describe("isUsingFallback()", () => {
    it("should return true when in memory fallback mode", () => {
      // Default state is fallback mode
      expect(service.isUsingFallback()).toBe(true);
    });

    it("should return boolean type", () => {
      const result = service.isUsingFallback();
      expect(typeof result).toBe("boolean");
    });
  });

  describe("getHealthStatus()", () => {
    it("should return degraded status when in fallback mode", () => {
      // Default state is fallback mode
      const status = service.getHealthStatus();

      expect(status).toEqual({
        healthy: true,
        mode: "memory",
        degraded: true,
        message: expect.stringContaining("in-memory fallback"),
      });
    });

    it("should indicate degraded mode message includes lack of sharing", () => {
      const status = service.getHealthStatus();

      expect(status.message).toContain("not shared across instances");
    });

    it("should always report healthy even in degraded mode", () => {
      // In degraded mode, the service is still functional
      const status = service.getHealthStatus();
      expect(status.healthy).toBe(true);
    });

    it("should have correct structure for health checks", () => {
      const status = service.getHealthStatus();

      expect(status).toHaveProperty("healthy");
      expect(status).toHaveProperty("mode");
      expect(status).toHaveProperty("degraded");
      expect(status).toHaveProperty("message");
    });

    it("should report mode as memory when in fallback", () => {
      const status = service.getHealthStatus();
      expect(status.mode).toBe("memory");
    });

    it("should report degraded as true when in fallback", () => {
      const status = service.getHealthStatus();
      expect(status.degraded).toBe(true);
    });
  });

  describe("getHealthStatus() with Redis (unit test via internal state)", () => {
    it("should return non-degraded status when Redis is available", () => {
      // Manually set the internal state to simulate Redis being available
      // This tests the method logic without requiring actual Redis connection
      const testService = new ThrottlerValkeyStorageService();

      // Access private property for testing (this is acceptable for unit testing)
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      (testService as any).useRedis = true;

      const status = testService.getHealthStatus();

      expect(status).toEqual({
        healthy: true,
        mode: "redis",
        degraded: false,
        message: expect.stringContaining("Redis storage"),
      });
    });

    it("should report distributed mode message when Redis is available", () => {
      const testService = new ThrottlerValkeyStorageService();
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      (testService as any).useRedis = true;

      const status = testService.getHealthStatus();

      expect(status.message).toContain("distributed mode");
    });

    it("should report isUsingFallback as false when Redis is available", () => {
      const testService = new ThrottlerValkeyStorageService();
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      (testService as any).useRedis = true;

      expect(testService.isUsingFallback()).toBe(false);
    });
  });

  describe("in-memory fallback operations", () => {
    it("should increment correctly in fallback mode", async () => {
      const result = await service.increment("test-key", 60000, 10, 0, "default");

      expect(result.totalHits).toBe(1);
      expect(result.isBlocked).toBe(false);
    });

    it("should accumulate hits in fallback mode", async () => {
      await service.increment("test-key", 60000, 10, 0, "default");
      await service.increment("test-key", 60000, 10, 0, "default");
      const result = await service.increment("test-key", 60000, 10, 0, "default");

      expect(result.totalHits).toBe(3);
    });

    it("should return correct blocked status when limit exceeded", async () => {
      // Make 3 requests with limit of 2
      await service.increment("test-key", 60000, 2, 1000, "default");
      await service.increment("test-key", 60000, 2, 1000, "default");
      const result = await service.increment("test-key", 60000, 2, 1000, "default");

      expect(result.totalHits).toBe(3);
      expect(result.isBlocked).toBe(true);
      expect(result.timeToBlockExpire).toBe(1000);
    });

    it("should return 0 for get on non-existent key in fallback mode", async () => {
      const result = await service.get("non-existent-key");
      expect(result).toBe(0);
    });

    it("should return correct timeToExpire in response", async () => {
      const ttl = 30000;
      const result = await service.increment("test-key", ttl, 10, 0, "default");

      expect(result.timeToExpire).toBe(ttl);
    });

    it("should isolate different keys in fallback mode", async () => {
      await service.increment("key-1", 60000, 10, 0, "default");
      await service.increment("key-1", 60000, 10, 0, "default");
      const result1 = await service.increment("key-1", 60000, 10, 0, "default");

      const result2 = await service.increment("key-2", 60000, 10, 0, "default");

      expect(result1.totalHits).toBe(3);
      expect(result2.totalHits).toBe(1);
    });
  });
});
