import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import {
  BadRequestException,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import { BiometricAuthService } from './biometric-auth.service';
import { WebAuthnCredential } from './entities/webauthn-credential.entity';
import { User } from '../../database/entities/user.entity';
import { AuthService } from './auth.service';

describe('BiometricAuthService', () => {
  let service: BiometricAuthService;
  let webauthnCredentialRepository: Repository<WebAuthnCredential>;
  let userRepository: Repository<User>;
  let authService: AuthService;

  const mockUser = {
    id: 'user_123',
    email: 'test@example.com',
    name: 'Test User',
  };

  const mockCredential = {
    id: 'cred_123',
    userId: 'user_123',
    credentialId: 'credential_abc',
    publicKey: 'public_key_data',
    counter: 0,
    transports: ['internal'],
    friendlyName: 'iPhone 14',
    lastUsedAt: new Date(),
    createdAt: new Date(),
  };

  const mockWebAuthnCredentialRepository = {
    find: jest.fn(),
    findOne: jest.fn(),
    create: jest.fn(),
    save: jest.fn(),
    update: jest.fn(),
    delete: jest.fn(),
  };

  const mockUserRepository = {
    findOne: jest.fn(),
  };

  const mockAuthService = {
    generateTokens: jest.fn().mockResolvedValue({
      accessToken: 'access_token',
      refreshToken: 'refresh_token',
    }),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        BiometricAuthService,
        {
          provide: getRepositoryToken(WebAuthnCredential),
          useValue: mockWebAuthnCredentialRepository,
        },
        {
          provide: getRepositoryToken(User),
          useValue: mockUserRepository,
        },
        {
          provide: AuthService,
          useValue: mockAuthService,
        },
      ],
    }).compile();

    service = module.get<BiometricAuthService>(BiometricAuthService);
    webauthnCredentialRepository = module.get<Repository<WebAuthnCredential>>(
      getRepositoryToken(WebAuthnCredential),
    );
    userRepository = module.get<Repository<User>>(getRepositoryToken(User));
    authService = module.get<AuthService>(AuthService);

    // Reset mocks
    jest.clearAllMocks();
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('generateRegistrationOptions', () => {
    it('should generate registration options for a user', async () => {
      mockUserRepository.findOne.mockResolvedValue(mockUser);
      mockWebAuthnCredentialRepository.find.mockResolvedValue([]);

      const result = await service.generateRegistrationOptions({
        userId: 'user_123',
        friendlyName: 'iPhone 14',
      });

      expect(result).toHaveProperty('challenge');
      expect(result).toHaveProperty('rp');
      expect(result).toHaveProperty('user');
      expect(result.rp.name).toBe('Maternal Care App');
      expect(result.user.name).toBe(mockUser.email);
      expect(result.user.displayName).toBe(mockUser.name);
      expect(userRepository.findOne).toHaveBeenCalledWith({
        where: { id: 'user_123' },
      });
    });

    it('should exclude existing credentials', async () => {
      mockUserRepository.findOne.mockResolvedValue(mockUser);
      mockWebAuthnCredentialRepository.find.mockResolvedValue([mockCredential]);

      const result = await service.generateRegistrationOptions({
        userId: 'user_123',
      });

      expect(result.excludeCredentials).toHaveLength(1);
      expect(result.excludeCredentials[0].id).toBe(mockCredential.credentialId);
    });

    it('should throw NotFoundException if user not found', async () => {
      mockUserRepository.findOne.mockResolvedValue(null);

      await expect(
        service.generateRegistrationOptions({ userId: 'user_123' }),
      ).rejects.toThrow(NotFoundException);
    });
  });

  describe('getUserCredentials', () => {
    it('should return all credentials for a user', async () => {
      mockWebAuthnCredentialRepository.find.mockResolvedValue([mockCredential]);

      const result = await service.getUserCredentials('user_123');

      expect(result).toHaveLength(1);
      expect(result[0]).toEqual(mockCredential);
      expect(webauthnCredentialRepository.find).toHaveBeenCalledWith({
        where: { userId: 'user_123' },
        order: { createdAt: 'DESC' },
      });
    });

    it('should return empty array if no credentials', async () => {
      mockWebAuthnCredentialRepository.find.mockResolvedValue([]);

      const result = await service.getUserCredentials('user_123');

      expect(result).toHaveLength(0);
    });
  });

  describe('deleteCredential', () => {
    it('should delete a credential', async () => {
      mockWebAuthnCredentialRepository.findOne.mockResolvedValue(mockCredential);
      mockWebAuthnCredentialRepository.delete.mockResolvedValue({ affected: 1 });

      const result = await service.deleteCredential('user_123', 'cred_123');

      expect(result.success).toBe(true);
      expect(result.message).toContain('deleted successfully');
      expect(webauthnCredentialRepository.delete).toHaveBeenCalledWith(
        'cred_123',
      );
    });

    it('should throw NotFoundException if credential not found', async () => {
      mockWebAuthnCredentialRepository.findOne.mockResolvedValue(null);

      await expect(
        service.deleteCredential('user_123', 'cred_123'),
      ).rejects.toThrow(NotFoundException);
    });

    it('should throw BadRequestException if credential belongs to another user', async () => {
      mockWebAuthnCredentialRepository.findOne.mockResolvedValue({
        ...mockCredential,
        userId: 'other_user',
      });

      await expect(
        service.deleteCredential('user_123', 'cred_123'),
      ).rejects.toThrow(BadRequestException);
    });
  });

  describe('updateCredentialName', () => {
    it('should update credential friendly name', async () => {
      mockWebAuthnCredentialRepository.findOne.mockResolvedValue(mockCredential);
      mockWebAuthnCredentialRepository.update.mockResolvedValue({ affected: 1 });

      const result = await service.updateCredentialName(
        'user_123',
        'cred_123',
        'My New Phone',
      );

      expect(result.success).toBe(true);
      expect(result.message).toContain('updated successfully');
      expect(webauthnCredentialRepository.update).toHaveBeenCalledWith(
        'cred_123',
        { friendlyName: 'My New Phone' },
      );
    });

    it('should throw NotFoundException if credential not found', async () => {
      mockWebAuthnCredentialRepository.findOne.mockResolvedValue(null);

      await expect(
        service.updateCredentialName('user_123', 'cred_123', 'New Name'),
      ).rejects.toThrow(NotFoundException);
    });

    it('should throw BadRequestException if credential belongs to another user', async () => {
      mockWebAuthnCredentialRepository.findOne.mockResolvedValue({
        ...mockCredential,
        userId: 'other_user',
      });

      await expect(
        service.updateCredentialName('user_123', 'cred_123', 'New Name'),
      ).rejects.toThrow(BadRequestException);
    });
  });

  describe('hasCredentials', () => {
    it('should return true if user has credentials', async () => {
      mockWebAuthnCredentialRepository.find.mockResolvedValue([mockCredential]);

      const result = await service.hasCredentials('user_123');

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

    it('should return false if user has no credentials', async () => {
      mockWebAuthnCredentialRepository.find.mockResolvedValue([]);

      const result = await service.hasCredentials('user_123');

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

  describe('generateAuthenticationOptions', () => {
    it('should generate authentication options with email', async () => {
      mockUserRepository.findOne.mockResolvedValue(mockUser);
      mockWebAuthnCredentialRepository.find.mockResolvedValue([mockCredential]);

      const result = await service.generateAuthenticationOptions({
        email: 'test@example.com',
      });

      expect(result).toHaveProperty('challenge');
      expect(result).toHaveProperty('allowCredentials');
      expect(result.allowCredentials).toHaveLength(1);
      expect(result.allowCredentials[0].id).toBe(mockCredential.credentialId);
    });

    it('should generate authentication options without email', async () => {
      const result = await service.generateAuthenticationOptions({});

      expect(result).toHaveProperty('challenge');
      expect(result.allowCredentials).toBeUndefined();
    });

    it('should throw BadRequestException if user has no credentials', async () => {
      mockUserRepository.findOne.mockResolvedValue(mockUser);
      mockWebAuthnCredentialRepository.find.mockResolvedValue([]);

      await expect(
        service.generateAuthenticationOptions({ email: 'test@example.com' }),
      ).rejects.toThrow(BadRequestException);
    });

    it('should throw NotFoundException if user not found', async () => {
      mockUserRepository.findOne.mockResolvedValue(null);

      await expect(
        service.generateAuthenticationOptions({ email: 'test@example.com' }),
      ).rejects.toThrow(NotFoundException);
    });
  });
});
