import { BadRequestException, ConflictException, UnauthorizedException } from '@nestjs/common';
import * as fs from 'fs';
import * as path from 'path';
import { validate } from 'class-validator';
import { hashValue } from '../../common/utils/hash.util';
import { ChangePasswordDto } from './dto/change-password.dto';
import { AuthService } from './auth.service';

describe('AuthService change password', () => {
  const createService = (passwordHash: string) => {
    const usersService = {
      findByIdWithPassword: jest.fn().mockResolvedValue({ id: 'user-1', password: passwordHash }),
      updatePassword: jest.fn().mockResolvedValue(undefined),
    };
    const authRepository = {
      revokeAllUserTokens: jest.fn().mockResolvedValue(undefined),
      removeExpiredAndRevoked: jest.fn().mockResolvedValue(undefined),
    };
    const configService = {
      get: jest.fn((key: string) => (key === 'security.bcryptSaltRounds' ? 8 : undefined)),
    };
    const service = new AuthService(
      usersService as any,
      authRepository as any,
      {} as any,
      configService as any,
      {} as any,
    );

    return { service, usersService, authRepository };
  };

  it('changes password and does not return the hash', async () => {
    const passwordHash = await hashValue('OldStrongPass123!', 8);
    const { service, usersService, authRepository } = createService(passwordHash);

    const result = await service.changePassword('user-1', {
      currentPassword: 'OldStrongPass123!',
      newPassword: 'NewStrongPass123!',
    });

    expect(result).toEqual({ message: 'Password changed successfully' });
    expect(usersService.updatePassword).toHaveBeenCalledWith('user-1', expect.any(String));
    expect(authRepository.revokeAllUserTokens).toHaveBeenCalledWith('user-1');
    expect(JSON.stringify(result)).not.toContain('password');
  });

  it('rejects an incorrect current password', async () => {
    const passwordHash = await hashValue('OldStrongPass123!', 8);
    const { service, usersService } = createService(passwordHash);

    await expect(
      service.changePassword('user-1', {
        currentPassword: 'WrongStrongPass123!',
        newPassword: 'NewStrongPass123!',
      }),
    ).rejects.toBeInstanceOf(UnauthorizedException);
    expect(usersService.updatePassword).not.toHaveBeenCalled();
  });

  it('rejects weak new passwords through DTO validation', async () => {
    const dto = Object.assign(new ChangePasswordDto(), {
      currentPassword: 'OldStrongPass123!',
      newPassword: 'weakpass',
    });

    const errors = await validate(dto);

    expect(errors.some((error) => error.property === 'newPassword')).toBe(true);
  });
});

