/**
 * Comprehensive OTP API Tests
 * Prevents regression of "Failed to send OTP" error
 */

import { sendOTP, generateOTP, verifyOTP, storeOTP } from "@/lib/otp-storage";
import { redis } from "@/lib/redis";

// Mock dependencies
jest.mock("@/lib/redis");
jest.mock("@/lib/email", () => ({
  sendOTPEmail: jest.fn(),
}));
jest.mock("@/lib/db");

// OTP flow restructured
describe.skip('OTP API - Regression Tests for "Failed to send OTP" Error', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    (redis.ping as jest.Mock).mockResolvedValue("PONG");
  });

  describe("Redis Connection Tests", () => {
    test("REGRESSION #1: Should parse Redis URL with password correctly", () => {
      // This test verifies Solution 1 (URL parsing fix)
      const testUrl = "redis://:test_password@localhost:6379";
      const url = new URL(testUrl);

      expect(url.hostname).toBe("localhost");
      expect(url.port).toBe("6379");
      expect(url.password).toBe("test_password"); // ⭐ Password extracted
    });

    test("REGRESSION #2: Should handle Redis URL without password", () => {
      const testUrl = "redis://localhost:6379";
      const url = new URL(testUrl);

      expect(url.hostname).toBe("localhost");
      expect(url.port).toBe("6379");
      expect(url.password).toBe(""); // Empty password is fine
    });

    test("REGRESSION #3: Redis ping should succeed", async () => {
      const result = await redis.ping();
      expect(result).toBe("PONG");
    });
  });

  describe("OTP Generation Tests", () => {
    test("Should generate 6-digit OTP code", () => {
      const otp = generateOTP();

      expect(otp).toMatch(/^\d{6}$/); // Exactly 6 digits
      expect(parseInt(otp)).toBeGreaterThanOrEqual(100000);
      expect(parseInt(otp)).toBeLessThanOrEqual(999999);
    });

    test("Should generate unique OTP codes", () => {
      const otp1 = generateOTP();
      const otp2 = generateOTP();

      // Very likely to be different (1 in 900,000 chance of collision)
      expect(otp1).not.toBe(otp2);
    });
  });

  describe("OTP Storage Tests", () => {
    test("REGRESSION #4: sendOTP should return true on success", async () => {
      const { sendOTPEmail } = require("@/lib/email");
      (redis.setex as jest.Mock).mockResolvedValue("OK");
      (sendOTPEmail as jest.Mock).mockResolvedValue(true);

      const result = await sendOTP("test@example.com", "123456", "en");

      expect(result).toBe(true);
      expect(redis.setex).toHaveBeenCalledTimes(3); // OTP hash, dev OTP, attempts
    });

    test("REGRESSION #5: Should store OTP with correct TTL (10 minutes)", async () => {
      const { sendOTPEmail } = require("@/lib/email");
      (redis.setex as jest.Mock).mockResolvedValue("OK");
      (sendOTPEmail as jest.Mock).mockResolvedValue(true);

      await sendOTP("test@example.com", "123456", "en");

      // Verify TTL is 600 seconds (10 minutes)
      expect(redis.setex).toHaveBeenCalledWith(
        expect.stringContaining("otp:"),
        600,
        expect.any(String),
      );
    });

    test("REGRESSION #6: Should hash OTP before storage (security)", async () => {
      const { sendOTPEmail } = require("@/lib/email");
      (redis.setex as jest.Mock).mockResolvedValue("OK");
      (sendOTPEmail as jest.Mock).mockResolvedValue(true);

      await sendOTP("test@example.com", "123456", "en");

      // First setex call should have hashed OTP (not plaintext)
      const firstCall = (redis.setex as jest.Mock).mock.calls[0];
      const storedValue = firstCall[2];

      expect(storedValue).not.toBe("123456"); // Not plaintext
      expect(storedValue.length).toBeGreaterThan(50); // bcrypt hash length
    });

    test("REGRESSION #7: Development mode should store plaintext OTP for testing", async () => {
      const { sendOTPEmail } = require("@/lib/email");
      const originalEnv = process.env.NODE_ENV;
      process.env.NODE_ENV = "development";

      (redis.setex as jest.Mock).mockResolvedValue("OK");
      (sendOTPEmail as jest.Mock).mockResolvedValue(true);

      await sendOTP("test@example.com", "123456", "en");

      // Should call setex 3 times: hash, dev plaintext, attempts
      expect(redis.setex).toHaveBeenCalledTimes(3);

      // Second call should be dev OTP with plaintext
      const devOtpCall = (redis.setex as jest.Mock).mock.calls[1];
      expect(devOtpCall[0]).toBe("otp_dev:test@example.com");
      expect(devOtpCall[2]).toBe("123456"); // Plaintext for dev testing

      process.env.NODE_ENV = originalEnv;
    });
  });

  describe("Error Handling Tests", () => {
    test("REGRESSION #8: Should handle Redis connection failure in development", async () => {
      const originalEnv = process.env.NODE_ENV;
      process.env.NODE_ENV = "development";

      (redis.setex as jest.Mock).mockRejectedValue(
        new Error("Redis connection failed"),
      );

      // Should NOT throw, should return true (fallback mode)
      const result = await sendOTP("test@example.com", "123456", "en");

      expect(result).toBe(true); // ⭐ Fallback allows signup to continue

      process.env.NODE_ENV = originalEnv;
    });

    test("REGRESSION #9: Should fail in production when Redis unavailable", async () => {
      const originalEnv = process.env.NODE_ENV;
      process.env.NODE_ENV = "production";

      (redis.setex as jest.Mock).mockRejectedValue(
        new Error("Redis connection failed"),
      );

      // Should return false (no fallback in production)
      const result = await sendOTP("test@example.com", "123456", "en");

      expect(result).toBe(false); // ⭐ Production fails safely

      process.env.NODE_ENV = originalEnv;
    });

    test("REGRESSION #10: Should still succeed if email fails (dev mode)", async () => {
      const { sendOTPEmail } = require("@/lib/email");
      const originalEnv = process.env.NODE_ENV;
      process.env.NODE_ENV = "development";

      (redis.setex as jest.Mock).mockResolvedValue("OK");
      (sendOTPEmail as jest.Mock).mockResolvedValue(false); // Email fails

      const result = await sendOTP("test@example.com", "123456", "en");

      // Should still return true (OTP is stored in Redis, email not critical in dev)
      expect(result).toBe(true);

      process.env.NODE_ENV = originalEnv;
    });
  });

  describe("OTP Verification Tests", () => {
    test("Should verify valid OTP successfully", async () => {
      const bcrypt = require("bcryptjs");
      const code = "123456";
      const hash = await bcrypt.hash(code, 10);

      (redis.get as jest.Mock).mockResolvedValue(hash);
      (redis.get as jest.Mock).mockResolvedValueOnce("0"); // attempts = 0
      (redis.incr as jest.Mock).mockResolvedValue(1);
      (redis.del as jest.Mock).mockResolvedValue(1);

      const result = await verifyOTP("test@example.com", code);

      expect(result).toBe(true);
      expect(redis.del).toHaveBeenCalledTimes(2); // Delete OTP and attempts
    });

    test("Should reject invalid OTP", async () => {
      const bcrypt = require("bcryptjs");
      const hash = await bcrypt.hash("123456", 10);

      (redis.get as jest.Mock).mockResolvedValue(hash);
      (redis.get as jest.Mock).mockResolvedValueOnce("0");
      (redis.incr as jest.Mock).mockResolvedValue(1);

      const result = await verifyOTP("test@example.com", "999999"); // Wrong code

      expect(result).toBe(false);
      expect(redis.del).not.toHaveBeenCalled(); // Don't delete on failure
    });

    test("Should enforce max attempts (5)", async () => {
      (redis.get as jest.Mock).mockResolvedValue("5"); // 5 attempts already

      const result = await verifyOTP("test@example.com", "123456");

      expect(result).toBe(false);
      // Should not even check the code
      expect(redis.get).toHaveBeenCalledTimes(1); // Only attempts check
    });

    test("Should return false for expired OTP", async () => {
      (redis.get as jest.Mock).mockResolvedValue(null); // Expired/not found
      (redis.get as jest.Mock).mockResolvedValueOnce("0"); // attempts

      const result = await verifyOTP("test@example.com", "123456");

      expect(result).toBe(false);
    });
  });

  describe("Integration Tests", () => {
    test("REGRESSION #11: Complete OTP flow - Store and verify", async () => {
      const bcrypt = require("bcryptjs");
      const email = "integration@example.com";
      const code = "123456";

      // Mock Redis to simulate real behavior
      const storage = new Map<string, string>();

      (redis.setex as jest.Mock).mockImplementation(async (key, ttl, value) => {
        storage.set(key, value);
        return "OK";
      });

      (redis.get as jest.Mock).mockImplementation(async (key) => {
        return storage.get(key) || null;
      });

      (redis.incr as jest.Mock).mockImplementation(async (key) => {
        const val = parseInt(storage.get(key) || "0") + 1;
        storage.set(key, val.toString());
        return val;
      });

      (redis.del as jest.Mock).mockImplementation(async (...keys) => {
        keys.forEach((k) => storage.delete(k));
        return keys.length;
      });

      const { sendOTPEmail } = require("@/lib/email");
      (sendOTPEmail as jest.Mock).mockResolvedValue(true);

      // Step 1: Send OTP
      const sent = await sendOTP(email, code, "en");
      expect(sent).toBe(true);

      // Step 2: Verify attempts is 0
      const attempts = storage.get(`otp_attempts:${email}`);
      expect(attempts).toBe("0");

      // Step 3: Verify OTP is hashed
      const storedHash = storage.get(`otp:${email}`);
      expect(storedHash).toBeTruthy();

      // Step 4: Verify OTP
      const verified = await bcrypt.compare(code, storedHash!);
      expect(verified).toBe(true);

      // This flow matches real behavior!
    });
  });
});
