import { Test, TestingModule } from '@nestjs/testing';
import { ExecutionContext, LoggerService, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

import { LogtoTokenGuard } from './guard';
import {
  LogtoTokenVerifier, LogtoTokenVerifierToken,
  AccessToken, AccessTokenPayload 
} from '../token';

import { p3Values } from 'point3-common-tool';
import { LogtoLoggerServiceToken } from 'client';

describe('LogtoTokenGuard 테스트', () => {
  let guard: LogtoTokenGuard;
  let tokenUtil: jest.Mocked<LogtoTokenVerifier>;
  let reflector: jest.Mocked<Reflector>;
  let logger: jest.Mocked<LoggerService>;

  // 사용자가 제공한 실제 JWT 토큰
  const testToken = 'eyJhbGciOiJFUzM4NCIsInR5cCI6ImF0K2p3dCIsImtpZCI6ImxKUjU3SkFqVmV1dHk4eWljVzUtdFFySDM2WFl6NUlzWFhXSDVzeXV0dEEifQ.eyJ1c2VyUm9sZXMiOlsicDMtQ0lTTy0wIl0sIm1hbmFnZXJJZCI6Im1hbmFnZXItMDE5NjQ0NWMtOGVjNy03MDc4LWExNDItNGU3ZGI5YTRhYWVhIiwiY2xpZW50SWQiOiJwb2ludDMtMDE5NjNjODUtNDQ2ZS03NGM5LWFmNzktNDhlMjU0NjVjMzI3IiwianRpIjoiV0RYTmxoTWkwT0tHQ1pTRzFKZnBrIiwic3ViIjoieXVsaXVmdHNvMWQwIiwiaWF0IjoxNzQ5MDI0NzIzLCJleHAiOjE3NDkwMjgzMjMsInNjb3BlIjoiIiwiY2xpZW50X2lkIjoiNXFydmk5eW0wajJ0YTJ6YXBnbHU0IiwiaXNzIjoiaHR0cHM6Ly9sb2d0by5wb2ludDMuaW8vb2lkYyIsImF1ZCI6Imh0dHBzOi8vZGVmYXVsdC5sb2d0by5hcHAvYXBpIn0.nZdzvdxQ74m2oFEklVTfQlcqYBkRrRxtHQEgz1L6DjST9_9Wa7H7J1gKJVEjm8NnjFCQXljYM_hTVx1ABTmUgDrEKVjtHFVKUyPoSzxQitXexwmBZY5l8WdyqJDqAy8d';

  // 토큰 데이터와 일치하는 모의 페이로드
  const mockPayload: AccessTokenPayload = {
    userRoles: ['p3-CISO-0'],
    managerId: 'manager-0196445c-8ec7-7078-a142-4e7db9a4aaea',
    clientId: 'point3-019663c85-446e-74c9-af79-48e25465c327',
    jti: 'WDXNlhMi0OKGCZSG1Jfpk',
    sub: 'yuliuftso1d0',
    iat: 1749024723,
    exp: 1749028323,
    scope: '',
    client_id: '5qrvi9ym0j2ta2zapglu4',
    iss: 'https://logto.point3.io/oidc',
    aud: 'https://default.logto.app/api'
  };

  beforeEach(async () => {
    const mockTokenUtil = {
      verifyToken: jest.fn(),
    };

    const mockReflector = {
      get: jest.fn(),
    };

    const mockLogger = {
      warn: jest.fn(),
      error: jest.fn(),
      log: jest.fn(),
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        LogtoTokenGuard,
        {
          provide: LogtoTokenVerifierToken,
          useValue: mockTokenUtil,
        },
        {
          provide: Reflector,
          useValue: mockReflector,
        },
        {
          provide: LogtoLoggerServiceToken,
          useValue: mockLogger,
        },
      ],
    }).compile();

    guard = module.get<LogtoTokenGuard>(LogtoTokenGuard);
    tokenUtil = module.get(LogtoTokenVerifierToken);
    reflector = module.get(Reflector);
    logger = module.get(LogtoLoggerServiceToken);

    // 각 테스트 전에 모의 함수 초기화
    jest.clearAllMocks();
  });

  const createMockExecutionContext = (headers: any = {}, route: any = { path: '/test' }): ExecutionContext => {
    const mockRequest = {
      headers,
      route,
      user: undefined // Guard에서 설정됨
    };

    return {
      switchToHttp: () => ({
        getRequest: () => mockRequest,
        getResponse: jest.fn(),
        getNext: jest.fn(),
      }),
      getHandler: jest.fn(),
      getClass: jest.fn(),
      getArgs: jest.fn(),
      getArgByIndex: jest.fn(),
      switchToRpc: jest.fn(),
      switchToWs: jest.fn(),
      getType: jest.fn(),
    } as ExecutionContext;
  };

  describe('🔐 성공적인 인증 테스트', () => {
    it('유효한 토큰이 제공되었을 때 인증하고 사용자 데이터를 설정해야 함', async () => {
      // 준비
      const context = createMockExecutionContext({
        authorization: `Bearer ${testToken}`,
      });
      
      // Reflector 모의 설정 - 특정 역할 요구사항
      reflector.get
        .mockReturnValueOnce(undefined) // requiredScopes
        .mockReturnValueOnce(['p3-CISO-0']); // requiredRoles

      // 성공적인 토큰 검증 모의
      tokenUtil.verifyToken.mockResolvedValueOnce(mockPayload);

      // 실행
      const result = await guard.canActivate(context);
      const request = context.switchToHttp().getRequest();

      // 검증
      expect(result).toBe(true);
      expect(tokenUtil.verifyToken).toHaveBeenCalledWith(
        testToken,
        undefined,
        ['p3-CISO-0']
      );

      // 사용자 데이터가 올바르게 설정되었는지 확인
      expect(request.user).toEqual({
        userId: 'yuliuftso1d0',
        managerId: expect.objectContaining({
          toString: expect.any(Function)
        }),
        clientId: expect.objectContaining({
          toString: expect.any(Function)
        }),
      });

      // GUID 값 검증
      expect(request.user.managerId.toString()).toContain('manager');
      expect(request.user.managerId.toString()).toContain('0196445c-8ec7-7078-a142-4e7db9a4aaea');
      expect(request.user.clientId.toString()).toContain('point3');
      expect(request.user.clientId.toString()).toContain('019663c85-446e-74c9-af79-48e25465c327');
    });

    it('필수 스코프나 역할이 없을 때도 동작해야 함', async () => {
      // 준비
      const context = createMockExecutionContext({
        authorization: `Bearer ${testToken}`,
      });
      
      // Reflector 모의 설정 - 요구사항 없음
      reflector.get
        .mockReturnValueOnce(undefined) // requiredScopes
        .mockReturnValueOnce(undefined); // requiredRoles

      // 성공적인 토큰 검증 모의
      tokenUtil.verifyToken.mockResolvedValueOnce(mockPayload);

      // 실행
      const result = await guard.canActivate(context);

      // 검증
      expect(result).toBe(true);
      expect(tokenUtil.verifyToken).toHaveBeenCalledWith(
        testToken,
        undefined,
        undefined
      );
    });
  });

  describe('🚫 토큰 추출 실패 테스트', () => {
    it('Authorization 헤더가 없을 때 UnauthorizedException을 던져야 함', async () => {
      // 준비
      const context = createMockExecutionContext({}); // 헤더 없음
      
      reflector.get
        .mockReturnValueOnce(undefined)
        .mockReturnValueOnce(['p3-CISO-0']);

      // 실행 & 검증
      await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException);
      await expect(guard.canActivate(context)).rejects.toThrow('Authorization header is missing');
    });

    it('Authorization 헤더가 Bearer가 아닐 때 UnauthorizedException을 던져야 함', async () => {
      // 준비
      const context = createMockExecutionContext({
        authorization: 'Basic sometoken',
      });
      
      reflector.get
        .mockReturnValueOnce(undefined)
        .mockReturnValueOnce(['p3-CISO-0']);

      // 실행 & 검증
      await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException);
      await expect(guard.canActivate(context)).rejects.toThrow('Authorization token type not supported');
    });

    it('Bearer 헤더에서 토큰을 올바르게 추출해야 함', async () => {
      // 준비
      const context = createMockExecutionContext({
        authorization: `Bearer ${testToken}`,
      });
      
      reflector.get
        .mockReturnValueOnce(undefined)
        .mockReturnValueOnce(['p3-CISO-0']);
      tokenUtil.verifyToken.mockResolvedValueOnce(mockPayload);

      // 실행
      await guard.canActivate(context);

      // 검증 - 토큰이 올바르게 추출되어 verifyToken에 전달됨
      expect(tokenUtil.verifyToken).toHaveBeenCalledWith(
        testToken,
        undefined,
        ['p3-CISO-0']
      );
    });
  });

  describe('❌ 토큰 검증 실패 테스트', () => {
    it('토큰 검증에서 UnauthorizedException이 발생하면 다시 던져야 함', async () => {
      // 준비
      const context = createMockExecutionContext({
        authorization: `Bearer ${testToken}`,
      });
      
      reflector.get
        .mockReturnValueOnce(undefined)
        .mockReturnValueOnce(['p3-CISO-0']);
      
      const authError = new UnauthorizedException('Invalid token');
      tokenUtil.verifyToken.mockRejectedValueOnce(authError);

      // 실행 & 검증
      await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException);
    });

    it('다른 에러가 발생하면 일반적인 에러 메시지를 던져야 함', async () => {
      // 준비
      const context = createMockExecutionContext({
        authorization: `Bearer ${testToken}`,
      });
      
      reflector.get
        .mockReturnValueOnce(undefined)
        .mockReturnValueOnce(['p3-CISO-0']);
      
      tokenUtil.verifyToken.mockRejectedValueOnce(new Error('Some other error'));

      // 실행 & 검증
      await expect(guard.canActivate(context)).rejects.toThrow('요청을 처리하지 못하였습니다.');
    });
  });

  describe('🔍 실제 JWT 토큰 분석', () => {
    it('제공된 JWT 토큰의 페이로드를 올바르게 디코딩해야 함', () => {
      // JWT 토큰을 수동으로 디코딩하여 모의 데이터와 비교
      const [header, payload, signature] = testToken.split('.');
      const decodedPayload = JSON.parse(Buffer.from(payload, 'base64url').toString());
      
      console.log('🔍 디코딩된 토큰 페이로드:');
      console.log(JSON.stringify(decodedPayload, null, 2));
      
      // 모의 페이로드가 실제 토큰과 일치하는지 확인
      expect(decodedPayload.userRoles).toEqual(['p3-CISO-0']);
      expect(decodedPayload.managerId).toBe('manager-0196445c-8ec7-7078-a142-4e7db9a4aaea');
      expect(decodedPayload.clientId).toBe('point3-01963c85-446e-74c9-af79-48e25465c327');
      expect(decodedPayload.sub).toBe('yuliuftso1d0');
      expect(decodedPayload.iss).toBe('https://logto.point3.io/oidc');
      
      // 토큰 만료 시간 확인 (Unix timestamp)
      const expirationDate = new Date(decodedPayload.exp * 1000);
      const issuedDate = new Date(decodedPayload.iat * 1000);
      
      console.log(`📅 토큰 발급 시간: ${issuedDate.toISOString()}`);
      console.log(`⏰ 토큰 만료 시간: ${expirationDate.toISOString()}`);
      console.log(`🏢 발급자: ${decodedPayload.iss}`);
      console.log(`👤 사용자 역할: ${decodedPayload.userRoles.join(', ')}`);
    });

    it('토큰에서 추출된 GUID 값들이 올바른 형식인지 확인해야 함', () => {
      const [header, payload, signature] = testToken.split('.');
      const decodedPayload = JSON.parse(Buffer.from(payload, 'base64url').toString());
      
      // managerId GUID 검증
      const managerId = p3Values.Guid.parse(decodedPayload.managerId);
      expect(managerId.Prefix == 'manager');
      
      // clientId GUID 검증
      const clientId = p3Values.Guid.parse(decodedPayload.clientId);
      expect(clientId.Prefix == 'point3');
      
      console.log('✅ GUID 형식 검증 완료:');
      console.log(`   Manager ID: ${managerId.toString()}`);
      console.log(`   Client ID: ${clientId.toString()}`);
    });
  });
}); 