import { jest, describe, it, expect, beforeEach } from '@jest/globals';
import { createMockRepository, createMockQueryBuilder } from '../../../__tests__/helpers.js';

// Mock factories
function createMockInvoice(overrides: Record<string, any> = {}) {
  return {
    id: 'invoice-uuid-1',
    tenantId: 'tenant-uuid-1',
    subscriptionId: 'sub-uuid-1',
    invoiceNumber: 'INV-202601-0001',
    invoiceDate: new Date('2026-01-15'),
    periodStart: new Date('2026-01-01'),
    periodEnd: new Date('2026-01-31'),
    billingName: 'Test Company',
    billingEmail: 'billing@test.com',
    billingAddress: { street: '123 Main St', city: 'Mexico City' },
    taxId: 'RFC123456789',
    subtotal: 499,
    taxAmount: 79.84,
    discountAmount: 0,
    total: 578.84,
    paidAmount: 0,
    currency: 'MXN',
    status: 'draft',
    dueDate: new Date('2026-01-30'),
    notes: '',
    internalNotes: '',
    items: [],
    createdAt: new Date(),
    updatedAt: new Date(),
    ...overrides,
  };
}

function createMockInvoiceItem(overrides: Record<string, any> = {}) {
  return {
    id: 'item-uuid-1',
    invoiceId: 'invoice-uuid-1',
    itemType: 'subscription',
    description: 'Suscripcion Starter - Mensual',
    quantity: 1,
    unitPrice: 499,
    subtotal: 499,
    metadata: {},
    ...overrides,
  };
}

function createMockSubscription(overrides: Record<string, any> = {}) {
  return {
    id: 'sub-uuid-1',
    tenantId: 'tenant-uuid-1',
    planId: 'plan-uuid-1',
    currentPrice: 499,
    billingCycle: 'monthly',
    contractedUsers: 10,
    contractedBranches: 3,
    billingName: 'Test Company',
    billingEmail: 'billing@test.com',
    billingAddress: { street: '123 Main St' },
    taxId: 'RFC123456789',
    plan: {
      id: 'plan-uuid-1',
      name: 'Starter',
      maxUsers: 10,
      maxBranches: 3,
      storageGb: 20,
    },
    ...overrides,
  };
}

function createMockUsage(overrides: Record<string, any> = {}) {
  return {
    id: 'usage-uuid-1',
    tenantId: 'tenant-uuid-1',
    periodStart: new Date('2026-01-01'),
    periodEnd: new Date('2026-01-31'),
    activeUsers: 5,
    activeBranches: 2,
    storageUsedGb: 10,
    apiCalls: 5000,
    ...overrides,
  };
}

// Mock repositories
const mockInvoiceRepository = {
  ...createMockRepository(),
  softDelete: jest.fn().mockResolvedValue({ affected: 1 }),
};
const mockItemRepository = createMockRepository();
const mockSubscriptionRepository = createMockRepository();
const mockUsageRepository = createMockRepository();
const mockQueryBuilder = createMockQueryBuilder();

// Mock DataSource
const mockDataSource = {
  getRepository: jest.fn((entity: any) => {
    const entityName = entity.name || entity;
    if (entityName === 'Invoice') return mockInvoiceRepository;
    if (entityName === 'InvoiceItem') return mockItemRepository;
    if (entityName === 'TenantSubscription') return mockSubscriptionRepository;
    if (entityName === 'UsageTracking') return mockUsageRepository;
    return mockInvoiceRepository;
  }),
};

jest.mock('../../../shared/utils/logger.js', () => ({
  logger: {
    info: jest.fn(),
    error: jest.fn(),
    debug: jest.fn(),
    warn: jest.fn(),
  },
}));

