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

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

const mockPrisma = {
  training: {
    findMany: jest.fn(),
    findFirst: jest.fn(),
    create: jest.fn(),
    update: jest.fn(),
    delete: jest.fn(),
    count: jest.fn(),
  },
  client: {
    update: jest.fn(),
  },
  $transaction: jest.fn(),
};

const CLUB_ID = 'club-uuid-001';
const TRAINING_ID = 'training-uuid-001';
const TRAINER_ID = 'trainer-uuid-001';
const CLIENT_ID = 'client-uuid-001';

const makeTraining = (overrides: Record<string, unknown> = {}) => ({
  id: TRAINING_ID,
  clubId: CLUB_ID,
  trainerId: TRAINER_ID,
  clientId: CLIENT_ID,
  status: 'PLANNED',
  scheduledAt: new Date('2024-07-01T10:00:00Z'),
  duration: 60,
  isRepeating: false,
  cancelReason: null,
  cancelComment: null,
  createdAt: new Date('2024-01-01'),
  updatedAt: new Date('2024-01-01'),
  ...overrides,
});

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

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

    service = module.get<ScheduleService>(ScheduleService);
    jest.clearAllMocks();
  });

  // ---------------------------------------------------------------------------
  // findAll
  // ---------------------------------------------------------------------------

  describe('findAll', () => {
    it('returns data with hasMore=false when results are within limit', async () => {
      const trainings = [
        makeTraining({ id: 'training-001' }),
        makeTraining({ id: 'training-002' }),
      ];
      mockPrisma.training.findMany.mockResolvedValue(trainings);

      const result = await service.findAll(CLUB_ID, { limit: 5 });

      expect(result.data).toHaveLength(2);
      expect(result.hasMore).toBe(false);
      expect(result.nextCursor).toBeNull();
      expect(mockPrisma.training.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ clubId: CLUB_ID }),
          take: 6,
        }),
      );
    });

    it('returns hasMore=true and nextCursor when more items exist', async () => {
      const trainings = [
        makeTraining({ id: 'training-001' }),
        makeTraining({ id: 'training-002' }),
        makeTraining({ id: 'training-003' }),
      ];
      mockPrisma.training.findMany.mockResolvedValue(trainings);

      const result = await service.findAll(CLUB_ID, { limit: 2 });

      expect(result.data).toHaveLength(2);
      expect(result.hasMore).toBe(true);
      expect(result.nextCursor).toBe('training-002');
    });

    it('passes cursor and skip=1 to findMany when cursor is provided', async () => {
      mockPrisma.training.findMany.mockResolvedValue([]);

      await service.findAll(CLUB_ID, { cursor: 'training-002', limit: 10 });

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

  // ---------------------------------------------------------------------------
  // findById
  // ---------------------------------------------------------------------------

  describe('findById', () => {
    it('returns training with relations when found', async () => {
      const training = makeTraining();
      mockPrisma.training.findFirst.mockResolvedValue(training);

      const result = await service.findById(CLUB_ID, TRAINING_ID);

      expect(result).toEqual(expect.objectContaining({ id: TRAINING_ID, clubId: CLUB_ID }));
      expect(result).toHaveProperty('date');
      expect(result).toHaveProperty('startTime');
      expect(result).toHaveProperty('endTime');
      expect(mockPrisma.training.findFirst).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: TRAINING_ID, clubId: CLUB_ID },
        }),
      );
    });

    it('throws NotFoundException when training does not exist', async () => {
      mockPrisma.training.findFirst.mockResolvedValue(null);

      await expect(
        service.findById(CLUB_ID, 'nonexistent'),
      ).rejects.toThrow(NotFoundException);
    });
  });

  // ---------------------------------------------------------------------------
  // create
  // ---------------------------------------------------------------------------

  describe('create', () => {
    it('creates a single training and emits training.created event', async () => {
      const dto = {
        clientId: CLIENT_ID,
        scheduledAt: '2024-07-01T10:00:00Z',
        duration: 60,
      };
      const created = makeTraining();
      mockPrisma.training.create.mockResolvedValue(created);

      const result = await service.create(CLUB_ID, TRAINER_ID, dto);

      expect(result).toEqual(expect.objectContaining({ id: TRAINING_ID, clubId: CLUB_ID }));
      expect(result).toHaveProperty('date');
      expect(result).toHaveProperty('startTime');
      expect(result).toHaveProperty('endTime');
      expect(mockPrisma.training.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            clubId: CLUB_ID,
            trainerId: TRAINER_ID,
            clientId: CLIENT_ID,
          }),
        }),
      );
      expect(mockEventEmitter.emit).toHaveBeenCalledWith(
        'training.created',
        expect.objectContaining({ clubId: CLUB_ID }),
      );
    });
  });

  // ---------------------------------------------------------------------------
  // update
  // ---------------------------------------------------------------------------

  describe('update', () => {
    it('updates training when status is PLANNED', async () => {
      const training = makeTraining({ status: 'PLANNED' });
      mockPrisma.training.findFirst.mockResolvedValue(training);
      const updated = makeTraining({ duration: 90 });
      mockPrisma.training.update.mockResolvedValue(updated);

      const result = await service.update(CLUB_ID, TRAINING_ID, { duration: 90 });

      expect(result).toEqual(expect.objectContaining({ id: TRAINING_ID, duration: 90 }));
      expect(result).toHaveProperty('date');
      expect(result).toHaveProperty('startTime');
      expect(result).toHaveProperty('endTime');
      expect(mockPrisma.training.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: TRAINING_ID },
          data: expect.objectContaining({ duration: 90 }),
        }),
      );
    });

    it('throws BadRequestException when status is not PLANNED', async () => {
      const training = makeTraining({ status: 'CONFIRMED' });
      mockPrisma.training.findFirst.mockResolvedValue(training);

      await expect(
        service.update(CLUB_ID, TRAINING_ID, { duration: 90 }),
      ).rejects.toThrow(BadRequestException);

      expect(mockPrisma.training.update).not.toHaveBeenCalled();
    });

    it('throws NotFoundException when training does not exist on update', async () => {
      mockPrisma.training.findFirst.mockResolvedValue(null);

      await expect(
        service.update(CLUB_ID, 'nonexistent', { duration: 90 }),
      ).rejects.toThrow(NotFoundException);

      expect(mockPrisma.training.update).not.toHaveBeenCalled();
    });
  });

  // ---------------------------------------------------------------------------
  // cancel
  // ---------------------------------------------------------------------------

  describe('cancel', () => {
    it('cancels training when status is PLANNED', async () => {
      const training = makeTraining({ status: 'PLANNED' });
      mockPrisma.training.findFirst.mockResolvedValue(training);
      const cancelled = makeTraining({ status: 'CANCELLED', cancelReason: 'CLIENT_CANCEL' });
      mockPrisma.training.update.mockResolvedValue(cancelled);

      const result = await service.cancel(CLUB_ID, TRAINING_ID, { cancelReason: 'CLIENT_CANCEL' as any });

      expect(result).toEqual(expect.objectContaining({ status: 'CANCELLED', cancelReason: 'CLIENT_CANCEL' }));
      expect(result).toHaveProperty('date');
      expect(result).toHaveProperty('startTime');
      expect(result).toHaveProperty('endTime');
      expect(mockPrisma.training.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: TRAINING_ID },
          data: expect.objectContaining({
            status: 'CANCELLED',
            cancelReason: 'CLIENT_CANCEL',
          }),
        }),
      );
      expect(mockEventEmitter.emit).toHaveBeenCalledWith(
        'training.cancelled',
        expect.objectContaining({ clubId: CLUB_ID }),
      );
    });

    it('cancels training when status is CONFIRMED', async () => {
      const training = makeTraining({ status: 'CONFIRMED' });
      mockPrisma.training.findFirst.mockResolvedValue(training);
      const cancelled = makeTraining({ status: 'CANCELLED' });
      mockPrisma.training.update.mockResolvedValue(cancelled);

      const result = await service.cancel(CLUB_ID, TRAINING_ID, { cancelReason: 'SICK' as any });

      expect(result).toEqual(expect.objectContaining({ status: 'CANCELLED' }));
      expect(mockEventEmitter.emit).toHaveBeenCalledWith(
        'training.cancelled',
        expect.any(Object),
      );
    });

    it('throws BadRequestException when cancelling a COMPLETED training', async () => {
      const training = makeTraining({ status: 'COMPLETED' });
      mockPrisma.training.findFirst.mockResolvedValue(training);

      await expect(
        service.cancel(CLUB_ID, TRAINING_ID, { cancelReason: 'CLIENT_CANCEL' as any }),
      ).rejects.toThrow(BadRequestException);

      expect(mockPrisma.training.update).not.toHaveBeenCalled();
      expect(mockEventEmitter.emit).not.toHaveBeenCalled();
    });
  });

  // ---------------------------------------------------------------------------
  // reschedule
  // ---------------------------------------------------------------------------

  describe('reschedule', () => {
    it('reschedules training and stores previous time in cancelComment', async () => {
      const training = makeTraining({
        status: 'PLANNED',
        scheduledAt: new Date('2024-07-01T10:00:00Z'),
      });
      mockPrisma.training.findFirst.mockResolvedValue(training);
      const rescheduled = makeTraining({
        scheduledAt: new Date('2024-07-02T14:00:00Z'),
      });
      mockPrisma.training.update.mockResolvedValue(rescheduled);

      const result = await service.reschedule(CLUB_ID, TRAINING_ID, {
        scheduledAt: '2024-07-02T14:00:00Z',
      });

      expect(result).toEqual(expect.objectContaining({ scheduledAt: rescheduled.scheduledAt }));
      expect(result).toHaveProperty('date', '2024-07-02');
      expect(result).toHaveProperty('startTime', '14:00');
      expect(result).toHaveProperty('endTime', '15:00');
      expect(mockPrisma.training.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: TRAINING_ID },
          data: expect.objectContaining({
            scheduledAt: new Date('2024-07-02T14:00:00Z'),
          }),
        }),
      );
      expect(mockEventEmitter.emit).toHaveBeenCalledWith(
        'training.rescheduled',
        expect.objectContaining({ clubId: CLUB_ID }),
      );
    });

    it('throws BadRequestException when rescheduling a CANCELLED training', async () => {
      const training = makeTraining({ status: 'CANCELLED' });
      mockPrisma.training.findFirst.mockResolvedValue(training);

      await expect(
        service.reschedule(CLUB_ID, TRAINING_ID, {
          scheduledAt: '2024-07-02T14:00:00Z',
        }),
      ).rejects.toThrow(BadRequestException);

      expect(mockPrisma.training.update).not.toHaveBeenCalled();
    });
  });

  // ---------------------------------------------------------------------------
  // complete
  // ---------------------------------------------------------------------------

  describe('complete', () => {
    it('completes training in transaction and updates client lastActivityAt', async () => {
      const training = makeTraining({ status: 'CONFIRMED' });
      mockPrisma.training.findFirst.mockResolvedValue(training);
      const completed = makeTraining({ status: 'COMPLETED' });
      mockPrisma.$transaction.mockImplementation(async (cb: (tx: unknown) => Promise<unknown>) => {
        const tx = {
          training: {
            update: jest.fn().mockResolvedValue(completed),
          },
          client: {
            update: jest.fn().mockResolvedValue(undefined),
          },
        };
        return cb(tx);
      });

      const result = await service.complete(CLUB_ID, TRAINING_ID);

      expect(result).toEqual(expect.objectContaining({ status: 'COMPLETED' }));
      expect(result).toHaveProperty('date', '2024-07-01');
      expect(result).toHaveProperty('startTime', '10:00');
      expect(result).toHaveProperty('endTime', '11:00');
      expect(mockPrisma.$transaction).toHaveBeenCalled();
      expect(mockEventEmitter.emit).toHaveBeenCalledWith(
        'training.completed',
        expect.objectContaining({ clubId: CLUB_ID }),
      );
    });

    it('throws BadRequestException when completing a CANCELLED training', async () => {
      const training = makeTraining({ status: 'CANCELLED' });
      mockPrisma.training.findFirst.mockResolvedValue(training);

      await expect(
        service.complete(CLUB_ID, TRAINING_ID),
      ).rejects.toThrow(BadRequestException);

      expect(mockPrisma.$transaction).not.toHaveBeenCalled();
      expect(mockEventEmitter.emit).not.toHaveBeenCalled();
    });

    it('throws NotFoundException when training does not exist on complete', async () => {
      mockPrisma.training.findFirst.mockResolvedValue(null);

      await expect(
        service.complete(CLUB_ID, 'nonexistent'),
      ).rejects.toThrow(NotFoundException);

      expect(mockPrisma.$transaction).not.toHaveBeenCalled();
    });
  });

  // ---------------------------------------------------------------------------
  // confirm
  // ---------------------------------------------------------------------------

  describe('confirm', () => {
    it('confirms training when status is PLANNED', async () => {
      const training = makeTraining({ status: 'PLANNED' });
      mockPrisma.training.findFirst.mockResolvedValue(training);
      const confirmed = makeTraining({ status: 'CONFIRMED' });
      mockPrisma.training.update.mockResolvedValue(confirmed);

      const result = await service.confirm(CLUB_ID, TRAINING_ID);

      expect(result).toEqual(expect.objectContaining({ status: 'CONFIRMED' }));
      expect(result).toHaveProperty('date', '2024-07-01');
      expect(result).toHaveProperty('startTime', '10:00');
      expect(result).toHaveProperty('endTime', '11:00');
      expect(mockPrisma.training.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: TRAINING_ID },
          data: expect.objectContaining({ status: 'CONFIRMED' }),
        }),
      );
    });

    it('throws BadRequestException when confirming a non-PLANNED training', async () => {
      const training = makeTraining({ status: 'COMPLETED' });
      mockPrisma.training.findFirst.mockResolvedValue(training);

      await expect(
        service.confirm(CLUB_ID, TRAINING_ID),
      ).rejects.toThrow(BadRequestException);

      expect(mockPrisma.training.update).not.toHaveBeenCalled();
    });
  });
});
