/**
 * Auth Service Unit Tests
 *
 * Tests for:
 * - Login with correct credentials
 * - Login with incorrect password
 * - Register with duplicate email
 * - Token generation and validation
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

// Mock external dependencies FIRST before importing anything else
vi.mock('bcrypt', () => ({
  default: {
    hash: vi.fn(),
    compare: vi.fn(),
  },
}));

vi.mock('jsonwebtoken', () => {
  const mockSign = vi.fn();
  const mockVerify = vi.fn();

  return {
    default: {
      sign: mockSign,
      verify: mockVerify,
      TokenExpiredError: class TokenExpiredError extends Error {
        constructor(message: string) {
          super(message);
          this.name = 'TokenExpiredError';
        }
      },
      JsonWebTokenError: class JsonWebTokenError extends Error {
        constructor(message: string) {
          super(message);
          this.name = 'JsonWebTokenError';
        }
      },
    },
  };
});

// Mock internal dependencies
vi.mock('../../src/shared/database/prisma.client', () => ({
  prisma: {
    user: {
      findUnique: vi.fn(),
      create: vi.fn(),
      update: vi.fn(),
    },
    refreshToken: {
      create: vi.fn(),
      deleteMany: vi.fn(),
      findUnique: vi.fn(),
      delete: vi.fn(),
    },
    passwordResetToken: {
      create: vi.fn(),
      findUnique: vi.fn(),
      delete: vi.fn(),
    },
  },
}));

vi.mock('../../src/shared/database/redis.client', () => ({
  blacklistToken: vi.fn().mockResolvedValue(undefined),
  isTokenBlacklisted: vi.fn().mockResolvedValue(false),
}));

vi.mock('../../src/shared/utils/logger', () => ({
  logger: {
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
    debug: vi.fn(),
  },
}));

vi.mock('../../src/modules/notification/telegram/telegram.client', () => ({
  telegramClient: {
    sendMessage: vi.fn(),
  },
}));

vi.mock('../../src/config/telegram', () => ({
  isTelegramEnabled: vi.fn().mockReturnValue(false),
  getTelegramAdminChatId: vi.fn().mockReturnValue(null),
}));

// NOW import the mocked modules and the service
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { AuthService } from '../../src/modules/auth/auth.service';
import { prisma } from '../../src/shared/database/prisma.client';
import { ConflictError, AuthenticationError, NotFoundError } from '../../src/shared/types';

const authService = new AuthService();

describe('AuthService', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    process.env.JWT_SECRET = 'test-jwt-secret';
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  // ============================================
  // LOGIN TESTS
  // ============================================

  describe('login', () => {
    it('should login successfully with correct credentials', async () => {
      const mockUser = {
        id: 'user-123',
        email: 'test@example.com',
        username: 'testuser',
        passwordHash: 'hashed-password',
        isActive: true,
        lastLoginAt: null,
        role: 'STUDENT',
      };

      (prisma.user.findUnique as any).mockResolvedValueOnce(mockUser);
      (prisma.user.update as any).mockResolvedValueOnce({
        ...mockUser,
        lastLoginAt: new Date(),
      });
      (prisma.refreshToken.deleteMany as any).mockResolvedValueOnce({ count: 0 });
      (prisma.refreshToken.create as any).mockResolvedValueOnce({});
      (bcrypt.compare as any).mockResolvedValueOnce(true);
      (jwt.sign as any).mockReturnValueOnce('mock-jwt-token');

      const result = await authService.login({
        email: 'test@example.com',
        password: 'correct-password',
      });

      expect(result.user.email).toBe('test@example.com');
      expect(result.user.username).toBe('testuser');
      expect(result.token).toBe('mock-jwt-token');
    });

    it('should throw AuthenticationError with incorrect password', async () => {
      const mockUser = {
        id: 'user-123',
        email: 'test@example.com',
        username: 'testuser',
        passwordHash: 'hashed-password',
        isActive: true,
        role: 'STUDENT',
      };

      (prisma.user.findUnique as any).mockResolvedValueOnce(mockUser);
      (bcrypt.compare as any).mockResolvedValueOnce(false);

      await expect(
        authService.login({
          email: 'test@example.com',
          password: 'wrong-password',
        })
      ).rejects.toThrow(AuthenticationError);

      expect(prisma.user.update).not.toHaveBeenCalled();
    });

    it('should throw AuthenticationError when user does not exist', async () => {
      (prisma.user.findUnique as any).mockResolvedValueOnce(null);

      await expect(
        authService.login({
          email: 'nonexistent@example.com',
          password: 'any-password',
        })
      ).rejects.toThrow(AuthenticationError);
    });

    it('should throw AuthenticationError when user is inactive', async () => {
      const mockUser = {
        id: 'user-123',
        email: 'test@example.com',
        username: 'testuser',
        passwordHash: 'hashed-password',
        isActive: false,
        role: 'STUDENT',
      };

      (prisma.user.findUnique as any).mockResolvedValueOnce(mockUser);

      await expect(
        authService.login({
          email: 'test@example.com',
          password: 'any-password',
        })
      ).rejects.toThrow(AuthenticationError);

      expect(bcrypt.compare).not.toHaveBeenCalled();
    });
  });

  // ============================================
  // REGISTER TESTS
  // ============================================

  describe('register', () => {
    it('should register a new user successfully', async () => {
      (prisma.user.findUnique as any).mockResolvedValueOnce(null); // email check
      (prisma.user.findUnique as any).mockResolvedValueOnce(null); // username check
      (bcrypt.hash as any).mockResolvedValueOnce('hashed-password');

      const mockCreatedUser = {
        id: 'new-user-123',
        email: 'newuser@example.com',
        username: 'newuser',
        isActive: true,
        createdAt: new Date(),
        role: 'STUDENT',
      };

      (prisma.user.create as any).mockResolvedValueOnce(mockCreatedUser);
      (prisma.refreshToken.create as any).mockResolvedValueOnce({});
      (jwt.sign as any).mockReturnValueOnce('mock-jwt-token');

      const result = await authService.register({
        email: 'newuser@example.com',
        username: 'newuser',
        password: 'secure-password',
      });

      expect(result.user.email).toBe('newuser@example.com');
      expect(result.user.username).toBe('newuser');
      expect(result.token).toBe('mock-jwt-token');
      expect(bcrypt.hash).toHaveBeenCalledWith('secure-password', 10);
    });

    it('should throw ConflictError with duplicate email', async () => {
      (prisma.user.findUnique as any).mockResolvedValueOnce({ id: 'existing-user', role: 'STUDENT' });

      await expect(
        authService.register({
          email: 'existing@example.com',
          username: 'newuser',
          password: 'password',
        })
      ).rejects.toThrow(ConflictError);

      expect(prisma.user.create).not.toHaveBeenCalled();
    });

    it('should throw ConflictError with duplicate username', async () => {
      (prisma.user.findUnique as any).mockResolvedValueOnce(null); // email unique
      (prisma.user.findUnique as any).mockResolvedValueOnce({ id: 'existing-user', role: 'STUDENT' }); // username exists

      await expect(
        authService.register({
          email: 'newemail@example.com',
          username: 'existingusername',
          password: 'password',
        })
      ).rejects.toThrow(ConflictError);
    });
  });

  // ============================================
  // TOKEN TESTS
  // ============================================

  describe('verifyToken', () => {
    it('should verify a valid token successfully', async () => {
      const mockPayload = {
        userId: 'user-123',
        email: 'test@example.com',
        username: 'testuser',
      };

      (jwt.verify as any).mockReturnValueOnce(mockPayload);

      const result = authService.verifyToken('valid-token');

      expect(result.userId).toBe('user-123');
      expect(result.email).toBe('test@example.com');
    });

    it('should throw AuthenticationError for expired token', async () => {
      const TokenExpiredError = (jwt as any).TokenExpiredError;
      (jwt.verify as any).mockImplementationOnce(() => {
        throw new TokenExpiredError('jwt expired');
      });

      expect(() => authService.verifyToken('expired-token')).toThrow(AuthenticationError);
    });

    it('should throw AuthenticationError for invalid token', async () => {
      const JsonWebTokenError = (jwt as any).JsonWebTokenError;
      (jwt.verify as any).mockImplementationOnce(() => {
        throw new JsonWebTokenError('invalid signature');
      });

      expect(() => authService.verifyToken('invalid-token')).toThrow(AuthenticationError);
    });
  });

  // ============================================
  // PROFILE TESTS
  // ============================================

  describe('getProfile', () => {
    it('should return user profile successfully', async () => {
      const mockProfile = {
        id: 'user-123',
        email: 'test@example.com',
        username: 'testuser',
        isActive: true,
        telegramChatId: null,
        createdAt: new Date(),
        updatedAt: new Date(),
        lastLoginAt: new Date(),
        role: 'STUDENT',
      };

      (prisma.user.findUnique as any).mockResolvedValueOnce(mockProfile);

      const result = await authService.getProfile('user-123');

      expect(result.id).toBe('user-123');
      expect(result.email).toBe('test@example.com');
      expect(result.username).toBe('testuser');
    });

    it('should throw NotFoundError when user not found', async () => {
      (prisma.user.findUnique as any).mockResolvedValueOnce(null);

      await expect(authService.getProfile('nonexistent-user')).rejects.toThrow(NotFoundError);
    });
  });
});