import { Test, type TestingModule } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import { DestinationService } from './destination.service';
import { DestinationRepository } from './repositories/destination.repository';
import { DestinationMediaRepository } from './repositories/destination-media.repository';
import { MediaService } from '../../common/media/media.service';

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

  const mockRepo = {
    findAllSorted: jest.fn(),
    findPaginated: jest.fn(),
    findById: jest.fn(),
    findByShortCode: 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: [
        DestinationService,
        { provide: DestinationRepository, useValue: mockRepo },
        { provide: DestinationMediaRepository, useValue: {} },
        { provide: MediaService, useValue: mockMedia },
      ],
    }).compile();

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

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

  describe('findAll', () => {
    it('should return paginated destinations', async () => {
      const destinations = [{ id: '1', name: 'Bali' }];
      const paginated = {
        items: destinations,
        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 destinations 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 destination when found', async () => {
      const dest = { id: '1', name: 'Bali' };
      mockRepo.findById.mockResolvedValue(dest);

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

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

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

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

  describe('create', () => {
    const createDto = {
      name: 'Bali',
      short_code: 'BAL',
      country: 'Indonesia',
      city: 'Denpasar',
      currency: 'IDR',
    };

    it('should create and return success response', async () => {
      mockRepo.findByShortCode.mockResolvedValue(null);
      mockRepo.save.mockResolvedValue({ id: 'new-1' });

      const result = await service.create(createDto);

      expect(result).toEqual({ id: 'new-1', message: 'Destination created successfully' });
      expect(mockRepo.save).toHaveBeenCalledTimes(1);
    });

    // short_code is now auto-derived from `name`. The DTO field is
    // accepted for seeder back-compat, but conflict resolution moves
    // into the helper (probes via findByShortCode and appends a
    // numeric suffix). When the DTO doesn't pass short_code, the
    // service derives one — that path is what we assert here.
    it('auto-derives a tenant-unique short_code from name when DTO omits it', async () => {
      mockRepo.findByShortCode.mockResolvedValueOnce({ id: 'existing' }); // base taken
      mockRepo.findByShortCode.mockResolvedValueOnce(null); // base+1 free
      mockRepo.save.mockImplementation(async (payload: any) => ({ id: 'new-1', ...payload }));

      await service.create({ ...createDto, short_code: undefined } as any);

      const saved = mockRepo.save.mock.calls[0][0];
      // "Bali" → first 6 alphanumeric upper = "BALI"; "BALI" taken → "BALI1"
      expect(saved.short_code).toBe('BALI1');
    });
  });

  describe('update', () => {
    const existingDest = { id: '1', short_code: 'BAL' };

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

      const result = await service.update('1', { name: 'Bali Updated' });

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

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

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

    // short_code is now immutable post-create. The UI never sends it on
    // update; if a stale payload includes one the service strips it.
    it('ignores short_code in update payload (immutable post-create)', async () => {
      mockRepo.findById.mockResolvedValue(existingDest);
      mockRepo.update.mockResolvedValue(undefined);

      await service.update('1', { short_code: 'NEW' } as any);

      const updateArg = mockRepo.update.mock.calls[0][1];
      expect(updateArg).not.toHaveProperty('short_code');
      // findByShortCode is no longer called from the update path
      expect(mockRepo.findByShortCode).not.toHaveBeenCalled();
    });
  });

  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: 'Destination deleted successfully' });
      expect(mockRepo.softDelete).toHaveBeenCalledWith('1');
    });

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

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