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

const mockPrisma = {
  room: {
    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 ROOM_ID = 'room-uuid-001';

const makeRoom = (overrides: Record<string, unknown> = {}) => ({
  id: ROOM_ID,
  clubId: CLUB_ID,
  name: 'Main Hall',
  capacity: 30,
  description: 'Large group fitness room',
  isActive: true,
  createdAt: new Date('2024-01-01'),
  updatedAt: new Date('2024-01-01'),
  ...overrides,
});

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

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

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

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

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

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

    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 rooms = [
        makeRoom({ id: 'room-001', name: 'Cardio Room' }),
        makeRoom({ id: 'room-002', name: 'Spin Studio' }),
        makeRoom({ id: 'room-003', name: 'Yoga Studio' }), // extra sentinel item
      ];
      mockPrisma.room.findMany.mockResolvedValue(rooms);

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

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

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

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

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

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

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

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

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

      await service.findAll(CLUB_ID, { isActive: false, limit: 10 });

      expect(mockPrisma.room.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { clubId: CLUB_ID, isActive: false },
        }),
      );
    });

    it('does not apply isActive filter when undefined', async () => {
      mockPrisma.room.findMany.mockResolvedValue([]);

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

      const callArgs = mockPrisma.room.findMany.mock.calls[0][0];
      expect(callArgs.where).not.toHaveProperty('isActive');
    });
  });

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

  describe('findById', () => {
    it('returns the room when found', async () => {
      const room = makeRoom();
      mockPrisma.room.findFirst.mockResolvedValue(room);

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

      expect(result).toEqual(room);
      expect(mockPrisma.room.findFirst).toHaveBeenCalledWith({
        where: { id: ROOM_ID, clubId: CLUB_ID },
      });
    });

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

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

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

  describe('create', () => {
    it('creates and returns a new room with all fields', async () => {
      const dto = { name: 'Spin Studio', capacity: 20, description: 'Spinning bikes', isActive: true };
      const created = makeRoom({ name: dto.name, capacity: dto.capacity, description: dto.description });
      mockPrisma.room.create.mockResolvedValue(created);

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

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

    it('defaults isActive to true when not provided', async () => {
      const dto = { name: 'Aqua Room', capacity: 15 };
      const created = makeRoom({ name: dto.name, capacity: dto.capacity });
      mockPrisma.room.create.mockResolvedValue(created);

      await service.create(CLUB_ID, dto);

      expect(mockPrisma.room.create).toHaveBeenCalledWith({
        data: expect.objectContaining({ isActive: true }),
      });
    });

    it('respects isActive=false when explicitly set', async () => {
      const dto = { name: 'Closed Room', capacity: 10, isActive: false };
      mockPrisma.room.create.mockResolvedValue(makeRoom({ isActive: false }));

      await service.create(CLUB_ID, dto);

      expect(mockPrisma.room.create).toHaveBeenCalledWith({
        data: expect.objectContaining({ isActive: false }),
      });
    });
  });

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

  describe('update', () => {
    it('updates and returns the room when it exists', async () => {
      mockPrisma.room.count.mockResolvedValue(1);
      const updated = makeRoom({ name: 'Renamed Hall', capacity: 40 });
      mockPrisma.room.update.mockResolvedValue(updated);

      const result = await service.update(CLUB_ID, ROOM_ID, { name: 'Renamed Hall', capacity: 40 });

      expect(result).toEqual(updated);
      expect(mockPrisma.room.count).toHaveBeenCalledWith({
        where: { id: ROOM_ID, clubId: CLUB_ID },
      });
      expect(mockPrisma.room.update).toHaveBeenCalledWith({
        where: { id: ROOM_ID },
        data: { name: 'Renamed Hall', capacity: 40 },
      });
    });

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

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

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

    it('omits undefined fields from update data', async () => {
      mockPrisma.room.count.mockResolvedValue(1);
      const updated = makeRoom({ isActive: false });
      mockPrisma.room.update.mockResolvedValue(updated);

      await service.update(CLUB_ID, ROOM_ID, { isActive: false });

      expect(mockPrisma.room.update).toHaveBeenCalledWith({
        where: { id: ROOM_ID },
        data: { isActive: false },
      });
    });
  });

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

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

      await service.remove(CLUB_ID, ROOM_ID);

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

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

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

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