import bcrypt from 'bcryptjs';

import { TestFactory } from '../../../__tests__/factories';
import { prisma } from '../../../config/database.config';
import { auditService } from '../../../core/events/audit.service';
import { rbacService } from '../../rbac/rbac.service';
import { sessionService } from '../../session/session.service';
import { jwtService } from '../../token/jwt.service';
import { accountLockoutService } from '../account-lockout.service';
import { AuthService } from '../auth.service';


// Mock dependencies
jest.mock('../../rbac/rbac.service');
jest.mock('../../session/session.service');
jest.mock('../../token/jwt.service');
jest.mock('../../../core/events/audit.service');
jest.mock('../account-lockout.service');
jest.mock('bcryptjs');

describe('AuthService', () => {
  let authService: AuthService;
  // Create a mock request object that satisfies the interface and avoiding unsafe access
  const mockReq = {
    ip: '127.0.0.1',
    headers: { 'user-agent': 'Jest-Test' },
    socket: { remoteAddress: '127.0.0.1' },
  } as any;

  beforeEach(() => {
    jest.clearAllMocks();
    authService = new AuthService();
  });

  describe('register', () => {
    it('should register a new user successfully', async () => {
      // Arrange
      const registerDto = {
        email: 'test@example.com',
        password: 'password123',
        username: 'testuser',
      };

      const mockUser = TestFactory.createUser({
        email: registerDto.email,
        username: registerDto.username
      });
      const mockTokens = { accessToken: 'access-token', refreshToken: 'refresh-token' };
      const mockDecodedRefresh = { tokenId: 'token-id', sub: mockUser.id };

      (prisma.user.findUnique as jest.Mock).mockResolvedValue(null);
      (bcrypt.hash as jest.Mock).mockResolvedValue('hashed-password' as never);
      (prisma.user.create as jest.Mock).mockResolvedValue(mockUser);
      (prisma.role.findFirst as jest.Mock).mockResolvedValue({ id: 'role-id' });
      (rbacService.assignRole as jest.Mock).mockResolvedValue(undefined);
      (rbacService.getUserRoles as jest.Mock).mockResolvedValue(['USER']);
      (rbacService.getUserPermissions as jest.Mock).mockResolvedValue([]);
      (jwtService.generateTokenSet as jest.Mock).mockResolvedValue(mockTokens);
      (jwtService.verifyRefreshToken as jest.Mock).mockReturnValue(mockDecodedRefresh);
      (prisma.refreshToken.create as jest.Mock).mockResolvedValue({} as any);
      (sessionService.createSession as jest.Mock).mockResolvedValue(undefined);

      // Act
      const result = await authService.register(registerDto, mockReq);

      // Assert
      expect(prisma.user.create).toHaveBeenCalledWith({
        data: expect.objectContaining({
          email: registerDto.email,
          username: registerDto.username,
          passwordHash: 'hashed-password',
        }),
      });
      expect(result).toHaveProperty('tokens', mockTokens);
      expect(result.user).toHaveProperty('email', registerDto.email);
    });

    it('should throw error if user already exists', async () => {
      const registerDto = { email: 'exists@example.com', password: '123' };
      (prisma.user.findUnique as jest.Mock).mockResolvedValue({ id: 'existing' });

      await expect(authService.register(registerDto, mockReq)).rejects.toThrow('User already exists');
    });
  });

  describe('login', () => {
    it('should login successfully with valid credentials', async () => {
      const loginDto = { email: 'test@example.com', password: 'password123' };
      const mockUser = TestFactory.createUser({
        email: loginDto.email,
        passwordHash: 'hashed-password'
      });
      const mockTokens = { accessToken: 'access', refreshToken: 'refresh' };

      (prisma.user.findUnique as jest.Mock).mockResolvedValue(mockUser);
      (accountLockoutService.isAccountLocked as jest.Mock).mockResolvedValue(false);
      (bcrypt.compare as jest.Mock).mockResolvedValue(true as never);
      (jwtService.generateTokenSet as jest.Mock).mockResolvedValue(mockTokens);
      (jwtService.verifyRefreshToken as jest.Mock).mockReturnValue({ tokenId: 'tid' });

      const result = await authService.login(loginDto, mockReq);

      expect(result).toEqual(expect.objectContaining({
        tokens: mockTokens,
        user: expect.objectContaining({ email: mockUser.email })
      }));
      expect(prisma.user.update).toHaveBeenCalled(); // Last login update
    });

    it('should throw if user not found', async () => {
      (prisma.user.findUnique as jest.Mock).mockResolvedValue(null);
      await expect(authService.login({ email: '404', password: '123' }, mockReq))
        .rejects.toThrow('Invalid credentials');
    });

    it('should throw if password invalid', async () => {
      const mockUser = TestFactory.createUser({ passwordHash: 'hash' });
      (prisma.user.findUnique as jest.Mock).mockResolvedValue(mockUser);
      (bcrypt.compare as jest.Mock).mockResolvedValue(false as never);

      await expect(authService.login({ email: 'test', password: 'wrong' }, mockReq))
        .rejects.toThrow('Invalid credentials');
      expect(accountLockoutService.recordFailedAttempt).toHaveBeenCalledWith(mockUser.id);
    });

    it('should throw if account is locked', async () => {
      const mockUser = TestFactory.createUser();
      (prisma.user.findUnique as jest.Mock).mockResolvedValue(mockUser);
      (accountLockoutService.isAccountLocked as jest.Mock).mockResolvedValue(true);
      (accountLockoutService.getLockoutStatus as jest.Mock).mockResolvedValue({ remainingMinutes: 15 });

      await expect(authService.login({ email: 'locked', password: '123' }, mockReq))
        .rejects.toThrow(/Account is locked/);
    });
  });

  describe('refreshToken', () => {
    it('should refresh token successfully', async () => {
      const token = 'valid-refresh-token';
      const mockDecoded = { sub: 'user-id', tokenId: 'family-id' };
      const mockTokenRecord = {
        id: 'record-id',
        token: 'hashed-token',
        family: 'family-id',
        deviceId: 'device-id',
        user: TestFactory.createUser()
      };

      (jwtService.verifyRefreshToken as jest.Mock).mockReturnValue(mockDecoded);
      (prisma.refreshToken.findFirst as jest.Mock).mockResolvedValue(mockTokenRecord);
      (jwtService.generateAccessToken as jest.Mock).mockReturnValue('new-access');
      (jwtService.generateRefreshToken as jest.Mock).mockReturnValue('new-refresh');
      // Mock transaction return
      (prisma.$transaction as jest.Mock).mockResolvedValue([{}, {}]);

      const result = await authService.refreshToken(token, mockReq);

      expect(result).toHaveProperty('accessToken', 'new-access');
      expect(result).toHaveProperty('refreshToken', 'new-refresh');
    });

    it('should detect token reuse (family mismatch) and revoke chain', async () => {
      const token = 'stolen-token';
      const mockDecoded = { sub: 'user-id', tokenId: 'old-family' };
      const mockTokenRecord = {
        id: 'record-id',
        family: 'new-family' // Mismatch!
      };

      (jwtService.verifyRefreshToken as jest.Mock).mockReturnValue(mockDecoded);
      (prisma.refreshToken.findFirst as jest.Mock).mockResolvedValue(mockTokenRecord);

      await expect(authService.refreshToken(token, mockReq))
        .rejects.toThrow(/Token family mismatch/i);

      expect(prisma.refreshToken.updateMany).toHaveBeenCalledWith(expect.objectContaining({
        where: { userId: 'user-id', family: 'new-family' }
      }));
    });
  });
});