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

const SaleStatus = {
  PENDING: 'PENDING',
  PAID: 'PAID',
  CANCELLED: 'CANCELLED',
  REFUNDED: 'REFUNDED',
} as const;

describe('SalesService', () => {
  let service: SalesService;
  let prisma: PrismaService;
  let eventEmitter: EventEmitter2;

  const mockPrisma = {
    sale: {
      findMany: jest.fn(),
      findFirst: jest.fn(),
      create: jest.fn(),
      update: jest.fn(),
      aggregate: jest.fn(),
    },
    client: {
      count: jest.fn(),
    },
    package: {
      findFirst: jest.fn(),
    },
    service: {
      findFirst: jest.fn(),
    },
    $transaction: jest.fn(),
  };

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

  const clubId = 'club-uuid-1';
  const trainerId = 'trainer-uuid-1';

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

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

    service = module.get<SalesService>(SalesService);
    prisma = module.get<PrismaService>(PrismaService);
    eventEmitter = module.get<EventEmitter2>(EventEmitter2);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  // ---------------------------------------------------------------------------
  // findAll
  // ---------------------------------------------------------------------------
  describe('findAll', () => {
    it('should return paginated sales', async () => {
      const sales = [
        { id: 'sale-1', status: SaleStatus.PENDING, clubId, amount: 5000 },
        { id: 'sale-2', status: SaleStatus.PAID, clubId, amount: 3000 },
      ];
      mockPrisma.sale.findMany.mockResolvedValue(sales);

      const result = await service.findAll(clubId, { limit: 10 });

      expect(mockPrisma.sale.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ clubId }),
        }),
      );
      expect(result).toEqual({
        data: sales,
        hasMore: false,
        nextCursor: null,
      });
    });

    it('should support cursor-based pagination', async () => {
      mockPrisma.sale.findMany.mockResolvedValue([]);

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

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

  // ---------------------------------------------------------------------------
  // findById
  // ---------------------------------------------------------------------------
  describe('findById', () => {
    it('should return the sale when found', async () => {
      const sale = { id: 'sale-1', status: SaleStatus.PENDING, clubId, amount: 5000 };
      mockPrisma.sale.findFirst.mockResolvedValue(sale);

      const result = await service.findById(clubId, 'sale-1');

      expect(mockPrisma.sale.findFirst).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ id: 'sale-1', clubId }),
        }),
      );
      expect(result).toEqual(sale);
    });

    it('should throw NotFoundException when sale is not found', async () => {
      mockPrisma.sale.findFirst.mockResolvedValue(null);

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

  // ---------------------------------------------------------------------------
  // create
  // ---------------------------------------------------------------------------
  describe('create', () => {
    const dto = {
      clientId: 'client-1',
      packageId: 'pkg-1',
      amount: 5000,
      description: 'Personal training',
    };

    it('should create a sale and emit sale.created event', async () => {
      const created = {
        id: 'sale-new',
        ...dto,
        trainerId,
        clubId,
        status: SaleStatus.PENDING,
      };

      mockPrisma.client.count.mockResolvedValue(1);
      mockPrisma.sale.create.mockResolvedValue(created);

      const result = await service.create(clubId, trainerId, dto as any);

      expect(mockPrisma.client.count).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ id: dto.clientId, clubId }),
        }),
      );
      expect(mockPrisma.sale.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            clientId: dto.clientId,
            trainerId,
            clubId,
            amount: 5000,
          }),
        }),
      );
      expect(mockEventEmitter.emit).toHaveBeenCalledWith(
        'sale.created',
        expect.objectContaining({ clubId, data: created }),
      );
    });

    it('should auto-fill amount from package price when amount is 0', async () => {
      const pkg = { id: 'pkg-1', price: 7500, clubId };
      const dtoZero = { ...dto, amount: 0 };
      const created = {
        id: 'sale-auto',
        ...dtoZero,
        amount: 7500,
        trainerId,
        clubId,
        status: SaleStatus.PENDING,
      };

      mockPrisma.client.count.mockResolvedValue(1);
      mockPrisma.package.findFirst.mockResolvedValue(pkg);
      mockPrisma.sale.create.mockResolvedValue(created);

      const result = await service.create(clubId, trainerId, dtoZero as any);

      expect(mockPrisma.package.findFirst).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ id: 'pkg-1', clubId }),
        }),
      );
      expect(mockPrisma.sale.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({ amount: 7500 }),
        }),
      );
    });

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

      await expect(
        service.create(clubId, trainerId, dto as any),
      ).rejects.toThrow(NotFoundException);
    });
  });

  // ---------------------------------------------------------------------------
  // update
  // ---------------------------------------------------------------------------
  describe('update', () => {
    const updateDto = { description: 'Updated description' };

    it('should update a PENDING sale', async () => {
      const existing = { id: 'sale-1', status: SaleStatus.PENDING, clubId };
      const updated = { ...existing, ...updateDto };

      mockPrisma.sale.findFirst.mockResolvedValue(existing);
      mockPrisma.sale.update.mockResolvedValue(updated);

      const result = await service.update(clubId, 'sale-1', updateDto as any);

      expect(mockPrisma.sale.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ id: 'sale-1' }),
          data: expect.objectContaining(updateDto),
        }),
      );
      expect(result).toEqual(updated);
    });

    it('should throw BadRequestException when sale is not PENDING', async () => {
      const existing = { id: 'sale-1', status: SaleStatus.PAID, clubId };
      mockPrisma.sale.findFirst.mockResolvedValue(existing);

      await expect(
        service.update(clubId, 'sale-1', updateDto as any),
      ).rejects.toThrow(BadRequestException);
    });
  });

  // ---------------------------------------------------------------------------
  // markAsPaid
  // ---------------------------------------------------------------------------
  describe('markAsPaid', () => {
    it('should mark a PENDING sale as PAID and set paidAt', async () => {
      const existing = { id: 'sale-1', status: SaleStatus.PENDING, clubId };
      const paid = {
        ...existing,
        status: SaleStatus.PAID,
        paidAt: expect.any(Date),
      };

      mockPrisma.sale.findFirst.mockResolvedValue(existing);
      mockPrisma.sale.update.mockResolvedValue(paid);

      const result = await service.markAsPaid(clubId, 'sale-1');

      expect(mockPrisma.sale.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ id: 'sale-1' }),
          data: expect.objectContaining({
            status: SaleStatus.PAID,
            paidAt: expect.any(Date),
          }),
        }),
      );
      expect(mockEventEmitter.emit).toHaveBeenCalledWith(
        'sale.paid',
        expect.objectContaining({ clubId, data: paid }),
      );
    });

    it('should throw BadRequestException when sale is not PENDING', async () => {
      const existing = { id: 'sale-1', status: SaleStatus.CANCELLED, clubId };
      mockPrisma.sale.findFirst.mockResolvedValue(existing);

      await expect(service.markAsPaid(clubId, 'sale-1')).rejects.toThrow(
        BadRequestException,
      );
    });
  });

  // ---------------------------------------------------------------------------
  // cancel
  // ---------------------------------------------------------------------------
  describe('cancel', () => {
    it('should cancel a PENDING sale', async () => {
      const existing = { id: 'sale-1', status: SaleStatus.PENDING, clubId };
      const cancelled = { ...existing, status: SaleStatus.CANCELLED };

      mockPrisma.sale.findFirst.mockResolvedValue(existing);
      mockPrisma.sale.update.mockResolvedValue(cancelled);

      const result = await service.cancel(clubId, 'sale-1');

      expect(mockPrisma.sale.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ id: 'sale-1' }),
          data: expect.objectContaining({ status: SaleStatus.CANCELLED }),
        }),
      );
    });

    it('should throw BadRequestException when sale is not PENDING', async () => {
      const existing = { id: 'sale-1', status: SaleStatus.PAID, clubId };
      mockPrisma.sale.findFirst.mockResolvedValue(existing);

      await expect(service.cancel(clubId, 'sale-1')).rejects.toThrow(
        BadRequestException,
      );
    });
  });

  // ---------------------------------------------------------------------------
  // refund
  // ---------------------------------------------------------------------------
  describe('refund', () => {
    it('should refund a PAID sale', async () => {
      const existing = { id: 'sale-1', status: SaleStatus.PAID, clubId };
      const refunded = { ...existing, status: SaleStatus.REFUNDED };

      mockPrisma.sale.findFirst.mockResolvedValue(existing);
      mockPrisma.sale.update.mockResolvedValue(refunded);

      const result = await service.refund(clubId, 'sale-1');

      expect(mockPrisma.sale.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ id: 'sale-1' }),
          data: expect.objectContaining({ status: SaleStatus.REFUNDED }),
        }),
      );
    });

    it('should throw BadRequestException when sale is PENDING', async () => {
      const existing = { id: 'sale-1', status: SaleStatus.PENDING, clubId };
      mockPrisma.sale.findFirst.mockResolvedValue(existing);

      await expect(service.refund(clubId, 'sale-1')).rejects.toThrow(
        BadRequestException,
      );
    });
  });

  // ---------------------------------------------------------------------------
  // getDebts
  // ---------------------------------------------------------------------------
  describe('getDebts', () => {
    it('should return only PENDING sales', async () => {
      const debts = [
        { id: 'sale-1', status: SaleStatus.PENDING, amount: 5000, clubId },
        { id: 'sale-2', status: SaleStatus.PENDING, amount: 3000, clubId },
      ];
      mockPrisma.sale.findMany.mockResolvedValue(debts);

      const result = await service.getDebts(clubId, { limit: 10 });

      expect(mockPrisma.sale.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            clubId,
            status: SaleStatus.PENDING,
          }),
        }),
      );
      expect(result).toEqual({
        data: debts,
        hasMore: false,
        nextCursor: null,
      });
    });
  });

  // ---------------------------------------------------------------------------
  // getDebtsSummary
  // ---------------------------------------------------------------------------
  describe('getDebtsSummary', () => {
    it('should return totalAmount and count for PENDING sales', async () => {
      mockPrisma.sale.aggregate.mockResolvedValue({
        _sum: { amount: 25000 },
        _count: { id: 8 },
      });

      const result = await service.getDebtsSummary(clubId);

      expect(mockPrisma.sale.aggregate).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            clubId,
            status: SaleStatus.PENDING,
          }),
          _sum: expect.objectContaining({ amount: true }),
          _count: expect.objectContaining({ id: true }),
        }),
      );
      expect(result).toEqual(
        expect.objectContaining({
          totalAmount: 25000,
          count: 8,
        }),
      );
    });

    it('should filter by trainerId when provided', async () => {
      mockPrisma.sale.aggregate.mockResolvedValue({
        _sum: { amount: 10000 },
        _count: { id: 3 },
      });

      await service.getDebtsSummary(clubId, trainerId);

      expect(mockPrisma.sale.aggregate).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            clubId,
            trainerId,
            status: SaleStatus.PENDING,
          }),
        }),
      );
    });
  });
});
