import { Test, TestingModule } from '@nestjs/testing';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ConflictException, UnauthorizedException } from '@nestjs/common';
import * as bcrypt from 'bcrypt';
import { AuthService } from './auth.service';
import {
  User,
  DeviceRegistry,
  RefreshToken,
  Family,
  FamilyMember,
  FamilyRole,
} from '../../database/entities';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
import { RefreshTokenDto } from './dto/refresh-token.dto';
import { LogoutDto } from './dto/logout.dto';
import { AuditService } from '../../common/services/audit.service';

describe('AuthService', () => {
  let service: AuthService;
  let userRepository: Repository<User>;
  let deviceRepository: Repository<DeviceRegistry>;
  let refreshTokenRepository: Repository<RefreshToken>;
  let familyRepository: Repository<Family>;
  let familyMemberRepository: Repository<FamilyMember>;
  let jwtService: JwtService;
  let configService: ConfigService;

  const mockUser = {
    id: 'usr_test123',
    email: 'test@example.com',
    passwordHash: '$2b$10$hashedpassword',
    name: 'Test User',
    phone: '+1234567890',
    locale: 'en-US',
    timezone: 'UTC',
    emailVerified: false,
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  const mockFamily = {
    id: 'fam_test123',
    name: "Test User's Family",
    shareCode: 'ABC123',
    subscriptionTier: 'free',
    createdBy: 'usr_test123',
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  const mockDevice = {
    id: 'dev_test123',
    userId: 'usr_test123',
    deviceFingerprint: 'device-123',
    platform: 'ios',
    trusted: false,
    lastSeen: new Date(),
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  const mockRefreshToken = {
    id: 'rtk_test123',
    userId: 'usr_test123',
    deviceId: 'dev_test123',
    tokenHash: 'hashedtoken',
    expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
    revoked: false,
    createdAt: new Date(),
    user: mockUser,
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        AuthService,
        {
          provide: getRepositoryToken(User),
          useValue: {
            findOne: jest.fn(),
            create: jest.fn(),
            save: jest.fn(),
          },
        },
        {
          provide: getRepositoryToken(DeviceRegistry),
          useValue: {
            findOne: jest.fn(),
            create: jest.fn(),
            save: jest.fn(),
          },
        },
        {
          provide: getRepositoryToken(RefreshToken),
          useValue: {
            findOne: jest.fn(),
            create: jest.fn(),
            save: jest.fn(),
            update: jest.fn(),
          },
        },
        {
          provide: getRepositoryToken(Family),
          useValue: {
            create: jest.fn(),
            save: jest.fn(),
          },
        },
        {
          provide: getRepositoryToken(FamilyMember),
          useValue: {
            create: jest.fn(),
            save: jest.fn(),
          },
        },
        {
          provide: JwtService,
          useValue: {
            sign: jest.fn(),
            verify: jest.fn(),
          },
        },
        {
          provide: ConfigService,
          useValue: {
            get: jest.fn((key: string, defaultValue?: any) => {
              const config = {
                JWT_SECRET: 'test-secret',
                JWT_REFRESH_SECRET: 'test-refresh-secret',
                JWT_EXPIRATION: '1h',
                JWT_REFRESH_EXPIRATION: '7d',
              };
              return config[key] || defaultValue;
            }),
          },
        },
        {
          provide: AuditService,
          useValue: {
            log: jest.fn(),
            logLogin: jest.fn(),
            logLogout: jest.fn(),
          },
        },
      ],
    }).compile();

    service = module.get<AuthService>(AuthService);
    userRepository = module.get<Repository<User>>(getRepositoryToken(User));
    deviceRepository = module.get<Repository<DeviceRegistry>>(
      getRepositoryToken(DeviceRegistry),
    );
    refreshTokenRepository = module.get<Repository<RefreshToken>>(
      getRepositoryToken(RefreshToken),
    );
    familyRepository = module.get<Repository<Family>>(
      getRepositoryToken(Family),
    );
    familyMemberRepository = module.get<Repository<FamilyMember>>(
      getRepositoryToken(FamilyMember),
    );
    jwtService = module.get<JwtService>(JwtService);
    configService = module.get<ConfigService>(ConfigService);
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('register', () => {
    const registerDto: RegisterDto = {
      email: 'test@example.com',
      password: 'SecurePass123!',
      name: 'Test User',
      phone: '+1234567890',
      locale: 'en-US',
      timezone: 'UTC',
      dateOfBirth: '1990-01-01',
      deviceInfo: {
        deviceId: 'device-123',
        platform: 'ios',
        model: 'iPhone 14',
        osVersion: '17.0',
      },
    };

    it('should successfully register a new user', async () => {
      jest.spyOn(userRepository, 'findOne').mockResolvedValue(null);
      jest.spyOn(userRepository, 'create').mockReturnValue(mockUser as any);
      jest.spyOn(userRepository, 'save').mockResolvedValue(mockUser as any);
      jest.spyOn(familyRepository, 'create').mockReturnValue(mockFamily as any);
      jest.spyOn(familyRepository, 'save').mockResolvedValue(mockFamily as any);
      jest.spyOn(familyMemberRepository, 'create').mockReturnValue({} as any);
      jest.spyOn(familyMemberRepository, 'save').mockResolvedValue({} as any);
      jest.spyOn(deviceRepository, 'create').mockReturnValue(mockDevice as any);
      jest.spyOn(deviceRepository, 'save').mockResolvedValue(mockDevice as any);
      jest.spyOn(jwtService, 'sign').mockReturnValue('mock-token');
      jest.spyOn(refreshTokenRepository, 'create').mockReturnValue({} as any);
      jest.spyOn(refreshTokenRepository, 'save').mockResolvedValue({} as any);

      const result = await service.register(registerDto);

      expect(result.success).toBe(true);
      expect(result.data.user.email).toBe(registerDto.email);
      expect(result.data.user.name).toBe(registerDto.name);
      expect(result.data.family.id).toBe(mockFamily.id);
      expect(result.data.family.shareCode).toBe(mockFamily.shareCode);
      expect(result.data.tokens.accessToken).toBe('mock-token');
      expect(result.data.deviceRegistered).toBe(true);
      expect(userRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({
          email: registerDto.email,
          name: registerDto.name,
        }),
      );
    });

    it('should throw ConflictException if user already exists', async () => {
      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser as any);

      await expect(service.register(registerDto)).rejects.toThrow(
        ConflictException,
      );
      expect(userRepository.findOne).toHaveBeenCalledWith({
        where: { email: registerDto.email },
      });
    });

    it('should hash password before saving', async () => {
      jest.spyOn(userRepository, 'findOne').mockResolvedValue(null);
      jest
        .spyOn(bcrypt, 'hash')
        .mockImplementation(() => Promise.resolve('hashedpassword'));
      jest.spyOn(userRepository, 'create').mockReturnValue(mockUser as any);
      jest.spyOn(userRepository, 'save').mockResolvedValue(mockUser as any);
      jest.spyOn(familyRepository, 'create').mockReturnValue(mockFamily as any);
      jest.spyOn(familyRepository, 'save').mockResolvedValue(mockFamily as any);
      jest.spyOn(familyMemberRepository, 'create').mockReturnValue({} as any);
      jest.spyOn(familyMemberRepository, 'save').mockResolvedValue({} as any);
      jest.spyOn(deviceRepository, 'create').mockReturnValue(mockDevice as any);
      jest.spyOn(deviceRepository, 'save').mockResolvedValue(mockDevice as any);
      jest.spyOn(jwtService, 'sign').mockReturnValue('mock-token');
      jest.spyOn(refreshTokenRepository, 'create').mockReturnValue({} as any);
      jest.spyOn(refreshTokenRepository, 'save').mockResolvedValue({} as any);

      await service.register(registerDto);

      expect(bcrypt.hash).toHaveBeenCalledWith(registerDto.password, 10);
    });

    it('should create family with user as parent', async () => {
      jest.spyOn(userRepository, 'findOne').mockResolvedValue(null);
      jest.spyOn(userRepository, 'create').mockReturnValue(mockUser as any);
      jest.spyOn(userRepository, 'save').mockResolvedValue(mockUser as any);
      jest.spyOn(familyRepository, 'create').mockReturnValue(mockFamily as any);
      jest.spyOn(familyRepository, 'save').mockResolvedValue(mockFamily as any);
      jest.spyOn(familyMemberRepository, 'create').mockReturnValue({} as any);
      jest.spyOn(familyMemberRepository, 'save').mockResolvedValue({} as any);
      jest.spyOn(deviceRepository, 'create').mockReturnValue(mockDevice as any);
      jest.spyOn(deviceRepository, 'save').mockResolvedValue(mockDevice as any);
      jest.spyOn(jwtService, 'sign').mockReturnValue('mock-token');
      jest.spyOn(refreshTokenRepository, 'create').mockReturnValue({} as any);
      jest.spyOn(refreshTokenRepository, 'save').mockResolvedValue({} as any);

      await service.register(registerDto);

      expect(familyMemberRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          userId: mockUser.id,
          familyId: mockFamily.id,
          role: FamilyRole.PARENT,
        }),
      );
    });
  });

  describe('login', () => {
    const loginDto: LoginDto = {
      email: 'test@example.com',
      password: 'SecurePass123!',
      deviceInfo: {
        deviceId: 'device-123',
        platform: 'ios',
        model: 'iPhone 14',
        osVersion: '17.0',
      },
    };

    it('should successfully login with valid credentials', async () => {
      const userWithRelations = {
        ...mockUser,
        familyMemberships: [{ familyId: 'fam_test123' }],
      };

      jest
        .spyOn(userRepository, 'findOne')
        .mockResolvedValue(userWithRelations as any);
      jest
        .spyOn(bcrypt, 'compare')
        .mockImplementation(() => Promise.resolve(true));
      jest
        .spyOn(deviceRepository, 'findOne')
        .mockResolvedValue(mockDevice as any);
      jest.spyOn(deviceRepository, 'save').mockResolvedValue(mockDevice as any);
      jest.spyOn(jwtService, 'sign').mockReturnValue('mock-token');
      jest.spyOn(refreshTokenRepository, 'create').mockReturnValue({} as any);
      jest.spyOn(refreshTokenRepository, 'save').mockResolvedValue({} as any);

      const result = await service.login(loginDto);

      expect(result.success).toBe(true);
      expect(result.data.user.email).toBe(mockUser.email);
      expect(result.data.tokens.accessToken).toBe('mock-token');
      expect(result.data.requiresMFA).toBe(false);
    });

    it('should throw UnauthorizedException if user not found', async () => {
      jest.spyOn(userRepository, 'findOne').mockResolvedValue(null);

      await expect(service.login(loginDto)).rejects.toThrow(
        UnauthorizedException,
      );
    });

    it('should throw UnauthorizedException if password is invalid', async () => {
      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser as any);
      jest
        .spyOn(bcrypt, 'compare')
        .mockImplementation(() => Promise.resolve(false));

      await expect(service.login(loginDto)).rejects.toThrow(
        UnauthorizedException,
      );
    });

    it('should register new device if not found', async () => {
      const userWithRelations = {
        ...mockUser,
        familyMemberships: [{ familyId: 'fam_test123' }],
      };

      jest
        .spyOn(userRepository, 'findOne')
        .mockResolvedValue(userWithRelations as any);
      jest
        .spyOn(bcrypt, 'compare')
        .mockImplementation(() => Promise.resolve(true));
      jest.spyOn(deviceRepository, 'findOne').mockResolvedValue(null);
      jest.spyOn(deviceRepository, 'create').mockReturnValue(mockDevice as any);
      jest.spyOn(deviceRepository, 'save').mockResolvedValue(mockDevice as any);
      jest.spyOn(jwtService, 'sign').mockReturnValue('mock-token');
      jest.spyOn(refreshTokenRepository, 'create').mockReturnValue({} as any);
      jest.spyOn(refreshTokenRepository, 'save').mockResolvedValue({} as any);

      await service.login(loginDto);

      expect(deviceRepository.create).toHaveBeenCalled();
      expect(deviceRepository.save).toHaveBeenCalled();
    });

    it('should update last seen for existing device', async () => {
      const userWithRelations = {
        ...mockUser,
        familyMemberships: [{ familyId: 'fam_test123' }],
      };

      jest
        .spyOn(userRepository, 'findOne')
        .mockResolvedValue(userWithRelations as any);
      jest
        .spyOn(bcrypt, 'compare')
        .mockImplementation(() => Promise.resolve(true));
      jest
        .spyOn(deviceRepository, 'findOne')
        .mockResolvedValue(mockDevice as any);
      jest.spyOn(deviceRepository, 'save').mockResolvedValue(mockDevice as any);
      jest.spyOn(jwtService, 'sign').mockReturnValue('mock-token');
      jest.spyOn(refreshTokenRepository, 'create').mockReturnValue({} as any);
      jest.spyOn(refreshTokenRepository, 'save').mockResolvedValue({} as any);

      await service.login(loginDto);

      expect(deviceRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({
          lastSeen: expect.any(Date),
        }),
      );
    });
  });

  describe('refreshAccessToken', () => {
    const refreshTokenDto: RefreshTokenDto = {
      refreshToken: 'valid-refresh-token',
      deviceId: 'dev_test123',
    };

    it('should successfully refresh access token', async () => {
      const payload = {
        sub: 'usr_test123',
        email: 'test@example.com',
        deviceId: 'dev_test123',
      };

      jest.spyOn(jwtService, 'verify').mockReturnValue(payload);
      jest
        .spyOn(refreshTokenRepository, 'findOne')
        .mockResolvedValue(mockRefreshToken as any);
      jest.spyOn(refreshTokenRepository, 'save').mockResolvedValue({} as any);
      jest.spyOn(jwtService, 'sign').mockReturnValue('new-token');
      jest.spyOn(refreshTokenRepository, 'create').mockReturnValue({} as any);

      const result = await service.refreshAccessToken(refreshTokenDto);

      expect(result.success).toBe(true);
      expect(result.data.tokens.accessToken).toBe('new-token');
      expect(refreshTokenRepository.findOne).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            deviceId: payload.deviceId,
          }),
        }),
      );
      expect(refreshTokenRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({
          revoked: true,
          revokedAt: expect.any(Date),
        }),
      );
    });

    it('should refresh even when deviceId is missing from request', async () => {
      const payload = {
        sub: 'usr_test123',
        email: 'test@example.com',
        deviceId: 'dev_test123',
      };

      jest.spyOn(jwtService, 'verify').mockReturnValue(payload);
      jest
        .spyOn(refreshTokenRepository, 'findOne')
        .mockResolvedValue(mockRefreshToken as any);
      jest.spyOn(refreshTokenRepository, 'save').mockResolvedValue({} as any);
      jest.spyOn(jwtService, 'sign').mockReturnValue('new-token');
      jest.spyOn(refreshTokenRepository, 'create').mockReturnValue({} as any);

      const dtoWithoutDevice: RefreshTokenDto = {
        refreshToken: 'valid-refresh-token',
      };

      const result = await service.refreshAccessToken(dtoWithoutDevice);

      expect(result.success).toBe(true);
      expect(refreshTokenRepository.findOne).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            deviceId: payload.deviceId,
          }),
        }),
      );
    });

    it('should throw UnauthorizedException if token not found', async () => {
      const payload = {
        sub: 'usr_test123',
        email: 'test@example.com',
        deviceId: 'dev_test123',
      };

      jest.spyOn(jwtService, 'verify').mockReturnValue(payload);
      jest.spyOn(refreshTokenRepository, 'findOne').mockResolvedValue(null);

      await expect(service.refreshAccessToken(refreshTokenDto)).rejects.toThrow(
        UnauthorizedException,
      );
    });

    it('should throw UnauthorizedException if token is expired', async () => {
      const payload = {
        sub: 'usr_test123',
        email: 'test@example.com',
        deviceId: 'dev_test123',
      };

      const expiredToken = {
        ...mockRefreshToken,
        expiresAt: new Date(Date.now() - 1000),
      };

      jest.spyOn(jwtService, 'verify').mockReturnValue(payload);
      jest
        .spyOn(refreshTokenRepository, 'findOne')
        .mockResolvedValue(expiredToken as any);

      await expect(service.refreshAccessToken(refreshTokenDto)).rejects.toThrow(
        UnauthorizedException,
      );
    });

    it('should throw UnauthorizedException if token is revoked', async () => {
      const payload = {
        sub: 'usr_test123',
        email: 'test@example.com',
        deviceId: 'dev_test123',
      };

      jest.spyOn(jwtService, 'verify').mockReturnValue(payload);
      jest.spyOn(refreshTokenRepository, 'findOne').mockResolvedValue(null);

      await expect(service.refreshAccessToken(refreshTokenDto)).rejects.toThrow(
        UnauthorizedException,
      );
    });
  });

  describe('logout', () => {
    it('should revoke refresh tokens for specific device', async () => {
      const logoutDto: LogoutDto = {
        deviceId: 'dev_test123',
        allDevices: false,
      };

      jest.spyOn(refreshTokenRepository, 'update').mockResolvedValue({} as any);

      const result = await service.logout('usr_test123', logoutDto);

      expect(result.success).toBe(true);
      expect(result.message).toBe('Successfully logged out');
      expect(refreshTokenRepository.update).toHaveBeenCalledWith(
        { userId: 'usr_test123', deviceId: 'dev_test123', revoked: false },
        { revoked: true, revokedAt: expect.any(Date) },
      );
    });

    it('should revoke all refresh tokens when allDevices is true', async () => {
      const logoutDto: LogoutDto = {
        deviceId: 'dev_test123',
        allDevices: true,
      };

      jest.spyOn(refreshTokenRepository, 'update').mockResolvedValue({} as any);

      const result = await service.logout('usr_test123', logoutDto);

      expect(result.success).toBe(true);
      expect(refreshTokenRepository.update).toHaveBeenCalledWith(
        { userId: 'usr_test123', revoked: false },
        { revoked: true, revokedAt: expect.any(Date) },
      );
    });
  });

  describe('validateUser', () => {
    it('should return user if credentials are valid', async () => {
      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser as any);
      jest
        .spyOn(bcrypt, 'compare')
        .mockImplementation(() => Promise.resolve(true));

      const result = await service.validateUser(
        'test@example.com',
        'SecurePass123!',
      );

      expect(result).toEqual(mockUser);
    });

    it('should return null if user not found', async () => {
      jest.spyOn(userRepository, 'findOne').mockResolvedValue(null);

      const result = await service.validateUser(
        'test@example.com',
        'SecurePass123!',
      );

      expect(result).toBeNull();
    });

    it('should return null if password is invalid', async () => {
      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser as any);
      jest
        .spyOn(bcrypt, 'compare')
        .mockImplementation(() => Promise.resolve(false));

      const result = await service.validateUser(
        'test@example.com',
        'WrongPassword',
      );

      expect(result).toBeNull();
    });
  });
});
