import { Test, TestingModule } from '@nestjs/testing';
import { DataSource, Repository } from 'typeorm';
import { getRepositoryToken } from '@nestjs/typeorm';
import { InvoicesService } from '../services/invoices.service';
import { Invoice, InvoiceItem, InvoiceStatus, PaymentStatus } from '../entities';
import { CreateInvoiceDto, UpdateInvoiceDto } from '../dto';

describe('InvoicesService', () => {
  let service: InvoicesService;
  let invoiceRepository: Repository<Invoice>;
  let invoiceItemRepository: Repository<InvoiceItem>;
  let dataSource: DataSource;

  const mockInvoice = {
    id: 'uuid-1',
    tenantId: 'tenant-1',
    customerId: 'customer-1',
    number: 'INV-2024-001',
    status: InvoiceStatus.DRAFT,
    paymentStatus: PaymentStatus.PENDING,
    issueDate: new Date('2024-01-01'),
    dueDate: new Date('2024-01-15'),
    subtotal: 1000,
    taxAmount: 160,
    totalAmount: 1160,
    currency: 'USD',
    notes: null,
    metadata: {},
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  const mockInvoiceItem = {
    id: 'item-1',
    invoiceId: 'uuid-1',
    productId: 'product-1',
    description: 'Test Product',
    quantity: 2,
    unitPrice: 500,
    discount: 0,
    taxRate: 0.08,
    total: 1080,
    metadata: {},
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        InvoicesService,
        {
          provide: DataSource,
          useValue: {
            getRepository: jest.fn(),
            query: jest.fn(),
          },
        },
      ],
    }).compile();

    service = module.get<InvoicesService>(InvoicesService);
    dataSource = module.get<DataSource>(DataSource);
    invoiceRepository = module.get<Repository<Invoice>>(
      getRepositoryToken(Invoice),
    );
    invoiceItemRepository = module.get<Repository<InvoiceItem>>(
      getRepositoryToken(InvoiceItem),
    );
  });

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

  describe('create', () => {
    it('should create a new invoice successfully', async () => {
      const dto: CreateInvoiceDto = {
        customerId: 'customer-1',
        issueDate: new Date('2024-01-01'),
        dueDate: new Date('2024-01-15'),
        items: [
          {
            productId: 'product-1',
            description: 'Test Product',
            quantity: 2,
            unitPrice: 500,
            discount: 0,
            taxRate: 0.08,
          },
        ],
        notes: 'Test invoice',
      };

      jest.spyOn(invoiceRepository, 'create').mockReturnValue(mockInvoice as any);
      jest.spyOn(invoiceRepository, 'save').mockResolvedValue(mockInvoice);
      jest.spyOn(invoiceItemRepository, 'create').mockReturnValue(mockInvoiceItem as any);
      jest.spyOn(invoiceItemRepository, 'save').mockResolvedValue(mockInvoiceItem);

      const result = await service.create(dto);

      expect(invoiceRepository.create).toHaveBeenCalled();
      expect(invoiceRepository.save).toHaveBeenCalled();
      expect(invoiceItemRepository.create).toHaveBeenCalled();
      expect(invoiceItemRepository.save).toHaveBeenCalled();
      expect(result).toEqual(mockInvoice);
    });

    it('should calculate totals correctly', async () => {
      const dto: CreateInvoiceDto = {
        customerId: 'customer-1',
        issueDate: new Date('2024-01-01'),
        dueDate: new Date('2024-01-15'),
        items: [
          {
            productId: 'product-1',
            description: 'Test Product 1',
            quantity: 2,
            unitPrice: 500,
            discount: 50,
            taxRate: 0.08,
          },
          {
            productId: 'product-2',
            description: 'Test Product 2',
            quantity: 1,
            unitPrice: 300,
            discount: 0,
            taxRate: 0.08,
          },
        ],
      };

      const expectedInvoice = {
        ...mockInvoice,
        subtotal: 1000,
        taxAmount: 120,
        totalAmount: 1120,
      };

      jest.spyOn(invoiceRepository, 'create').mockReturnValue(expectedInvoice as any);
      jest.spyOn(invoiceRepository, 'save').mockResolvedValue(expectedInvoice);
      jest.spyOn(invoiceItemRepository, 'create').mockReturnValue(mockInvoiceItem as any);
      jest.spyOn(invoiceItemRepository, 'save').mockResolvedValue(mockInvoiceItem);

      const result = await service.create(dto);

      expect(result.subtotal).toBe(1000);
      expect(result.taxAmount).toBe(120);
      expect(result.totalAmount).toBe(1120);
    });
  });

  describe('findById', () => {
    it('should find invoice by id', async () => {
      jest.spyOn(invoiceRepository, 'findOne').mockResolvedValue(mockInvoice as any);

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

      expect(invoiceRepository.findOne).toHaveBeenCalledWith({
        where: { id: 'uuid-1' },
        relations: ['items'],
      });
      expect(result).toEqual(mockInvoice);
    });

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

      const result = await service.findById('invalid-id');

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

  describe('findByTenant', () => {
    it('should find invoices by tenant', async () => {
      const mockInvoices = [mockInvoice, { ...mockInvoice, id: 'uuid-2' }];
      jest.spyOn(invoiceRepository, 'find').mockResolvedValue(mockInvoices as any);

      const result = await service.findByTenant('tenant-1', {
        page: 1,
        limit: 10,
      });

      expect(invoiceRepository.find).toHaveBeenCalledWith({
        where: { tenantId: 'tenant-1' },
        relations: ['items'],
        order: { createdAt: 'DESC' },
        skip: 0,
        take: 10,
      });
      expect(result).toEqual(mockInvoices);
    });
  });

  describe('update', () => {
    it('should update invoice successfully', async () => {
      const dto: UpdateInvoiceDto = {
        status: InvoiceStatus.SENT,
        notes: 'Updated notes',
      };

      const updatedInvoice = { ...mockInvoice, status: InvoiceStatus.SENT, notes: 'Updated notes' };

      jest.spyOn(invoiceRepository, 'findOne').mockResolvedValue(mockInvoice as any);
      jest.spyOn(invoiceRepository, 'save').mockResolvedValue(updatedInvoice as any);

      const result = await service.update('uuid-1', dto);

      expect(invoiceRepository.findOne).toHaveBeenCalledWith({
        where: { id: 'uuid-1' },
      });
      expect(invoiceRepository.save).toHaveBeenCalled();
      expect(result.status).toBe(InvoiceStatus.SENT);
      expect(result.notes).toBe('Updated notes');
    });

    it('should throw error if invoice not found', async () => {
      const dto: UpdateInvoiceDto = {
        status: InvoiceStatus.SENT,
      };

      jest.spyOn(invoiceRepository, 'findOne').mockResolvedValue(null);

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

  describe('updateStatus', () => {
    it('should update invoice status', async () => {
      jest.spyOn(invoiceRepository, 'findOne').mockResolvedValue(mockInvoice as any);
      jest.spyOn(invoiceRepository, 'save').mockResolvedValue({
        ...mockInvoice,
        status: InvoiceStatus.PAID,
      } as any);

      const result = await service.updateStatus('uuid-1', InvoiceStatus.PAID);

      expect(result.status).toBe(InvoiceStatus.PAID);
    });
  });

  describe('updatePaymentStatus', () => {
    it('should update payment status', async () => {
      jest.spyOn(invoiceRepository, 'findOne').mockResolvedValue(mockInvoice as any);
      jest.spyOn(invoiceRepository, 'save').mockResolvedValue({
        ...mockInvoice,
        paymentStatus: PaymentStatus.PAID,
      } as any);

      const result = await service.updatePaymentStatus('uuid-1', PaymentStatus.PAID);

      expect(result.paymentStatus).toBe(PaymentStatus.PAID);
    });
  });

  describe('delete', () => {
    it('should delete invoice successfully', async () => {
      jest.spyOn(invoiceRepository, 'findOne').mockResolvedValue(mockInvoice as any);
      jest.spyOn(invoiceRepository, 'remove').mockResolvedValue(undefined);

      await service.delete('uuid-1');

      expect(invoiceRepository.remove).toHaveBeenCalledWith(mockInvoice);
    });

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

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

  describe('addItem', () => {
    it('should add item to invoice', async () => {
      const itemDto = {
        productId: 'product-2',
        description: 'New Product',
        quantity: 1,
        unitPrice: 300,
        discount: 0,
        taxRate: 0.08,
      };

      jest.spyOn(invoiceRepository, 'findOne').mockResolvedValue(mockInvoice as any);
      jest.spyOn(invoiceItemRepository, 'create').mockReturnValue(mockInvoiceItem as any);
      jest.spyOn(invoiceItemRepository, 'save').mockResolvedValue(mockInvoiceItem);
      jest.spyOn(invoiceRepository, 'save').mockResolvedValue({
        ...mockInvoice,
        subtotal: 1500,
        taxAmount: 120,
        totalAmount: 1620,
      } as any);

      const result = await service.addItem('uuid-1', itemDto);

      expect(invoiceItemRepository.create).toHaveBeenCalled();
      expect(invoiceItemRepository.save).toHaveBeenCalled();
      expect(result.totalAmount).toBe(1620);
    });
  });

  describe('removeItem', () => {
    it('should remove item from invoice', async () => {
      jest.spyOn(invoiceItemRepository, 'findOne').mockResolvedValue(mockInvoiceItem as any);
      jest.spyOn(invoiceItemRepository, 'remove').mockResolvedValue(undefined);
      jest.spyOn(invoiceRepository, 'save').mockResolvedValue({
        ...mockInvoice,
        subtotal: 500,
        taxAmount: 40,
        totalAmount: 540,
      } as any);

      const result = await service.removeItem('uuid-1', 'item-1');

      expect(invoiceItemRepository.remove).toHaveBeenCalled();
      expect(result.totalAmount).toBe(540);
    });
  });

  describe('sendInvoice', () => {
    it('should mark invoice as sent', async () => {
      jest.spyOn(invoiceRepository, 'findOne').mockResolvedValue(mockInvoice as any);
      jest.spyOn(invoiceRepository, 'save').mockResolvedValue({
        ...mockInvoice,
        status: InvoiceStatus.SENT,
        sentAt: new Date(),
      } as any);

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

      expect(result.status).toBe(InvoiceStatus.SENT);
      expect(result.sentAt).toBeDefined();
    });
  });

  describe('calculateTotals', () => {
    it('should calculate totals from items', () => {
      const items = [
        { quantity: 2, unitPrice: 500, discount: 50, taxRate: 0.08 },
        { quantity: 1, unitPrice: 300, discount: 0, taxRate: 0.08 },
      ];

      const totals = service.calculateTotals(items);

      expect(totals.subtotal).toBe(1000);
      expect(totals.taxAmount).toBe(120);
      expect(totals.totalAmount).toBe(1120);
    });

    it('should handle empty items array', () => {
      const totals = service.calculateTotals([]);

      expect(totals.subtotal).toBe(0);
      expect(totals.taxAmount).toBe(0);
      expect(totals.totalAmount).toBe(0);
    });
  });
});