describe('AuthService Google token login', () => {
  const googleClientId =
    '581263241018-j2rbg7nco2g341m65c76tfltgl5l2vho.apps.googleusercontent.com';

  const createService = () => {
    const storedUser = {
      id: 'user-1',
      username: 'google.user',
      role: 'user',
      googleId: 'google-sub-1',
      isDisabled: false,
      toObject: () => ({ id: 'user-1', email: 'google.user@example.com' }),
    };
    const usersService = {
      findByGoogleId: jest.fn().mockResolvedValue(storedUser),
      findByEmail: jest.fn(),
      findByIdOrFail: jest.fn().mockResolvedValue(storedUser),
      linkGoogleAccount: jest.fn(),
      create: jest.fn(),
      findByUsername: jest.fn(),
    };
    const authRepository = {
      createRefreshToken: jest.fn().mockResolvedValue(undefined),
    };
    const jwtService = {
      signAsync: jest.fn().mockResolvedValueOnce('access-token').mockResolvedValueOnce('refresh-token'),
    };
    const configService = {
      get: jest.fn((key: string) => {
        const values: Record<string, string | number> = {
          'google.clientId': ` ${googleClientId} `,
          'jwt.accessSecret': 'access-secret-123',
          'jwt.accessExpiresIn': '15m',
          'jwt.refreshSecret': 'refresh-secret-123',
          'jwt.refreshExpiresIn': '30d',
          'security.refreshTokenHashSecret': 'refresh-hash-secret-123',
        };

        return values[key];
      }),
    };
    const googleOAuthClient = {
      verifyIdToken: jest.fn(),
    };
    const service = new AuthService(
      usersService as any,
      authRepository as any,
      jwtService as any,
      configService as any,
      {} as any,
    );
    (service as any).googleOAuthClient = googleOAuthClient;

    return { service, usersService, googleOAuthClient };
  };

  it('verifies a Google ID token with the configured Web Client ID audience', async () => {
    const { service, usersService, googleOAuthClient } = createService();
    googleOAuthClient.verifyIdToken.mockResolvedValue({
      getPayload: () => ({
        sub: 'google-sub-1',
        email: 'Google.User@example.com',
        email_verified: true,
        name: 'Google User',
        picture: 'https://example.com/avatar.png',
      }),
    });

    const result = await service.loginWithGoogleIdToken({
      idToken: 'valid-google-id-token-with-correct-audience',
    });

    expect(googleOAuthClient.verifyIdToken).toHaveBeenCalledWith({
      idToken: 'valid-google-id-token-with-correct-audience',
      audience: googleClientId,
    });
    expect(usersService.findByGoogleId).toHaveBeenCalledWith('google-sub-1');
    expect(result.accessToken).toBe('access-token');
  });

  it('rejects a Google ID token when the audience is wrong', async () => {
    const { service, googleOAuthClient } = createService();
    googleOAuthClient.verifyIdToken.mockRejectedValue(new Error('Wrong recipient, payload audience mismatch'));

    await expect(
      service.loginWithGoogleIdToken({
        idToken: 'valid-google-id-token-with-wrong-audience',
      }),
    ).rejects.toBeInstanceOf(UnauthorizedException);

    expect(googleOAuthClient.verifyIdToken).toHaveBeenCalledWith({
      idToken: 'valid-google-id-token-with-wrong-audience',
      audience: googleClientId,
    });
  });

  it('rejects an invalid Google ID token', async () => {
    const { service, googleOAuthClient } = createService();
    googleOAuthClient.verifyIdToken.mockRejectedValue(new Error('Invalid token signature'));

    await expect(
      service.loginWithGoogleIdToken({
        idToken: 'not-a-valid-google-id-token',
      }),
    ).rejects.toBeInstanceOf(UnauthorizedException);
  });

  it('keeps the Google ID token path on google-auth-library instead of Firebase Admin', () => {
    const authServiceSource = fs.readFileSync(path.join(__dirname, 'auth.service.ts'), 'utf8');

    expect(authServiceSource).toContain('googleOAuthClient.verifyIdToken');
    expect(authServiceSource).not.toContain('firebase-admin');
    expect(authServiceSource).not.toContain('admin.auth().verifyIdToken');
  });
});

