import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { UserProfile } from '../types';
import type * as jsonwebtoken from 'jsonwebtoken';

describe('AuthService', () => {
  let authService: typeof import('./authService').authService;
  let bcrypt: typeof import('bcrypt');
  let jwt: typeof jsonwebtoken & { default: typeof jsonwebtoken };
  let userRepo: typeof import('./db/index.db').userRepo;
  let adminRepo: typeof import('./db/index.db').adminRepo;
  let logger: typeof import('./logger.server').logger;
  let sendPasswordResetEmail: typeof import('./emailService.server').sendPasswordResetEmail;
  let UniqueConstraintError: typeof import('./db/errors.db').UniqueConstraintError;

  const reqLog = {}; // Mock request logger object
  const mockUser = {
    user_id: 'user-123',
    email: 'test@example.com',
    password_hash: 'hashed-password',
  };
  const mockUserProfile: UserProfile = {
    user: mockUser,
    role: 'user',
  } as unknown as UserProfile;

  beforeEach(async () => {
    vi.clearAllMocks();
    vi.resetModules();

    // Set environment variables before any modules are imported
    process.env.JWT_SECRET = 'test-secret';
    process.env.FRONTEND_URL = 'http://localhost:3000';

    // Mock all dependencies before dynamically importing the service
    // Core modules like bcrypt, jsonwebtoken, and crypto are now mocked globally in tests-setup-unit.ts
    vi.mock('bcrypt');
    vi.mock('./db/index.db', () => ({
      userRepo: {
        createUser: vi.fn(),
        saveRefreshToken: vi.fn(),
        findUserByEmail: vi.fn(),
        createPasswordResetToken: vi.fn(),
        getValidResetTokens: vi.fn(),
        updateUserPassword: vi.fn(),
        deleteResetToken: vi.fn(),
        findUserByRefreshToken: vi.fn(),
        findUserProfileById: vi.fn(),
        deleteRefreshToken: vi.fn(),
      },
      adminRepo: {
        logActivity: vi.fn(),
      },
    }));
    vi.mock('./logger.server', () => ({
      logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() },
    }));
    vi.mock('./emailService.server', () => ({
      sendPasswordResetEmail: vi.fn(),
    }));
    vi.mock('./db/connection.db', () => ({ getPool: vi.fn() }));
    vi.mock('../utils/authUtils', () => ({ validatePasswordStrength: vi.fn() }));

    // Dynamically import modules to get the mocked versions and the service instance
    authService = (await import('./authService')).authService;
    bcrypt = await import('bcrypt');
    jwt = (await import('jsonwebtoken')) as typeof jwt;
    const dbModule = await import('./db/index.db');
    userRepo = dbModule.userRepo;
    adminRepo = dbModule.adminRepo;
    logger = (await import('./logger.server')).logger;
    sendPasswordResetEmail = (await import('./emailService.server')).sendPasswordResetEmail;
    UniqueConstraintError = (await import('./db/errors.db')).UniqueConstraintError;
  });

  describe('registerUser', () => {
    it('should successfully register a new user', async () => {
      vi.mocked(bcrypt.hash).mockImplementation(async () => 'hashed-password');
      vi.mocked(userRepo.createUser).mockResolvedValue(mockUserProfile);

      const result = await authService.registerUser(
        'test@example.com',
        'password123',
        'Test User',
        undefined,
        reqLog,
      );

      expect(bcrypt.hash).toHaveBeenCalledWith('password123', 10);
      expect(userRepo.createUser).toHaveBeenCalledWith(
        'test@example.com',
        'hashed-password',
        { full_name: 'Test User', avatar_url: undefined },
        reqLog,
      );
      expect(adminRepo.logActivity).toHaveBeenCalledWith(
        expect.objectContaining({
          action: 'user_registered',
          userId: 'user-123',
        }),
        reqLog,
      );
      expect(result).toEqual(mockUserProfile);
    });

    it('should throw UniqueConstraintError if email already exists', async () => {
      vi.mocked(bcrypt.hash).mockImplementation(async () => 'hashed-password');
      const error = new UniqueConstraintError('Email exists');
      vi.mocked(userRepo.createUser).mockRejectedValue(error);

      await expect(
        authService.registerUser('test@example.com', 'password123', undefined, undefined, reqLog),
      ).rejects.toThrow(UniqueConstraintError);

      expect(logger.error).not.toHaveBeenCalled(); // Should not log expected unique constraint errors as system errors
    });

    it('should log and throw other errors', async () => {
      vi.mocked(bcrypt.hash).mockImplementation(async () => 'hashed-password');
      const error = new Error('Database failed');
      vi.mocked(userRepo.createUser).mockRejectedValue(error);

      await expect(
        authService.registerUser('test@example.com', 'password123', undefined, undefined, reqLog),
      ).rejects.toThrow('Database failed');

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

  describe('registerAndLoginUser', () => {
    it('should register user and return tokens', async () => {
      // Mock registerUser logic (since we can't easily spy on the same class instance method without prototype spying, we rely on the underlying calls)
      vi.mocked(bcrypt.hash).mockImplementation(async () => 'hashed-password');
      vi.mocked(userRepo.createUser).mockResolvedValue(mockUserProfile);
      // FIX: The global mock for jsonwebtoken provides a `default` export.
      // The code under test (`authService`) uses `import jwt from 'jsonwebtoken'`, so it gets the default export.
      // We must mock `jwt.default.sign` to affect the code under test.
      vi.mocked(jwt.default.sign).mockImplementation(() => 'access-token');

      const result = await authService.registerAndLoginUser(
        'test@example.com',
        'password123',
        'Test User',
        undefined,
        reqLog,
      );

      expect(result).toEqual({
        newUserProfile: mockUserProfile,
        accessToken: 'access-token',
        refreshToken: 'mocked_random_id',
      });
      expect(userRepo.saveRefreshToken).toHaveBeenCalledWith(
        'user-123',
        'mocked_random_id',
        reqLog,
      );
    });
  });

  describe('generateAuthTokens', () => {
    it('should generate access and refresh tokens', () => {
      // FIX: The global mock for jsonwebtoken provides a `default` export.
      // The code under test (`authService`) uses `import jwt from 'jsonwebtoken'`, so it gets the default export.
      // We must mock `jwt.default.sign` to affect the code under test.
      vi.mocked(jwt.default.sign).mockImplementation(() => 'access-token');

      const result = authService.generateAuthTokens(mockUserProfile);

      expect(vi.mocked(jwt.default.sign)).toHaveBeenCalledWith(
        {
          user_id: 'user-123',
          email: 'test@example.com',
          role: 'user',
        },
        'test-secret',
        { expiresIn: '15m' },
      );
      expect(result).toEqual({
        accessToken: 'access-token',
        refreshToken: 'mocked_random_id',
      });
    });
  });

  describe('saveRefreshToken', () => {
    it('should save refresh token to db', async () => {
      await authService.saveRefreshToken('user-123', 'token', reqLog);
      expect(userRepo.saveRefreshToken).toHaveBeenCalledWith('user-123', 'token', reqLog);
    });

    it('should log and throw error on failure', async () => {
      const error = new Error('DB Error');
      vi.mocked(userRepo.saveRefreshToken).mockRejectedValue(error);

      await expect(authService.saveRefreshToken('user-123', 'token', reqLog)).rejects.toThrow(
        'DB Error',
      );
      expect(logger.error).toHaveBeenCalledWith(
        expect.objectContaining({ error }),
        expect.stringContaining('Failed to save refresh token'),
      );
    });
  });

  describe('resetPassword', () => {
    it('should process password reset for existing user', async () => {
      vi.mocked(userRepo.findUserByEmail).mockResolvedValue(mockUser as any);
      vi.mocked(bcrypt.hash).mockImplementation(async () => 'hashed-token');

      const result = await authService.resetPassword('test@example.com', reqLog);

      expect(userRepo.createPasswordResetToken).toHaveBeenCalledWith(
        'user-123',
        'hashed-token',
        expect.any(Date),
        reqLog,
      );
      expect(sendPasswordResetEmail).toHaveBeenCalledWith(
        'test@example.com',
        expect.stringContaining('/reset-password/mocked_random_id'),
        reqLog,
      );
      expect(result).toBe('mocked_random_id');
    });

    it('should log warning and return undefined for non-existent user', async () => {
      vi.mocked(userRepo.findUserByEmail).mockResolvedValue(undefined);

      const result = await authService.resetPassword('unknown@example.com', reqLog);

      expect(logger.warn).toHaveBeenCalledWith(
        expect.stringContaining('Password reset requested for non-existent email'),
      );
      expect(sendPasswordResetEmail).not.toHaveBeenCalled();
      expect(result).toBeUndefined();
    });

    it('should log error and throw on failure', async () => {
      const error = new Error('DB Error');
      vi.mocked(userRepo.findUserByEmail).mockRejectedValue(error);

      await expect(authService.resetPassword('test@example.com', reqLog)).rejects.toThrow(
        'DB Error',
      );
      expect(logger.error).toHaveBeenCalled();
    });
  });

  describe('updatePassword', () => {
    it('should update password if token is valid', async () => {
      const mockTokenRecord = {
        user_id: 'user-123',
        token_hash: 'hashed-token',
      };
      vi.mocked(userRepo.getValidResetTokens).mockResolvedValue([mockTokenRecord] as any);
      vi.mocked(bcrypt.compare).mockImplementation(async () => true); // Match found
      vi.mocked(bcrypt.hash).mockImplementation(async () => 'new-hashed-password');

      const result = await authService.updatePassword('valid-token', 'newPassword', reqLog);

      expect(userRepo.updateUserPassword).toHaveBeenCalledWith(
        'user-123',
        'new-hashed-password',
        reqLog,
      );
      expect(userRepo.deleteResetToken).toHaveBeenCalledWith('hashed-token', reqLog);
      expect(adminRepo.logActivity).toHaveBeenCalledWith(
        expect.objectContaining({ action: 'password_reset' }),
        reqLog,
      );
      expect(result).toBe(true);
    });

    it('should return null if token is invalid or not found', async () => {
      vi.mocked(userRepo.getValidResetTokens).mockResolvedValue([]);

      const result = await authService.updatePassword('invalid-token', 'newPassword', reqLog);

      expect(userRepo.updateUserPassword).not.toHaveBeenCalled();
      expect(result).toBeNull();
    });
  });

  describe('getUserByRefreshToken', () => {
    it('should return user profile if token exists', async () => {
      vi.mocked(userRepo.findUserByRefreshToken).mockResolvedValue({ user_id: 'user-123' } as any);
      vi.mocked(userRepo.findUserProfileById).mockResolvedValue(mockUserProfile);

      const result = await authService.getUserByRefreshToken('valid-token', reqLog);

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

    it('should return null if token not found', async () => {
      vi.mocked(userRepo.findUserByRefreshToken).mockResolvedValue(undefined);

      const result = await authService.getUserByRefreshToken('invalid-token', reqLog);

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

  describe('logout', () => {
    it('should delete refresh token', async () => {
      await authService.logout('token', reqLog);
      expect(userRepo.deleteRefreshToken).toHaveBeenCalledWith('token', reqLog);
    });

    it('should log and throw on error', async () => {
      const error = new Error('DB Error');
      vi.mocked(userRepo.deleteRefreshToken).mockRejectedValue(error);

      await expect(authService.logout('token', reqLog)).rejects.toThrow('DB Error');
      expect(logger.error).toHaveBeenCalled();
    });
  });

  describe('refreshAccessToken', () => {
    it('should return new access token if user found', async () => {
      vi.mocked(userRepo.findUserByRefreshToken).mockResolvedValue({ user_id: 'user-123' } as any);
      vi.mocked(userRepo.findUserProfileById).mockResolvedValue(mockUserProfile);
      // FIX: The global mock for jsonwebtoken provides a `default` export.
      // The code under test (`authService`) uses `import jwt from 'jsonwebtoken'`, so it gets the default export.
      // We must mock `jwt.default.sign` to affect the code under test.
      vi.mocked(jwt.default.sign).mockImplementation(() => 'new-access-token');

      const result = await authService.refreshAccessToken('valid-token', reqLog);

      expect(result).toEqual({ accessToken: 'new-access-token' });
    });

    it('should return null if user not found', async () => {
      vi.mocked(userRepo.findUserByRefreshToken).mockResolvedValue(undefined);
      const result = await authService.refreshAccessToken('invalid-token', reqLog);
      expect(result).toBeNull();
    });
  });
});