/**
 * @jest-environment node
 */

/**
 * Secure Tokens Test Suite
 *
 * Tests for the new JWT-based secure token implementation
 */

import { describe, it, expect, beforeEach, jest } from "@jest/globals";

// Mock environment variables
process.env.NEXTAUTH_SECRET = "test-secret-key-for-unit-tests";
(process.env as Record<string, string | undefined>).NODE_ENV = "test";

describe("Secure Tokens", () => {
  let generateSecureToken: any;
  let verifySecureToken: any;
  let generateResetToken: any;
  let verifyResetToken: any;

  beforeEach(async () => {
    // Dynamically import to ensure environment is set
    const module = await import("../../lib/secure-tokens");
    generateSecureToken = module.generateSecureToken;
    verifySecureToken = module.verifySecureToken;
    generateResetToken = module.generateResetToken;
    verifyResetToken = module.verifyResetToken;
  });

  describe("generateSecureToken", () => {
    it("should generate a valid JWT token", async () => {
      const payload = { userId: "123", email: "test@example.com" };
      const token = await generateSecureToken(payload, "15m");

      expect(token).toBeDefined();
      expect(typeof token).toBe("string");
      expect(token.split(".")).toHaveLength(3); // JWT has 3 parts
    });

    it("should include payload data in token", async () => {
      const payload = { userId: "123", email: "test@example.com" };
      const token = await generateSecureToken(payload, "15m");

      const verified = await verifySecureToken(token);
      expect(verified).toBeDefined();
      expect(verified?.userId).toBe("123");
      expect(verified?.email).toBe("test@example.com");
    });

    it("should create different tokens for same payload with delay", async () => {
      const payload = { userId: "123", email: "test@example.com" };
      const token1 = await generateSecureToken(payload, "15m");

      // Wait 1 second to ensure different iat timestamp
      await new Promise((resolve) => setTimeout(resolve, 1100));

      const token2 = await generateSecureToken(payload, "15m");

      // Tokens should be different (different iat timestamps)
      expect(token1).not.toBe(token2);
    }, 3000);
  });

  describe("verifySecureToken", () => {
    it("should verify valid token", async () => {
      const payload = { userId: "123", email: "test@example.com" };
      const token = await generateSecureToken(payload, "15m");

      const verified = await verifySecureToken(token);
      expect(verified).toBeDefined();
      expect(verified?.userId).toBe("123");
    });

    it("should reject invalid token", async () => {
      const invalidToken = "invalid.token.here";
      const verified = await verifySecureToken(invalidToken);
      expect(verified).toBeNull();
    });

    it("should reject tampered token", async () => {
      const payload = { userId: "123", email: "test@example.com" };
      const token = await generateSecureToken(payload, "15m");

      // Tamper with the token
      const parts = token.split(".");
      const tamperedPayload = Buffer.from(
        JSON.stringify({ userId: "999", email: "hacker@example.com" }),
      ).toString("base64url");
      const tamperedToken = `${parts[0]}.${tamperedPayload}.${parts[2]}`;

      const verified = await verifySecureToken(tamperedToken);
      expect(verified).toBeNull();
    });

    it("should reject token with wrong issuer", async () => {
      // Create a token with different issuer
      const { SignJWT } = await import("jose");
      const secret = new TextEncoder().encode("test-secret-key-for-unit-tests");

      const fakeToken = await new SignJWT({ userId: "123" })
        .setProtectedHeader({ alg: "HS256" })
        .setIssuedAt()
        .setExpirationTime("15m")
        .setIssuer("fake-issuer") // Wrong issuer
        .sign(secret);

      const verified = await verifySecureToken(fakeToken);
      expect(verified).toBeNull();
    });
  });

  describe("expiration handling", () => {
    it("should accept token that has not expired", async () => {
      const payload = { userId: "123" };
      const token = await generateSecureToken(payload, "1h");

      const verified = await verifySecureToken(token);
      expect(verified).toBeDefined();
    });

    it("should reject expired token", async () => {
      // Create an expired token (1 second expiration)
      const payload = { userId: "123" };
      const token = await generateSecureToken(payload, "1s");

      // Wait for token to expire
      await new Promise((resolve) => setTimeout(resolve, 1100));

      const verified = await verifySecureToken(token);
      expect(verified).toBeNull();
    }, 2000); // Increase test timeout
  });

  describe("password reset tokens", () => {
    let mockRedis: any;

    beforeEach(() => {
      // Mock Redis client
      const storage = new Map<string, string>();

      mockRedis = {
        set: jest.fn(async (key: string, value: string, options?: any) => {
          storage.set(key, value);
          return "OK";
        }),
        get: jest.fn(async (key: string) => {
          return storage.get(key) || null;
        }),
        del: jest.fn(async (key: string) => {
          const existed = storage.has(key);
          storage.delete(key);
          return existed ? 1 : 0;
        }),
      };
    });

    it.skip("should generate reset token with Redis tracking", async () => {
      const token = await generateResetToken(
        "user@example.com",
        "email",
        mockRedis,
      );

      expect(token).toBeDefined();
      expect(typeof token).toBe("string");
      expect(mockRedis.set).toHaveBeenCalled();
    });

    it.skip("should verify and consume reset token", async () => {
      const token = await generateResetToken(
        "user@example.com",
        "email",
        mockRedis,
      );

      const verified = await verifyResetToken(token, mockRedis);

      expect(verified).toBeDefined();
      expect(verified?.identifier).toBe("user@example.com");
      expect(verified?.type).toBe("email");
      expect(mockRedis.del).toHaveBeenCalled();
    });

    it("should reject reused reset token", async () => {
      const token = await generateResetToken(
        "user@example.com",
        "email",
        mockRedis,
      );

      // First verification should succeed
      const verified1 = await verifyResetToken(token, mockRedis);
      expect(verified1).toBeDefined();

      // Second verification should fail (token consumed)
      const verified2 = await verifyResetToken(token, mockRedis);
      expect(verified2).toBeNull();
    });

    it.skip("should reject reset token with wrong identifier in Redis", async () => {
      const token = await generateResetToken(
        "user@example.com",
        "email",
        mockRedis,
      );

      // Get the last call to set() and extract the key
      const setCallArgs =
        mockRedis.set.mock.calls[mockRedis.set.mock.calls.length - 1];
      const redisKey = setCallArgs[0];

      // Override Redis to return a different identifier
      mockRedis.get.mockImplementationOnce(async (key: string) => {
        if (key === redisKey) {
          return "different@example.com"; // Wrong identifier
        }
        return null;
      });

      const verified = await verifyResetToken(token, mockRedis);
      expect(verified).toBeNull();
    });
  });

  describe("production environment checks", () => {
    it("should throw error in production without secret", async () => {
      const originalEnv = process.env.NEXTAUTH_SECRET;
      const originalNodeEnv = process.env.NODE_ENV;

      try {
        delete process.env.NEXTAUTH_SECRET;
        (process.env as Record<string, string | undefined>).NODE_ENV =
          "production";

        // Clear module cache for this test only
        jest.resetModules();

        // Re-import to trigger error
        await expect(async () => {
          const module = await import("../../lib/secure-tokens");
          await module.generateSecureToken({ test: "data" });
        }).rejects.toThrow(/NEXTAUTH_SECRET is required in production/);
      } finally {
        // Restore environment
        process.env.NEXTAUTH_SECRET = originalEnv;
        (process.env as Record<string, string | undefined>).NODE_ENV =
          originalNodeEnv;

        // Reset modules again to restore clean state
        jest.resetModules();

        // Re-import the clean module for subsequent tests
        const cleanModule = await import("../../lib/secure-tokens");
        generateSecureToken = cleanModule.generateSecureToken;
        verifySecureToken = cleanModule.verifySecureToken;
        generateResetToken = cleanModule.generateResetToken;
        verifyResetToken = cleanModule.verifyResetToken;
      }
    });
  });

  describe("token format validation", () => {
    it("should generate JWT with correct structure", async () => {
      const token = await generateSecureToken({ userId: "123" }, "15m");
      const parts = token.split(".");

      expect(parts).toHaveLength(3);

      // Decode header
      const header = JSON.parse(Buffer.from(parts[0], "base64url").toString());
      expect(header.alg).toBe("HS256");

      // Decode payload
      const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
      expect(payload.userId).toBe("123");
      expect(payload.iat).toBeDefined();
      expect(payload.exp).toBeDefined();
      expect(payload.iss).toBe("mawidi-auth");
    });
  });
});
