/**
 * Integration tests for the complete purchase flow
 *
 * Tests the following flows:
 * 1. RFQ -> Purchase Order conversion
 * 2. Purchase Order -> Partial Receipt -> Complete Receipt
 * 3. Receipt -> Supplier Invoice
 * 4. Supplier Returns
 * 5. Cost control and variance tracking
 */
import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import {
  createMockRepository,
  createMockQueryBuilder,
  createMockPurchaseOrder,
  createMockPurchaseOrderLine,
  createMockRfq,
  createMockRfqLine,
} 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',
    PICKING_IN: 'WH/IN',
    INVOICE_SUPPLIER: 'BILL',
  },
}));

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

describe('Purchases Flow Integration Tests', () => {
  const tenantId = 'test-tenant-uuid';
  const userId = 'test-user-uuid';
  const companyId = 'company-uuid';
  const supplierId = 'supplier-uuid';
  const currencyId = 'currency-uuid';
  const productId = 'product-uuid-1';
  const uomId = 'uom-uuid-1';

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

  beforeEach(() => {
    jest.clearAllMocks();
    mockGetClient.mockResolvedValue(mockClient);
    mockGetNextNumber.mockImplementation((code: string) => {
      if (code === 'PO') return Promise.resolve('PO-00001');
      if (code === 'WH/IN') return Promise.resolve('WH/IN/00001');
      if (code === 'BILL') return Promise.resolve('BILL-00001');
      return Promise.resolve(`${code}-00001`);
    });
  });

  afterEach(() => {
    jest.restoreAllMocks();
  });

  // ==================== Flow 1: RFQ -> Purchase Order ====================
  describe('Flow 1: RFQ to Purchase Order Conversion', () => {
    describe('create RFQ and convert to PO', () => {
      it('should create an RFQ with lines', async () => {
        const rfqData = {
          company_id: companyId,
          partner_ids: [supplierId],
          request_date: '2024-06-15',
          lines: [
            {
              product_id: productId,
              description: 'Product A',
              quantity: 100,
              uom_id: uomId,
            },
            {
              product_id: 'product-uuid-2',
              description: 'Product B',
              quantity: 50,
              uom_id: uomId,
            },
          ],
        };

        const mockRfq = createMockRfq({
          id: 'rfq-001',
          name: 'RFQ-000001',
          status: 'draft',
          ...rfqData,
        });

        mockClient.query
          .mockResolvedValueOnce(undefined) // BEGIN
          .mockResolvedValueOnce({ rows: [{ next_num: 1 }] }) // sequence
          .mockResolvedValueOnce({ rows: [mockRfq] }) // INSERT RFQ
          .mockResolvedValueOnce(undefined) // INSERT line 1
          .mockResolvedValueOnce(undefined) // INSERT line 2
          .mockResolvedValueOnce(undefined); // COMMIT

        mockQueryOne.mockResolvedValue(mockRfq);
        mockQuery.mockResolvedValue([createMockRfqLine(), createMockRfqLine({ id: 'line-2' })]);

        const result = await rfqsService.create(rfqData, tenantId, userId);

        expect(result).toBeDefined();
        expect(result.name).toBe('RFQ-000001');
        expect(mockClient.query).toHaveBeenCalledWith('BEGIN');
        expect(mockClient.query).toHaveBeenCalledWith('COMMIT');
      });

      it('should send RFQ to suppliers', async () => {
        const draftRfq = createMockRfq({
          id: 'rfq-001',
          status: 'draft',
          lines: [createMockRfqLine()],
        });

        mockQueryOne.mockResolvedValue(draftRfq);
        mockQuery.mockResolvedValue([createMockRfqLine()]);

        await rfqsService.send('rfq-001', tenantId, userId);

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

      it('should accept RFQ and prepare for PO creation', async () => {
        const sentRfq = createMockRfq({
          id: 'rfq-001',
          status: 'sent',
          lines: [createMockRfqLine()],
        });

        mockQueryOne.mockResolvedValue(sentRfq);
        mockQuery.mockResolvedValue([]);

        const result = await rfqsService.accept('rfq-001', tenantId, userId);

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

      it('should create purchase order from accepted RFQ data', async () => {
        // Simulating PO creation with data from an accepted RFQ
        const poData = {
          company_id: companyId,
          partner_id: supplierId,
          order_date: '2024-06-15',
          currency_id: currencyId,
          lines: [
            {
              product_id: productId,
              description: 'Product A',
              quantity: 100,
              uom_id: uomId,
              price_unit: 50,
              amount_untaxed: 5000,
            },
          ],
        };

        const mockOrder = createMockPurchaseOrder({
          id: 'po-001',
          name: 'PO-00001',
          status: 'draft',
        });

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

        mockQueryOne.mockResolvedValue(mockOrder);
        mockQuery.mockResolvedValue([createMockPurchaseOrderLine()]);

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

        expect(result).toBeDefined();
        expect(result.name).toBe(mockOrder.name);
      });
    });
  });

  // ==================== Flow 2: PO -> Partial Receipt -> Complete Receipt ====================
  describe('Flow 2: Purchase Order to Receipt', () => {
    describe('confirm order and create receipt', () => {
      it('should confirm purchase order and create incoming picking', async () => {
        const draftOrder = createMockPurchaseOrder({
          id: 'po-001',
          status: 'draft',
          company_id: companyId,
          partner_id: supplierId,
          amount_total: 5000,
        });
        const mockLines = [
          createMockPurchaseOrderLine({
            id: 'line-1',
            quantity: 100,
            price_unit: 50,
          }),
        ];

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

        mockClient.query
          .mockResolvedValueOnce(undefined) // BEGIN
          .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(undefined) // INSERT stock_move
          .mockResolvedValueOnce(undefined) // UPDATE order status
          .mockResolvedValueOnce(undefined); // COMMIT

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

        expect(mockClient.query).toHaveBeenCalledWith('BEGIN');
        expect(mockClient.query).toHaveBeenCalledWith('COMMIT');
        expect(mockGetNextNumber).toHaveBeenCalledWith('WH/IN', tenantId);
      });

      it('should handle partial receipt of ordered quantities', async () => {
        // Scenario: Order 100 units, receive 60 units first
        const confirmedOrder = createMockPurchaseOrder({
          id: 'po-001',
          status: 'confirmed',
          company_id: companyId,
          lines: [
            createMockPurchaseOrderLine({
              id: 'line-1',
              quantity: 100,
              qty_received: 0,
              price_unit: 50,
            }),
          ],
        });

        mockQueryOne.mockResolvedValue(confirmedOrder);
        mockQuery.mockResolvedValue(confirmedOrder.lines);

        // Verify order is in confirmed status, ready for receipt
        const order = await purchasesService.findById('po-001', tenantId);

        expect(order.status).toBe('confirmed');
        expect(order.lines![0].quantity).toBe(100);
        expect(order.lines![0].qty_received).toBe(0);
      });

      it('should update order status to partial after partial receipt', async () => {
        // After receiving 60 of 100 units
        const partialReceivedOrder = createMockPurchaseOrder({
          id: 'po-001',
          status: 'partial',
          receipt_status: 'partial',
          lines: [
            createMockPurchaseOrderLine({
              id: 'line-1',
              quantity: 100,
              qty_received: 60,
              price_unit: 50,
              status: 'partial',
            }),
          ],
        });

        mockQueryOne.mockResolvedValue(partialReceivedOrder);
        mockQuery.mockResolvedValue(partialReceivedOrder.lines);

        const order = await purchasesService.findById('po-001', tenantId);

        expect(order.status).toBe('partial');
        expect(order.receipt_status).toBe('partial');
        expect(order.lines![0].qty_received).toBe(60);
      });

      it('should complete receipt when all quantities received', async () => {
        // After receiving remaining 40 units (total 100)
        const fullyReceivedOrder = createMockPurchaseOrder({
          id: 'po-001',
          status: 'received',
          receipt_status: 'received',
          lines: [
            createMockPurchaseOrderLine({
              id: 'line-1',
              quantity: 100,
              qty_received: 100,
              price_unit: 50,
              status: 'received',
            }),
          ],
        });

        mockQueryOne.mockResolvedValue(fullyReceivedOrder);
        mockQuery.mockResolvedValue(fullyReceivedOrder.lines);

        const order = await purchasesService.findById('po-001', tenantId);

        expect(order.receipt_status).toBe('received');
        expect(order.lines![0].qty_received).toBe(order.lines![0].quantity);
      });
    });
  });

  // ==================== Flow 3: Receipt -> Supplier Invoice ====================
  describe('Flow 3: Receipt to Supplier Invoice', () => {
    describe('create supplier invoice from received order', () => {
      it('should validate order state allows invoicing', () => {
        // Valid states for invoicing
        const validStates = ['confirmed', 'done'];
        const invalidStates = ['draft', 'cancelled'];

        expect(validStates.includes('confirmed')).toBe(true);
        expect(validStates.includes('done')).toBe(true);
        expect(validStates.includes('draft')).toBe(false);
        expect(invalidStates.includes('draft')).toBe(true);
      });

      it('should calculate quantities to invoice based on received minus invoiced', () => {
        const line = {
          quantity: 100,
          qty_received: 100,
          qty_invoiced: 0,
          price_unit: 50,
        };

        const qtyToInvoice = line.qty_received - line.qty_invoiced;
        const invoiceAmount = qtyToInvoice * line.price_unit;

        expect(qtyToInvoice).toBe(100);
        expect(invoiceAmount).toBe(5000);
      });

      it('should throw error when trying to invoice draft order', async () => {
        const draftOrder = createMockPurchaseOrder({
          id: 'po-001',
          status: 'draft',
        });

        mockQueryOne.mockResolvedValue(draftOrder);
        mockQuery.mockResolvedValue([]);

        await expect(
          purchasesService.createSupplierInvoice('po-001', tenantId, userId)
        ).rejects.toThrow(ValidationError);
      });

      it('should throw error when order already fully invoiced', async () => {
        const fullyInvoicedOrder = createMockPurchaseOrder({
          id: 'po-001',
          status: 'done',
          invoice_status: 'invoiced',
        });

        mockQueryOne.mockResolvedValue(fullyInvoicedOrder);
        mockQuery.mockResolvedValue([]);

        await expect(
          purchasesService.createSupplierInvoice('po-001', tenantId, userId)
        ).rejects.toThrow(ValidationError);
      });

      it('should throw error when no received quantities to invoice', async () => {
        const confirmedOrder = createMockPurchaseOrder({
          id: 'po-001',
          status: 'confirmed',
          invoice_status: 'pending',
          lines: [
            createMockPurchaseOrderLine({
              id: 'line-1',
              quantity: 100,
              qty_received: 0,
              qty_invoiced: 0,
            }),
          ],
        });

        mockQueryOne.mockResolvedValue(confirmedOrder);
        mockQuery.mockResolvedValue(confirmedOrder.lines);

        await expect(
          purchasesService.createSupplierInvoice('po-001', tenantId, userId)
        ).rejects.toThrow(ValidationError);
      });

      it('should calculate partial invoice amount correctly', () => {
        // Received 100, already invoiced 50, should invoice remaining 50
        const line = {
          quantity: 100,
          qty_received: 100,
          qty_invoiced: 50,
          price_unit: 50,
        };

        const qtyToInvoice = line.qty_received - line.qty_invoiced;
        const invoiceAmount = qtyToInvoice * line.price_unit;

        expect(qtyToInvoice).toBe(50);
        expect(invoiceAmount).toBe(2500);
      });

      it('should determine invoice status based on invoiced vs received', () => {
        const determineInvoiceStatus = (lines: Array<{ qty_invoiced: number; quantity: number }>) => {
          const totalQty = lines.reduce((sum, l) => sum + l.quantity, 0);
          const totalInvoiced = lines.reduce((sum, l) => sum + l.qty_invoiced, 0);

          if (totalInvoiced >= totalQty) return 'invoiced';
          if (totalInvoiced > 0) return 'partial';
          return 'pending';
        };

        expect(determineInvoiceStatus([{ quantity: 100, qty_invoiced: 100 }])).toBe('invoiced');
        expect(determineInvoiceStatus([{ quantity: 100, qty_invoiced: 50 }])).toBe('partial');
        expect(determineInvoiceStatus([{ quantity: 100, qty_invoiced: 0 }])).toBe('pending');
      });
    });
  });

  // ==================== Flow 4: Supplier Returns ====================
  describe('Flow 4: Supplier Returns', () => {
    describe('handling supplier returns', () => {
      it('should track returned quantities in order lines', async () => {
        // Order received 100 units, returning 10
        const orderWithReturns = createMockPurchaseOrder({
          id: 'po-001',
          status: 'received',
          lines: [
            createMockPurchaseOrderLine({
              id: 'line-1',
              quantity: 100,
              qty_received: 100,
              qty_invoiced: 100,
            }),
          ],
        });

        mockQueryOne.mockResolvedValue(orderWithReturns);
        mockQuery.mockResolvedValue(orderWithReturns.lines);

        const order = await purchasesService.findById('po-001', tenantId);

        expect(order.status).toBe('received');
        expect(order.lines![0].qty_received).toBe(100);
      });

      it('should not allow returns exceeding received quantities', async () => {
        // This is a validation scenario - return qty should not exceed received qty
        const orderLine = createMockPurchaseOrderLine({
          quantity: 100,
          qty_received: 50,
        });

        // Attempting to return more than received
        const returnQty = 60;
        const maxReturnQty = orderLine.qty_received;

        expect(returnQty).toBeGreaterThan(maxReturnQty);
        // Business rule: cannot return more than received
      });
    });
  });

  // ==================== Flow 5: Cost Control and Variances ====================
  describe('Flow 5: Cost Control and Variance Tracking', () => {
    describe('price and quantity variance detection', () => {
      it('should detect price variance between PO and invoice', async () => {
        const orderedPrice = 50.00;
        const invoicedPrice = 55.00;
        const variancePercent = ((invoicedPrice - orderedPrice) / orderedPrice) * 100;

        expect(variancePercent).toBe(10);
        expect(variancePercent).toBeGreaterThan(2); // Exceeds typical 2% tolerance
      });

      it('should detect quantity variance (over-receipt)', async () => {
        const orderedQty = 100;
        const receivedQty = 105;
        const variancePercent = ((receivedQty - orderedQty) / orderedQty) * 100;

        expect(variancePercent).toBe(5);
        expect(variancePercent).toBeGreaterThan(0.5); // Exceeds typical 0.5% tolerance
      });

      it('should detect quantity variance (short receipt)', async () => {
        const orderedQty = 100;
        const receivedQty = 95;
        const variancePercent = ((orderedQty - receivedQty) / orderedQty) * 100;

        expect(variancePercent).toBe(5);
        expect(variancePercent).toBeGreaterThan(0.5); // Exceeds tolerance
      });

      it('should calculate total order variance', async () => {
        const orderLine = {
          quantity: 100,
          price_unit: 50,
          amount_untaxed: 5000,
        };

        const receivedLine = {
          quantity: 95,
          price_invoiced: 52,
          amount_invoiced: 95 * 52, // 4940
        };

        const expectedTotal = orderLine.amount_untaxed;
        const actualTotal = receivedLine.amount_invoiced;
        const variance = expectedTotal - actualTotal;

        expect(variance).toBe(60); // $5000 - $4940 = $60
      });

      it('should apply tolerance rules for matching', async () => {
        const QUANTITY_TOLERANCE = 0.5; // 0.5%
        const PRICE_TOLERANCE = 2.0; // 2%

        // Within tolerance
        const qtyVariance1 = 0.4; // 0.4% - within tolerance
        expect(qtyVariance1).toBeLessThanOrEqual(QUANTITY_TOLERANCE);

        // Outside tolerance
        const qtyVariance2 = 1.0; // 1% - outside tolerance
        expect(qtyVariance2).toBeGreaterThan(QUANTITY_TOLERANCE);

        // Price within tolerance
        const priceVariance1 = 1.5; // 1.5% - within tolerance
        expect(priceVariance1).toBeLessThanOrEqual(PRICE_TOLERANCE);

        // Price outside tolerance
        const priceVariance2 = 3.0; // 3% - outside tolerance
        expect(priceVariance2).toBeGreaterThan(PRICE_TOLERANCE);
      });
    });

    describe('three-way matching validation', () => {
      it('should validate PO -> Receipt -> Invoice matching', async () => {
        // PO values
        const poQty = 100;
        const poPrice = 50.00;
        const poTotal = poQty * poPrice;

        // Receipt values
        const receiptQty = 100;

        // Invoice values
        const invoiceQty = 100;
        const invoicePrice = 50.00;
        const invoiceTotal = invoiceQty * invoicePrice;

        // Matching validation
        const qtyMatch = receiptQty === poQty && invoiceQty === receiptQty;
        const priceMatch = invoicePrice === poPrice;
        const totalMatch = invoiceTotal === poTotal;

        expect(qtyMatch).toBe(true);
        expect(priceMatch).toBe(true);
        expect(totalMatch).toBe(true);
      });

      it('should flag mismatches for review', async () => {
        // PO values
        const poQty = 100;
        const poPrice = 50.00;

        // Receipt values - short receipt
        const receiptQty = 95;

        // Invoice values - price increase
        const invoiceQty = 95;
        const invoicePrice = 52.00;

        // Matching validation
        const qtyMismatch = receiptQty !== poQty;
        const priceMismatch = invoicePrice !== poPrice;

        expect(qtyMismatch).toBe(true);
        expect(priceMismatch).toBe(true);

        // Calculate variances for reporting
        const qtyVariance = poQty - receiptQty;
        const priceVariance = invoicePrice - poPrice;

        expect(qtyVariance).toBe(5);
        expect(priceVariance).toBe(2);
      });
    });
  });

  // ==================== Error Handling ====================
  describe('Error Handling', () => {
    it('should handle order not found', async () => {
      mockQueryOne.mockResolvedValue(null);

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

    it('should handle database transaction rollback', async () => {
      const poData = {
        company_id: companyId,
        partner_id: supplierId,
        currency_id: currencyId,
        lines: [
          {
            product_id: productId,
            description: 'Test',
            quantity: 10,
            uom_id: uomId,
            price_unit: 100,
            amount_untaxed: 1000,
          },
        ],
      };

      const dbError = new Error('DB constraint violation');
      mockClient.query
        .mockResolvedValueOnce(undefined) // BEGIN
        .mockRejectedValueOnce(dbError); // INSERT fails

      await expect(purchasesService.create(poData, tenantId, userId)).rejects.toThrow();

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

    it('should prevent operations on cancelled orders', async () => {
      const cancelledOrder = createMockPurchaseOrder({
        id: 'po-001',
        status: 'cancelled',
      });

      mockQueryOne.mockResolvedValue(cancelledOrder);
      mockQuery.mockResolvedValue([]);

      // Cannot update cancelled order
      await expect(
        purchasesService.update('po-001', { notes: 'test' }, tenantId, userId)
      ).rejects.toThrow(ConflictError);

      // Cannot cancel already cancelled order
      await expect(purchasesService.cancel('po-001', tenantId, userId)).rejects.toThrow(
        ConflictError
      );
    });

    it('should prevent deletion of confirmed orders', async () => {
      const confirmedOrder = createMockPurchaseOrder({
        id: 'po-001',
        status: 'confirmed',
      });

      mockQueryOne.mockResolvedValue(confirmedOrder);
      mockQuery.mockResolvedValue([]);

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

  // ==================== Multi-line Order Tests ====================
  describe('Multi-line Order Processing', () => {
    it('should handle orders with multiple products', async () => {
      const multiLineOrder = createMockPurchaseOrder({
        id: 'po-001',
        status: 'draft',
        amount_untaxed: 10000,
        amount_total: 10000,
        lines: [
          createMockPurchaseOrderLine({
            id: 'line-1',
            product_id: 'product-1',
            quantity: 100,
            price_unit: 50,
            amount_untaxed: 5000,
          }),
          createMockPurchaseOrderLine({
            id: 'line-2',
            product_id: 'product-2',
            quantity: 50,
            price_unit: 100,
            amount_untaxed: 5000,
          }),
        ],
      });

      mockQueryOne.mockResolvedValue(multiLineOrder);
      mockQuery.mockResolvedValue(multiLineOrder.lines);

      const order = await purchasesService.findById('po-001', tenantId);

      expect(order.lines).toHaveLength(2);
      expect(order.amount_untaxed).toBe(10000);
    });

    it('should handle partial receipt of some lines', async () => {
      const partiallyReceivedOrder = createMockPurchaseOrder({
        id: 'po-001',
        status: 'partial',
        receipt_status: 'partial',
        lines: [
          createMockPurchaseOrderLine({
            id: 'line-1',
            quantity: 100,
            qty_received: 100, // Fully received
            status: 'received',
          }),
          createMockPurchaseOrderLine({
            id: 'line-2',
            quantity: 50,
            qty_received: 25, // Partially received
            status: 'partial',
          }),
        ],
      });

      mockQueryOne.mockResolvedValue(partiallyReceivedOrder);
      mockQuery.mockResolvedValue(partiallyReceivedOrder.lines);

      const order = await purchasesService.findById('po-001', tenantId);

      expect(order.status).toBe('partial');
      expect(order.lines![0].status).toBe('received');
      expect(order.lines![1].status).toBe('partial');
    });
  });
});
