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

const CLUB_INCLUDE = {
  license: true,
  clubModules: { select: { moduleId: true, enabled: true } },
  _count: { select: { users: true, clients: true } },
};

const mockClub = (overrides: Record<string, unknown> = {}) => ({
  id: 'club-uuid-1',
  name: 'FitLife Moscow',
  slug: 'fitlife-moscow',
  address: 'Tverskaya 1',
  phone: '+70001234567',
  email: 'info@fitlife.ru',
  logoUrl: null,
  timezone: 'Europe/Moscow',
  isActive: true,
  createdAt: new Date('2024-01-01'),
  updatedAt: new Date('2024-01-01'),
  license: null,
  clubModules: [],
  _count: { users: 5, clients: 100 },
  ...overrides,
});

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

  const mockPrisma = {
    club: {
      findMany: jest.fn(),
      findUnique: jest.fn(),
      update: jest.fn(),
      count: jest.fn(),
    },
  };

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

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

    service = module.get<ClubsService>(ClubsService);
  });

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

  describe('findAll', () => {
    it('returns paginated clubs without filters', async () => {
      const clubs = [mockClub({ id: 'c1' }), mockClub({ id: 'c2' })];
      mockPrisma.club.findMany.mockResolvedValue(clubs);

      const result = await service.findAll({});

      expect(mockPrisma.club.findMany).toHaveBeenCalledWith({
        where: {},
        take: 21, // limit(20) + 1
        orderBy: { createdAt: 'desc' },
        include: CLUB_INCLUDE,
      });
      expect(result.data).toHaveLength(2);
      expect(result.hasMore).toBe(false);
      expect(result.nextCursor).toBeNull();
    });

    it('sets hasMore and nextCursor when more items exist', async () => {
      // Return limit+1 items to signal there is a next page
      const clubs = Array.from({ length: 21 }, (_, i) =>
        mockClub({ id: `c${i}` }),
      );
      mockPrisma.club.findMany.mockResolvedValue(clubs);

      const result = await service.findAll({ limit: 20 });

      expect(result.hasMore).toBe(true);
      expect(result.data).toHaveLength(20);
      expect(result.nextCursor).toBe('c19');
    });

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

      await service.findAll({ cursor: 'some-uuid', limit: 10 });

      expect(mockPrisma.club.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          cursor: { id: 'some-uuid' },
          skip: 1,
          take: 11,
        }),
      );
    });

    it('builds search where clause with OR on name and slug', async () => {
      mockPrisma.club.findMany.mockResolvedValue([]);

      await service.findAll({ search: 'fit' });

      expect(mockPrisma.club.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: {
            OR: [
              { name: { contains: 'fit', mode: 'insensitive' } },
              { slug: { contains: 'fit', mode: 'insensitive' } },
            ],
          },
        }),
      );
    });

    it('filters by isActive when provided', async () => {
      mockPrisma.club.findMany.mockResolvedValue([]);

      await service.findAll({ isActive: false });

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

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

  describe('findById', () => {
    it('returns a club when found', async () => {
      const club = mockClub();
      mockPrisma.club.findUnique.mockResolvedValue(club);

      const result = await service.findById('club-uuid-1');

      expect(mockPrisma.club.findUnique).toHaveBeenCalledWith({
        where: { id: 'club-uuid-1' },
        include: CLUB_INCLUDE,
      });
      expect(result).toEqual(club);
    });

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

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

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

  describe('update', () => {
    it('updates a club and returns the updated record', async () => {
      mockPrisma.club.count.mockResolvedValue(1);
      const updated = mockClub({ name: 'FitLife SPb', timezone: 'Europe/Moscow' });
      mockPrisma.club.update.mockResolvedValue(updated);

      const result = await service.update('club-uuid-1', {
        name: 'FitLife SPb',
        timezone: 'Europe/Moscow',
      });

      expect(mockPrisma.club.count).toHaveBeenCalledWith({
        where: { id: 'club-uuid-1' },
      });
      expect(mockPrisma.club.update).toHaveBeenCalledWith({
        where: { id: 'club-uuid-1' },
        data: { name: 'FitLife SPb', timezone: 'Europe/Moscow' },
        include: CLUB_INCLUDE,
      });
      expect(result).toEqual(updated);
    });

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

      await expect(
        service.update('nonexistent-id', { name: 'Ghost Club' }),
      ).rejects.toThrow(
        new NotFoundException('Club with id nonexistent-id not found'),
      );
      expect(mockPrisma.club.update).not.toHaveBeenCalled();
    });

    it('omits undefined fields from the update data payload', async () => {
      mockPrisma.club.count.mockResolvedValue(1);
      mockPrisma.club.update.mockResolvedValue(mockClub());

      // Only provide isActive — other fields should be absent in data
      await service.update('club-uuid-1', { isActive: false });

      const updateCall = mockPrisma.club.update.mock.calls[0][0];
      expect(updateCall.data).toEqual({ isActive: false });
      expect(updateCall.data).not.toHaveProperty('name');
      expect(updateCall.data).not.toHaveProperty('email');
    });
  });

  // ---------------------------------------------------------------------------
  // softDelete
  // ---------------------------------------------------------------------------

  describe('softDelete', () => {
    it('sets isActive to false for an existing club', async () => {
      mockPrisma.club.count.mockResolvedValue(1);
      const deactivated = mockClub({ isActive: false });
      mockPrisma.club.update.mockResolvedValue(deactivated);

      const result = await service.softDelete('club-uuid-1');

      expect(mockPrisma.club.count).toHaveBeenCalledWith({
        where: { id: 'club-uuid-1' },
      });
      expect(mockPrisma.club.update).toHaveBeenCalledWith({
        where: { id: 'club-uuid-1' },
        data: { isActive: false },
      });
      expect(result.isActive).toBe(false);
    });

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

      await expect(service.softDelete('nonexistent-id')).rejects.toThrow(
        new NotFoundException('Club with id nonexistent-id not found'),
      );
      expect(mockPrisma.club.update).not.toHaveBeenCalled();
    });
  });
});
