/**
 * AdminOperationLogService 单元测试
 * 
 * 功能描述：
 * - 测试管理员操作日志服务的所有方法
 * - 验证日志记录和查询的正确性
 * - 测试统计功能和清理功能
 * 
 * 职责分离：
 * - 业务逻辑测试，不涉及HTTP层
 * - Mock数据库操作，专注服务逻辑
 * - 验证日志处理的正确性
 * 
 * 最近修改：
 * - 2026-01-09: 功能新增 - 创建AdminOperationLogService单元测试 (修改者: moyin)
 * 
 * @author moyin
 * @version 1.0.0
 * @since 2026-01-09
 * @lastModified 2026-01-09
 */

import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AdminOperationLogService, CreateLogParams, LogQueryParams } from './admin_operation_log.service';
import { AdminOperationLog } from './admin_operation_log.entity';

describe('AdminOperationLogService', () => {
  let service: AdminOperationLogService;
  let repository: jest.Mocked<Repository<AdminOperationLog>>;

  const mockRepository = {
    create: jest.fn(),
    save: jest.fn(),
    createQueryBuilder: jest.fn(),
    findOne: jest.fn(),
    find: jest.fn(),
    findAndCount: jest.fn(),
  };

  const mockQueryBuilder = {
    andWhere: jest.fn().mockReturnThis(),
    orderBy: jest.fn().mockReturnThis(),
    limit: jest.fn().mockReturnThis(),
    offset: jest.fn().mockReturnThis(),
    getManyAndCount: jest.fn(),
    getCount: jest.fn(),
    clone: jest.fn().mockReturnThis(),
    select: jest.fn().mockReturnThis(),
    addSelect: jest.fn().mockReturnThis(),
    groupBy: jest.fn().mockReturnThis(),
    getRawMany: jest.fn(),
    getRawOne: jest.fn(),
    delete: jest.fn().mockReturnThis(),
    where: jest.fn().mockReturnThis(),
    execute: jest.fn(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        AdminOperationLogService,
        {
          provide: getRepositoryToken(AdminOperationLog),
          useValue: mockRepository,
        },
      ],
    }).compile();

    service = module.get<AdminOperationLogService>(AdminOperationLogService);
    repository = module.get(getRepositoryToken(AdminOperationLog));

    // Setup default mock behavior
    mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('createLog', () => {
    it('should create log successfully', async () => {
      const logParams: CreateLogParams = {
        adminUserId: 'admin1',
        adminUsername: 'admin',
        operationType: 'CREATE',
        targetType: 'users',
        targetId: '1',
        operationDescription: 'Create user',
        httpMethodPath: 'POST /admin/users',
        operationResult: 'SUCCESS',
        durationMs: 100,
        requestId: 'req_123',
      };

      const mockLog = { 
        id: 'log1',
        admin_user_id: logParams.adminUserId,
        admin_username: logParams.adminUsername,
        operation_type: logParams.operationType,
        target_type: logParams.targetType,
        target_id: logParams.targetId,
        operation_description: logParams.operationDescription,
        http_method_path: logParams.httpMethodPath,
        operation_result: logParams.operationResult,
        duration_ms: logParams.durationMs,
        request_id: logParams.requestId,
        is_sensitive: false,
        affected_records: 0,
        created_at: new Date(),
        updated_at: new Date()
      } as AdminOperationLog;

      mockRepository.create.mockReturnValue(mockLog);
      mockRepository.save.mockResolvedValue(mockLog);

      const result = await service.createLog(logParams);

      expect(mockRepository.create).toHaveBeenCalledWith({
        admin_user_id: logParams.adminUserId,
        admin_username: logParams.adminUsername,
        operation_type: logParams.operationType,
        target_type: logParams.targetType,
        target_id: logParams.targetId,
        operation_description: logParams.operationDescription,
        http_method_path: logParams.httpMethodPath,
        request_params: logParams.requestParams,
        before_data: logParams.beforeData,
        after_data: logParams.afterData,
        operation_result: logParams.operationResult,
        error_message: logParams.errorMessage,
        error_code: logParams.errorCode,
        duration_ms: logParams.durationMs,
        client_ip: logParams.clientIp,
        user_agent: logParams.userAgent,
        request_id: logParams.requestId,
        context: logParams.context,
        is_sensitive: false,
        affected_records: 0,
        batch_id: logParams.batchId,
      });

      expect(mockRepository.save).toHaveBeenCalledWith(mockLog);
      expect(result).toEqual(mockLog);
    });

    it('should handle creation error', async () => {
      const logParams: CreateLogParams = {
        adminUserId: 'admin1',
        adminUsername: 'admin',
        operationType: 'CREATE',
        targetType: 'users',
        operationDescription: 'Create user',
        httpMethodPath: 'POST /admin/users',
        operationResult: 'SUCCESS',
        durationMs: 100,
        requestId: 'req_123',
      };

      mockRepository.create.mockReturnValue({} as AdminOperationLog);
      mockRepository.save.mockRejectedValue(new Error('Database error'));

      await expect(service.createLog(logParams)).rejects.toThrow('Database error');
    });
  });

  describe('queryLogs', () => {
    it('should query logs successfully', async () => {
      const queryParams: LogQueryParams = {
        adminUserId: 'admin1',
        operationType: 'CREATE',
        limit: 10,
        offset: 0,
      };

      const mockLogs = [
        { id: 'log1', admin_user_id: 'admin1' },
        { id: 'log2', admin_user_id: 'admin1' },
      ] as AdminOperationLog[];

      mockQueryBuilder.getManyAndCount.mockResolvedValue([mockLogs, 2]);

      const result = await service.queryLogs(queryParams);

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('log.admin_user_id = :adminUserId', { adminUserId: 'admin1' });
      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('log.operation_type = :operationType', { operationType: 'CREATE' });
      expect(mockQueryBuilder.orderBy).toHaveBeenCalledWith('log.created_at', 'DESC');
      expect(mockQueryBuilder.limit).toHaveBeenCalledWith(10);
      expect(mockQueryBuilder.offset).toHaveBeenCalledWith(0);

      expect(result.logs).toEqual(mockLogs);
      expect(result.total).toBe(2);
    });

    it('should query logs with date range', async () => {
      const startDate = new Date('2026-01-01');
      const endDate = new Date('2026-01-31');
      const queryParams: LogQueryParams = {
        startDate,
        endDate,
        isSensitive: true,
      };

      const mockLogs = [] as AdminOperationLog[];
      mockQueryBuilder.getManyAndCount.mockResolvedValue([mockLogs, 0]);

      const result = await service.queryLogs(queryParams);

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('log.created_at BETWEEN :startDate AND :endDate', {
        startDate,
        endDate
      });
      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('log.is_sensitive = :isSensitive', { isSensitive: true });
    });

    it('should handle query error', async () => {
      const queryParams: LogQueryParams = {};

      mockQueryBuilder.getManyAndCount.mockRejectedValue(new Error('Query error'));

      await expect(service.queryLogs(queryParams)).rejects.toThrow('Query error');
    });
  });

  describe('getLogById', () => {
    it('should get log by id successfully', async () => {
      const mockLog = { id: 'log1', admin_user_id: 'admin1' } as AdminOperationLog;

      mockRepository.findOne.mockResolvedValue(mockLog);

      const result = await service.getLogById('log1');

      expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { id: 'log1' } });
      expect(result).toEqual(mockLog);
    });

    it('should return null when log not found', async () => {
      mockRepository.findOne.mockResolvedValue(null);

      const result = await service.getLogById('nonexistent');

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

    it('should handle get error', async () => {
      mockRepository.findOne.mockRejectedValue(new Error('Database error'));

      await expect(service.getLogById('log1')).rejects.toThrow('Database error');
    });
  });

  describe('getStatistics', () => {
    it('should get statistics successfully', async () => {
      // Mock basic statistics
      mockQueryBuilder.getCount
        .mockResolvedValueOnce(100) // total
        .mockResolvedValueOnce(80)  // successful
        .mockResolvedValueOnce(10); // sensitive

      // Mock operation type statistics
      mockQueryBuilder.getRawMany.mockResolvedValueOnce([
        { type: 'CREATE', count: '50' },
        { type: 'UPDATE', count: '30' },
        { type: 'DELETE', count: '20' },
      ]);

      // Mock target type statistics
      mockQueryBuilder.getRawMany.mockResolvedValueOnce([
        { type: 'users', count: '60' },
        { type: 'profiles', count: '40' },
      ]);

      // Mock performance statistics
      mockQueryBuilder.getRawOne
        .mockResolvedValueOnce({ avgDuration: '150.5' }) // average duration
        .mockResolvedValueOnce({ uniqueAdmins: '5' });   // unique admins

      const result = await service.getStatistics();

      expect(result.totalOperations).toBe(100);
      expect(result.successfulOperations).toBe(80);
      expect(result.failedOperations).toBe(20);
      expect(result.sensitiveOperations).toBe(10);
      expect(result.operationsByType).toEqual({
        CREATE: 50,
        UPDATE: 30,
        DELETE: 20,
      });
      expect(result.operationsByTarget).toEqual({
        users: 60,
        profiles: 40,
      });
      expect(result.averageDuration).toBe(150.5);
      expect(result.uniqueAdmins).toBe(5);
    });

    it('should get statistics with date range', async () => {
      const startDate = new Date('2026-01-01');
      const endDate = new Date('2026-01-31');

      mockQueryBuilder.getCount.mockResolvedValue(50);
      mockQueryBuilder.getRawMany.mockResolvedValue([]);
      mockQueryBuilder.getRawOne.mockResolvedValue({ avgDuration: '100', uniqueAdmins: '3' });

      const result = await service.getStatistics(startDate, endDate);

      expect(mockQueryBuilder.where).toHaveBeenCalledWith('log.created_at BETWEEN :startDate AND :endDate', {
        startDate,
        endDate
      });
      expect(result.totalOperations).toBe(50);
    });
  });

  describe('cleanupExpiredLogs', () => {
    it('should cleanup expired logs successfully', async () => {
      mockQueryBuilder.execute.mockResolvedValue({ affected: 25 });

      const result = await service.cleanupExpiredLogs(30);

      expect(mockQueryBuilder.delete).toHaveBeenCalled();
      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('is_sensitive = :sensitive', { sensitive: false });
      expect(result).toBe(25);
    });

    it('should use default retention days', async () => {
      mockQueryBuilder.execute.mockResolvedValue({ affected: 10 });

      const result = await service.cleanupExpiredLogs();

      expect(result).toBe(10);
    });

    it('should handle cleanup error', async () => {
      mockQueryBuilder.execute.mockRejectedValue(new Error('Cleanup error'));

      await expect(service.cleanupExpiredLogs(30)).rejects.toThrow('Cleanup error');
    });
  });

  describe('getAdminOperationHistory', () => {
    it('should get admin operation history successfully', async () => {
      const mockLogs = [
        { id: 'log1', admin_user_id: 'admin1' },
        { id: 'log2', admin_user_id: 'admin1' },
      ] as AdminOperationLog[];

      mockRepository.find.mockResolvedValue(mockLogs);

      const result = await service.getAdminOperationHistory('admin1', 10);

      expect(mockRepository.find).toHaveBeenCalledWith({
        where: { admin_user_id: 'admin1' },
        order: { created_at: 'DESC' },
        take: 10
      });
      expect(result).toEqual(mockLogs);
    });

    it('should use default limit', async () => {
      mockRepository.find.mockResolvedValue([]);

      const result = await service.getAdminOperationHistory('admin1');

      expect(mockRepository.find).toHaveBeenCalledWith({
        where: { admin_user_id: 'admin1' },
        order: { created_at: 'DESC' },
        take: 20 // DEFAULT_LIMIT
      });
    });
  });

  describe('getSensitiveOperations', () => {
    it('should get sensitive operations successfully', async () => {
      const mockLogs = [
        { id: 'log1', is_sensitive: true },
      ] as AdminOperationLog[];

      mockRepository.findAndCount.mockResolvedValue([mockLogs, 1]);

      const result = await service.getSensitiveOperations(10, 0);

      expect(mockRepository.findAndCount).toHaveBeenCalledWith({
        where: { is_sensitive: true },
        order: { created_at: 'DESC' },
        take: 10,
        skip: 0
      });
      expect(result.logs).toEqual(mockLogs);
      expect(result.total).toBe(1);
    });

    it('should use default pagination', async () => {
      mockRepository.findAndCount.mockResolvedValue([[], 0]);

      const result = await service.getSensitiveOperations();

      expect(mockRepository.findAndCount).toHaveBeenCalledWith({
        where: { is_sensitive: true },
        order: { created_at: 'DESC' },
        take: 50, // DEFAULT_LIMIT
        skip: 0
      });
    });
  });
});