import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { DataSource } from 'typeorm';
import { FileService } from '@d8d/server/modules/files/file.service';
import { File } from '@d8d/server/modules/files/file.entity';
import { MinioService } from '@d8d/server/modules/files/minio.service';
import { logger } from '@d8d/server/utils/logger';

// Mock dependencies
vi.mock('@d8d/server/modules/files/minio.service');
vi.mock('@d8d/server/utils/logger');
vi.mock('uuid', () => ({
  v4: () => 'test-uuid-123'
}));

describe('FileService', () => {
  let mockDataSource: DataSource;

  beforeEach(() => {
    mockDataSource = {
      getRepository: vi.fn(() => ({
        findOneBy: vi.fn(),
        save: vi.fn()
      }))
    } as unknown as DataSource;

  });

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

  describe('createFile', () => {
    it('should create file with upload policy successfully', async () => {
      const mockFileData = {
        name: 'test.txt',
        type: 'text/plain',
        size: 1024,
        uploadUserId: 1
      };

      const mockUploadPolicy = {
        'x-amz-algorithm': 'test-algorithm',
        'x-amz-credential': 'test-credential',
        host: 'https://minio.example.com'
      };

      const mockSavedFile = {
        id: 1,
        ...mockFileData,
        path: '1/test-uuid-123-test.txt',
        uploadTime: new Date(),
        createdAt: new Date(),
        updatedAt: new Date()
      };


      const mockGenerateUploadPolicy = vi.fn().mockResolvedValue(mockUploadPolicy);
      vi.mocked(MinioService).mockImplementation(() => ({
        generateUploadPolicy: mockGenerateUploadPolicy
      } as unknown as MinioService));

      const fileService = new FileService(mockDataSource);

      // Mock GenericCrudService methods
      vi.spyOn(fileService, 'create').mockResolvedValue(mockSavedFile as File);

      const result = await fileService.createFile(mockFileData);

      expect(mockGenerateUploadPolicy).toHaveBeenCalledWith('1/test-uuid-123-test.txt');
      expect(fileService.create).toHaveBeenCalledWith(expect.objectContaining({
        name: 'test.txt',
        path: '1/test-uuid-123-test.txt',
        uploadUserId: 1
      }));
      expect(result).toEqual({
        file: mockSavedFile,
        uploadPolicy: mockUploadPolicy
      });
    });

    it('should handle errors during file creation', async () => {
      const mockFileData = {
        name: 'test.txt',
        uploadUserId: 1
      };

      const mockGenerateUploadPolicy = vi.fn().mockRejectedValue(new Error('MinIO error'));
      vi.mocked(MinioService).mockImplementation(() => ({
        generateUploadPolicy: mockGenerateUploadPolicy
      } as unknown as MinioService));

      const fileService = new FileService(mockDataSource);

      await expect(fileService.createFile(mockFileData)).rejects.toThrow('文件创建失败');
      expect(logger.error).toHaveBeenCalled();
    });
  });

  describe('deleteFile', () => {
    it('should delete file successfully when file exists', async () => {
      const mockFile = {
        id: 1,
        path: '1/test-file.txt',
        name: 'test-file.txt'
      } as File;

      const mockObjectExists = vi.fn().mockResolvedValue(true);
      const mockDeleteObject = vi.fn().mockResolvedValue(undefined);

      vi.mocked(MinioService).mockImplementation(() => ({
        objectExists: mockObjectExists,
        deleteObject: mockDeleteObject,
        bucketName: 'd8dai'
      } as unknown as MinioService));

      const fileService = new FileService(mockDataSource);
      vi.spyOn(fileService, 'getById').mockResolvedValue(mockFile);
      vi.spyOn(fileService, 'delete').mockResolvedValue(true);

      const result = await fileService.deleteFile(1);

      expect(fileService.getById).toHaveBeenCalledWith(1);
      expect(mockObjectExists).toHaveBeenCalledWith('d8dai', '1/test-file.txt');
      expect(mockDeleteObject).toHaveBeenCalledWith('d8dai', '1/test-file.txt');
      expect(fileService.delete).toHaveBeenCalledWith(1);
      expect(result).toBe(true);
    });

    it('should delete database record even when MinIO file not found', async () => {
      const mockFile = {
        id: 1,
        path: '1/test-file.txt',
        name: 'test-file.txt'
      } as File;

      const mockObjectExists = vi.fn().mockResolvedValue(false);

      vi.mocked(MinioService).mockImplementation(() => ({
        objectExists: mockObjectExists,
        deleteObject: vi.fn(),
        bucketName: 'd8dai'
      } as unknown as MinioService));

      const fileService = new FileService(mockDataSource);
      vi.spyOn(fileService, 'getById').mockResolvedValue(mockFile);
      vi.spyOn(fileService, 'delete').mockResolvedValue(true);

      const result = await fileService.deleteFile(1);

      expect(mockObjectExists).toHaveBeenCalledWith('d8dai', '1/test-file.txt');
      expect(fileService.delete).toHaveBeenCalledWith(1);
      expect(result).toBe(true);
      expect(logger.error).toHaveBeenCalled();
    });

    it('should throw error when file not found', async () => {
      const fileService = new FileService(mockDataSource);
      vi.spyOn(fileService, 'getById').mockResolvedValue(null);

      await expect(fileService.deleteFile(999)).rejects.toThrow('文件不存在');
    });
  });

  describe('getFileUrl', () => {
    it('should return file URL successfully', async () => {
      const mockFile = {
        id: 1,
        path: '1/test-file.txt'
      } as File;

      const mockPresignedUrl = 'https://minio.example.com/presigned-url';

      const mockGetPresignedFileUrl = vi.fn().mockResolvedValue(mockPresignedUrl);

      vi.mocked(MinioService).mockImplementation(() => ({
        getPresignedFileUrl: mockGetPresignedFileUrl,
        bucketName: 'd8dai'
      } as unknown as MinioService));

      const fileService = new FileService(mockDataSource);
      vi.spyOn(fileService, 'getById').mockResolvedValue(mockFile);

      const result = await fileService.getFileUrl(1);

      expect(fileService.getById).toHaveBeenCalledWith(1);
      expect(mockGetPresignedFileUrl).toHaveBeenCalledWith('d8dai', '1/test-file.txt');
      expect(result).toBe(mockPresignedUrl);
    });

    it('should throw error when file not found', async () => {
      const fileService = new FileService(mockDataSource);
      vi.spyOn(fileService, 'getById').mockResolvedValue(null);

      await expect(fileService.getFileUrl(999)).rejects.toThrow('文件不存在');
    });
  });

  describe('getFileDownloadUrl', () => {
    it('should return download URL with filename', async () => {
      const mockFile = {
        id: 1,
        path: '1/test-file.txt',
        name: '测试文件.txt'
      } as File;

      const mockPresignedUrl = 'https://minio.example.com/download-url';

      const mockGetPresignedFileDownloadUrl = vi.fn().mockResolvedValue(mockPresignedUrl);

      vi.mocked(MinioService).mockImplementation(() => ({
        getPresignedFileDownloadUrl: mockGetPresignedFileDownloadUrl,
        bucketName: 'd8dai'
      } as unknown as MinioService));

      const fileService = new FileService(mockDataSource);
      vi.spyOn(fileService, 'getById').mockResolvedValue(mockFile);

      const result = await fileService.getFileDownloadUrl(1);

      expect(fileService.getById).toHaveBeenCalledWith(1);
      expect(mockGetPresignedFileDownloadUrl).toHaveBeenCalledWith(
        'd8dai',
        '1/test-file.txt',
        '测试文件.txt'
      );
      expect(result).toEqual({
        url: mockPresignedUrl,
        filename: '测试文件.txt'
      });
    });

    it('should throw error when file not found', async () => {
      const fileService = new FileService(mockDataSource);
      vi.spyOn(fileService, 'getById').mockResolvedValue(null);

      await expect(fileService.getFileDownloadUrl(999)).rejects.toThrow('文件不存在');
    });
  });

  describe('createMultipartUploadPolicy', () => {
    it('should create multipart upload policy successfully', async () => {
      const mockFileData = {
        name: 'large-file.zip',
        type: 'application/zip',
        uploadUserId: 1
      };

      const mockUploadId = 'upload-123';
      const mockUploadUrls = ['url1', 'url2', 'url3'];
      const mockSavedFile = {
        id: 1,
        ...mockFileData,
        path: '1/test-uuid-123-large-file.zip',
        uploadTime: new Date(),
        createdAt: new Date(),
        updatedAt: new Date()
      } as File;

      const mockCreateMultipartUpload = vi.fn().mockResolvedValue(mockUploadId);
      const mockGenerateMultipartUploadUrls = vi.fn().mockResolvedValue(mockUploadUrls);

      vi.mocked(MinioService).mockImplementation(() => ({
        createMultipartUpload: mockCreateMultipartUpload,
        generateMultipartUploadUrls: mockGenerateMultipartUploadUrls,
        bucketName: 'd8dai'
      } as unknown as MinioService));

      const fileService = new FileService(mockDataSource);
      vi.spyOn(fileService, 'create').mockResolvedValue(mockSavedFile);

      const result = await fileService.createMultipartUploadPolicy(mockFileData, 3);

      expect(mockCreateMultipartUpload).toHaveBeenCalledWith('d8dai', '1/test-uuid-123-large-file.zip');
      expect(mockGenerateMultipartUploadUrls).toHaveBeenCalledWith(
        'd8dai',
        '1/test-uuid-123-large-file.zip',
        mockUploadId,
        3
      );
      expect(result).toEqual({
        file: mockSavedFile,
        uploadId: mockUploadId,
        uploadUrls: mockUploadUrls,
        bucket: 'd8dai',
        key: '1/test-uuid-123-large-file.zip'
      });
    });

    it('should handle errors during multipart upload creation', async () => {
      const mockFileData = {
        name: 'large-file.zip',
        uploadUserId: 1
      };

      const mockCreateMultipartUpload = vi.fn().mockRejectedValue(new Error('MinIO error'));

      vi.mocked(MinioService).mockImplementation(() => ({
        createMultipartUpload: mockCreateMultipartUpload,
        bucketName: 'd8dai'
      } as unknown as MinioService));

      const fileService = new FileService(mockDataSource);

      await expect(fileService.createMultipartUploadPolicy(mockFileData, 3)).rejects.toThrow('创建多部分上传策略失败');
      expect(logger.error).toHaveBeenCalled();
    });
  });

  describe('completeMultipartUpload', () => {
    it('should complete multipart upload successfully', async () => {
      const uploadData = {
        uploadId: 'upload-123',
        bucket: 'd8dai',
        key: '1/test-file.txt',
        parts: [
          { partNumber: 1, etag: 'etag1' },
          { partNumber: 2, etag: 'etag2' }
        ]
      };

      const mockFile = {
        id: 1,
        path: '1/test-file.txt',
        size: 0,
        updatedAt: new Date()
      } as File;

      const mockCompleteResult = { size: 2048 };
      const mockFileUrl = 'https://minio.example.com/file.txt';

      const mockCompleteMultipartUpload = vi.fn().mockResolvedValue(mockCompleteResult);
      const mockGetFileUrl = vi.fn().mockReturnValue(mockFileUrl);

      vi.mocked(MinioService).mockImplementation(() => ({
        completeMultipartUpload: mockCompleteMultipartUpload,
        getFileUrl: mockGetFileUrl
      } as unknown as MinioService));

      const mockRepository = {
        findOneBy: vi.fn().mockResolvedValue(mockFile),
        save: vi.fn().mockResolvedValue({ ...mockFile, size: 2048 } as File)
      };

      mockDataSource.getRepository = vi.fn().mockReturnValue(mockRepository);
      const fileService = new FileService(mockDataSource);

      const result = await fileService.completeMultipartUpload(uploadData);

      expect(mockCompleteMultipartUpload).toHaveBeenCalledWith(
        'd8dai',
        '1/test-file.txt',
        'upload-123',
        [{ PartNumber: 1, ETag: 'etag1' }, { PartNumber: 2, ETag: 'etag2' }]
      );
      expect(mockRepository.findOneBy).toHaveBeenCalledWith({ path: '1/test-file.txt' });
      expect(mockRepository.save).toHaveBeenCalledWith(expect.objectContaining({
        size: 2048
      }));
      expect(result).toEqual({
        fileId: 1,
        url: mockFileUrl,
        key: '1/test-file.txt',
        size: 2048
      });
    });

    it('should throw error when file record not found', async () => {
      const uploadData = {
        uploadId: 'upload-123',
        bucket: 'd8dai',
        key: '1/nonexistent.txt',
        parts: [{ partNumber: 1, etag: 'etag1' }]
      };

      const mockCompleteMultipartUpload = vi.fn().mockResolvedValue({ size: 1024 });

      vi.mocked(MinioService).mockImplementation(() => ({
        completeMultipartUpload: mockCompleteMultipartUpload
      } as unknown as MinioService));

      const mockRepository = {
        findOneBy: vi.fn().mockResolvedValue(null)
      };

      mockDataSource.getRepository = vi.fn().mockReturnValue(mockRepository);
      const fileService = new FileService(mockDataSource);

      await expect(fileService.completeMultipartUpload(uploadData)).rejects.toThrow('文件记录不存在');
    });

    it('should handle errors during completion', async () => {
      const uploadData = {
        uploadId: 'upload-123',
        bucket: 'd8dai',
        key: '1/test-file.txt',
        parts: [{ partNumber: 1, etag: 'etag1' }]
      };

      const mockFile = {
        id: 1,
        path: '1/test-file.txt',
        size: 0,
        updatedAt: new Date()
      } as File;

      const mockRepository = {
        findOneBy: vi.fn().mockResolvedValue(mockFile),
        save: vi.fn()
      };

      const mockCompleteMultipartUpload = vi.fn().mockRejectedValue(new Error('Completion failed'));

      mockDataSource.getRepository = vi.fn().mockReturnValue(mockRepository);
      vi.mocked(MinioService).mockImplementation(() => ({
        completeMultipartUpload: mockCompleteMultipartUpload
      } as unknown as MinioService));

      const fileService = new FileService(mockDataSource);

      await expect(fileService.completeMultipartUpload(uploadData)).rejects.toThrow('完成分片上传失败');
      expect(logger.error).toHaveBeenCalled();
    });
  });
});