// Import after mocking
import { InvoicesService } from '../services/invoices.service.js';

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

  beforeEach(() => {
    jest.clearAllMocks();
    service = new InvoicesService(mockDataSource as any);
    mockInvoiceRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);
  });

  describe('create', () => {
    it('should create invoice with items', async () => {
      const dto = {
        tenantId: 'tenant-uuid-1',
        subscriptionId: 'sub-uuid-1',
        periodStart: new Date('2026-01-01'),
        periodEnd: new Date('2026-01-31'),
        billingName: 'Test Company',
        billingEmail: 'billing@test.com',
        dueDate: new Date('2026-01-30'),
        items: [
          {
            itemType: 'subscription' as const,
            description: 'Suscripcion Starter',
            quantity: 1,
            unitPrice: 499,
          },
        ],
      };

      // Mock invoice number generation
      mockQueryBuilder.getOne.mockResolvedValueOnce(null);

      const mockInvoice = createMockInvoice({ ...dto, id: 'new-invoice-uuid' });
      mockInvoiceRepository.create.mockReturnValue(mockInvoice);
      mockInvoiceRepository.save.mockResolvedValue(mockInvoice);
      mockItemRepository.create.mockReturnValue(createMockInvoiceItem());
      mockItemRepository.save.mockResolvedValue(createMockInvoiceItem());
      mockInvoiceRepository.findOne.mockResolvedValue({
        ...mockInvoice,
        items: [createMockInvoiceItem()],
      });

      const result = await service.create(dto);

      expect(mockInvoiceRepository.create).toHaveBeenCalled();
      expect(mockInvoiceRepository.save).toHaveBeenCalled();
      expect(mockItemRepository.create).toHaveBeenCalled();
      expect(result.tenantId).toBe('tenant-uuid-1');
    });

    it('should calculate totals with tax', async () => {
      const dto = {
        tenantId: 'tenant-uuid-1',
        items: [
          {
            itemType: 'subscription' as const,
            description: 'Plan',
            quantity: 1,
            unitPrice: 1000,
          },
        ],
      };

      mockQueryBuilder.getOne.mockResolvedValueOnce(null);

      mockInvoiceRepository.create.mockImplementation((data: any) => ({
        ...createMockInvoice(),
        ...data,
        id: 'invoice-id',
      }));
      mockInvoiceRepository.save.mockImplementation((inv: any) => Promise.resolve(inv));
      mockInvoiceRepository.findOne.mockImplementation((opts: any) => Promise.resolve({
        ...createMockInvoice(),
        id: opts.where.id,
        items: [],
      }));
      mockItemRepository.create.mockReturnValue(createMockInvoiceItem());
      mockItemRepository.save.mockResolvedValue(createMockInvoiceItem());

      await service.create(dto);

      // Verify subtotal calculation (1000)
      // Tax should be 16% = 160
      // Total should be 1160
      expect(mockInvoiceRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          subtotal: 1000,
          taxAmount: 160,
          total: 1160,
        })
      );
    });

    it('should apply item discounts', async () => {
      const dto = {
        tenantId: 'tenant-uuid-1',
        items: [
          {
            itemType: 'subscription' as const,
            description: 'Plan',
            quantity: 1,
            unitPrice: 1000,
            discountPercent: 10, // 10% off
          },
        ],
      };

      mockQueryBuilder.getOne.mockResolvedValueOnce(null);
      mockInvoiceRepository.create.mockImplementation((data: any) => data);
      mockInvoiceRepository.save.mockImplementation((inv: any) => Promise.resolve({ ...inv, id: 'inv-id' }));
      mockInvoiceRepository.findOne.mockResolvedValue(createMockInvoice());
      mockItemRepository.create.mockReturnValue(createMockInvoiceItem());
      mockItemRepository.save.mockResolvedValue(createMockInvoiceItem());

      await service.create(dto);

      // Subtotal after 10% discount: 1000 - 100 = 900
      // Tax 16%: 144
      // Total: 1044
      expect(mockInvoiceRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          subtotal: 900,
          taxAmount: 144,
          total: 1044,
        })
      );
    });
  });

  describe('generateFromSubscription', () => {
    it('should generate invoice from subscription', async () => {
      const dto = {
        tenantId: 'tenant-uuid-1',
        subscriptionId: 'sub-uuid-1',
        periodStart: new Date('2026-01-01'),
        periodEnd: new Date('2026-01-31'),
      };

      const mockSub = createMockSubscription();
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockQueryBuilder.getOne.mockResolvedValueOnce(null);
      mockInvoiceRepository.create.mockImplementation((data: any) => ({
        ...createMockInvoice(),
        ...data,
      }));
      mockInvoiceRepository.save.mockImplementation((inv: any) => Promise.resolve({ ...inv, id: 'inv-id' }));
      mockInvoiceRepository.findOne.mockResolvedValue(createMockInvoice());
      mockItemRepository.create.mockReturnValue(createMockInvoiceItem());
      mockItemRepository.save.mockResolvedValue(createMockInvoiceItem());

      const result = await service.generateFromSubscription(dto);

      expect(mockSubscriptionRepository.findOne).toHaveBeenCalledWith({
        where: { id: 'sub-uuid-1' },
        relations: ['plan'],
      });
      expect(result).toBeDefined();
    });

    it('should throw error if subscription not found', async () => {
      mockSubscriptionRepository.findOne.mockResolvedValue(null);

      await expect(
        service.generateFromSubscription({
          tenantId: 'tenant-uuid-1',
          subscriptionId: 'invalid-id',
          periodStart: new Date(),
          periodEnd: new Date(),
        })
      ).rejects.toThrow('Subscription not found');
    });

    it('should include usage charges when requested', async () => {
      const dto = {
        tenantId: 'tenant-uuid-1',
        subscriptionId: 'sub-uuid-1',
        periodStart: new Date('2026-01-01'),
        periodEnd: new Date('2026-01-31'),
        includeUsageCharges: true,
      };

      const mockSub = createMockSubscription();
      const mockUsage = createMockUsage({
        activeUsers: 15, // 5 extra users
        activeBranches: 5, // 2 extra branches
        storageUsedGb: 25, // 5 extra GB
      });

      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockUsageRepository.findOne.mockResolvedValue(mockUsage);
      mockQueryBuilder.getOne.mockResolvedValueOnce(null);

      let createdItems: any[] = [];
      mockInvoiceRepository.create.mockImplementation((data: any) => ({
        ...createMockInvoice(),
        ...data,
      }));
      mockInvoiceRepository.save.mockImplementation((inv: any) => Promise.resolve({ ...inv, id: 'inv-id' }));
      mockInvoiceRepository.findOne.mockResolvedValue(createMockInvoice());
      mockItemRepository.create.mockImplementation((item: any) => {
        createdItems.push(item);
        return item;
      });
      mockItemRepository.save.mockImplementation((item: any) => Promise.resolve(item));

      await service.generateFromSubscription(dto);

      // Should have created items for: subscription + extra users + extra branches + extra storage
      expect(createdItems.length).toBeGreaterThan(1);
      expect(createdItems.some((i: any) => i.description.includes('Usuarios adicionales'))).toBe(true);
      expect(createdItems.some((i: any) => i.description.includes('Sucursales adicionales'))).toBe(true);
      expect(createdItems.some((i: any) => i.description.includes('Almacenamiento adicional'))).toBe(true);
    });
  });

  describe('findById', () => {
    it('should return invoice by id with items', async () => {
      const mockInvoice = createMockInvoice();
      mockInvoiceRepository.findOne.mockResolvedValue(mockInvoice);

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

      expect(mockInvoiceRepository.findOne).toHaveBeenCalledWith({
        where: { id: 'invoice-uuid-1' },
        relations: ['items'],
      });
      expect(result?.id).toBe('invoice-uuid-1');
    });

    it('should return null if invoice not found', async () => {
      mockInvoiceRepository.findOne.mockResolvedValue(null);

      const result = await service.findById('non-existent');

      expect(result).toBeNull();
    });
  });

  describe('findByNumber', () => {
    it('should return invoice by invoice number', async () => {
      const mockInvoice = createMockInvoice();
      mockInvoiceRepository.findOne.mockResolvedValue(mockInvoice);

      const result = await service.findByNumber('INV-202601-0001');

      expect(mockInvoiceRepository.findOne).toHaveBeenCalledWith({
        where: { invoiceNumber: 'INV-202601-0001' },
        relations: ['items'],
      });
      expect(result?.invoiceNumber).toBe('INV-202601-0001');
    });
  });

  describe('findAll', () => {
    it('should return invoices with filters', async () => {
      const mockInvoices = [
        createMockInvoice({ id: 'inv-1' }),
        createMockInvoice({ id: 'inv-2' }),
      ];
      mockQueryBuilder.getMany.mockResolvedValue(mockInvoices);
      mockQueryBuilder.getCount.mockResolvedValue(2);

      const result = await service.findAll({ tenantId: 'tenant-uuid-1' });

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'invoice.tenantId = :tenantId',
        { tenantId: 'tenant-uuid-1' }
      );
      expect(result.data).toHaveLength(2);
      expect(result.total).toBe(2);
    });

    it('should filter by status', async () => {
      mockQueryBuilder.getMany.mockResolvedValue([]);
      mockQueryBuilder.getCount.mockResolvedValue(0);

      await service.findAll({ status: 'paid' });

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'invoice.status = :status',
        { status: 'paid' }
      );
    });

    it('should filter by date range', async () => {
      mockQueryBuilder.getMany.mockResolvedValue([]);
      mockQueryBuilder.getCount.mockResolvedValue(0);

      const dateFrom = new Date('2026-01-01');
      const dateTo = new Date('2026-01-31');

      await service.findAll({ dateFrom, dateTo });

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'invoice.invoiceDate >= :dateFrom',
        { dateFrom }
      );
      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'invoice.invoiceDate <= :dateTo',
        { dateTo }
      );
    });

    it('should filter overdue invoices', async () => {
      mockQueryBuilder.getMany.mockResolvedValue([]);
      mockQueryBuilder.getCount.mockResolvedValue(0);

      await service.findAll({ overdue: true });

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'invoice.dueDate < :now',
        expect.any(Object)
      );
      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        "invoice.status IN ('sent', 'partial')"
      );
    });

    it('should apply pagination', async () => {
      mockQueryBuilder.getMany.mockResolvedValue([]);
      mockQueryBuilder.getCount.mockResolvedValue(100);

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

      expect(mockQueryBuilder.take).toHaveBeenCalledWith(10);
      expect(mockQueryBuilder.skip).toHaveBeenCalledWith(20);
    });
  });

  describe('update', () => {
    it('should update draft invoice', async () => {
      const mockInvoice = createMockInvoice({ status: 'draft' });
      mockInvoiceRepository.findOne.mockResolvedValue(mockInvoice);
      mockInvoiceRepository.save.mockImplementation((inv: any) => Promise.resolve(inv));

      const result = await service.update('invoice-uuid-1', { notes: 'Updated note' });

      expect(result.notes).toBe('Updated note');
    });

    it('should throw error if invoice not found', async () => {
      mockInvoiceRepository.findOne.mockResolvedValue(null);

      await expect(service.update('invalid-id', { notes: 'test' })).rejects.toThrow(
        'Invoice not found'
      );
    });

    it('should throw error if invoice is not draft', async () => {
      const mockInvoice = createMockInvoice({ status: 'sent' });
      mockInvoiceRepository.findOne.mockResolvedValue(mockInvoice);

      await expect(service.update('invoice-uuid-1', { notes: 'test' })).rejects.toThrow(
        'Only draft invoices can be updated'
      );
    });
  });

  describe('send', () => {
    it('should send draft invoice', async () => {
      const mockInvoice = createMockInvoice({ status: 'draft' });
      mockInvoiceRepository.findOne.mockResolvedValue(mockInvoice);
      mockInvoiceRepository.save.mockImplementation((inv: any) => Promise.resolve(inv));

      const result = await service.send('invoice-uuid-1');

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

    it('should throw error if invoice not found', async () => {
      mockInvoiceRepository.findOne.mockResolvedValue(null);

      await expect(service.send('invalid-id')).rejects.toThrow('Invoice not found');
    });

    it('should throw error if invoice is not draft', async () => {
      const mockInvoice = createMockInvoice({ status: 'paid' });
      mockInvoiceRepository.findOne.mockResolvedValue(mockInvoice);

      await expect(service.send('invoice-uuid-1')).rejects.toThrow(
        'Only draft invoices can be sent'
      );
    });
  });

  describe('recordPayment', () => {
    it('should record full payment', async () => {
      const mockInvoice = createMockInvoice({ status: 'sent', total: 578.84, paidAmount: 0 });
      mockInvoiceRepository.findOne.mockResolvedValue(mockInvoice);
      mockInvoiceRepository.save.mockImplementation((inv: any) => Promise.resolve(inv));

      const result = await service.recordPayment('invoice-uuid-1', {
        amount: 578.84,
        paymentMethod: 'card',
        paymentReference: 'PAY-123',
      });

      expect(result.status).toBe('paid');
      expect(result.paidAmount).toBe(578.84);
    });

    it('should record partial payment', async () => {
      const mockInvoice = createMockInvoice({ status: 'sent', total: 578.84, paidAmount: 0 });
      mockInvoiceRepository.findOne.mockResolvedValue(mockInvoice);
      mockInvoiceRepository.save.mockImplementation((inv: any) => Promise.resolve(inv));

      const result = await service.recordPayment('invoice-uuid-1', {
        amount: 300,
        paymentMethod: 'transfer',
      });

      expect(result.status).toBe('partial');
      expect(result.paidAmount).toBe(300);
    });

    it('should throw error if invoice not found', async () => {
      mockInvoiceRepository.findOne.mockResolvedValue(null);

      await expect(
        service.recordPayment('invalid-id', { amount: 100, paymentMethod: 'card' })
      ).rejects.toThrow('Invoice not found');
    });

    it('should throw error for voided invoice', async () => {
      const mockInvoice = createMockInvoice({ status: 'void' });
      mockInvoiceRepository.findOne.mockResolvedValue(mockInvoice);

      await expect(
        service.recordPayment('invoice-uuid-1', { amount: 100, paymentMethod: 'card' })
      ).rejects.toThrow('Cannot record payment for voided or refunded invoice');
    });

    it('should throw error for refunded invoice', async () => {
      const mockInvoice = createMockInvoice({ status: 'refunded' });
      mockInvoiceRepository.findOne.mockResolvedValue(mockInvoice);

      await expect(
        service.recordPayment('invoice-uuid-1', { amount: 100, paymentMethod: 'card' })
      ).rejects.toThrow('Cannot record payment for voided or refunded invoice');
    });
  });

  describe('void', () => {
    it('should void draft invoice', async () => {
      const mockInvoice = createMockInvoice({ status: 'draft' });
      mockInvoiceRepository.findOne.mockResolvedValue(mockInvoice);
      mockInvoiceRepository.save.mockImplementation((inv: any) => Promise.resolve(inv));

      const result = await service.void('invoice-uuid-1', { reason: 'Created by mistake' });

      expect(result.status).toBe('void');
      expect(result.internalNotes).toContain('Voided: Created by mistake');
    });

    it('should void sent invoice', async () => {
      const mockInvoice = createMockInvoice({ status: 'sent' });
      mockInvoiceRepository.findOne.mockResolvedValue(mockInvoice);
      mockInvoiceRepository.save.mockImplementation((inv: any) => Promise.resolve(inv));

      const result = await service.void('invoice-uuid-1', { reason: 'Customer cancelled' });

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

    it('should throw error if invoice not found', async () => {
      mockInvoiceRepository.findOne.mockResolvedValue(null);

      await expect(service.void('invalid-id', { reason: 'test' })).rejects.toThrow(
        'Invoice not found'
      );
    });

    it('should throw error for paid invoice', async () => {
      const mockInvoice = createMockInvoice({ status: 'paid' });
      mockInvoiceRepository.findOne.mockResolvedValue(mockInvoice);

      await expect(service.void('invoice-uuid-1', { reason: 'test' })).rejects.toThrow(
        'Cannot void paid or refunded invoice'
      );
    });

    it('should throw error for already refunded invoice', async () => {
      const mockInvoice = createMockInvoice({ status: 'refunded' });
      mockInvoiceRepository.findOne.mockResolvedValue(mockInvoice);

      await expect(service.void('invoice-uuid-1', { reason: 'test' })).rejects.toThrow(
        'Cannot void paid or refunded invoice'
      );
    });
  });

  describe('refund', () => {
    it('should refund paid invoice fully', async () => {
      const mockInvoice = createMockInvoice({ status: 'paid', paidAmount: 578.84 });
      mockInvoiceRepository.findOne.mockResolvedValue(mockInvoice);
      mockInvoiceRepository.save.mockImplementation((inv: any) => Promise.resolve(inv));

      const result = await service.refund('invoice-uuid-1', { reason: 'Customer requested' });

      expect(result.status).toBe('refunded');
      expect(result.internalNotes).toContain('Refunded: 578.84');
    });

    it('should refund partial amount', async () => {
      const mockInvoice = createMockInvoice({ status: 'paid', paidAmount: 578.84 });
      mockInvoiceRepository.findOne.mockResolvedValue(mockInvoice);
      mockInvoiceRepository.save.mockImplementation((inv: any) => Promise.resolve(inv));

      const result = await service.refund('invoice-uuid-1', {
        amount: 200,
        reason: 'Partial service',
      });

      expect(result.status).toBe('refunded');
      expect(result.internalNotes).toContain('Refunded: 200');
    });

    it('should throw error if invoice not found', async () => {
      mockInvoiceRepository.findOne.mockResolvedValue(null);

      await expect(service.refund('invalid-id', { reason: 'test' })).rejects.toThrow(
        'Invoice not found'
      );
    });

    it('should throw error for unpaid invoice', async () => {
      const mockInvoice = createMockInvoice({ status: 'draft' });
      mockInvoiceRepository.findOne.mockResolvedValue(mockInvoice);

      await expect(service.refund('invoice-uuid-1', { reason: 'test' })).rejects.toThrow(
        'Only paid invoices can be refunded'
      );
    });

    it('should throw error if refund amount exceeds paid amount', async () => {
      const mockInvoice = createMockInvoice({ status: 'paid', paidAmount: 100 });
      mockInvoiceRepository.findOne.mockResolvedValue(mockInvoice);

      await expect(
        service.refund('invoice-uuid-1', { amount: 200, reason: 'test' })
      ).rejects.toThrow('Refund amount cannot exceed paid amount');
    });
  });

  describe('markOverdueInvoices', () => {
    it('should mark overdue invoices', async () => {
      const mockUpdateBuilder = {
        update: jest.fn().mockReturnThis(),
        set: jest.fn().mockReturnThis(),
        where: jest.fn().mockReturnThis(),
        andWhere: jest.fn().mockReturnThis(),
        execute: jest.fn().mockResolvedValue({ affected: 5 }),
      };
      mockInvoiceRepository.createQueryBuilder.mockReturnValue(mockUpdateBuilder);

      const result = await service.markOverdueInvoices();

      expect(result).toBe(5);
      expect(mockUpdateBuilder.set).toHaveBeenCalledWith({ status: 'overdue' });
    });

    it('should return 0 when no invoices are overdue', async () => {
      const mockUpdateBuilder = {
        update: jest.fn().mockReturnThis(),
        set: jest.fn().mockReturnThis(),
        where: jest.fn().mockReturnThis(),
        andWhere: jest.fn().mockReturnThis(),
        execute: jest.fn().mockResolvedValue({ affected: 0 }),
      };
      mockInvoiceRepository.createQueryBuilder.mockReturnValue(mockUpdateBuilder);

      const result = await service.markOverdueInvoices();

      expect(result).toBe(0);
    });
  });

  describe('getStats', () => {
    it('should return invoice statistics', async () => {
      const mockInvoices = [
        createMockInvoice({ status: 'paid', paidAmount: 500, total: 500 }),
        createMockInvoice({ status: 'paid', paidAmount: 300, total: 300 }),
        createMockInvoice({ status: 'sent', paidAmount: 0, total: 400, dueDate: new Date('2025-01-01') }),
        createMockInvoice({ status: 'draft', paidAmount: 0, total: 200 }),
      ];
      mockQueryBuilder.getMany.mockResolvedValue(mockInvoices);

      const result = await service.getStats('tenant-uuid-1');

      expect(mockQueryBuilder.where).toHaveBeenCalledWith(
        'invoice.tenantId = :tenantId',
        { tenantId: 'tenant-uuid-1' }
      );
      expect(result.total).toBe(4);
      expect(result.byStatus.paid).toBe(2);
      expect(result.byStatus.sent).toBe(1);
      expect(result.byStatus.draft).toBe(1);
      expect(result.totalRevenue).toBe(800);
      expect(result.pendingAmount).toBe(400);
      expect(result.overdueAmount).toBe(400); // The sent invoice is overdue
    });

    it('should return stats without tenant filter', async () => {
      mockQueryBuilder.getMany.mockResolvedValue([]);

      const result = await service.getStats();

      expect(mockQueryBuilder.where).not.toHaveBeenCalled();
      expect(result.total).toBe(0);
    });
  });

  describe('generateInvoiceNumber (via create)', () => {
    it('should generate sequential invoice numbers', async () => {
      // First invoice of the month
      mockQueryBuilder.getOne.mockResolvedValueOnce(null);

      const dto = {
        tenantId: 'tenant-uuid-1',
        items: [{ itemType: 'subscription' as const, description: 'Test', quantity: 1, unitPrice: 100 }],
      };

      mockInvoiceRepository.create.mockImplementation((data: any) => data);
      mockInvoiceRepository.save.mockImplementation((inv: any) => Promise.resolve({ ...inv, id: 'inv-1' }));
      mockInvoiceRepository.findOne.mockResolvedValue(createMockInvoice());
      mockItemRepository.create.mockReturnValue(createMockInvoiceItem());
      mockItemRepository.save.mockResolvedValue(createMockInvoiceItem());

      await service.create(dto);

      // Verify the invoice number format (INV-YYYYMM-0001)
      expect(mockInvoiceRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          invoiceNumber: expect.stringMatching(/^INV-\d{6}-0001$/),
        })
      );
    });

    it('should increment sequence for existing invoices', async () => {
      // Return existing invoice for the month
      mockQueryBuilder.getOne.mockResolvedValueOnce(
        createMockInvoice({ invoiceNumber: 'INV-202601-0005' })
      );

      const dto = {
        tenantId: 'tenant-uuid-1',
        items: [{ itemType: 'subscription' as const, description: 'Test', quantity: 1, unitPrice: 100 }],
      };

      mockInvoiceRepository.create.mockImplementation((data: any) => data);
      mockInvoiceRepository.save.mockImplementation((inv: any) => Promise.resolve({ ...inv, id: 'inv-1' }));
      mockInvoiceRepository.findOne.mockResolvedValue(createMockInvoice());
      mockItemRepository.create.mockReturnValue(createMockInvoiceItem());
      mockItemRepository.save.mockResolvedValue(createMockInvoiceItem());

      await service.create(dto);

      // Should be 0006
      expect(mockInvoiceRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          invoiceNumber: expect.stringMatching(/^INV-\d{6}-0006$/),
        })
      );
    });
  });
});
