import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundException, BadRequestException } from '@nestjs/common';
import { LicensesService } from './licenses.service';
import { PrismaService } from '../../prisma/prisma.service';
import { LicenseType } from '@prisma/client';

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/** Returns a Date that is `days` calendar days from now. */
function daysFromNow(days: number): Date {
  const d = new Date();
  d.setDate(d.getDate() + days);
  return d;
}

const mockClubRef = { id: 'club-uuid-1', name: 'FitLife', slug: 'fitlife' };

const mockLicense = (overrides: Record<string, unknown> = {}) => ({
  id: 'lic-uuid-1',
  clubId: 'club-uuid-1',
  type: LicenseType.STANDARD,
  startDate: new Date('2024-01-01'),
  endDate: daysFromNow(60), // ACTIVE by default
  isActive: true,
  gracePeriodEnd: null,
  maxUsers: 10,
  maxClients: 1000,
  createdAt: new Date('2024-01-01'),
  updatedAt: new Date('2024-01-01'),
  club: mockClubRef,
  ...overrides,
});

// ---------------------------------------------------------------------------
// Describe
// ---------------------------------------------------------------------------

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

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

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

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

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

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

  describe('findAll', () => {
    it('returns paginated licenses without filters', async () => {
      const licenses = [mockLicense({ id: 'l1' }), mockLicense({ id: 'l2' })];
      mockPrisma.license.findMany.mockResolvedValue(licenses);

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

      expect(mockPrisma.license.findMany).toHaveBeenCalledWith({
        where: {},
        take: 21,
        orderBy: { createdAt: 'desc' },
        include: { club: { select: { id: true, name: true, slug: true } } },
      });
      expect(result.data).toHaveLength(2);
      expect(result.hasMore).toBe(false);
      expect(result.nextCursor).toBeNull();
    });

    it('sets hasMore and nextCursor when more items exist', async () => {
      const licenses = Array.from({ length: 21 }, (_, i) =>
        mockLicense({ id: `l${i}` }),
      );
      mockPrisma.license.findMany.mockResolvedValue(licenses);

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

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

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

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

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

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

      await service.findAll({ type: LicenseType.PREMIUM, isActive: true });

      expect(mockPrisma.license.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { type: LicenseType.PREMIUM, isActive: true },
        }),
      );
    });
  });

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

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

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

      expect(mockPrisma.license.findUnique).toHaveBeenCalledWith({
        where: { id: 'lic-uuid-1' },
        include: { club: { select: { id: true, name: true, slug: true } } },
      });
      expect(result).toEqual(lic);
    });

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

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

  // -------------------------------------------------------------------------
  // findByClubId — also exercises enrichWithStatus
  // -------------------------------------------------------------------------

  describe('findByClubId', () => {
    it('throws NotFoundException when no license exists for the club', async () => {
      mockPrisma.license.findUnique.mockResolvedValue(null);

      await expect(service.findByClubId('club-no-lic')).rejects.toThrow(
        new NotFoundException('License for club club-no-lic not found'),
      );
    });

    it('enriches status as ACTIVE when endDate is > 30 days away', async () => {
      mockPrisma.license.findUnique.mockResolvedValue(
        mockLicense({ endDate: daysFromNow(60), isActive: true }),
      );

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

      expect(result.status).toBe('ACTIVE');
      expect(result.daysUntilExpiry).toBeGreaterThan(30);
    });

    it('enriches status as EXPIRING_SOON when endDate is 8–30 days away', async () => {
      mockPrisma.license.findUnique.mockResolvedValue(
        mockLicense({ endDate: daysFromNow(15), isActive: true }),
      );

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

      expect(result.status).toBe('EXPIRING_SOON');
    });

    it('enriches status as EXPIRING_CRITICAL when endDate is 1–7 days away', async () => {
      mockPrisma.license.findUnique.mockResolvedValue(
        mockLicense({ endDate: daysFromNow(3), isActive: true }),
      );

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

      expect(result.status).toBe('EXPIRING_CRITICAL');
    });

    it('enriches status as GRACE_PERIOD when endDate is 0 to -3 days (just expired)', async () => {
      mockPrisma.license.findUnique.mockResolvedValue(
        mockLicense({ endDate: daysFromNow(-1), isActive: true }),
      );

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

      expect(result.status).toBe('GRACE_PERIOD');
    });

    it('enriches status as READ_ONLY when endDate is -4 to -14 days past', async () => {
      mockPrisma.license.findUnique.mockResolvedValue(
        mockLicense({ endDate: daysFromNow(-7), isActive: true }),
      );

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

      expect(result.status).toBe('READ_ONLY');
    });

    it('enriches status as BLOCKED when endDate is more than 14 days past', async () => {
      mockPrisma.license.findUnique.mockResolvedValue(
        mockLicense({ endDate: daysFromNow(-20), isActive: true }),
      );

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

      expect(result.status).toBe('BLOCKED');
    });

    it('enriches status as BLOCKED when isActive is false regardless of endDate', async () => {
      mockPrisma.license.findUnique.mockResolvedValue(
        mockLicense({ endDate: daysFromNow(90), isActive: false }),
      );

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

      expect(result.status).toBe('BLOCKED');
    });

    it('attaches daysUntilExpiry to the enriched result', async () => {
      mockPrisma.license.findUnique.mockResolvedValue(
        mockLicense({ endDate: daysFromNow(10), isActive: true }),
      );

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

      // daysUntilExpiry is computed via Math.ceil; allow ±1 due to timing
      expect(result.daysUntilExpiry).toBeGreaterThanOrEqual(9);
      expect(result.daysUntilExpiry).toBeLessThanOrEqual(11);
    });
  });

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

  describe('create', () => {
    const createDto = {
      clubId: 'club-uuid-1',
      type: LicenseType.STANDARD,
      startDate: '2025-01-01',
      endDate: '2026-01-01',
      maxUsers: 10,
      maxClients: 500,
    };

    it('creates and returns a license when club has none', async () => {
      mockPrisma.license.findUnique.mockResolvedValue(null); // no existing license
      mockPrisma.club.count.mockResolvedValue(1);            // club exists
      const created = mockLicense();
      mockPrisma.license.create.mockResolvedValue(created);

      const result = await service.create(createDto);

      expect(mockPrisma.license.create).toHaveBeenCalledWith({
        data: {
          clubId: createDto.clubId,
          type: createDto.type,
          startDate: new Date(createDto.startDate),
          endDate: new Date(createDto.endDate),
          maxUsers: createDto.maxUsers,
          maxClients: createDto.maxClients,
          isActive: true,
        },
        include: { club: { select: { id: true, name: true, slug: true } } },
      });
      expect(result).toEqual(created);
    });

    it('throws BadRequestException when club already has a license', async () => {
      mockPrisma.license.findUnique.mockResolvedValue(mockLicense()); // existing

      await expect(service.create(createDto)).rejects.toThrow(
        BadRequestException,
      );
      expect(mockPrisma.license.create).not.toHaveBeenCalled();
    });

    it('throws NotFoundException when the referenced club does not exist', async () => {
      mockPrisma.license.findUnique.mockResolvedValue(null); // no existing license
      mockPrisma.club.count.mockResolvedValue(0);            // club missing

      await expect(service.create(createDto)).rejects.toThrow(
        new NotFoundException(`Club with id ${createDto.clubId} not found`),
      );
      expect(mockPrisma.license.create).not.toHaveBeenCalled();
    });
  });

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

  describe('update', () => {
    it('updates and returns the modified license', async () => {
      mockPrisma.license.count.mockResolvedValue(1);
      const updated = mockLicense({ maxUsers: 20 });
      mockPrisma.license.update.mockResolvedValue(updated);

      const result = await service.update('lic-uuid-1', { maxUsers: 20 });

      expect(mockPrisma.license.count).toHaveBeenCalledWith({
        where: { id: 'lic-uuid-1' },
      });
      expect(mockPrisma.license.update).toHaveBeenCalledWith({
        where: { id: 'lic-uuid-1' },
        data: { maxUsers: 20 },
        include: { club: { select: { id: true, name: true, slug: true } } },
      });
      expect(result).toEqual(updated);
    });

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

      await expect(
        service.update('no-such-id', { isActive: false }),
      ).rejects.toThrow(
        new NotFoundException('License with id no-such-id not found'),
      );
      expect(mockPrisma.license.update).not.toHaveBeenCalled();
    });

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

      await service.update('lic-uuid-1', { isActive: false });

      const updateCall = mockPrisma.license.update.mock.calls[0][0];
      expect(updateCall.data).toEqual({ isActive: false });
      expect(updateCall.data).not.toHaveProperty('type');
      expect(updateCall.data).not.toHaveProperty('endDate');
      expect(updateCall.data).not.toHaveProperty('maxUsers');
    });
  });

  // -------------------------------------------------------------------------
  // renew
  // -------------------------------------------------------------------------

  describe('renew', () => {
    it('renews a license with a future end date and resets grace period', async () => {
      const existing = mockLicense({ isActive: false, gracePeriodEnd: new Date('2024-03-01') });
      mockPrisma.license.findUnique.mockResolvedValue(existing);

      const futureDate = daysFromNow(365).toISOString();
      const renewed = mockLicense({ isActive: true, gracePeriodEnd: null });
      mockPrisma.license.update.mockResolvedValue(renewed);

      const result = await service.renew('lic-uuid-1', { newEndDate: futureDate });

      expect(mockPrisma.license.update).toHaveBeenCalledWith({
        where: { id: 'lic-uuid-1' },
        data: {
          endDate: new Date(futureDate),
          isActive: true,
          gracePeriodEnd: null,
        },
        include: { club: { select: { id: true, name: true, slug: true } } },
      });
      expect(result).toEqual(renewed);
    });

    it('also updates the type when type is provided in the renewal dto', async () => {
      mockPrisma.license.findUnique.mockResolvedValue(mockLicense());
      mockPrisma.license.update.mockResolvedValue(
        mockLicense({ type: LicenseType.PREMIUM }),
      );

      const futureDate = daysFromNow(365).toISOString();
      await service.renew('lic-uuid-1', {
        newEndDate: futureDate,
        type: LicenseType.PREMIUM,
      });

      const updateCall = mockPrisma.license.update.mock.calls[0][0];
      expect(updateCall.data.type).toBe(LicenseType.PREMIUM);
    });

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

      await expect(
        service.renew('no-such-id', { newEndDate: daysFromNow(365).toISOString() }),
      ).rejects.toThrow(
        new NotFoundException('License with id no-such-id not found'),
      );
      expect(mockPrisma.license.update).not.toHaveBeenCalled();
    });

    it('throws BadRequestException when the new end date is in the past', async () => {
      mockPrisma.license.findUnique.mockResolvedValue(mockLicense());

      const pastDate = daysFromNow(-10).toISOString();

      await expect(
        service.renew('lic-uuid-1', { newEndDate: pastDate }),
      ).rejects.toThrow(
        new BadRequestException('New end date must be in the future'),
      );
      expect(mockPrisma.license.update).not.toHaveBeenCalled();
    });
  });
});