describe('AuthService deferred signup flow', () => {
  const createDeferredSignupService = (overrides?: {
    usersService?: Record<string, jest.Mock>;
    authRepository?: Record<string, jest.Mock>;
    jwtService?: Record<string, jest.Mock>;
  }) => {
    const usersService = {
      findByEmail: jest.fn().mockResolvedValue(null),
      findByUsername: jest.fn().mockResolvedValue(null),
      createWithOptionalAvatarFile: jest.fn(),
      findByIdOrFail: jest.fn(),
      rollbackSignupCreatedUser: jest.fn(),
      ...(overrides?.usersService ?? {}),
    };
    const authRepository = {
      upsertSignupDraft: jest.fn().mockResolvedValue({ id: 'draft-1' }),
      findSignupDraftWithSecrets: jest.fn(),
      incrementSignupDraftAttempts: jest.fn().mockResolvedValue(undefined),
      markSignupDraftVerified: jest.fn().mockResolvedValue(undefined),
      findSignupDraftByTokenHash: jest.fn(),
      markSignupDraftConsumed: jest.fn().mockResolvedValue(undefined),
      createRefreshToken: jest.fn().mockResolvedValue(undefined),
      ...(overrides?.authRepository ?? {}),
    };
    const jwtService = {
      signAsync: jest.fn().mockResolvedValueOnce('access-token').mockResolvedValueOnce('refresh-token'),
      ...(overrides?.jwtService ?? {}),
    };
    const configService = {
      get: jest.fn((key: string) => {
        const values: Record<string, string | number> = {
          nodeEnv: 'test',
          'security.bcryptSaltRounds': 4,
          'emailVerification.maxAttempts': 5,
          'jwt.accessSecret': 'access-secret',
          'jwt.accessExpiresIn': '15m',
          'jwt.refreshSecret': 'refresh-secret',
          'jwt.refreshExpiresIn': '30d',
          'security.refreshTokenHashSecret': 'refresh-hash-secret',
        };
        return values[key];
      }),
    };
    const emailService = {
      sendVerificationCode: jest.fn().mockResolvedValue(undefined),
    };

    const service = new AuthService(
      usersService as any,
      authRepository as any,
      jwtService as any,
      configService as any,
      emailService as any,
    );

    return { service, usersService, authRepository, jwtService, emailService };
  };

  it('signup/start sends an OTP and does not create a user', async () => {
    const { service, usersService, authRepository, emailService } = createDeferredSignupService();

    const result = await service.signupStart({
      email: 'New.User@Example.com',
      password: 'StrongPass123!',
      confirmPassword: 'StrongPass123!',
    });

    expect(result).toMatchObject({
      message: 'Verification code sent',
      email: 'new.user@example.com',
      signupId: 'draft-1',
    });
    expect(result.debugCode).toMatch(/^\d{6}$/);
    expect(usersService.findByEmail).toHaveBeenCalledWith('new.user@example.com');
    expect(usersService.createWithOptionalAvatarFile).not.toHaveBeenCalled();
    expect(authRepository.upsertSignupDraft).toHaveBeenCalledWith(
      expect.objectContaining({ email: 'new.user@example.com' }),
    );
    expect(emailService.sendVerificationCode).toHaveBeenCalledWith(
      'new.user@example.com',
      expect.stringMatching(/^\d{6}$/),
      20,
    );
  });

  it('signup/start fails if the email is already used', async () => {
    const { service, authRepository } = createDeferredSignupService({
      usersService: { findByEmail: jest.fn().mockResolvedValue({ id: 'existing-user' }) },
    });

    await expect(
      service.signupStart({
        email: 'used@example.com',
        password: 'StrongPass123!',
        confirmPassword: 'StrongPass123!',
      }),
    ).rejects.toBeInstanceOf(ConflictException);
    expect(authRepository.upsertSignupDraft).not.toHaveBeenCalled();
  });

  it('signup/start fails if password confirmation does not match', async () => {
    const { service, authRepository } = createDeferredSignupService();

    await expect(
      service.signupStart({
        email: 'new@example.com',
        password: 'StrongPass123!',
        confirmPassword: 'DifferentPass123!',
      }),
    ).rejects.toBeInstanceOf(BadRequestException);
    expect(authRepository.upsertSignupDraft).not.toHaveBeenCalled();
  });

  it('signup/verify returns a signup token without auth tokens', async () => {
    const codeHash = await hashValue('123456', 4);
    const { service, authRepository, usersService } = createDeferredSignupService();
    authRepository.findSignupDraftWithSecrets.mockResolvedValue({
      id: 'draft-1',
      email: 'new@example.com',
      codeHash,
      codeExpiresAt: new Date(Date.now() + 60_000),
      expiresAt: new Date(Date.now() + 60_000),
      attempts: 0,
      consumed: false,
    });

    const result = await service.signupVerify({
      signupId: 'draft-1',
      email: 'new@example.com',
      code: '123456',
    });

    expect(result.message).toBe('Email verified');
    expect(result.signupToken).toEqual(expect.any(String));
    expect((result as any).accessToken).toBeUndefined();
    expect((result as any).refreshToken).toBeUndefined();
    expect(authRepository.markSignupDraftVerified).toHaveBeenCalledWith(
      'draft-1',
      expect.stringMatching(/^sha256:/),
      expect.any(Date),
    );
    expect(usersService.createWithOptionalAvatarFile).not.toHaveBeenCalled();
  });

  it('signup/verify fails with a wrong code', async () => {
    const codeHash = await hashValue('123456', 4);
    const { service, authRepository } = createDeferredSignupService();
    authRepository.findSignupDraftWithSecrets.mockResolvedValue({
      id: 'draft-1',
      email: 'new@example.com',
      codeHash,
      codeExpiresAt: new Date(Date.now() + 60_000),
      expiresAt: new Date(Date.now() + 60_000),
      attempts: 0,
      consumed: false,
    });

    await expect(
      service.signupVerify({
        signupId: 'draft-1',
        email: 'new@example.com',
        code: '654321',
      }),
    ).rejects.toBeInstanceOf(BadRequestException);
    expect(authRepository.incrementSignupDraftAttempts).toHaveBeenCalledWith('draft-1');
  });

  it('signup/complete creates the user and returns auth tokens', async () => {
    const createdUser = {
      id: 'user-1',
      username: 'new_user',
      role: 'user',
      avatar: '',
    };
    const safeUser = {
      toObject: () => ({ id: 'user-1', email: 'new@example.com', username: 'new_user' }),
    };
    const { service, usersService, authRepository } = createDeferredSignupService();
    authRepository.findSignupDraftByTokenHash.mockResolvedValue({
      id: 'draft-1',
      email: 'new@example.com',
      passwordHash: 'hashed-password',
      verifiedAt: new Date(),
      signupTokenExpiresAt: new Date(Date.now() + 60_000),
      expiresAt: new Date(Date.now() + 60_000),
      consumed: false,
    });
    usersService.createWithOptionalAvatarFile.mockResolvedValue(createdUser);
    usersService.findByIdOrFail.mockResolvedValue(safeUser);

    const result = await service.signupComplete({
      signupToken: 'temporary-token',
      name: 'New User',
      username: 'new_user',
      musicRoles: [],
      musicGenres: ['Tarab'],
      favoriteInstruments: ['Oud'],
      favoriteMaqamat: ['Bayati'],
    });

    expect(usersService.createWithOptionalAvatarFile).toHaveBeenCalledWith(
      expect.objectContaining({
        email: 'new@example.com',
        password: 'hashed-password',
        isEmailVerified: true,
      }),
      undefined,
    );
    expect(result).toMatchObject({
      message: 'Account created successfully',
      accessToken: 'access-token',
      refreshToken: 'refresh-token',
      user: { id: 'user-1' },
    });
    expect(authRepository.markSignupDraftConsumed).toHaveBeenCalledWith('draft-1');
  });

  it('signup/complete fails with an invalid signup token', async () => {
    const { service, usersService } = createDeferredSignupService();

    await expect(
      service.signupComplete({
        signupToken: 'bad-token',
        name: 'New User',
      }),
    ).rejects.toBeInstanceOf(UnauthorizedException);
    expect(usersService.createWithOptionalAvatarFile).not.toHaveBeenCalled();
  });

  it('rolls back the created user if complete fails after user creation', async () => {
    const createdUser = {
      id: 'user-1',
      username: 'new_user',
      role: 'user',
      avatar: 'uploads/users/avatar/avatar.webp',
    };
    const { service, usersService, authRepository } = createDeferredSignupService({
      authRepository: {
        markSignupDraftConsumed: jest.fn().mockRejectedValue(new Error('draft update failed')),
      },
    });
    authRepository.findSignupDraftByTokenHash.mockResolvedValue({
      id: 'draft-1',
      email: 'new@example.com',
      passwordHash: 'hashed-password',
      verifiedAt: new Date(),
      signupTokenExpiresAt: new Date(Date.now() + 60_000),
      expiresAt: new Date(Date.now() + 60_000),
      consumed: false,
    });
    usersService.createWithOptionalAvatarFile.mockResolvedValue(createdUser);

    await expect(
      service.signupComplete({
        signupToken: 'temporary-token',
        name: 'New User',
        username: 'new_user',
      }),
    ).rejects.toThrow('draft update failed');
    expect(usersService.rollbackSignupCreatedUser).toHaveBeenCalledWith(
      'user-1',
      'uploads/users/avatar/avatar.webp',
    );
  });
});
