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

// Mock query functions
const mockQuery = jest.fn();
const mockQueryOne = jest.fn();
const mockGetClient = jest.fn();

jest.mock('../../../config/database.js', () => ({
  query: (...args: any[]) => mockQuery(...args),
  queryOne: (...args: any[]) => mockQueryOne(...args),
  getClient: () => mockGetClient(),
}));

// Mock sequences service
const mockGetNextNumber = jest.fn();
jest.mock('../../core/sequences.service.js', () => ({
  sequencesService: {
    getNextNumber: (...args: any[]) => mockGetNextNumber(...args),
  },
  SEQUENCE_CODES: {
    PURCHASE_ORDER: 'PO',
  },
}));

// Import after mocking
import { purchasesService } from '../purchases.service.js';
import { NotFoundError, ConflictError, ValidationError } from '../../../shared/errors/index.js';

describe('PurchasesService', () => {
  const tenantId = 'test-tenant-uuid';
  const userId = 'test-user-uuid';

  const mockClient = {
    query: jest.fn(),
    release: jest.fn(),
  };

  beforeEach(() => {
    jest.clearAllMocks();
    mockGetClient.mockResolvedValue(mockClient);
    mockGetNextNumber.mockResolvedValue('PO-00001');
  });

  describe('findAll', () => {
    it('should return purchase orders with pagination', async () => {
      const mockOrders = [
        createMockPurchaseOrder({ id: '1', name: 'PO-000001' }),
        createMockPurchaseOrder({ id: '2', name: 'PO-000002' }),
      ];

      mockQueryOne.mockResolvedValue({ count: '2' });
      mockQuery.mockResolvedValue(mockOrders);

      const result = await purchasesService.findAll(tenantId, { page: 1, limit: 20 });

      expect(result.data).toHaveLength(2);
      expect(result.total).toBe(2);
    });

    it('should filter by company_id', async () => {
      mockQueryOne.mockResolvedValue({ count: '0' });
      mockQuery.mockResolvedValue([]);

      await purchasesService.findAll(tenantId, { company_id: 'company-uuid' });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('po.company_id = $'),
        expect.arrayContaining([tenantId, 'company-uuid'])
      );
    });

    it('should filter by partner_id', async () => {
      mockQueryOne.mockResolvedValue({ count: '0' });
      mockQuery.mockResolvedValue([]);

      await purchasesService.findAll(tenantId, { partner_id: 'partner-uuid' });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('po.partner_id = $'),
        expect.arrayContaining([tenantId, 'partner-uuid'])
      );
    });

    it('should filter by status', async () => {
      mockQueryOne.mockResolvedValue({ count: '0' });
      mockQuery.mockResolvedValue([]);

      await purchasesService.findAll(tenantId, { status: 'draft' });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('po.status = $'),
        expect.arrayContaining([tenantId, 'draft'])
      );
    });

    it('should filter by date range', async () => {
      mockQueryOne.mockResolvedValue({ count: '0' });
      mockQuery.mockResolvedValue([]);

      await purchasesService.findAll(tenantId, { date_from: '2024-01-01', date_to: '2024-12-31' });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('po.order_date >= $'),
        expect.arrayContaining([tenantId, '2024-01-01', '2024-12-31'])
      );
    });

    it('should filter by search term', async () => {
      mockQueryOne.mockResolvedValue({ count: '0' });
      mockQuery.mockResolvedValue([]);

      await purchasesService.findAll(tenantId, { search: 'PO-001' });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('po.name ILIKE'),
        expect.arrayContaining([tenantId, '%PO-001%'])
      );
    });

    it('should apply pagination correctly', async () => {
      mockQueryOne.mockResolvedValue({ count: '50' });
      mockQuery.mockResolvedValue([]);

      await purchasesService.findAll(tenantId, { page: 3, limit: 10 });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('LIMIT'),
        expect.arrayContaining([10, 20])
      );
    });
  });

  describe('findById', () => {
    it('should return purchase order with lines when found', async () => {
      const mockOrder = createMockPurchaseOrder();
      const mockLines = [createMockPurchaseOrderLine()];

      mockQueryOne.mockResolvedValue(mockOrder);
      mockQuery.mockResolvedValue(mockLines);

      const result = await purchasesService.findById('po-uuid-1', tenantId);

      expect(result).toEqual({ ...mockOrder, lines: mockLines });
    });

    it('should throw NotFoundError when purchase order not found', async () => {
      mockQueryOne.mockResolvedValue(null);

      await expect(
        purchasesService.findById('nonexistent-id', tenantId)
      ).rejects.toThrow(NotFoundError);
    });
  });

  describe('create', () => {
    const createDto = {
      company_id: 'company-uuid',
      name: 'PO-000001',
      partner_id: 'partner-uuid',
      order_date: '2024-06-15',
      currency_id: 'currency-uuid',
      lines: [
        {
          product_id: 'product-uuid',
          description: 'Test product',
          quantity: 10,
          uom_id: 'uom-uuid',
          price_unit: 100,
          amount_untaxed: 1000,
        },
      ],
    };

    it('should create purchase order with lines', async () => {
      const mockOrder = createMockPurchaseOrder();
      const mockLines = [createMockPurchaseOrderLine()];

      mockClient.query
        .mockResolvedValueOnce(undefined) // BEGIN
        .mockResolvedValueOnce({ rows: [mockOrder] }) // INSERT order
        .mockResolvedValueOnce(undefined) // INSERT line
        .mockResolvedValueOnce(undefined); // COMMIT

      mockQueryOne.mockResolvedValue({ ...mockOrder, lines: mockLines });
      mockQuery.mockResolvedValue(mockLines);

      const result = await purchasesService.create(createDto, tenantId, userId);

      expect(result.name).toBe(mockOrder.name);
      expect(mockClient.query).toHaveBeenCalledWith('BEGIN');
      expect(mockClient.query).toHaveBeenCalledWith('COMMIT');
    });

    it('should throw ValidationError when no lines provided', async () => {
      const emptyDto = { ...createDto, lines: [] };

      await expect(
        purchasesService.create(emptyDto, tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should rollback on error', async () => {
      mockClient.query
        .mockResolvedValueOnce(undefined) // BEGIN
        .mockRejectedValueOnce(new Error('DB Error')); // INSERT fails

      await expect(
        purchasesService.create(createDto, tenantId, userId)
      ).rejects.toThrow('DB Error');

      expect(mockClient.query).toHaveBeenCalledWith('ROLLBACK');
      expect(mockClient.release).toHaveBeenCalled();
    });
  });

  describe('update', () => {
    it('should update purchase order in draft status', async () => {
      const existingOrder = createMockPurchaseOrder({ status: 'draft' });
      const mockLines = [createMockPurchaseOrderLine()];

      mockQueryOne.mockResolvedValue(existingOrder);
      mockQuery.mockResolvedValue(mockLines);
      mockClient.query
        .mockResolvedValueOnce(undefined) // BEGIN
        .mockResolvedValueOnce(undefined) // UPDATE
        .mockResolvedValueOnce(undefined); // COMMIT

      await purchasesService.update(
        'po-uuid-1',
        { partner_id: 'new-partner-uuid' },
        tenantId,
        userId
      );

      expect(mockClient.query).toHaveBeenCalledWith('BEGIN');
      expect(mockClient.query).toHaveBeenCalledWith('COMMIT');
    });

    it('should throw ConflictError when order is not draft', async () => {
      const confirmedOrder = createMockPurchaseOrder({ status: 'confirmed' });
      mockQueryOne.mockResolvedValue(confirmedOrder);
      mockQuery.mockResolvedValue([]);

      await expect(
        purchasesService.update('po-uuid-1', { partner_id: 'new-partner' }, tenantId, userId)
      ).rejects.toThrow(ConflictError);
    });

    it('should update lines when provided', async () => {
      const existingOrder = createMockPurchaseOrder({ status: 'draft' });
      const mockLines = [createMockPurchaseOrderLine()];

      mockQueryOne.mockResolvedValue(existingOrder);
      mockQuery.mockResolvedValue(mockLines);
      mockClient.query
        .mockResolvedValueOnce(undefined) // BEGIN
        .mockResolvedValueOnce(undefined) // UPDATE
        .mockResolvedValueOnce(undefined) // DELETE lines
        .mockResolvedValueOnce(undefined) // INSERT line
        .mockResolvedValueOnce(undefined) // UPDATE totals
        .mockResolvedValueOnce(undefined); // COMMIT

      await purchasesService.update(
        'po-uuid-1',
        {
          lines: [
            {
              product_id: 'product-uuid',
              description: 'Updated product',
              quantity: 20,
              uom_id: 'uom-uuid',
              price_unit: 150,
              amount_untaxed: 3000,
            },
          ],
        },
        tenantId,
        userId
      );

      expect(mockClient.query).toHaveBeenCalledWith(
        expect.stringContaining('DELETE FROM purchase.purchase_order_lines'),
        expect.any(Array)
      );
    });
  });

  describe('confirm', () => {
    it('should confirm draft order with lines', async () => {
      const draftOrder = createMockPurchaseOrder({
        status: 'draft',
        company_id: 'company-uuid',
        lines: [createMockPurchaseOrderLine()],
      });

      mockQueryOne.mockResolvedValue(draftOrder);
      mockQuery
        .mockResolvedValueOnce([createMockPurchaseOrderLine()]) // findById lines
        .mockResolvedValueOnce(undefined); // UPDATE status

      // Mock client.query calls for confirm flow
      mockClient.query
        .mockResolvedValueOnce({ rows: [] }) // BEGIN returns empty
        .mockResolvedValueOnce({ rows: [{ id: 'supplier-loc-uuid' }] }) // supplier location
        .mockResolvedValueOnce({ rows: [{ location_id: 'internal-loc-uuid', warehouse_id: 'wh-uuid' }] }) // internal location
        .mockResolvedValueOnce({ rows: [{ id: 'picking-uuid' }] }) // INSERT picking
        .mockResolvedValueOnce({ rows: [] }) // INSERT stock_move
        .mockResolvedValueOnce({ rows: [] }) // UPDATE status
        .mockResolvedValueOnce({ rows: [] }); // COMMIT

      await purchasesService.confirm('po-uuid-1', tenantId, userId);

      expect(mockClient.query).toHaveBeenCalledWith('BEGIN');
      expect(mockClient.query).toHaveBeenCalledWith('COMMIT');
    });

    it('should throw ConflictError when order is not draft', async () => {
      const confirmedOrder = createMockPurchaseOrder({ status: 'confirmed' });
      mockQueryOne.mockResolvedValue(confirmedOrder);
      mockQuery.mockResolvedValue([]);

      await expect(
        purchasesService.confirm('po-uuid-1', tenantId, userId)
      ).rejects.toThrow(ConflictError);
    });

    it('should throw ValidationError when order has no lines', async () => {
      const draftOrder = createMockPurchaseOrder({ status: 'draft', lines: [] });
      mockQueryOne.mockResolvedValue(draftOrder);
      mockQuery.mockResolvedValue([]);

      await expect(
        purchasesService.confirm('po-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('cancel', () => {
    it('should cancel draft order', async () => {
      const draftOrder = createMockPurchaseOrder({ status: 'draft' });
      mockQueryOne.mockResolvedValue(draftOrder);
      mockQuery.mockResolvedValue([]);

      await purchasesService.cancel('po-uuid-1', tenantId, userId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining("status = 'cancelled'"),
        expect.any(Array)
      );
    });

    it('should cancel confirmed order', async () => {
      const confirmedOrder = createMockPurchaseOrder({ status: 'confirmed' });
      mockQueryOne.mockResolvedValue(confirmedOrder);
      mockQuery.mockResolvedValue([]);

      await purchasesService.cancel('po-uuid-1', tenantId, userId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining("status = 'cancelled'"),
        expect.any(Array)
      );
    });

    it('should throw ConflictError when order is already cancelled', async () => {
      const cancelledOrder = createMockPurchaseOrder({ status: 'cancelled' });
      mockQueryOne.mockResolvedValue(cancelledOrder);
      mockQuery.mockResolvedValue([]);

      await expect(
        purchasesService.cancel('po-uuid-1', tenantId, userId)
      ).rejects.toThrow(ConflictError);
    });

    it('should throw ConflictError when order is done', async () => {
      const doneOrder = createMockPurchaseOrder({ status: 'done' });
      mockQueryOne.mockResolvedValue(doneOrder);
      mockQuery.mockResolvedValue([]);

      await expect(
        purchasesService.cancel('po-uuid-1', tenantId, userId)
      ).rejects.toThrow(ConflictError);
    });
  });

  describe('delete', () => {
    it('should delete purchase order in draft status', async () => {
      const draftOrder = createMockPurchaseOrder({ status: 'draft' });
      mockQueryOne.mockResolvedValue(draftOrder);
      mockQuery.mockResolvedValue([]);

      await purchasesService.delete('po-uuid-1', tenantId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('DELETE FROM purchase.purchase_orders'),
        expect.any(Array)
      );
    });

    it('should throw ConflictError when order is not draft', async () => {
      const confirmedOrder = createMockPurchaseOrder({ status: 'confirmed' });
      mockQueryOne.mockResolvedValue(confirmedOrder);
      mockQuery.mockResolvedValue([]);

      await expect(
        purchasesService.delete('po-uuid-1', tenantId)
      ).rejects.toThrow(ConflictError);
    });
  });
});
