import { Test, type TestingModule } from '@nestjs/testing';
import { BadRequestException, ConflictException, NotFoundException } from '@nestjs/common';
import { HotelService } from './hotel.service';
import { HotelRepository } from './repositories/hotel.repository';
import { HotelMediaRepository } from './repositories/hotel-media.repository';
import { MediaService } from '../../common/media/media.service';

describe('HotelService', () => {
  let service: HotelService;

  const mockRepo = {
    findAllWithDestination: jest.fn(),
    findPaginated: jest.fn(),
    findByIdWithDestination: jest.fn(),
    findByNameAndDestination: jest.fn(),
    findByShortCode: jest.fn(),
    findById: jest.fn(),
    save: jest.fn(),
    update: jest.fn(),
    softDelete: jest.fn(),
  };

  const mockMedia = {
    list: jest.fn().mockResolvedValue([]),
    prepare: jest.fn(),
    sync: jest.fn(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        HotelService,
        { provide: HotelRepository, useValue: mockRepo },
        { provide: HotelMediaRepository, useValue: {} },
        { provide: MediaService, useValue: mockMedia },
      ],
    }).compile();

    service = module.get<HotelService>(HotelService);
  });

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

  describe('findAll', () => {
    it('should return paginated hotels with destinations', async () => {
      const hotels = [{ id: '1', name: 'Grand Hotel' }];
      const paginated = {
        items: hotels,
        meta: { total: 1, page: 1, limit: 10, totalPages: 1 },
      };
      mockRepo.findPaginated.mockResolvedValue(paginated);

      const result = await service.findAll();

      expect(result).toEqual(paginated);
      expect(mockRepo.findPaginated).toHaveBeenCalledTimes(1);
    });

    it('should return empty paginated result when no hotels exist', async () => {
      const paginated = {
        items: [],
        meta: { total: 0, page: 1, limit: 10, totalPages: 0 },
      };
      mockRepo.findPaginated.mockResolvedValue(paginated);
      const result = await service.findAll();
      expect(result).toEqual(paginated);
    });
  });

  describe('findById', () => {
    it('should return the hotel when found', async () => {
      const hotel = { id: '1', name: 'Grand Hotel' };
      mockRepo.findByIdWithDestination.mockResolvedValue(hotel);

      const result = await service.findById('1');

      expect(result).toEqual({ ...hotel, media: [] });
      expect(mockRepo.findByIdWithDestination).toHaveBeenCalledWith('1');
    });

    it('should throw NotFoundException when hotel not found', async () => {
      mockRepo.findByIdWithDestination.mockResolvedValue(null);

      await expect(service.findById('999')).rejects.toThrow(NotFoundException);
    });
  });

  describe('create', () => {
    const createDto = {
      name: 'Grand Hotel',
      destination_id: 'dest-1',
      address: '123 Main St',
      contact_person: 'Jane',
    };

    it('should create and return success response', async () => {
      mockRepo.findByNameAndDestination.mockResolvedValue(null);
      // short_code is auto-derived → helper probes findByShortCode.
      mockRepo.findByShortCode.mockResolvedValue(null);
      mockRepo.save.mockResolvedValue({ id: 'new-1' });

      const result = await service.create(createDto as any);

      expect(result).toEqual({ id: 'new-1', message: 'Hotel created successfully' });
      expect(mockRepo.save).toHaveBeenCalledTimes(1);
      // "Grand Hotel" → first 6 alphanumeric upper = "GRANDH"
      expect(mockRepo.save.mock.calls[0][0].short_code).toBe('GRANDH');
    });

    it('should throw ConflictException when name+destination already exists', async () => {
      mockRepo.findByNameAndDestination.mockResolvedValue({ id: 'existing' });

      await expect(service.create(createDto as any)).rejects.toThrow(ConflictException);
      expect(mockRepo.save).not.toHaveBeenCalled();
    });

    it('should default address and contact_person when omitted', async () => {
      mockRepo.findByNameAndDestination.mockResolvedValue(null);
      mockRepo.findByShortCode.mockResolvedValue(null);
      mockRepo.save.mockResolvedValue({ id: 'new-2' });

      await service.create({ name: 'Bare Hotel', destination_id: 'dest-1' } as any);

      expect(mockRepo.save.mock.calls[0][0].address).toBe('(TBC)');
      expect(mockRepo.save.mock.calls[0][0].contact_person).toBe('Bare Hotel');
    });
  });

  describe('createPlaceholder', () => {
    it('should return existing hotel when name+destination already match', async () => {
      mockRepo.findByNameAndDestination.mockResolvedValue({
        id: 'h-1',
        name: 'Grand Hotel',
        destination_id: 'dest-1',
      });

      const result = await service.createPlaceholder({
        name: 'Grand Hotel',
        destination_id: 'dest-1',
      });

      expect(result).toEqual({ id: 'h-1', name: 'Grand Hotel', destination_id: 'dest-1' });
      expect(mockRepo.save).not.toHaveBeenCalled();
    });

    it('should create a hotel with placeholder address when name is new', async () => {
      mockRepo.findByNameAndDestination.mockResolvedValue(null);
      mockRepo.findByShortCode.mockResolvedValue(null);
      mockRepo.save.mockResolvedValue({
        id: 'h-new',
        name: 'New Stay',
        destination_id: 'dest-1',
      });

      const result = await service.createPlaceholder({
        name: 'New Stay',
        destination_id: 'dest-1',
      });

      expect(result.id).toBe('h-new');
      expect(mockRepo.save.mock.calls[0][0].address).toBe('(TBC)');
      expect(mockRepo.save.mock.calls[0][0].contact_person).toBe('New Stay');
    });

    it('should reject blank name', async () => {
      await expect(
        service.createPlaceholder({ name: '  ', destination_id: 'dest-1' }),
      ).rejects.toThrow(BadRequestException);
    });
  });

  describe('update', () => {
    const existingHotel = { id: '1', name: 'Grand Hotel', destination_id: 'dest-1' };

    it('should update and return success response', async () => {
      mockRepo.findById.mockResolvedValue(existingHotel);
      mockRepo.update.mockResolvedValue(undefined);

      const result = await service.update('1', { address: 'New Address' });

      expect(result).toEqual({ id: '1', message: 'Hotel updated successfully' });
    });

    it('should throw NotFoundException when hotel not found', async () => {
      mockRepo.findById.mockResolvedValue(null);

      await expect(service.update('999', {} as any)).rejects.toThrow(NotFoundException);
    });

    it('should throw ConflictException when name+destination conflicts with another hotel', async () => {
      mockRepo.findById.mockResolvedValue(existingHotel);
      mockRepo.findByNameAndDestination.mockResolvedValue({ id: 'other-id' });

      await expect(service.update('1', { name: 'New Name' } as any)).rejects.toThrow(
        ConflictException,
      );
    });
  });

  describe('remove', () => {
    it('should soft delete and return success message', async () => {
      mockRepo.findById.mockResolvedValue({ id: '1' });
      mockRepo.softDelete.mockResolvedValue(undefined);

      const result = await service.remove('1');

      expect(result).toEqual({ message: 'Hotel deleted successfully' });
      expect(mockRepo.softDelete).toHaveBeenCalledWith('1');
    });

    it('should throw NotFoundException when hotel not found', async () => {
      mockRepo.findById.mockResolvedValue(null);

      await expect(service.remove('999')).rejects.toThrow(NotFoundException);
    });
  });
});
