import { Test, TestingModule } from '@nestjs/testing';
import { ExecutionContext, UnauthorizedException, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';
import { HttpService } from '@nestjs/axios';
import { IamGuard } from './iam-adapter';
import { of, throwError } from 'rxjs';

describe('IamGuard', () => {
  let guard: IamGuard;
  let httpService: HttpService;
  let configService: ConfigService;
  let reflector: Reflector;

  const mockConfigService = {
    get: jest.fn((key: string) => {
      const config: Record<string, string> = {
        IAM_API_URL: 'https://iam.test.com/api',
        IAM_ENABLED: 'true',
        IAM_API_KEY: 'test-api-key',
      };
      return config[key];
    }),
  };

  const mockHttpService = {
    post: jest.fn(),
  };

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

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        IamGuard,
        { provide: ConfigService, useValue: mockConfigService },
        { provide: HttpService, useValue: mockHttpService },
        { provide: Reflector, useValue: mockReflector },
      ],
    }).compile();

    guard = module.get<IamGuard>(IamGuard);
    httpService = module.get<HttpService>(HttpService);
    configService = module.get<ConfigService>(ConfigService);
    reflector = module.get<Reflector>(Reflector);

    jest.clearAllMocks();
  });

  const createMockContext = (token?: string, roles?: string[]): ExecutionContext => {
    const request = {
      headers: token ? { authorization: `Bearer ${token}` } : {},
      user: undefined,
    };

    return {
      switchToHttp: () => ({
        getRequest: () => request,
      }),
      getHandler: () => ({}),
    } as ExecutionContext;
  };

  describe('canActivate', () => {
    it('should allow access when IAM is disabled', async () => {
      mockConfigService.get.mockReturnValueOnce('false'); // IAM_ENABLED

      const context = createMockContext();
      const result = await guard.canActivate(context);

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

    it('should throw UnauthorizedException when no token provided', async () => {
      const context = createMockContext();

      await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException);
    });

    it('should validate token and allow access', async () => {
      const mockValidationResponse = {
        data: {
          valid: true,
          payload: {
            sub: 'user-123',
            email: 'admin@test.com',
            roles: ['ADMIN'],
            permissions: ['read', 'write'],
            exp: Date.now() + 3600000,
            iat: Date.now(),
          },
        },
      };

      mockHttpService.post.mockReturnValue(of(mockValidationResponse));
      mockReflector.get.mockReturnValue(null);

      const context = createMockContext('valid-token');
      const result = await guard.canActivate(context);

      expect(result).toBe(true);
      expect(mockHttpService.post).toHaveBeenCalledWith(
        'https://iam.test.com/api/v1/auth/validate',
        { token: 'valid-token' },
        expect.objectContaining({
          headers: expect.objectContaining({
            'X-API-Key': 'test-api-key',
          }),
        }),
      );
    });

    it('should throw UnauthorizedException for invalid token', async () => {
      const mockValidationResponse = {
        data: {
          valid: false,
          error: 'Token expired',
        },
      };

      mockHttpService.post.mockReturnValue(of(mockValidationResponse));

      const context = createMockContext('invalid-token');

      await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException);
    });

    it('should check required roles', async () => {
      const mockValidationResponse = {
        data: {
          valid: true,
          payload: {
            sub: 'user-123',
            email: 'agent@test.com',
            roles: ['AGENT'],
            permissions: [],
            exp: Date.now() + 3600000,
            iat: Date.now(),
          },
        },
      };

      mockHttpService.post.mockReturnValue(of(mockValidationResponse));
      mockReflector.get.mockReturnValue(['ADMIN', 'SUPERVISOR']);

      const context = createMockContext('valid-token');

      await expect(guard.canActivate(context)).rejects.toThrow(ForbiddenException);
    });

    it('should allow access when user has required role', async () => {
      const mockValidationResponse = {
        data: {
          valid: true,
          payload: {
            sub: 'user-123',
            email: 'admin@test.com',
            roles: ['ADMIN'],
            permissions: [],
            exp: Date.now() + 3600000,
            iat: Date.now(),
          },
        },
      };

      mockHttpService.post.mockReturnValue(of(mockValidationResponse));
      mockReflector.get.mockReturnValue(['ADMIN', 'SUPERVISOR']);

      const context = createMockContext('valid-token');
      const result = await guard.canActivate(context);

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

    it('should handle HTTP errors gracefully', async () => {
      mockHttpService.post.mockReturnValue(
        throwError(() => new Error('Network error')),
      );

      const context = createMockContext('valid-token');

      await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException);
    });

    it('should attach user to request', async () => {
      const mockValidationResponse = {
        data: {
          valid: true,
          payload: {
            sub: 'user-123',
            email: 'admin@test.com',
            roles: ['ADMIN'],
            permissions: ['read', 'write'],
            organizationId: 'org-456',
            exp: Date.now() + 3600000,
            iat: Date.now(),
          },
        },
      };

      mockHttpService.post.mockReturnValue(of(mockValidationResponse));
      mockReflector.get.mockReturnValue(null);

      const context = createMockContext('valid-token');
      await guard.canActivate(context);

      const request = context.switchToHttp().getRequest();
      expect(request.user).toEqual({
        userId: 'user-123',
        email: 'admin@test.com',
        roles: ['ADMIN'],
        permissions: ['read', 'write'],
        organizationId: 'org-456',
      });
    });
  });

  describe('token extraction', () => {
    it('should extract token from Bearer header', async () => {
      const mockValidationResponse = {
        data: {
          valid: true,
          payload: {
            sub: 'user-123',
            email: 'test@test.com',
            roles: [],
            permissions: [],
            exp: Date.now() + 3600000,
            iat: Date.now(),
          },
        },
      };

      mockHttpService.post.mockReturnValue(of(mockValidationResponse));
      mockReflector.get.mockReturnValue(null);

      const context = createMockContext('my-token-123');
      await guard.canActivate(context);

      expect(mockHttpService.post).toHaveBeenCalledWith(
        expect.any(String),
        { token: 'my-token-123' },
        expect.any(Object),
      );
    });

    it('should reject malformed authorization header', async () => {
      const request = {
        headers: { authorization: 'InvalidFormat token' },
      };

      const context = {
        switchToHttp: () => ({
          getRequest: () => request,
        }),
        getHandler: () => ({}),
      } as ExecutionContext;

      await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException);
    });
  });
});
