import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import { DepartmentsService } from './departments.service';
import { PrismaService } from '../../prisma/prisma.service';

const mockPrisma = {
  department: {
    findMany: jest.fn(),
    findFirst: jest.fn(),
    create: jest.fn(),
    update: jest.fn(),
    delete: jest.fn(),
    count: jest.fn(),
  },
};

const CLUB_ID = 'club-uuid-001';
const DEPT_ID = 'dept-uuid-001';

const makeDepartment = (overrides: Record<string, unknown> = {}) => ({
  id: DEPT_ID,
  clubId: CLUB_ID,
  name: 'Personal Training',
  description: 'Personal training department',
  createdAt: new Date('2024-01-01'),
  updatedAt: new Date('2024-01-01'),
  _count: { users: 3 },
  ...overrides,
});

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

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

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

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

  describe('findAll', () => {
    it('returns data with hasMore=false when results are within limit', async () => {
      const departments = [
        makeDepartment({ id: 'dept-001', name: 'Cardio' }),
        makeDepartment({ id: 'dept-002', name: 'Yoga' }),
      ];
      mockPrisma.department.findMany.mockResolvedValue(departments);

      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.department.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { clubId: CLUB_ID },
          take: 6,
          orderBy: { name: 'asc' },
          include: { _count: { select: { users: true } } },
        }),
      );
    });

    it('returns hasMore=true and nextCursor when more items exist', async () => {
      // limit=2, so findMany is called with take=3; return 3 items to signal more
      const departments = [
        makeDepartment({ id: 'dept-001', name: 'Cardio' }),
        makeDepartment({ id: 'dept-002', name: 'Pilates' }),
        makeDepartment({ id: 'dept-003', name: 'Yoga' }), // extra sentinel item
      ];
      mockPrisma.department.findMany.mockResolvedValue(departments);

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

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

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

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

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

    it('applies search filter as case-insensitive name contains', async () => {
      mockPrisma.department.findMany.mockResolvedValue([]);

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

      expect(mockPrisma.department.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: {
            clubId: CLUB_ID,
            name: { contains: 'yoga', mode: 'insensitive' },
          },
        }),
      );
    });
  });

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

  describe('findById', () => {
    it('returns department with users when found', async () => {
      const dept = {
        ...makeDepartment(),
        users: [
          { id: 'user-001', firstName: 'Ivan', lastName: 'Petrov', role: 'trainer' },
        ],
      };
      mockPrisma.department.findFirst.mockResolvedValue(dept);

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

      expect(result).toEqual(dept);
      expect(mockPrisma.department.findFirst).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: DEPT_ID, clubId: CLUB_ID },
          include: {
            users: {
              select: { id: true, firstName: true, lastName: true, role: true },
            },
          },
        }),
      );
    });

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

      await expect(service.findById(CLUB_ID, 'nonexistent')).rejects.toThrow(
        new NotFoundException('Department with id nonexistent not found'),
      );
    });
  });

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

  describe('create', () => {
    it('creates and returns a new department', async () => {
      const dto = { name: 'Strength', description: 'Strength training' };
      const created = makeDepartment({ name: dto.name, description: dto.description });
      mockPrisma.department.create.mockResolvedValue(created);

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

      expect(result).toEqual(created);
      expect(mockPrisma.department.create).toHaveBeenCalledWith({
        data: {
          clubId: CLUB_ID,
          name: dto.name,
          description: dto.description,
        },
      });
    });

    it('creates department without description when omitted', async () => {
      const dto = { name: 'Aqua' };
      const created = makeDepartment({ name: 'Aqua', description: undefined });
      mockPrisma.department.create.mockResolvedValue(created);

      await service.create(CLUB_ID, dto);

      expect(mockPrisma.department.create).toHaveBeenCalledWith({
        data: { clubId: CLUB_ID, name: 'Aqua', description: undefined },
      });
    });
  });

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

  describe('update', () => {
    it('updates and returns the department when it exists', async () => {
      mockPrisma.department.count.mockResolvedValue(1);
      const updated = makeDepartment({ name: 'Group Fitness' });
      mockPrisma.department.update.mockResolvedValue(updated);

      const result = await service.update(CLUB_ID, DEPT_ID, { name: 'Group Fitness' });

      expect(result).toEqual(updated);
      expect(mockPrisma.department.count).toHaveBeenCalledWith({
        where: { id: DEPT_ID, clubId: CLUB_ID },
      });
      expect(mockPrisma.department.update).toHaveBeenCalledWith({
        where: { id: DEPT_ID },
        data: { name: 'Group Fitness' },
      });
    });

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

      await expect(
        service.update(CLUB_ID, 'nonexistent', { name: 'Ghost' }),
      ).rejects.toThrow(
        new NotFoundException('Department with id nonexistent not found'),
      );

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

    it('omits undefined fields from update data', async () => {
      mockPrisma.department.count.mockResolvedValue(1);
      const updated = makeDepartment({ description: 'New desc' });
      mockPrisma.department.update.mockResolvedValue(updated);

      await service.update(CLUB_ID, DEPT_ID, { description: 'New desc' });

      expect(mockPrisma.department.update).toHaveBeenCalledWith({
        where: { id: DEPT_ID },
        data: { description: 'New desc' },
      });
    });
  });

  // ---------------------------------------------------------------------------
  // remove
  // ---------------------------------------------------------------------------

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

      await service.remove(CLUB_ID, DEPT_ID);

      expect(mockPrisma.department.count).toHaveBeenCalledWith({
        where: { id: DEPT_ID, clubId: CLUB_ID },
      });
      expect(mockPrisma.department.delete).toHaveBeenCalledWith({
        where: { id: DEPT_ID },
      });
    });

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

      await expect(service.remove(CLUB_ID, 'nonexistent')).rejects.toThrow(
        new NotFoundException('Department with id nonexistent not found'),
      );

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