import { Test, type TestingModule } from '@nestjs/testing';
import { BadRequestException, ConflictException, NotFoundException } from '@nestjs/common';
import { TripSourceService } from './trip-source.service';
import { ClsService } from 'nestjs-cls';
import { TripSourceRepository } from './repositories/trip-source.repository';
import { AgentCostRepository } from '../agent-cost/repositories/agent-cost.repository';
import { TripSourceMediaRepository } from './repositories/trip-source-media.repository';
import { MediaService } from '../../common/media/media.service';

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

  const mockAgentCosts = {
    findActiveById: jest.fn(),
  };

  const mockRepo = {
    findAllSorted: jest.fn(),
    findPaginated: jest.fn(),
    findById: jest.fn(),
    findByShortName: jest.fn(),
    findByShortCode: jest.fn(),
    findByName: 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: [
        TripSourceService,
        { provide: TripSourceRepository, useValue: mockRepo },
        { provide: AgentCostRepository, useValue: mockAgentCosts },
        // view_all flag drives the agent-roster gate in findAll; default
        // to a view-all caller so the existing list tests see results.
        { provide: ClsService, useValue: { get: () => true } },
        { provide: TripSourceMediaRepository, useValue: {} },
        { provide: MediaService, useValue: mockMedia },
      ],
    }).compile();

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

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

  describe('findAll', () => {
    it('should return paginated trip sources', async () => {
      const items = [{ id: '1', name: 'Agency A' }];
      const paginated = {
        items,
        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('filters the list by agent cost', async () => {
      mockRepo.findPaginated.mockResolvedValue({
        items: [],
        meta: { total: 0, page: 1, limit: 10, totalPages: 0 },
      });

      await service.findAll(undefined, { agentCostId: 'cost-1' });

      expect(mockRepo.findPaginated).toHaveBeenCalledWith(
        expect.objectContaining({ where: { agent_cost_id: 'cost-1' } }),
      );
    });

    it('should return empty paginated result when no trip sources 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 trip source when found', async () => {
      const item = { id: '1', name: 'Agency A' };
      mockRepo.findById.mockResolvedValue(item);

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

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

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

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

  describe('create', () => {
    const createDto = {
      source_type: 'agent',
      name: 'Agency A',
      short_name: 'AA',
      contact_name: 'John',
    };

    it('should create and return success response', async () => {
      mockRepo.findByShortName.mockResolvedValue(null);
      // short_code is now auto-derived → the helper probes
      // findByShortCode for collisions. Free slot → save fires.
      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: 'Trip source created successfully' });
      expect(mockRepo.save).toHaveBeenCalledTimes(1);
      // Auto-derived short_code: "Agency A" → first 6 alphanumeric upper = "AGENCY"
      expect(mockRepo.save.mock.calls[0][0].short_code).toBe('AGENCY');
    });

    it('rejects an unknown agent cost for any source type', async () => {
      mockRepo.findByShortName.mockResolvedValue(null);
      mockRepo.findByShortCode.mockResolvedValue(null);
      mockAgentCosts.findActiveById.mockResolvedValue(null);

      await expect(
        service.create({ ...createDto, source_type: 'DIRECT', agent_cost_id: 'missing' } as any),
      ).rejects.toThrow(BadRequestException);
      expect(mockRepo.save).not.toHaveBeenCalled();
    });

    it('stores the agent cost id for any source type', async () => {
      mockRepo.findByShortName.mockResolvedValue(null);
      mockRepo.findByShortCode.mockResolvedValue(null);
      mockAgentCosts.findActiveById.mockResolvedValue({ id: 'cost-1' });
      mockRepo.save.mockResolvedValue({ id: 'new-1' });

      await service.create({
        ...createDto,
        source_type: 'DIRECT',
        agent_cost_id: 'cost-1',
      } as any);

      expect(mockRepo.save.mock.calls[0][0].agent_cost_id).toBe('cost-1');
    });

    it('should throw ConflictException when short_name already exists', async () => {
      mockRepo.findByShortName.mockResolvedValue({ id: 'existing' });

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

  describe('update', () => {
    const existingItem = { id: '1', short_name: 'AA' };

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

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

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

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

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

    it('should throw ConflictException when short_name conflicts with another source', async () => {
      mockRepo.findById.mockResolvedValue(existingItem);
      mockRepo.findByShortName.mockResolvedValue({ id: 'other-id' });

      await expect(service.update('1', { short_name: 'BB' } as any)).rejects.toThrow(
        ConflictException,
      );
    });

    it('should skip uniqueness check when short_name unchanged', async () => {
      mockRepo.findById.mockResolvedValue(existingItem);
      mockRepo.update.mockResolvedValue(undefined);

      await service.update('1', { short_name: 'AA' });

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

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

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