import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { NotFoundException } from '@nestjs/common';
import { PhotosService, UploadPhotoDto } from './photos.service';
import { Photo, PhotoType } from '../../database/entities/photo.entity';
import { StorageService } from '../../common/services/storage.service';
import { AuditService } from '../../common/services/audit.service';

describe('PhotosService', () => {
  let service: PhotosService;
  let photoRepository: Repository<Photo>;
  let storageService: StorageService;
  let auditService: AuditService;

  const mockPhoto = {
    id: 'photo_123',
    userId: 'user_123',
    childId: 'child_123',
    activityId: null,
    type: PhotoType.GENERAL,
    originalFilename: 'baby_smile.jpg',
    mimeType: 'image/jpeg',
    fileSize: 1024000,
    storageKey: 'photos/user_123/child_123/abc123.jpg',
    thumbnailKey: 'photos/user_123/child_123/thumbnails/abc123_thumb.jpg',
    width: 1920,
    height: 1080,
    caption: 'Beautiful smile!',
    description: null,
    takenAt: new Date('2025-01-01'),
    metadata: null,
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  const mockFile: Express.Multer.File = {
    fieldname: 'photo',
    originalname: 'test.jpg',
    encoding: '7bit',
    mimetype: 'image/jpeg',
    size: 1024000,
    buffer: Buffer.from('fake-image-data'),
    stream: null as any,
    destination: '',
    filename: '',
    path: '',
  };

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

  const mockStorageService = {
    uploadImage: jest.fn().mockResolvedValue({
      key: 'photos/user_123/child_123/abc123.jpg',
      url: 'https://storage.com/photo.jpg',
      metadata: {
        size: 1024000,
        width: 1920,
        height: 1080,
      },
    }),
    generateThumbnail: jest.fn().mockResolvedValue({
      key: 'photos/user_123/child_123/thumbnails/abc123_thumb.jpg',
      url: 'https://storage.com/thumb.jpg',
    }),
    getPresignedUrl: jest
      .fn()
      .mockResolvedValue('https://storage.com/presigned/photo.jpg'),
    deleteFile: jest.fn().mockResolvedValue(true),
  };

  const mockAuditService = {
    logCreate: jest.fn().mockResolvedValue(undefined),
    logDelete: jest.fn().mockResolvedValue(undefined),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        PhotosService,
        {
          provide: getRepositoryToken(Photo),
          useValue: mockPhotoRepository,
        },
        {
          provide: StorageService,
          useValue: mockStorageService,
        },
        {
          provide: AuditService,
          useValue: mockAuditService,
        },
      ],
    }).compile();

    service = module.get<PhotosService>(PhotosService);
    photoRepository = module.get<Repository<Photo>>(
      getRepositoryToken(Photo),
    );
    storageService = module.get<StorageService>(StorageService);
    auditService = module.get<AuditService>(AuditService);

    jest.clearAllMocks();
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('uploadPhoto', () => {
    it('should upload photo with thumbnail generation', async () => {
      const uploadDto: UploadPhotoDto = {
        userId: 'user_123',
        childId: 'child_123',
        type: PhotoType.GENERAL,
        caption: 'Test photo',
      };

      mockPhotoRepository.create.mockReturnValue(mockPhoto);
      mockPhotoRepository.save.mockResolvedValue(mockPhoto);

      const result = await service.uploadPhoto(mockFile, uploadDto);

      expect(storageService.uploadImage).toHaveBeenCalledWith(
        mockFile.buffer,
        expect.stringContaining('photos/user_123/child_123'),
        {
          maxWidth: 1920,
          maxHeight: 1920,
          quality: 85,
        },
      );
      expect(storageService.generateThumbnail).toHaveBeenCalledWith(
        mockFile.buffer,
        expect.stringContaining('thumbnails'),
        300,
        300,
      );
      expect(photoRepository.save).toHaveBeenCalled();
      expect(auditService.logCreate).toHaveBeenCalled();
      expect(result).toEqual(mockPhoto);
    });

    it('should handle photo upload without childId', async () => {
      const uploadDto: UploadPhotoDto = {
        userId: 'user_123',
        type: PhotoType.GENERAL,
      };

      mockPhotoRepository.create.mockReturnValue(mockPhoto);
      mockPhotoRepository.save.mockResolvedValue(mockPhoto);

      await service.uploadPhoto(mockFile, uploadDto);

      expect(storageService.uploadImage).toHaveBeenCalledWith(
        mockFile.buffer,
        expect.stringContaining('general'),
        expect.any(Object),
      );
    });

    it('should throw error if upload fails', async () => {
      mockStorageService.uploadImage.mockRejectedValue(
        new Error('Storage error'),
      );

      const uploadDto: UploadPhotoDto = {
        userId: 'user_123',
        type: PhotoType.GENERAL,
      };

      await expect(service.uploadPhoto(mockFile, uploadDto)).rejects.toThrow(
        'Photo upload failed',
      );
    });

    it('should include optional metadata', async () => {
      const uploadDto: UploadPhotoDto = {
        userId: 'user_123',
        childId: 'child_123',
        type: PhotoType.MILESTONE,
        caption: 'First steps',
        description: 'Baby walked for the first time!',
        takenAt: new Date('2025-01-01'),
        metadata: { milestone: 'first_steps', ageMonths: 12 },
      };

      mockPhotoRepository.create.mockReturnValue(mockPhoto);
      mockPhotoRepository.save.mockResolvedValue(mockPhoto);

      await service.uploadPhoto(mockFile, uploadDto);

      expect(photoRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          caption: 'First steps',
          description: 'Baby walked for the first time!',
          takenAt: uploadDto.takenAt,
          metadata: uploadDto.metadata,
        }),
      );
    });
  });

  describe('getChildPhotos', () => {
    it('should get all photos for a child', async () => {
      const mockQueryBuilder = {
        where: jest.fn().mockReturnThis(),
        andWhere: jest.fn().mockReturnThis(),
        orderBy: jest.fn().mockReturnThis(),
        take: jest.fn().mockReturnThis(),
        skip: jest.fn().mockReturnThis(),
        getCount: jest.fn().mockResolvedValue(5),
        getMany: jest.fn().mockResolvedValue([mockPhoto, mockPhoto]),
      };

      mockPhotoRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);

      const result = await service.getChildPhotos('child_123');

      expect(result.photos).toHaveLength(2);
      expect(result.total).toBe(5);
      expect(mockQueryBuilder.where).toHaveBeenCalledWith(
        'photo.childId = :childId',
        { childId: 'child_123' },
      );
    });

    it('should filter by photo type', async () => {
      const mockQueryBuilder = {
        where: jest.fn().mockReturnThis(),
        andWhere: jest.fn().mockReturnThis(),
        orderBy: jest.fn().mockReturnThis(),
        take: jest.fn().mockReturnThis(),
        skip: jest.fn().mockReturnThis(),
        getCount: jest.fn().mockResolvedValue(3),
        getMany: jest.fn().mockResolvedValue([mockPhoto]),
      };

      mockPhotoRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);

      await service.getChildPhotos('child_123', {
        type: PhotoType.MILESTONE,
      });

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'photo.type = :type',
        { type: PhotoType.MILESTONE },
      );
    });

    it('should apply limit and offset', async () => {
      const mockQueryBuilder = {
        where: jest.fn().mockReturnThis(),
        andWhere: jest.fn().mockReturnThis(),
        orderBy: jest.fn().mockReturnThis(),
        take: jest.fn().mockReturnThis(),
        skip: jest.fn().mockReturnThis(),
        getCount: jest.fn().mockResolvedValue(100),
        getMany: jest.fn().mockResolvedValue([]),
      };

      mockPhotoRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);

      await service.getChildPhotos('child_123', {
        limit: 10,
        offset: 20,
      });

      expect(mockQueryBuilder.take).toHaveBeenCalledWith(10);
      expect(mockQueryBuilder.skip).toHaveBeenCalledWith(20);
    });
  });

  describe('getActivityPhotos', () => {
    it('should get photos for an activity', async () => {
      mockPhotoRepository.find.mockResolvedValue([mockPhoto]);

      const result = await service.getActivityPhotos('activity_123');

      expect(photoRepository.find).toHaveBeenCalledWith({
        where: { activityId: 'activity_123' },
        order: { createdAt: 'ASC' },
      });
      expect(result).toEqual([mockPhoto]);
    });
  });

  describe('getPhoto', () => {
    it('should get photo by ID', async () => {
      mockPhotoRepository.findOne.mockResolvedValue(mockPhoto);

      const result = await service.getPhoto('photo_123', 'user_123');

      expect(photoRepository.findOne).toHaveBeenCalledWith({
        where: { id: 'photo_123', userId: 'user_123' },
        relations: ['child', 'activity'],
      });
      expect(result).toEqual(mockPhoto);
    });

    it('should throw NotFoundException if photo not found', async () => {
      mockPhotoRepository.findOne.mockResolvedValue(null);

      await expect(service.getPhoto('photo_123', 'user_123')).rejects.toThrow(
        NotFoundException,
      );
    });
  });

  describe('getPhotoWithUrl', () => {
    it('should return photo with presigned URLs', async () => {
      mockPhotoRepository.findOne.mockResolvedValue(mockPhoto);

      const result = await service.getPhotoWithUrl('photo_123', 'user_123');

      expect(storageService.getPresignedUrl).toHaveBeenCalledWith(
        mockPhoto.storageKey,
        3600,
      );
      expect(storageService.getPresignedUrl).toHaveBeenCalledWith(
        mockPhoto.thumbnailKey,
        3600,
      );
      expect(result).toHaveProperty('url');
      expect(result).toHaveProperty('thumbnailUrl');
    });

    it('should use main URL for thumbnail if no thumbnail key', async () => {
      const photoWithoutThumbnail = { ...mockPhoto, thumbnailKey: null };
      mockPhotoRepository.findOne.mockResolvedValue(photoWithoutThumbnail);

      const result = await service.getPhotoWithUrl('photo_123', 'user_123');

      expect(result.url).toBe(result.thumbnailUrl);
    });
  });

  describe('getGallery', () => {
    it('should get photos with presigned URLs', async () => {
      const mockQueryBuilder = {
        where: jest.fn().mockReturnThis(),
        andWhere: jest.fn().mockReturnThis(),
        orderBy: jest.fn().mockReturnThis(),
        take: jest.fn().mockReturnThis(),
        skip: jest.fn().mockReturnThis(),
        getCount: jest.fn().mockResolvedValue(2),
        getMany: jest.fn().mockResolvedValue([mockPhoto, mockPhoto]),
      };

      mockPhotoRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);

      const result = await service.getGallery('child_123', 'user_123');

      expect(result.total).toBe(2);
      expect(result.photos).toHaveLength(2);
      expect(result.photos[0]).toHaveProperty('url');
      expect(result.photos[0]).toHaveProperty('thumbnailUrl');
    });
  });

  describe('updatePhoto', () => {
    it('should update photo metadata', async () => {
      mockPhotoRepository.findOne.mockResolvedValue(mockPhoto);
      const updatedPhoto = { ...mockPhoto, caption: 'Updated caption' };
      mockPhotoRepository.save.mockResolvedValue(updatedPhoto);

      const result = await service.updatePhoto('photo_123', 'user_123', {
        caption: 'Updated caption',
        description: 'New description',
      });

      expect(photoRepository.save).toHaveBeenCalled();
      expect(result.caption).toBe('Updated caption');
    });

    it('should throw error if photo not found', async () => {
      mockPhotoRepository.findOne.mockResolvedValue(null);

      await expect(
        service.updatePhoto('photo_123', 'user_123', { caption: 'Test' }),
      ).rejects.toThrow(NotFoundException);
    });
  });

  describe('deletePhoto', () => {
    it('should delete photo from storage and database', async () => {
      mockPhotoRepository.findOne.mockResolvedValue(mockPhoto);
      mockPhotoRepository.remove.mockResolvedValue(mockPhoto);

      await service.deletePhoto('photo_123', 'user_123');

      expect(storageService.deleteFile).toHaveBeenCalledWith(
        mockPhoto.storageKey,
      );
      expect(storageService.deleteFile).toHaveBeenCalledWith(
        mockPhoto.thumbnailKey,
      );
      expect(photoRepository.remove).toHaveBeenCalledWith(mockPhoto);
      expect(auditService.logDelete).toHaveBeenCalled();
    });

    it('should continue with database deletion if storage deletion fails', async () => {
      mockPhotoRepository.findOne.mockResolvedValue(mockPhoto);
      mockPhotoRepository.remove.mockResolvedValue(mockPhoto);
      mockStorageService.deleteFile.mockRejectedValue(
        new Error('Storage error'),
      );

      await service.deletePhoto('photo_123', 'user_123');

      expect(photoRepository.remove).toHaveBeenCalledWith(mockPhoto);
    });

    it('should throw error if photo not found', async () => {
      mockPhotoRepository.findOne.mockResolvedValue(null);

      await expect(
        service.deletePhoto('photo_123', 'user_123'),
      ).rejects.toThrow(NotFoundException);
    });
  });

  describe('getMilestonePhotos', () => {
    it('should get milestone photos ordered by takenAt', async () => {
      const milestonePhotos = [
        { ...mockPhoto, type: PhotoType.MILESTONE },
        { ...mockPhoto, type: PhotoType.MILESTONE },
      ];
      mockPhotoRepository.find.mockResolvedValue(milestonePhotos);

      const result = await service.getMilestonePhotos('child_123');

      expect(photoRepository.find).toHaveBeenCalledWith({
        where: { childId: 'child_123', type: PhotoType.MILESTONE },
        order: { takenAt: 'ASC', createdAt: 'ASC' },
      });
      expect(result).toEqual(milestonePhotos);
    });
  });

  describe('getRecentPhotos', () => {
    it('should get recent photos with default limit', async () => {
      mockPhotoRepository.find.mockResolvedValue([mockPhoto]);

      const result = await service.getRecentPhotos('user_123');

      expect(photoRepository.find).toHaveBeenCalledWith({
        where: { userId: 'user_123' },
        order: { createdAt: 'DESC' },
        take: 10,
        relations: ['child'],
      });
      expect(result).toEqual([mockPhoto]);
    });

    it('should apply custom limit', async () => {
      mockPhotoRepository.find.mockResolvedValue([]);

      await service.getRecentPhotos('user_123', 5);

      expect(photoRepository.find).toHaveBeenCalledWith(
        expect.objectContaining({
          take: 5,
        }),
      );
    });
  });

  describe('getPhotoStats', () => {
    it('should calculate photo statistics', async () => {
      const photos = [
        { ...mockPhoto, type: PhotoType.MILESTONE, fileSize: 500000 },
        { ...mockPhoto, type: PhotoType.MILESTONE, fileSize: 600000 },
        { ...mockPhoto, type: PhotoType.ACTIVITY, fileSize: 400000 },
        { ...mockPhoto, type: PhotoType.GENERAL, fileSize: 300000 },
      ];
      mockPhotoRepository.find.mockResolvedValue(photos);

      const result = await service.getPhotoStats('child_123');

      expect(result.total).toBe(4);
      expect(result.byType[PhotoType.MILESTONE]).toBe(2);
      expect(result.byType[PhotoType.ACTIVITY]).toBe(1);
      expect(result.byType[PhotoType.GENERAL]).toBe(1);
      expect(result.byType[PhotoType.PROFILE]).toBe(0);
      expect(result.totalSize).toBe(1800000);
    });

    it('should handle empty photo collection', async () => {
      mockPhotoRepository.find.mockResolvedValue([]);

      const result = await service.getPhotoStats('child_123');

      expect(result.total).toBe(0);
      expect(result.totalSize).toBe(0);
      expect(result.byType[PhotoType.MILESTONE]).toBe(0);
    });
  });
});
