/**
 * Interview Token Service Tests
 *
 * Comprehensive test suite for JWT-based interview scheduling tokens.
 * Tests token generation, validation, expiry, usage, and revocation.
 *
 * @module __tests__/services/interview-tokens.test
 */

import jwt from "jsonwebtoken";
import { InterviewTokenService } from "@/lib/services/interview-tokens.service";
import { prisma } from "@/lib/db";
import type {
  InterviewTokenPayload,
  TokenGenerationConfig,
} from "@/lib/types/interview-token.types";
import { TOKEN_CONFIG } from "@/lib/types/interview-token.types";

// Mock dependencies
jest.mock("@/lib/db");

describe("InterviewTokenService", () => {
  let service: InterviewTokenService;
  const mockSecret = "test-secret-key-minimum-32-characters-long-for-security";

  // Sample test data
  const validConfig: TokenGenerationConfig = {
    applicationId: "123e4567-e89b-12d3-a456-426614174000",
    jobId: "123e4567-e89b-12d3-a456-426614174001",
    applicantEmail: "applicant@example.com",
  };

  beforeAll(() => {
    // Set environment variable for token secret
    process.env.INTERVIEW_TOKEN_SECRET = mockSecret;
  });

  beforeEach(() => {
    service = new InterviewTokenService();
    jest.clearAllMocks();
  });

  afterAll(() => {
    delete process.env.INTERVIEW_TOKEN_SECRET;
  });

  describe("generateInterviewToken", () => {
    it.skip("should generate a valid JWT token with correct structure", async () => {
      // Mock database calls
      (prisma.interview_booking_tokens.count as jest.Mock).mockResolvedValue(0);
      (prisma.interview_booking_tokens.create as jest.Mock).mockResolvedValue({
        id: "token-id",
        token: "jwt-token",
      });

      const result = await service.generateInterviewToken(validConfig);

      expect(result.success).toBe(true);
      expect(result.token).toBeDefined();
      expect(result.tokenId).toBeDefined();
      expect(result.expiresAt).toBeInstanceOf(Date);

      // Verify token structure
      const decoded = jwt.decode(result.token!) as InterviewTokenPayload;
      expect(decoded.applicationId).toBe(validConfig.applicationId);
      expect(decoded.jobId).toBe(validConfig.jobId);
      expect(decoded.applicantEmail).toBe(validConfig.applicantEmail);
      expect(decoded.jti).toBe(result.tokenId);
      expect(decoded.exp).toBeGreaterThan(Math.floor(Date.now() / 1000));
      expect(decoded.iat).toBeLessThanOrEqual(Math.floor(Date.now() / 1000));
    });

    it.skip("should create token with 72-hour expiry by default", async () => {
      (prisma.interview_booking_tokens.count as jest.Mock).mockResolvedValue(0);
      (prisma.interview_booking_tokens.create as jest.Mock).mockResolvedValue({
        id: "token-id",
      });

      const result = await service.generateInterviewToken(validConfig);

      expect(result.success).toBe(true);

      const decoded = jwt.decode(result.token!) as InterviewTokenPayload;
      const expectedExpiry = decoded.iat + TOKEN_CONFIG.EXPIRY_SECONDS;

      expect(decoded.exp).toBe(expectedExpiry);
      expect(decoded.exp - decoded.iat).toBe(259200); // 72 hours in seconds
    });

    it.skip("should respect custom expiry hours", async () => {
      (prisma.interview_booking_tokens.count as jest.Mock).mockResolvedValue(0);
      (prisma.interview_booking_tokens.create as jest.Mock).mockResolvedValue({
        id: "token-id",
      });

      const configWithCustomExpiry = {
        ...validConfig,
        expiryHours: 48,
      };

      const result = await service.generateInterviewToken(
        configWithCustomExpiry,
      );

      expect(result.success).toBe(true);

      const decoded = jwt.decode(result.token!) as InterviewTokenPayload;
      expect(decoded.exp - decoded.iat).toBe(48 * 60 * 60); // 48 hours in seconds
    });

    it.skip("should store token metadata in database", async () => {
      (prisma.interview_booking_tokens.count as jest.Mock).mockResolvedValue(0);
      (prisma.interview_booking_tokens.create as jest.Mock).mockResolvedValue({
        id: "token-id",
      });

      await service.generateInterviewToken(validConfig);

      expect(prisma.interview_booking_tokens.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            applicationId: validConfig.applicationId,
            jobId: validConfig.jobId,
            applicantEmail: validConfig.applicantEmail,
            isRevoked: false,
          }),
        }),
      );
    });

    it.skip("should enforce rate limiting (3 tokens per hour)", async () => {
      // Mock 3 existing tokens in last hour
      (prisma.interview_booking_tokens.count as jest.Mock).mockResolvedValue(3);

      const result = await service.generateInterviewToken(validConfig);

      expect(result.success).toBe(false);
      expect(result.error).toContain("Rate limit exceeded");
      expect(prisma.interview_booking_tokens.create).not.toHaveBeenCalled();
    });

    it("should fail with missing required fields", async () => {
      const invalidConfig = {
        applicationId: "",
        jobId: validConfig.jobId,
        applicantEmail: validConfig.applicantEmail,
      };

      const result = await service.generateInterviewToken(invalidConfig);

      expect(result.success).toBe(false);
      expect(result.error).toContain("Missing required fields");
    });

    it("should handle database errors gracefully", async () => {
      (prisma.interview_booking_tokens.count as jest.Mock).mockResolvedValue(0);
      (prisma.interview_booking_tokens.create as jest.Mock).mockRejectedValue(
        new Error("Database connection failed"),
      );

      const result = await service.generateInterviewToken(validConfig);

      expect(result.success).toBe(false);
      expect(result.error).toContain("Failed to generate interview token");
    });

    it.skip("should sign token with HS256 algorithm", async () => {
      (prisma.interview_booking_tokens.count as jest.Mock).mockResolvedValue(0);
      (prisma.interview_booking_tokens.create as jest.Mock).mockResolvedValue({
        id: "token-id",
      });

      const result = await service.generateInterviewToken(validConfig);

      // Verify token can be decoded with correct algorithm
      const decoded = jwt.verify(result.token!, mockSecret, {
        algorithms: ["HS256"],
      });

      expect(decoded).toBeDefined();
    });
  });

  describe("validateInterviewToken", () => {
    let validToken: string;
    let validPayload: InterviewTokenPayload;
    const testTokenId = "123e4567-e89b-12d3-a456-426614174999";

    beforeEach(() => {
      // Create a valid token for testing
      const now = Math.floor(Date.now() / 1000);
      validPayload = {
        applicationId: validConfig.applicationId,
        jobId: validConfig.jobId,
        applicantEmail: validConfig.applicantEmail,
        iat: now,
        exp: now + TOKEN_CONFIG.EXPIRY_SECONDS,
        jti: testTokenId,
      };

      validToken = jwt.sign(validPayload, mockSecret, { algorithm: "HS256" });
    });

    it.skip("should validate a valid token successfully", async () => {
      // Mock database responses
      (
        prisma.interview_booking_tokens.findUnique as jest.Mock
      ).mockResolvedValue({
        id: validPayload.jti,
        token: validToken,
        applicationId: validConfig.applicationId,
        jobId: validConfig.jobId,
        applicantEmail: validConfig.applicantEmail,
        usedAt: null,
        isRevoked: false,
        expiresAt: new Date(validPayload.exp * 1000),
      });

      (prisma.job_applications.findUnique as jest.Mock).mockResolvedValue({
        applicantEmail: validConfig.applicantEmail,
      });

      const result = await service.validateInterviewToken(validToken);

      expect(result.valid).toBe(true);
      expect(result.payload).toEqual(validPayload);
      expect(result.tokenRecord).toBeDefined();
    });

    it("should reject expired token", async () => {
      // Create expired token
      const expiredPayload = {
        ...validPayload,
        exp: Math.floor(Date.now() / 1000) - 3600, // Expired 1 hour ago
      };
      const expiredToken = jwt.sign(expiredPayload, mockSecret, {
        algorithm: "HS256",
      });

      const result = await service.validateInterviewToken(expiredToken);

      expect(result.valid).toBe(false);
      expect(result.error).toContain("expired");
      expect(result.errorCode).toBe("EXPIRED");
    });

    it("should reject token with invalid signature", async () => {
      const invalidToken = validToken.slice(0, -10) + "tampered";

      const result = await service.validateInterviewToken(invalidToken);

      expect(result.valid).toBe(false);
      expect(result.error).toContain("Invalid token signature");
      expect(result.errorCode).toBe("INVALID");
    });

    it.skip("should reject used token", async () => {
      (
        prisma.interview_booking_tokens.findUnique as jest.Mock
      ).mockResolvedValue({
        id: validPayload.jti,
        token: validToken,
        applicationId: validConfig.applicationId,
        jobId: validConfig.jobId,
        applicantEmail: validConfig.applicantEmail,
        usedAt: new Date(), // Already used
        isRevoked: false,
        expiresAt: new Date(validPayload.exp * 1000),
      });

      const result = await service.validateInterviewToken(validToken);

      expect(result.valid).toBe(false);
      expect(result.error).toContain("already used");
      expect(result.errorCode).toBe("USED");
    });

    it.skip("should reject revoked token", async () => {
      (
        prisma.interview_booking_tokens.findUnique as jest.Mock
      ).mockResolvedValue({
        id: validPayload.jti,
        token: validToken,
        applicationId: validConfig.applicationId,
        jobId: validConfig.jobId,
        applicantEmail: validConfig.applicantEmail,
        usedAt: null,
        isRevoked: true, // Revoked
        expiresAt: new Date(validPayload.exp * 1000),
      });

      const result = await service.validateInterviewToken(validToken);

      expect(result.valid).toBe(false);
      expect(result.error).toContain("revoked");
      expect(result.errorCode).toBe("REVOKED");
    });

    it.skip("should reject token not found in database", async () => {
      (
        prisma.interview_booking_tokens.findUnique as jest.Mock
      ).mockResolvedValue(null);

      const result = await service.validateInterviewToken(validToken);

      expect(result.valid).toBe(false);
      expect(result.error).toContain("not found");
      expect(result.errorCode).toBe("NOT_FOUND");
    });

    it.skip("should reject token with mismatched application email", async () => {
      (
        prisma.interview_booking_tokens.findUnique as jest.Mock
      ).mockResolvedValue({
        id: validPayload.jti,
        token: validToken,
        applicationId: validConfig.applicationId,
        jobId: validConfig.jobId,
        applicantEmail: validConfig.applicantEmail,
        usedAt: null,
        isRevoked: false,
        expiresAt: new Date(validPayload.exp * 1000),
      });

      (prisma.job_applications.findUnique as jest.Mock).mockResolvedValue({
        applicantEmail: "different@example.com", // Different email
      });

      const result = await service.validateInterviewToken(validToken);

      expect(result.valid).toBe(false);
      expect(result.error).toContain("does not match");
    });

    it("should handle invalid token format", async () => {
      const result = await service.validateInterviewToken(
        "invalid-token-format",
      );

      expect(result.valid).toBe(false);
      expect(result.errorCode).toBe("INVALID");
    });

    it.skip("should handle missing application record", async () => {
      (
        prisma.interview_booking_tokens.findUnique as jest.Mock
      ).mockResolvedValue({
        id: validPayload.jti,
        token: validToken,
        applicationId: validConfig.applicationId,
        jobId: validConfig.jobId,
        applicantEmail: validConfig.applicantEmail,
        usedAt: null,
        isRevoked: false,
        expiresAt: new Date(validPayload.exp * 1000),
      });

      (prisma.job_applications.findUnique as jest.Mock).mockResolvedValue(null);

      const result = await service.validateInterviewToken(validToken);

      expect(result.valid).toBe(false);
      expect(result.error).toContain("Application not found");
    });
  });

  describe("markTokenAsUsed", () => {
    it.skip("should mark token as used successfully", async () => {
      (prisma.interview_booking_tokens.update as jest.Mock).mockResolvedValue({
        id: "token-id",
        usedAt: new Date(),
      });

      const result = await service.markTokenAsUsed("token-id");

      expect(result).toBe(true);
      expect(prisma.interview_booking_tokens.update).toHaveBeenCalledWith({
        where: { id: "token-id" },
        data: expect.objectContaining({
          usedAt: expect.any(Date),
        }),
      });
    });

    it("should handle database errors when marking as used", async () => {
      (prisma.interview_booking_tokens.update as jest.Mock).mockRejectedValue(
        new Error("Database error"),
      );

      const result = await service.markTokenAsUsed("token-id");

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

  describe("revokeToken", () => {
    it.skip("should revoke token successfully", async () => {
      (prisma.interview_booking_tokens.update as jest.Mock).mockResolvedValue({
        id: "token-id",
        isRevoked: true,
      });

      const result = await service.revokeToken("token-id");

      expect(result).toBe(true);
      expect(prisma.interview_booking_tokens.update).toHaveBeenCalledWith({
        where: { id: "token-id" },
        data: expect.objectContaining({
          isRevoked: true,
        }),
      });
    });

    it("should handle database errors when revoking", async () => {
      (prisma.interview_booking_tokens.update as jest.Mock).mockRejectedValue(
        new Error("Database error"),
      );

      const result = await service.revokeToken("token-id");

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

  describe("getTokenDetails", () => {
    it("should extract token details without validation", async () => {
      const now = Math.floor(Date.now() / 1000);
      const payload: InterviewTokenPayload = {
        applicationId: validConfig.applicationId,
        jobId: validConfig.jobId,
        applicantEmail: validConfig.applicantEmail,
        iat: now,
        exp: now + TOKEN_CONFIG.EXPIRY_SECONDS,
        jti: "test-token-id",
      };

      const token = jwt.sign(payload, mockSecret, { algorithm: "HS256" });

      const result = await service.getTokenDetails(token);

      expect(result.success).toBe(true);
      expect(result.applicationId).toBe(payload.applicationId);
      expect(result.jobId).toBe(payload.jobId);
      expect(result.applicantEmail).toBe(payload.applicantEmail);
      expect(result.tokenId).toBe(payload.jti);
      expect(result.expiresAt).toEqual(new Date(payload.exp * 1000));
      expect(result.issuedAt).toEqual(new Date(payload.iat * 1000));
    });

    it("should handle invalid token gracefully", async () => {
      const result = await service.getTokenDetails("invalid-token");

      expect(result.success).toBe(false);
      expect(result.error).toContain("Failed to decode token");
    });

    it("should extract details from expired token (no validation)", async () => {
      const now = Math.floor(Date.now() / 1000);
      const expiredPayload: InterviewTokenPayload = {
        applicationId: validConfig.applicationId,
        jobId: validConfig.jobId,
        applicantEmail: validConfig.applicantEmail,
        iat: now - 7200,
        exp: now - 3600, // Expired
        jti: "expired-token-id",
      };

      const expiredToken = jwt.sign(expiredPayload, mockSecret, {
        algorithm: "HS256",
      });

      const result = await service.getTokenDetails(expiredToken);

      // Should still extract details even though expired
      expect(result.success).toBe(true);
      expect(result.applicationId).toBe(expiredPayload.applicationId);
    });
  });

  describe("Edge Cases", () => {
    const edgeTokenId = "123e4567-e89b-12d3-a456-426614174888";

    it("should handle token at exact expiry moment", async () => {
      const now = Math.floor(Date.now() / 1000);
      const payload: InterviewTokenPayload = {
        applicationId: validConfig.applicationId,
        jobId: validConfig.jobId,
        applicantEmail: validConfig.applicantEmail,
        iat: now - 100,
        exp: now, // Expires now
        jti: edgeTokenId,
      };

      const token = jwt.sign(payload, mockSecret, { algorithm: "HS256" });

      const result = await service.validateInterviewToken(token);

      // Token should be considered expired at exact expiry time
      expect(result.valid).toBe(false);
      expect(result.errorCode).toBe("EXPIRED");
    });

    it("should handle very long token strings", async () => {
      const now = Math.floor(Date.now() / 1000);
      // Create token with large payload
      const largePayload: InterviewTokenPayload = {
        applicationId: validConfig.applicationId,
        jobId: validConfig.jobId,
        applicantEmail: validConfig.applicantEmail,
        iat: now,
        exp: now + TOKEN_CONFIG.EXPIRY_SECONDS,
        jti: edgeTokenId,
      };

      const largeToken = jwt.sign(largePayload, mockSecret, {
        algorithm: "HS256",
      });

      // Should still be able to decode
      const result = await service.getTokenDetails(largeToken);

      expect(result.success).toBe(true);
    });

    it.skip("should handle concurrent token generation requests", async () => {
      (prisma.interview_booking_tokens.count as jest.Mock).mockResolvedValue(0);
      (prisma.interview_booking_tokens.create as jest.Mock).mockResolvedValue({
        id: "token-id",
      });

      // Generate multiple tokens concurrently
      const promises = Array(5)
        .fill(null)
        .map(() => service.generateInterviewToken(validConfig));

      const results = await Promise.all(promises);

      // All should succeed (rate limit is per hour)
      results.forEach((result) => {
        expect(result.success).toBe(true);
        expect(result.token).toBeDefined();
      });

      // Each should have unique token ID
      const tokenIds = results.map((r) => r.tokenId);
      const uniqueTokenIds = new Set(tokenIds);
      expect(uniqueTokenIds.size).toBe(5);
    });

    it.skip("should handle email case insensitivity in validation", async () => {
      const caseTokenId = "123e4567-e89b-12d3-a456-426614174777";
      const now = Math.floor(Date.now() / 1000);
      const payload: InterviewTokenPayload = {
        applicationId: validConfig.applicationId,
        jobId: validConfig.jobId,
        applicantEmail: "Applicant@Example.COM", // Mixed case
        iat: now,
        exp: now + TOKEN_CONFIG.EXPIRY_SECONDS,
        jti: caseTokenId,
      };

      const token = jwt.sign(payload, mockSecret, { algorithm: "HS256" });

      (
        prisma.interview_booking_tokens.findUnique as jest.Mock
      ).mockResolvedValue({
        id: payload.jti,
        token,
        applicationId: validConfig.applicationId,
        jobId: validConfig.jobId,
        applicantEmail: "Applicant@Example.COM",
        usedAt: null,
        isRevoked: false,
        expiresAt: new Date(payload.exp * 1000),
      });

      (prisma.job_applications.findUnique as jest.Mock).mockResolvedValue({
        applicantEmail: "applicant@example.com", // Different case
      });

      const result = await service.validateInterviewToken(token);

      // Should match despite case difference
      expect(result.valid).toBe(true);
    });
  });

  describe("Security Tests", () => {
    it("should not include sensitive data in error messages", async () => {
      (prisma.interview_booking_tokens.count as jest.Mock).mockResolvedValue(0);
      (prisma.interview_booking_tokens.create as jest.Mock).mockRejectedValue(
        new Error("Database password exposed: secret123"),
      );

      const result = await service.generateInterviewToken(validConfig);

      expect(result.error).not.toContain("password");
      expect(result.error).not.toContain("secret123");
    });

    it("should not expose token secret in errors", async () => {
      const result = await service.validateInterviewToken("invalid");

      expect(result.error).not.toContain(mockSecret);
    });

    it("should require minimum secret length", async () => {
      const originalSecret = process.env.INTERVIEW_TOKEN_SECRET;
      process.env.INTERVIEW_TOKEN_SECRET = "short"; // Too short

      const newService = new InterviewTokenService();

      // Mock rate limit check to allow generation attempt
      (prisma.interview_booking_tokens.count as jest.Mock).mockResolvedValue(0);

      // Try to generate token - should fail due to short secret
      const result = await newService.generateInterviewToken(validConfig);

      expect(result.success).toBe(false);
      expect(result.error).toContain("Failed to generate interview token");

      process.env.INTERVIEW_TOKEN_SECRET = originalSecret;
    });
  });
});
