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

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

const mockPrisma = {
  client: {
    findMany: jest.fn(),
    findFirst: jest.fn(),
    create: jest.fn(),
    update: jest.fn(),
    delete: jest.fn(),
    count: jest.fn().mockResolvedValue(1),
  },
  clientGoal: {
    findMany: jest.fn(),
    create: jest.fn(),
  },
  clientInjury: {
    findMany: jest.fn(),
    create: jest.fn(),
  },
};

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

const makeClient = (overrides: Record<string, unknown> = {}) => ({
  id: CLIENT_ID,
  clubId: CLUB_ID,
  firstName: 'Anna',
  lastName: 'Ivanova',
  phone: '+79001112233',
  email: 'anna@example.com',
  assignedTrainerId: TRAINER_ID,
  lastActivityAt: new Date('2024-06-01'),
  createdAt: new Date('2024-01-01'),
  updatedAt: new Date('2024-01-01'),
  ...overrides,
});

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

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

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

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

  describe('findAll', () => {
    it('returns data with hasMore=false when results are within limit', async () => {
      const clients = [
        makeClient({ id: 'client-001', firstName: 'Anna' }),
        makeClient({ id: 'client-002', firstName: 'Boris' }),
      ];
      mockPrisma.client.findMany.mockResolvedValue(clients);

      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.client.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ clubId: CLUB_ID }),
          take: 6,
        }),
      );
    });

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

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

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

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

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

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

    it('applies search filter when search param is provided', async () => {
      mockPrisma.client.findMany.mockResolvedValue([]);

      await service.findAll(CLUB_ID, { search: 'Anna', limit: 10 });

      expect(mockPrisma.client.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            clubId: CLUB_ID,
            OR: expect.arrayContaining([
              expect.objectContaining({
                firstName: expect.objectContaining({ contains: 'Anna', mode: 'insensitive' }),
              }),
            ]),
          }),
        }),
      );
    });

    it('applies assignedTrainerId filter when provided', async () => {
      mockPrisma.client.findMany.mockResolvedValue([]);

      await service.findAll(CLUB_ID, { assignedTrainerId: TRAINER_ID, limit: 10 });

      expect(mockPrisma.client.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            clubId: CLUB_ID,
            assignedTrainerId: TRAINER_ID,
          }),
        }),
      );
    });
  });

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

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

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

      expect(result).toEqual(client);
      expect(mockPrisma.client.findFirst).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: CLIENT_ID, clubId: CLUB_ID },
        }),
      );
    });

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

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

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

  describe('create', () => {
    it('creates a client and emits client.created event', async () => {
      const dto = { firstName: 'Anna', lastName: 'Ivanova', phone: '+79001112233' };
      const created = makeClient();
      mockPrisma.client.create.mockResolvedValue(created);

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

      expect(result).toEqual(created);
      expect(mockPrisma.client.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            clubId: CLUB_ID,
            firstName: dto.firstName,
            lastName: dto.lastName,
            phone: dto.phone,
          }),
        }),
      );
      expect(mockEventEmitter.emit).toHaveBeenCalledWith(
        'client.created',
        expect.objectContaining({ clubId: CLUB_ID }),
      );
    });
  });

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

  describe('update', () => {
    it('updates a client and emits client.updated event', async () => {
      mockPrisma.client.count.mockResolvedValue(1);
      const updated = makeClient({ firstName: 'Maria' });
      mockPrisma.client.update.mockResolvedValue(updated);

      const result = await service.update(CLUB_ID, CLIENT_ID, { firstName: 'Maria' });

      expect(result).toEqual(updated);
      expect(mockPrisma.client.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: CLIENT_ID },
          data: expect.objectContaining({ firstName: 'Maria' }),
        }),
      );
      expect(mockEventEmitter.emit).toHaveBeenCalledWith(
        'client.updated',
        expect.objectContaining({ clubId: CLUB_ID }),
      );
    });

    it('throws NotFoundException when client does not exist on update', async () => {
      mockPrisma.client.count.mockResolvedValue(0);

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

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

  // ---------------------------------------------------------------------------
  // delete
  // ---------------------------------------------------------------------------

  describe('delete', () => {
    it('deletes the client when it exists', async () => {
      mockPrisma.client.count.mockResolvedValue(1);
      mockPrisma.client.delete.mockResolvedValue(undefined);

      await service.delete(CLUB_ID, CLIENT_ID);

      expect(mockPrisma.client.delete).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: CLIENT_ID },
        }),
      );
    });

    it('throws NotFoundException when client does not exist on delete', async () => {
      mockPrisma.client.count.mockResolvedValue(0);

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

      expect(mockPrisma.client.delete).not.toHaveBeenCalled();
    });
  });

  // ---------------------------------------------------------------------------
  // findSleeping
  // ---------------------------------------------------------------------------

  describe('findSleeping', () => {
    it('returns sleeping clients with date filter', async () => {
      const sleepingClients = [
        makeClient({ id: 'client-sleeping-001', lastActivityAt: new Date('2024-01-01') }),
      ];
      mockPrisma.client.findMany.mockResolvedValue(sleepingClients);

      const result = await service.findSleeping(CLUB_ID, { limit: 10 });

      expect(result.data).toHaveLength(1);
      expect(mockPrisma.client.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            clubId: CLUB_ID,
            lastActivityAt: expect.objectContaining({
              lt: expect.any(Date),
            }),
          }),
        }),
      );
    });
  });

  // ---------------------------------------------------------------------------
  // assignTrainer
  // ---------------------------------------------------------------------------

  describe('assignTrainer', () => {
    it('updates the assignedTrainerId on the client', async () => {
      mockPrisma.client.count.mockResolvedValue(1);
      const updated = makeClient({ assignedTrainerId: 'trainer-uuid-002' });
      mockPrisma.client.update.mockResolvedValue(updated);

      const result = await service.assignTrainer(CLUB_ID, CLIENT_ID, 'trainer-uuid-002');

      expect(result).toEqual(updated);
      expect(mockPrisma.client.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: CLIENT_ID },
          data: expect.objectContaining({ assignedTrainerId: 'trainer-uuid-002' }),
        }),
      );
    });
  });
});
