import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundException, BadRequestException } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { FunnelService } from './funnel.service';
import { PrismaService } from '../../prisma/prisma.service';

const FunnelStage = {
  NEW: 'NEW',
  ASSIGNED: 'ASSIGNED',
  COMPLETED: 'COMPLETED',
  REGULAR: 'REGULAR',
  TRANSFERRED: 'TRANSFERRED',
  REFUSED: 'REFUSED',
} as const;

describe('FunnelService', () => {
  let service: FunnelService;
  let prisma: PrismaService;
  let eventEmitter: EventEmitter2;

  const mockPrisma = {
    funnelEntry: {
      findMany: jest.fn(),
      findFirst: jest.fn(),
      create: jest.fn(),
      deleteMany: jest.fn(),
      groupBy: jest.fn(),
      count: jest.fn(),
    },
    client: {
      findUnique: jest.fn(),
      update: jest.fn(),
      count: jest.fn(),
    },
    $transaction: jest.fn(),
  };

  const mockEventEmitter = {
    emit: jest.fn(),
  };

  const clubId = 'club-uuid-1';

  beforeEach(async () => {
    jest.clearAllMocks();

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        FunnelService,
        { provide: PrismaService, useValue: mockPrisma },
        { provide: EventEmitter2, useValue: mockEventEmitter },
      ],
    }).compile();

    service = module.get<FunnelService>(FunnelService);
    prisma = module.get<PrismaService>(PrismaService);
    eventEmitter = module.get<EventEmitter2>(EventEmitter2);
  });

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

  // ---------------------------------------------------------------------------
  // findByStage
  // ---------------------------------------------------------------------------
  describe('findByStage', () => {
    it('should return paginated entries for a given stage', async () => {
      const entries = [
        { id: 'entry-1', stage: FunnelStage.NEW, clientId: 'c1', clubId },
        { id: 'entry-2', stage: FunnelStage.NEW, clientId: 'c2', clubId },
      ];
      mockPrisma.funnelEntry.findMany.mockResolvedValue(entries);

      const result = await service.findByStage(clubId, {
        stage: FunnelStage.NEW as any,
        limit: 10,
      });

      expect(mockPrisma.funnelEntry.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            clubId,
            stage: FunnelStage.NEW,
          }),
        }),
      );
      expect(result).toEqual({
        data: entries,
        nextCursor: null,
        hasMore: false,
      });
    });

    it('should pass cursor when provided', async () => {
      mockPrisma.funnelEntry.findMany.mockResolvedValue([]);

      await service.findByStage(clubId, {
        stage: FunnelStage.ASSIGNED as any,
        cursor: 'entry-cursor',
        limit: 5,
      });

      expect(mockPrisma.funnelEntry.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          cursor: expect.objectContaining({ id: 'entry-cursor' }),
          skip: 1,
        }),
      );
    });
  });

  // ---------------------------------------------------------------------------
  // assignClient
  // ---------------------------------------------------------------------------
  describe('assignClient', () => {
    const dto = { clientId: 'client-1', trainerId: 'trainer-1' };
    const assignedById = 'coordinator-1';

    it('should create an ASSIGNED entry and update client trainer', async () => {
      const createdEntry = {
        id: 'entry-new',
        stage: FunnelStage.ASSIGNED,
        clientId: dto.clientId,
        trainerId: dto.trainerId,
        clubId,
      };

      mockPrisma.client.count.mockResolvedValue(1);
      mockPrisma.funnelEntry.findFirst.mockResolvedValue(null);
      mockPrisma.$transaction.mockResolvedValue([createdEntry, {}, { count: 1 }]);

      const result = await service.assignClient(clubId, dto as any, assignedById);

      expect(mockPrisma.$transaction).toHaveBeenCalled();
      expect(result).toEqual(createdEntry);
      expect(mockEventEmitter.emit).toHaveBeenCalledWith(
        'funnel.assigned',
        expect.objectContaining({
          clubId,
          data: createdEntry,
        }),
      );
    });
  });

  // ---------------------------------------------------------------------------
  // moveStage
  // ---------------------------------------------------------------------------
  describe('moveStage', () => {
    const clientId = 'client-1';

    it('should create a new entry when moving to a different stage', async () => {
      const lastEntry = {
        id: 'entry-prev',
        stage: FunnelStage.ASSIGNED,
        clientId,
        clubId,
        trainerId: 'trainer-1',
      };
      const newEntry = {
        id: 'entry-new',
        stage: FunnelStage.COMPLETED,
        clientId,
        clubId,
      };

      mockPrisma.client.count.mockResolvedValue(1);
      mockPrisma.funnelEntry.findFirst.mockResolvedValue(lastEntry);
      mockPrisma.$transaction.mockResolvedValue([newEntry, { count: 1 }]);

      const result = await service.moveStage(clubId, clientId, {
        stage: FunnelStage.COMPLETED as any,
      } as any);

      expect(mockPrisma.funnelEntry.findFirst).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ clubId, clientId }),
          orderBy: expect.objectContaining({ createdAt: 'desc' }),
        }),
      );
      expect(mockPrisma.$transaction).toHaveBeenCalled();
      expect(result).toEqual(newEntry);
      expect(mockEventEmitter.emit).toHaveBeenCalledWith(
        'funnel.stage_changed',
        expect.objectContaining({
          clubId,
          data: expect.objectContaining({
            stage: FunnelStage.COMPLETED,
          }),
        }),
      );
    });

    it('should throw if no funnel history exists for the client', async () => {
      mockPrisma.client.count.mockResolvedValue(1);
      mockPrisma.funnelEntry.findFirst.mockResolvedValue(null);

      await expect(
        service.moveStage(clubId, clientId, { stage: FunnelStage.COMPLETED as any } as any),
      ).rejects.toThrow(BadRequestException);
    });

    it('should throw if attempting to move to the same stage', async () => {
      const lastEntry = {
        id: 'entry-prev',
        stage: FunnelStage.ASSIGNED,
        clientId,
        clubId,
      };
      mockPrisma.client.count.mockResolvedValue(1);
      mockPrisma.funnelEntry.findFirst.mockResolvedValue(lastEntry);

      await expect(
        service.moveStage(clubId, clientId, { stage: FunnelStage.ASSIGNED as any } as any),
      ).rejects.toThrow(BadRequestException);
    });
  });

  // ---------------------------------------------------------------------------
  // getConversionStats
  // ---------------------------------------------------------------------------
  describe('getConversionStats', () => {
    it('should calculate conversion rates between stages', async () => {
      // Mock count calls for each stage
      mockPrisma.funnelEntry.count
        .mockResolvedValueOnce(100) // NEW
        .mockResolvedValueOnce(60)  // ASSIGNED
        .mockResolvedValueOnce(40)  // COMPLETED
        .mockResolvedValueOnce(25); // REGULAR

      const result = await service.getConversionStats(clubId);

      expect(result).toEqual(
        expect.objectContaining({
          newToAssigned: 60,        // (60/100)*100 = 60
          assignedToCompleted: 66.67, // round((40/60)*10000)/100
          completedToRegular: 62.5,   // (25/40)*100 = 62.5
        }),
      );
    });

    it('should return 0 for conversion rates when denominator is zero', async () => {
      mockPrisma.funnelEntry.count
        .mockResolvedValueOnce(0) // NEW
        .mockResolvedValueOnce(0) // ASSIGNED
        .mockResolvedValueOnce(0) // COMPLETED
        .mockResolvedValueOnce(0); // REGULAR

      const result = await service.getConversionStats(clubId);

      expect(result).toEqual(
        expect.objectContaining({
          newToAssigned: 0,
          assignedToCompleted: 0,
          completedToRegular: 0,
        }),
      );
    });

    it('should accept optional date range filters', async () => {
      mockPrisma.funnelEntry.count
        .mockResolvedValue(10);

      const dateFrom = '2026-01-01';
      const dateTo = '2026-01-31';

      await service.getConversionStats(clubId, dateFrom, dateTo);

      expect(mockPrisma.funnelEntry.count).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            clubId,
            createdAt: expect.objectContaining({
              gte: expect.any(Date),
              lte: expect.any(Date),
            }),
          }),
        }),
      );
    });
  });

  // ---------------------------------------------------------------------------
  // getDistribution
  // ---------------------------------------------------------------------------
  describe('getDistribution', () => {
    it('should return funnel entries grouped by stage', async () => {
      const grouped = [
        { stage: FunnelStage.NEW, _count: { stage: 15 } },
        { stage: FunnelStage.ASSIGNED, _count: { stage: 10 } },
        { stage: FunnelStage.COMPLETED, _count: { stage: 5 } },
      ];
      mockPrisma.funnelEntry.groupBy.mockResolvedValue(grouped);

      const result = await service.getDistribution(clubId);

      expect(mockPrisma.funnelEntry.groupBy).toHaveBeenCalledWith(
        expect.objectContaining({
          by: ['stage'],
          where: expect.objectContaining({ clubId }),
          _count: expect.anything(),
        }),
      );
      expect(result).toEqual(
        expect.arrayContaining([
          expect.objectContaining({ stage: FunnelStage.NEW, count: 15 }),
          expect.objectContaining({ stage: FunnelStage.ASSIGNED, count: 10 }),
          expect.objectContaining({ stage: FunnelStage.COMPLETED, count: 5 }),
        ]),
      );
    });
  });

  // ---------------------------------------------------------------------------
  // transferClient
  // ---------------------------------------------------------------------------
  describe('transferClient', () => {
    it('should create a TRANSFERRED entry and reset assignedTrainerId', async () => {
      const dto = {
        clientId: 'client-1',
        fromTrainerId: 'trainer-1',
        toDepartmentId: 'dept-2',
      };
      const transferredById = 'coordinator-1';
      const created = {
        id: 'entry-t',
        stage: FunnelStage.TRANSFERRED,
        clientId: dto.clientId,
        clubId,
      };

      mockPrisma.client.count.mockResolvedValue(1);
      mockPrisma.funnelEntry.findFirst.mockResolvedValue(null);
      mockPrisma.funnelEntry.create.mockResolvedValue(created);
      mockPrisma.client.update.mockResolvedValue({});

      await service.transferClient(clubId, dto as any, transferredById);

      expect(mockPrisma.funnelEntry.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            stage: FunnelStage.TRANSFERRED,
            clientId: dto.clientId,
            clubId,
          }),
        }),
      );
      expect(mockPrisma.client.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ id: dto.clientId }),
          data: expect.objectContaining({ assignedTrainerId: null }),
        }),
      );
    });
  });

  // ---------------------------------------------------------------------------
  // refuseClient
  // ---------------------------------------------------------------------------
  describe('refuseClient', () => {
    it('should create a REFUSED entry with comment', async () => {
      const clientId = 'client-1';
      const dto = { comment: 'Client declined all offers' };
      const created = {
        id: 'entry-r',
        stage: FunnelStage.REFUSED,
        clientId,
        clubId,
        comment: dto.comment,
      };

      mockPrisma.client.count.mockResolvedValue(1);
      mockPrisma.funnelEntry.findFirst.mockResolvedValue(null);
      mockPrisma.funnelEntry.create.mockResolvedValue(created);

      const result = await service.refuseClient(clubId, clientId, dto as any);

      expect(mockPrisma.funnelEntry.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            stage: FunnelStage.REFUSED,
            clientId,
            clubId,
            comment: dto.comment,
          }),
        }),
      );
    });
  });
});
