import { Test, TestingModule } from '@nestjs/testing';
import { StatsService } from './stats.service';
import { PrismaService } from '../../prisma/prisma.service';

const TrainingStatus = {
  PLANNED: 'PLANNED',
  CONFIRMED: 'CONFIRMED',
  COMPLETED: 'COMPLETED',
  CANCELLED: 'CANCELLED',
  NO_SHOW: 'NO_SHOW',
} as const;

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

describe('StatsService', () => {
  let service: StatsService;
  let prisma: PrismaService;

  const mockPrisma = {
    training: {
      count: jest.fn(),
      groupBy: jest.fn(),
    },
    client: {
      count: jest.fn(),
      groupBy: jest.fn(),
    },
    sale: {
      count: jest.fn(),
      aggregate: jest.fn(),
      groupBy: jest.fn(),
    },
    funnelEntry: {
      count: jest.fn(),
      groupBy: jest.fn(),
    },
    user: {
      findMany: jest.fn(),
    },
    ratingPeriod: {
      findMany: jest.fn(),
    },
    $queryRaw: jest.fn(),
  };

  const clubId = 'club-uuid-1';
  const trainerId = 'trainer-uuid-1';

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

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

    service = module.get<StatsService>(StatsService);
    prisma = module.get<PrismaService>(PrismaService);
  });

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

  // ---------------------------------------------------------------------------
  // getSummary
  // ---------------------------------------------------------------------------
  describe('getSummary', () => {
    it('should return all dashboard KPIs', async () => {
      mockPrisma.training.count
        .mockResolvedValueOnce(50)  // total trainings
        .mockResolvedValueOnce(30)  // completed trainings
        .mockResolvedValueOnce(10)  // cancelled trainings
        .mockResolvedValueOnce(8);  // upcoming trainings
      mockPrisma.client.count
        .mockResolvedValueOnce(120) // total clients
        .mockResolvedValueOnce(15)  // new clients
        .mockResolvedValueOnce(5);  // sleeping clients
      mockPrisma.sale.aggregate
        .mockResolvedValueOnce({ _sum: { amount: 150000 } })  // total sales amount
        .mockResolvedValueOnce({ _sum: { amount: 100000 } }); // paid sales amount

      const result = await service.getSummary(clubId);

      expect(result).toEqual(
        expect.objectContaining({
          totalTrainings: 50,
          completedTrainings: 30,
          cancelledTrainings: 10,
          totalClients: 120,
          newClients: 15,
          totalSalesAmount: 150000,
          paidSalesAmount: 100000,
          upcomingTrainings: 8,
          sleepingClients: 5,
        }),
      );
    });

    it('should filter by trainerId when provided', async () => {
      mockPrisma.training.count.mockResolvedValue(15);
      mockPrisma.client.count.mockResolvedValue(20);
      mockPrisma.sale.aggregate.mockResolvedValue({
        _sum: { amount: 40000 },
      });

      await service.getSummary(clubId, trainerId);

      expect(mockPrisma.training.count).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ clubId, trainerId }),
        }),
      );
    });

    it('should filter by date range when provided', async () => {
      mockPrisma.training.count.mockResolvedValue(10);
      mockPrisma.client.count.mockResolvedValue(15);
      mockPrisma.sale.aggregate.mockResolvedValue({
        _sum: { amount: 20000 },
      });

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

      await service.getSummary(clubId, undefined, dateFrom, dateTo);

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

  // ---------------------------------------------------------------------------
  // getFunnelConversion
  // ---------------------------------------------------------------------------
  describe('getFunnelConversion', () => {
    it('should calculate conversion rates between funnel stages', async () => {
      mockPrisma.funnelEntry.count
        .mockResolvedValueOnce(200) // NEW
        .mockResolvedValueOnce(120) // ASSIGNED
        .mockResolvedValueOnce(80)  // COMPLETED
        .mockResolvedValueOnce(50); // REGULAR

      // getPerTrainerFunnelBreakdown is called when no trainerId filter
      mockPrisma.funnelEntry.groupBy.mockResolvedValue([]);

      const result = await service.getFunnelConversion(clubId);

      // safeDiv: round((num/denom)*10000)/100
      expect(result).toEqual(
        expect.objectContaining({
          totalNew: 200,
          totalAssigned: 120,
          totalCompleted: 80,
          totalRegular: 50,
          newToAssigned: 60,          // (120/200)*100
          assignedToCompleted: 66.67, // round((80/120)*10000)/100
          completedToRegular: 62.5,   // (50/80)*100
          perTrainer: [],
        }),
      );
    });

    it('should return 0 for all rates when all counts are zero', async () => {
      mockPrisma.funnelEntry.count.mockResolvedValue(0);
      mockPrisma.funnelEntry.groupBy.mockResolvedValue([]);

      const result = await service.getFunnelConversion(clubId);

      expect(result).toEqual(
        expect.objectContaining({
          totalNew: 0,
          totalAssigned: 0,
          totalCompleted: 0,
          totalRegular: 0,
          newToAssigned: 0,
          assignedToCompleted: 0,
          completedToRegular: 0,
          perTrainer: [],
        }),
      );
    });

    it('should handle partial zero denominators gracefully', async () => {
      mockPrisma.funnelEntry.count
        .mockResolvedValueOnce(100) // NEW
        .mockResolvedValueOnce(0)   // ASSIGNED — zero means next rate is 0
        .mockResolvedValueOnce(0)   // COMPLETED
        .mockResolvedValueOnce(0);  // REGULAR

      mockPrisma.funnelEntry.groupBy.mockResolvedValue([]);

      const result = await service.getFunnelConversion(clubId);

      expect(result.newToAssigned).toBe(0);
      expect(result.assignedToCompleted).toBe(0);
      expect(result.completedToRegular).toBe(0);
    });

    it('should apply trainerId filter when provided', async () => {
      mockPrisma.funnelEntry.count.mockResolvedValue(10);

      await service.getFunnelConversion(clubId, trainerId);

      expect(mockPrisma.funnelEntry.count).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ clubId, trainerId }),
        }),
      );
    });
  });

  // ---------------------------------------------------------------------------
  // getRating
  // ---------------------------------------------------------------------------
  describe('getRating', () => {
    it('should return ratings from ratingPeriod table when available', async () => {
      const ratings = [
        {
          userId: 'tr-1',
          user: { id: 'tr-1', firstName: 'Ivan', lastName: 'Petrov' },
          score: 95.5,
          rank: 1,
          totalTrainings: 50,
          totalSalesAmount: 100000,
          totalClients: 20,
          newClients: 5,
          conversionNewToAssigned: 80,
          conversionAssignedToCompleted: 70,
          conversionCompletedToRegular: 60,
        },
        {
          userId: 'tr-2',
          user: { id: 'tr-2', firstName: 'Maria', lastName: 'Ivanova' },
          score: 88.0,
          rank: 2,
          totalTrainings: 40,
          totalSalesAmount: 80000,
          totalClients: 15,
          newClients: 3,
          conversionNewToAssigned: 75,
          conversionAssignedToCompleted: 65,
          conversionCompletedToRegular: 55,
        },
      ];
      mockPrisma.ratingPeriod.findMany.mockResolvedValue(ratings);

      const result = await service.getRating(clubId, '2026-01-01');

      expect(mockPrisma.ratingPeriod.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            clubId,
          }),
        }),
      );
      expect(result).toHaveLength(2);
      expect(result[0].userId).toBe('tr-1');
      expect(result[0].score).toBe(95.5);
    });

    it('should fall back to on-the-fly calculation when no stored ratings exist', async () => {
      mockPrisma.ratingPeriod.findMany.mockResolvedValue([]);

      // calculateRatingOnTheFly calls user.findMany, training.groupBy, sale.groupBy, client.groupBy
      mockPrisma.user.findMany.mockResolvedValue([
        { id: 'tr-1', firstName: 'Ivan', lastName: 'Petrov' },
      ]);
      mockPrisma.training.groupBy.mockResolvedValue([
        { trainerId: 'tr-1', _count: { trainerId: 20 } },
      ]);
      mockPrisma.sale.groupBy.mockResolvedValue([
        { trainerId: 'tr-1', _sum: { amount: 50000 } },
      ]);
      mockPrisma.client.groupBy.mockResolvedValue([
        { assignedTrainerId: 'tr-1', _count: { assignedTrainerId: 10 } },
      ]);

      const result = await service.getRating(clubId);

      expect(mockPrisma.ratingPeriod.findMany).toHaveBeenCalled();
      expect(mockPrisma.user.findMany).toHaveBeenCalled();
      expect(result).toHaveLength(1);
      expect(result[0].userId).toBe('tr-1');
      expect(result[0].rank).toBe(1);
    });
  });

  // ---------------------------------------------------------------------------
  // getTrainerStats
  // ---------------------------------------------------------------------------
  describe('getTrainerStats', () => {
    it('should return training counts grouped by status', async () => {
      const groupedTrainings = [
        { status: TrainingStatus.COMPLETED, _count: { status: 25 } },
        { status: TrainingStatus.CANCELLED, _count: { status: 5 } },
        { status: TrainingStatus.PLANNED, _count: { status: 10 } },
        { status: TrainingStatus.NO_SHOW, _count: { status: 3 } },
      ];
      mockPrisma.training.groupBy.mockResolvedValue(groupedTrainings);
      mockPrisma.sale.aggregate
        .mockResolvedValueOnce({ _sum: { amount: 75000 } })   // total
        .mockResolvedValueOnce({ _sum: { amount: 50000 } })   // paid
        .mockResolvedValueOnce({ _sum: { amount: 25000 } });  // pending
      mockPrisma.sale.count.mockResolvedValue(18);
      mockPrisma.funnelEntry.count
        .mockResolvedValueOnce(30)  // NEW
        .mockResolvedValueOnce(25)  // ASSIGNED
        .mockResolvedValueOnce(20)  // COMPLETED (funnel)
        .mockResolvedValueOnce(15); // REGULAR
      mockPrisma.client.count.mockResolvedValue(12);

      const result = await service.getTrainerStats(clubId, trainerId);

      expect(mockPrisma.training.groupBy).toHaveBeenCalledWith(
        expect.objectContaining({
          by: ['status'],
          where: expect.objectContaining({ clubId, trainerId }),
          _count: expect.objectContaining({ status: true }),
        }),
      );

      expect(result).toEqual(
        expect.objectContaining({
          trainings: expect.objectContaining({
            completed: 25,
            cancelled: 5,
            planned: 10,
            noShow: 3,
          }),
          sales: expect.objectContaining({
            totalAmount: 75000,
            count: 18,
          }),
          funnel: expect.objectContaining({
            totalNew: 30,
            totalAssigned: 25,
            totalCompleted: 20,
            totalRegular: 15,
          }),
        }),
      );
    });

    it('should aggregate sales amount and count for the trainer', async () => {
      mockPrisma.training.groupBy.mockResolvedValue([]);
      mockPrisma.sale.aggregate
        .mockResolvedValueOnce({ _sum: { amount: 120000 } })  // total
        .mockResolvedValueOnce({ _sum: { amount: 80000 } })   // paid
        .mockResolvedValueOnce({ _sum: { amount: 40000 } });  // pending
      mockPrisma.sale.count.mockResolvedValue(30);
      mockPrisma.funnelEntry.count.mockResolvedValue(0);
      mockPrisma.client.count.mockResolvedValue(0);

      const result = await service.getTrainerStats(clubId, trainerId);

      expect(mockPrisma.sale.aggregate).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ clubId, trainerId }),
          _sum: expect.objectContaining({ amount: true }),
        }),
      );
      expect(result.sales.totalAmount).toBe(120000);
      expect(result.sales.count).toBe(30);
    });

    it('should return funnel counts for the trainer', async () => {
      mockPrisma.training.groupBy.mockResolvedValue([]);
      mockPrisma.sale.aggregate.mockResolvedValue({ _sum: { amount: 0 } });
      mockPrisma.sale.count.mockResolvedValue(0);
      mockPrisma.funnelEntry.count
        .mockResolvedValueOnce(40)  // NEW
        .mockResolvedValueOnce(35)  // ASSIGNED
        .mockResolvedValueOnce(25)  // COMPLETED
        .mockResolvedValueOnce(18); // REGULAR
      mockPrisma.client.count.mockResolvedValue(0);

      const result = await service.getTrainerStats(clubId, trainerId);

      expect(mockPrisma.funnelEntry.count).toHaveBeenCalledTimes(4);
      expect(result.funnel.totalNew).toBe(40);
      expect(result.funnel.totalAssigned).toBe(35);
      expect(result.funnel.totalCompleted).toBe(25);
      expect(result.funnel.totalRegular).toBe(18);
    });

    it('should apply date range filters when provided', async () => {
      mockPrisma.training.groupBy.mockResolvedValue([]);
      mockPrisma.sale.aggregate.mockResolvedValue({ _sum: { amount: 0 } });
      mockPrisma.sale.count.mockResolvedValue(0);
      mockPrisma.funnelEntry.count.mockResolvedValue(0);
      mockPrisma.client.count.mockResolvedValue(0);

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

      await service.getTrainerStats(clubId, trainerId, dateFrom, dateTo);

      expect(mockPrisma.training.groupBy).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            clubId,
            trainerId,
            scheduledAt: expect.objectContaining({
              gte: expect.any(Date),
              lte: expect.any(Date),
            }),
          }),
        }),
      );
      expect(mockPrisma.sale.aggregate).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            clubId,
            trainerId,
            createdAt: expect.objectContaining({
              gte: expect.any(Date),
              lte: expect.any(Date),
            }),
          }),
        }),
      );
    });
  });
});
