// Mock dependencies - these must be at the top level
jest.mock('../../models/UserModel');
jest.mock('../../services/sessionService');
jest.mock('../../utils/auth', () => ({
  generateAuthTokens: jest.fn(),
  verifyRefreshToken: jest.fn(),
  hashPassword: jest.fn(),
  comparePassword: jest.fn(),
  validatePassword: jest.fn()
}));
jest.mock('../../utils/logger', () => ({
  info: jest.fn(),
  error: jest.fn()
}));

import { Response } from 'express';
import {
  register,
  login,
  logout,
  refreshToken,
  getProfile,
  updateProfile
} from '../authController';
import { UserModel } from '../../models/UserModel';
import { sessionService } from '../../services/sessionService';

import { AuthenticatedRequest } from '../../middleware/auth';

// Import mocked modules
const mockUserModel = UserModel as jest.Mocked<typeof UserModel>;
const mockSessionService = sessionService as jest.Mocked<typeof sessionService>;
const mockAuthUtils = jest.requireMock('../../utils/auth');

describe('Auth Controller', () => {
  let mockRequest: Partial<AuthenticatedRequest>;
  let mockResponse: Partial<Response>;

  beforeEach(() => {
    mockRequest = {
      body: {},
      headers: {}
    };
    mockResponse = {
      status: jest.fn().mockReturnThis(),
      json: jest.fn().mockReturnThis()
    };

    // Reset all mocks
    jest.clearAllMocks();
    
    // Setup default mock implementations
    mockUserModel.findByEmail.mockResolvedValue(null);
    mockUserModel.create.mockResolvedValue({} as any);
    mockUserModel.findById.mockResolvedValue({} as any);
    mockUserModel.updateLastLogin.mockResolvedValue();
    mockAuthUtils.hashPassword.mockResolvedValue('hashed-password');
    mockAuthUtils.generateAuthTokens.mockReturnValue({
      accessToken: 'access-token',
      refreshToken: 'refresh-token',
      expiresIn: 3600
    });
    mockAuthUtils.validatePassword.mockReturnValue({
      isValid: true,
      errors: []
    });
    mockSessionService.storeSession.mockResolvedValue();
    mockSessionService.removeSession.mockResolvedValue();
    mockSessionService.getSession.mockResolvedValue(null);
  });

  describe('register', () => {
    const validUserData = {
      email: 'test@example.com',
      name: 'Test User',
      password: 'StrongPass123!'
    };

    it('should register a new user successfully', async () => {
      mockRequest.body = validUserData;
      
      const mockUser = {
        id: 'user-123',
        email: validUserData.email,
        name: validUserData.name,
        role: 'user'
      };

      const mockTokens = {
        accessToken: 'access-token',
        refreshToken: 'refresh-token',
        expiresIn: 3600
      };

      mockUserModel.findByEmail.mockResolvedValue(null);
      mockUserModel.create.mockResolvedValue(mockUser as any);
      mockAuthUtils.hashPassword.mockResolvedValue('hashed-password');
      mockAuthUtils.generateAuthTokens.mockReturnValue(mockTokens);
      mockSessionService.storeSession.mockResolvedValue();

      await register(mockRequest as any, mockResponse as any);

      expect(mockUserModel.findByEmail).toHaveBeenCalledWith(validUserData.email);
      expect(mockUserModel.create).toHaveBeenCalledWith({
        email: validUserData.email,
        name: validUserData.name,
        password: 'hashed-password',
        role: 'user'
      });
      expect(mockAuthUtils.generateAuthTokens).toHaveBeenCalledWith({
        userId: mockUser.id,
        email: mockUser.email,
        role: mockUser.role
      });
      expect(mockSessionService.storeSession).toHaveBeenCalled();
      expect(mockResponse.status).toHaveBeenCalledWith(201);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: true,
        message: 'User registered successfully',
        data: {
          user: {
            id: mockUser.id,
            email: mockUser.email,
            name: mockUser.name,
            role: mockUser.role
          },
          tokens: mockTokens
        }
      });
    });

    it('should return error for missing required fields', async () => {
      mockRequest.body = { email: 'test@example.com' };

      await register(mockRequest as any, mockResponse as any);

      expect(mockResponse.status).toHaveBeenCalledWith(400);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: false,
        message: 'Email, name, and password are required'
      });
    });

    it('should return error for invalid email format', async () => {
      mockRequest.body = {
        ...validUserData,
        email: 'invalid-email'
      };

      await register(mockRequest as any, mockResponse as any);

      expect(mockResponse.status).toHaveBeenCalledWith(400);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: false,
        message: 'Invalid email format'
      });
    });

    it('should return error for weak password', async () => {
      mockRequest.body = {
        ...validUserData,
        password: 'weak'
      };

      // Override the default mock to return validation error
      mockAuthUtils.validatePassword.mockReturnValue({
        isValid: false,
        errors: ['Password must be at least 8 characters long']
      });

      await register(mockRequest as any, mockResponse as any);

      expect(mockResponse.status).toHaveBeenCalledWith(400);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: false,
        message: 'Password does not meet requirements',
        errors: expect.arrayContaining([
          'Password must be at least 8 characters long'
        ])
      });
    });

    it('should return error for existing user', async () => {
      mockRequest.body = validUserData;
      
      const existingUser = { id: 'existing-user' };
      mockUserModel.findByEmail.mockResolvedValue(existingUser as any);

      await register(mockRequest as any, mockResponse as any);

      expect(mockResponse.status).toHaveBeenCalledWith(409);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: false,
        message: 'User with this email already exists'
      });
    });
  });

  describe('login', () => {
    const validLoginData = {
      email: 'test@example.com',
      password: 'StrongPass123!'
    };

    it('should login user successfully', async () => {
      mockRequest.body = validLoginData;
      
      const mockUser = {
        id: 'user-123',
        email: validLoginData.email,
        name: 'Test User',
        role: 'user',
        is_active: true,
        password_hash: 'hashed-password'
      };

      const mockTokens = {
        accessToken: 'access-token',
        refreshToken: 'refresh-token',
        expiresIn: 3600
      };

      mockUserModel.findByEmail.mockResolvedValue(mockUser as any);
      mockUserModel.updateLastLogin.mockResolvedValue();
      mockAuthUtils.generateAuthTokens.mockReturnValue(mockTokens);
      mockSessionService.storeSession.mockResolvedValue();

      // Mock comparePassword to return true
      mockAuthUtils.comparePassword.mockResolvedValue(true);

      await login(mockRequest as any, mockResponse as any);

      expect(mockUserModel.findByEmail).toHaveBeenCalledWith(validLoginData.email);
      expect(mockAuthUtils.generateAuthTokens).toHaveBeenCalledWith({
        userId: mockUser.id,
        email: mockUser.email,
        role: mockUser.role
      });
      expect(mockSessionService.storeSession).toHaveBeenCalled();
      expect(mockUserModel.updateLastLogin).toHaveBeenCalledWith(mockUser.id);
      expect(mockResponse.status).toHaveBeenCalledWith(200);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: true,
        message: 'Login successful',
        data: {
          user: {
            id: mockUser.id,
            email: mockUser.email,
            name: mockUser.name,
            role: mockUser.role
          },
          tokens: mockTokens
        }
      });
    });

    it('should return error for missing credentials', async () => {
      mockRequest.body = { email: 'test@example.com' };

      await login(mockRequest as any, mockResponse as any);

      expect(mockResponse.status).toHaveBeenCalledWith(400);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: false,
        message: 'Email and password are required'
      });
    });

    it('should return error for non-existent user', async () => {
      mockRequest.body = validLoginData;
      mockUserModel.findByEmail.mockResolvedValue(null);

      await login(mockRequest as any, mockResponse as any);

      expect(mockResponse.status).toHaveBeenCalledWith(401);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: false,
        message: 'Invalid email or password'
      });
    });

    it('should return error for inactive user', async () => {
      mockRequest.body = validLoginData;
      
      const mockUser = {
        id: 'user-123',
        email: validLoginData.email,
        is_active: false
      };

      mockUserModel.findByEmail.mockResolvedValue(mockUser as any);

      await login(mockRequest as any, mockResponse as any);

      expect(mockResponse.status).toHaveBeenCalledWith(401);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: false,
        message: 'Account is deactivated'
      });
    });

    it('should return error for incorrect password', async () => {
      mockRequest.body = validLoginData;
      
      const mockUser = {
        id: 'user-123',
        email: validLoginData.email,
        is_active: true,
        password_hash: 'hashed-password'
      };

      mockUserModel.findByEmail.mockResolvedValue(mockUser as any);

      // Mock comparePassword to return false (incorrect password)
      mockAuthUtils.comparePassword.mockResolvedValue(false);

      await login(mockRequest as any, mockResponse as any);

      expect(mockResponse.status).toHaveBeenCalledWith(401);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: false,
        message: 'Invalid email or password'
      });
    });
  });

  describe('logout', () => {
    it('should logout user successfully', async () => {
      mockRequest.user = {
        id: 'user-123',
        email: 'test@example.com',
        role: 'user'
      };
      mockRequest.headers = {
        authorization: 'Bearer access-token'
      };

      mockSessionService.removeSession.mockResolvedValue();
      mockUserModel.updateLastLogin.mockResolvedValue();

      await logout(mockRequest as any, mockResponse as any);

      expect(mockSessionService.removeSession).toHaveBeenCalledWith('user-123');
      expect(mockResponse.status).toHaveBeenCalledWith(200);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: true,
        message: 'Logout successful'
      });
    });

    it('should return error when user is not authenticated', async () => {
      await logout(mockRequest as any, mockResponse as any);

      expect(mockResponse.status).toHaveBeenCalledWith(401);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: false,
        message: 'Authentication required'
      });
    });
  });

  describe('refreshToken', () => {
    it('should refresh token successfully', async () => {
      mockRequest.body = { refreshToken: 'valid-refresh-token' };
      
      const mockUser = {
        id: 'user-123',
        email: 'test@example.com',
        role: 'user',
        is_active: true
      };

      const mockSession = {
        id: 'user-123',
        refreshToken: 'valid-refresh-token'
      };

      const mockTokens = {
        accessToken: 'new-access-token',
        refreshToken: 'new-refresh-token',
        expiresIn: 3600
      };

      mockUserModel.findById.mockResolvedValue(mockUser as any);
      mockSessionService.getSession.mockResolvedValue(mockSession as any);
      mockAuthUtils.generateAuthTokens.mockReturnValue(mockTokens);
      mockSessionService.storeSession.mockResolvedValue();
      mockSessionService.blacklistToken.mockResolvedValue();

      // Mock verifyRefreshToken to return decoded token
      mockAuthUtils.verifyRefreshToken.mockReturnValue({
        userId: 'user-123',
        email: 'test@example.com',
        role: 'user'
      });

      await refreshToken(mockRequest as any, mockResponse as any);

      expect(mockUserModel.findById).toHaveBeenCalledWith('user-123');
      expect(mockSessionService.getSession).toHaveBeenCalledWith('user-123');
      expect(mockAuthUtils.generateAuthTokens).toHaveBeenCalled();
      expect(mockSessionService.storeSession).toHaveBeenCalled();
      expect(mockSessionService.blacklistToken).toHaveBeenCalledWith('valid-refresh-token', 86400);
      expect(mockResponse.status).toHaveBeenCalledWith(200);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: true,
        message: 'Token refreshed successfully',
        data: {
          tokens: mockTokens
        }
      });
    });

    it('should return error for missing refresh token', async () => {
      mockRequest.body = {};

      await refreshToken(mockRequest as any, mockResponse as any);

      expect(mockResponse.status).toHaveBeenCalledWith(400);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: false,
        message: 'Refresh token is required'
      });
    });
  });

  describe('getProfile', () => {
    it('should return user profile successfully', async () => {
      mockRequest.user = {
        id: 'user-123',
        email: 'test@example.com',
        role: 'user'
      };

      const mockUser = {
        id: 'user-123',
        email: 'test@example.com',
        name: 'Test User',
        role: 'user',
        created_at: new Date(),
        last_login: new Date()
      };

      mockUserModel.findById.mockResolvedValue(mockUser as any);

      await getProfile(mockRequest as any, mockResponse as any);

      expect(mockUserModel.findById).toHaveBeenCalledWith('user-123');
      expect(mockResponse.status).toHaveBeenCalledWith(200);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: true,
        data: {
          user: {
            id: mockUser.id,
            email: mockUser.email,
            name: mockUser.name,
            role: mockUser.role,
            created_at: mockUser.created_at,
            last_login: mockUser.last_login
          }
        }
      });
    });

    it('should return error when user is not authenticated', async () => {
      await getProfile(mockRequest as any, mockResponse as any);

      expect(mockResponse.status).toHaveBeenCalledWith(401);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: false,
        message: 'Authentication required'
      });
    });

    it('should return error when user not found', async () => {
      mockRequest.user = {
        id: 'user-123',
        email: 'test@example.com',
        role: 'user'
      };

      mockUserModel.findById.mockResolvedValue(null);

      await getProfile(mockRequest as any, mockResponse as any);

      expect(mockResponse.status).toHaveBeenCalledWith(404);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: false,
        message: 'User not found'
      });
    });
  });

  describe('updateProfile', () => {
    it('should update user profile successfully', async () => {
      mockRequest.user = {
        id: 'user-123',
        email: 'test@example.com',
        role: 'user'
      };
      mockRequest.body = {
        name: 'Updated Name',
        email: 'updated@example.com'
      };

      const mockUpdatedUser = {
        id: 'user-123',
        email: 'updated@example.com',
        name: 'Updated Name',
        role: 'user',
        created_at: new Date(),
        last_login: new Date()
      };

      mockUserModel.findByEmail.mockResolvedValue(null);
      mockUserModel.update.mockResolvedValue(mockUpdatedUser as any);

      await updateProfile(mockRequest as any, mockResponse as any);

      expect(mockUserModel.findByEmail).toHaveBeenCalledWith('updated@example.com');
      expect(mockUserModel.update).toHaveBeenCalledWith('user-123', {
        name: 'Updated Name',
        email: 'updated@example.com'
      });
      expect(mockResponse.status).toHaveBeenCalledWith(200);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: true,
        message: 'Profile updated successfully',
        data: {
          user: {
            id: mockUpdatedUser.id,
            email: mockUpdatedUser.email,
            name: mockUpdatedUser.name,
            role: mockUpdatedUser.role,
            created_at: mockUpdatedUser.created_at,
            last_login: mockUpdatedUser.last_login
          }
        }
      });
    });

    it('should return error when user is not authenticated', async () => {
      await updateProfile(mockRequest as any, mockResponse as any);

      expect(mockResponse.status).toHaveBeenCalledWith(401);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: false,
        message: 'Authentication required'
      });
    });

    it('should return error for invalid email format', async () => {
      mockRequest.user = {
        id: 'user-123',
        email: 'test@example.com',
        role: 'user'
      };
      mockRequest.body = {
        email: 'invalid-email'
      };

      await updateProfile(mockRequest as any, mockResponse as any);

      expect(mockResponse.status).toHaveBeenCalledWith(400);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: false,
        message: 'Invalid email format'
      });
    });

    it('should return error for email already taken', async () => {
      mockRequest.user = {
        id: 'user-123',
        email: 'test@example.com',
        role: 'user'
      };
      mockRequest.body = {
        email: 'taken@example.com'
      };

      const existingUser = { id: 'other-user' };
      mockUserModel.findByEmail.mockResolvedValue(existingUser as any);

      await updateProfile(mockRequest as any, mockResponse as any);

      expect(mockResponse.status).toHaveBeenCalledWith(409);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: false,
        message: 'Email is already taken'
      });
    });
  });
}); 