const UserService = require('../../../services/UserService');
const { User, UserAddress } = require('../../../models');
const emailServices = require('../../../services/email');
const logger = require('../../../utils/logger');

// Mock dependencies
jest.mock('../../../models', () => ({
  User: {
    findByPk: jest.fn(),
  },
  UserAddress: {
    create: jest.fn(),
    findOne: jest.fn(),
  },
}));

jest.mock('../../../services/email', () => ({
  auth: {
    sendPersonalInfoChangedEmail: jest.fn().mockResolvedValue(),
  },
}));

jest.mock('../../../utils/logger', () => ({
  info: jest.fn(),
  error: jest.fn(),
  warn: jest.fn(),
}));

describe('UserService', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    process.env.NODE_ENV = 'test';
  });

  describe('updateProfile', () => {
    const mockUser = {
      id: 'user-123',
      email: 'original@example.com',
      firstName: 'John',
      lastName: 'Doe',
      address1: '123 Main St',
      address2: null,
      city: 'New York',
      state: 'NY',
      zipCode: '10001',
      country: 'USA',
      update: jest.fn().mockResolvedValue(),
    };

    it('should update user profile successfully', async () => {
      User.findByPk
        .mockResolvedValueOnce(mockUser) // First call to find user
        .mockResolvedValueOnce({ ...mockUser, firstName: 'Jane' }); // Second call for return

      const updateData = { firstName: 'Jane' };
      const result = await UserService.updateProfile('user-123', updateData);

      expect(User.findByPk).toHaveBeenCalledWith('user-123');
      expect(mockUser.update).toHaveBeenCalledWith({ firstName: 'Jane' }, {});
      expect(result.firstName).toBe('Jane');
    });

    it('should throw error when user not found', async () => {
      User.findByPk.mockResolvedValue(null);

      await expect(UserService.updateProfile('non-existent', { firstName: 'Test' }))
        .rejects.toThrow('User not found');
    });

    it('should trim email and ignore empty email', async () => {
      User.findByPk
        .mockResolvedValueOnce(mockUser)
        .mockResolvedValueOnce(mockUser);

      await UserService.updateProfile('user-123', { email: '  new@example.com  ' });

      expect(mockUser.update).toHaveBeenCalledWith(
        expect.objectContaining({ email: 'new@example.com' }),
        {}
      );
    });

    it('should not update email if empty string', async () => {
      User.findByPk
        .mockResolvedValueOnce(mockUser)
        .mockResolvedValueOnce(mockUser);

      await UserService.updateProfile('user-123', { email: '   ' });

      // Email should not be in the update call
      expect(mockUser.update).toHaveBeenCalledWith({}, {});
    });

    it('should convert empty phone to null', async () => {
      User.findByPk
        .mockResolvedValueOnce(mockUser)
        .mockResolvedValueOnce(mockUser);

      await UserService.updateProfile('user-123', { phone: '' });

      expect(mockUser.update).toHaveBeenCalledWith(
        expect.objectContaining({ phone: null }),
        {}
      );
    });

    it('should trim phone number', async () => {
      User.findByPk
        .mockResolvedValueOnce(mockUser)
        .mockResolvedValueOnce(mockUser);

      await UserService.updateProfile('user-123', { phone: '  555-1234  ' });

      expect(mockUser.update).toHaveBeenCalledWith(
        expect.objectContaining({ phone: '555-1234' }),
        {}
      );
    });

    it('should pass options to update call', async () => {
      User.findByPk
        .mockResolvedValueOnce(mockUser)
        .mockResolvedValueOnce(mockUser);

      const mockTransaction = { id: 'tx-123' };
      await UserService.updateProfile('user-123', { firstName: 'Jane' }, { transaction: mockTransaction });

      expect(mockUser.update).toHaveBeenCalledWith(
        expect.any(Object),
        { transaction: mockTransaction }
      );
    });

    it('should not send email notification in test environment', async () => {
      User.findByPk
        .mockResolvedValueOnce(mockUser)
        .mockResolvedValueOnce({ ...mockUser, firstName: 'Jane' });

      await UserService.updateProfile('user-123', { firstName: 'Jane' });

      // Email should not be sent in test environment
      expect(emailServices.auth.sendPersonalInfoChangedEmail).not.toHaveBeenCalled();
    });

    it('should send email notification in production when personal info changes', async () => {
      process.env.NODE_ENV = 'production';

      User.findByPk
        .mockResolvedValueOnce(mockUser)
        .mockResolvedValueOnce({ ...mockUser, firstName: 'Jane' });

      await UserService.updateProfile('user-123', { firstName: 'Jane' });

      expect(emailServices.auth.sendPersonalInfoChangedEmail).toHaveBeenCalledWith(mockUser);
      expect(logger.info).toHaveBeenCalledWith(
        'Personal information changed notification sent',
        expect.objectContaining({
          userId: 'user-123',
          changedFields: ['firstName'],
        })
      );
    });

    it('should handle email notification failure gracefully', async () => {
      process.env.NODE_ENV = 'production';
      emailServices.auth.sendPersonalInfoChangedEmail.mockRejectedValueOnce(
        new Error('Email service down')
      );

      User.findByPk
        .mockResolvedValueOnce(mockUser)
        .mockResolvedValueOnce({ ...mockUser, email: 'new@example.com' });

      // Should not throw despite email failure
      const result = await UserService.updateProfile('user-123', { email: 'new@example.com' });

      expect(result).toBeDefined();
      expect(logger.error).toHaveBeenCalledWith(
        'Failed to send personal information changed notification',
        expect.objectContaining({
          error: 'Email service down',
          userId: 'user-123',
        })
      );
    });
  });

  describe('createUserAddress', () => {
    const mockUser = {
      id: 'user-123',
      email: 'user@example.com',
    };

    const addressData = {
      label: 'Home',
      address1: '456 Oak Ave',
      city: 'Boston',
      state: 'MA',
      zipCode: '02101',
      country: 'USA',
    };

    it('should create a new address successfully', async () => {
      const mockAddress = { id: 'addr-123', ...addressData, userId: 'user-123' };

      User.findByPk.mockResolvedValue(mockUser);
      UserAddress.create.mockResolvedValue(mockAddress);

      const result = await UserService.createUserAddress('user-123', addressData);

      expect(User.findByPk).toHaveBeenCalledWith('user-123');
      expect(UserAddress.create).toHaveBeenCalledWith({
        ...addressData,
        userId: 'user-123',
      });
      expect(result.id).toBe('addr-123');
    });

    it('should throw error when user not found', async () => {
      User.findByPk.mockResolvedValue(null);

      await expect(UserService.createUserAddress('non-existent', addressData))
        .rejects.toThrow('User not found');
    });

    it('should send notification in production', async () => {
      process.env.NODE_ENV = 'production';
      const mockAddress = { id: 'addr-123', ...addressData };

      User.findByPk.mockResolvedValue(mockUser);
      UserAddress.create.mockResolvedValue(mockAddress);

      await UserService.createUserAddress('user-123', addressData);

      expect(emailServices.auth.sendPersonalInfoChangedEmail).toHaveBeenCalledWith(mockUser);
    });
  });

  describe('updateUserAddress', () => {
    const mockAddress = {
      id: 'addr-123',
      userId: 'user-123',
      address1: '123 Old St',
      update: jest.fn().mockResolvedValue(),
    };

    it('should update address successfully', async () => {
      UserAddress.findOne.mockResolvedValue(mockAddress);
      User.findByPk.mockResolvedValue({ id: 'user-123', email: 'user@example.com' });

      const result = await UserService.updateUserAddress('user-123', 'addr-123', {
        address1: '789 New St',
      });

      expect(UserAddress.findOne).toHaveBeenCalledWith({
        where: { id: 'addr-123', userId: 'user-123' },
      });
      expect(mockAddress.update).toHaveBeenCalledWith({ address1: '789 New St' });
      expect(result.id).toBe('addr-123');
    });

    it('should throw error when address not found', async () => {
      UserAddress.findOne.mockResolvedValue(null);

      await expect(
        UserService.updateUserAddress('user-123', 'non-existent', { address1: 'New' })
      ).rejects.toThrow('Address not found');
    });

    it('should send notification in production', async () => {
      process.env.NODE_ENV = 'production';
      const mockUser = { id: 'user-123', email: 'user@example.com' };

      UserAddress.findOne.mockResolvedValue(mockAddress);
      User.findByPk.mockResolvedValue(mockUser);

      await UserService.updateUserAddress('user-123', 'addr-123', { city: 'Chicago' });

      expect(emailServices.auth.sendPersonalInfoChangedEmail).toHaveBeenCalledWith(mockUser);
    });

    it('should handle email failure gracefully', async () => {
      process.env.NODE_ENV = 'production';
      emailServices.auth.sendPersonalInfoChangedEmail.mockRejectedValueOnce(
        new Error('Email failed')
      );

      UserAddress.findOne.mockResolvedValue(mockAddress);
      User.findByPk.mockResolvedValue({ id: 'user-123', email: 'user@example.com' });

      const result = await UserService.updateUserAddress('user-123', 'addr-123', { city: 'Chicago' });

      expect(result).toBeDefined();
      expect(logger.error).toHaveBeenCalled();
    });
  });

  describe('deleteUserAddress', () => {
    const mockAddress = {
      id: 'addr-123',
      userId: 'user-123',
      destroy: jest.fn().mockResolvedValue(),
    };

    it('should delete address successfully', async () => {
      UserAddress.findOne.mockResolvedValue(mockAddress);
      User.findByPk.mockResolvedValue({ id: 'user-123', email: 'user@example.com' });

      await UserService.deleteUserAddress('user-123', 'addr-123');

      expect(UserAddress.findOne).toHaveBeenCalledWith({
        where: { id: 'addr-123', userId: 'user-123' },
      });
      expect(mockAddress.destroy).toHaveBeenCalled();
    });

    it('should throw error when address not found', async () => {
      UserAddress.findOne.mockResolvedValue(null);

      await expect(
        UserService.deleteUserAddress('user-123', 'non-existent')
      ).rejects.toThrow('Address not found');
    });

    it('should send notification in production', async () => {
      process.env.NODE_ENV = 'production';
      const mockUser = { id: 'user-123', email: 'user@example.com' };

      UserAddress.findOne.mockResolvedValue(mockAddress);
      User.findByPk.mockResolvedValue(mockUser);

      await UserService.deleteUserAddress('user-123', 'addr-123');

      expect(emailServices.auth.sendPersonalInfoChangedEmail).toHaveBeenCalledWith(mockUser);
    });

    it('should handle email failure gracefully', async () => {
      process.env.NODE_ENV = 'production';
      emailServices.auth.sendPersonalInfoChangedEmail.mockRejectedValueOnce(
        new Error('Email failed')
      );

      UserAddress.findOne.mockResolvedValue(mockAddress);
      User.findByPk.mockResolvedValue({ id: 'user-123', email: 'user@example.com' });

      // Should not throw
      await UserService.deleteUserAddress('user-123', 'addr-123');

      expect(logger.error).toHaveBeenCalled();
    });
  });
});
