import { jest, describe, it, expect, beforeEach } from '@jest/globals';
import { createMockSalesOrder, createMockSalesOrderLine } 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 taxesService
jest.mock('../../financial/taxes.service.js', () => ({
  taxesService: {
    calculateTaxes: jest.fn(() => Promise.resolve({
      amountUntaxed: 1000,
      amountTax: 160,
      amountTotal: 1160,
    })),
  },
}));

// Mock sequencesService
jest.mock('../../core/sequences.service.js', () => ({
  sequencesService: {
    getNextNumber: jest.fn(() => Promise.resolve('SO-000001')),
  },
  SEQUENCE_CODES: {
    SALES_ORDER: 'SO',
    PICKING_OUT: 'WH/OUT',
  },
}));

// Mock stockReservationService
jest.mock('../../inventory/stock-reservation.service.js', () => ({
  stockReservationService: {
    reserveWithClient: jest.fn(() => Promise.resolve({ success: true, errors: [] })),
    releaseWithClient: jest.fn(() => Promise.resolve()),
    checkAvailability: jest.fn(() => Promise.resolve({ available: true, lines: [] })),
  },
}));

// Import after mocking
import { ordersService } from '../orders.service.js';
import { NotFoundError, ValidationError } from '../../../shared/errors/index.js';

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

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

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

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

      const result = await ordersService.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 ordersService.findAll(tenantId, { company_id: 'company-uuid' });

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

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

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

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

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

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

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

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

      await ordersService.findAll(tenantId, { invoice_status: 'pending' });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('so.invoice_status = $'),
        expect.arrayContaining([tenantId, 'pending'])
      );
    });

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

      await ordersService.findAll(tenantId, { delivery_status: 'pending' });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('so.delivery_status = $'),
        expect.arrayContaining([tenantId, 'pending'])
      );
    });

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

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

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('so.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 ordersService.findAll(tenantId, { search: 'Test' });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('so.name ILIKE'),
        expect.arrayContaining([tenantId, '%Test%'])
      );
    });

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

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

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('LIMIT'),
        expect.arrayContaining([10, 20]) // limit=10, offset=20 (page 3)
      );
    });
  });

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

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

      const result = await ordersService.findById('order-uuid-1', tenantId);

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

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

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

  describe('create', () => {
    const createDto = {
      company_id: 'company-uuid',
      partner_id: 'partner-uuid',
      currency_id: 'currency-uuid',
    };

    it('should create order with auto-generated number', async () => {
      mockQueryOne.mockResolvedValue(createMockSalesOrder({ name: 'SO-000001' }));

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

      expect(result.name).toBe('SO-000001');
    });

    it('should use provided order_date', async () => {
      mockQueryOne.mockResolvedValue(createMockSalesOrder());

      await ordersService.create({ ...createDto, order_date: '2024-06-15' }, tenantId, userId);

      expect(mockQueryOne).toHaveBeenCalledWith(
        expect.stringContaining('INSERT INTO sales.sales_orders'),
        expect.arrayContaining(['2024-06-15'])
      );
    });

    it('should set default invoice_policy to order', async () => {
      mockQueryOne.mockResolvedValue(createMockSalesOrder());

      await ordersService.create(createDto, tenantId, userId);

      expect(mockQueryOne).toHaveBeenCalledWith(
        expect.stringContaining('INSERT INTO sales.sales_orders'),
        expect.arrayContaining(['order'])
      );
    });
  });

  describe('update', () => {
    it('should update order in draft status', async () => {
      const existingOrder = createMockSalesOrder({ status: 'draft' });
      mockQueryOne.mockResolvedValue(existingOrder);
      mockQuery.mockResolvedValue([]);

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

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('UPDATE sales.sales_orders SET'),
        expect.any(Array)
      );
    });

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

      await expect(
        ordersService.update('order-uuid-1', { partner_id: 'new-partner' }, tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should return unchanged order when no fields to update', async () => {
      const existingOrder = createMockSalesOrder({ status: 'draft' });
      mockQueryOne.mockResolvedValue(existingOrder);
      mockQuery.mockResolvedValue([]);

      const result = await ordersService.update(
        'order-uuid-1',
        {},
        tenantId,
        userId
      );

      expect(result.id).toBe(existingOrder.id);
    });
  });

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

      await ordersService.delete('order-uuid-1', tenantId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('DELETE FROM sales.sales_orders'),
        expect.any(Array)
      );
    });

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

      await expect(
        ordersService.delete('order-uuid-1', tenantId)
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('addLine', () => {
    const lineDto = {
      product_id: 'product-uuid',
      description: 'Test product',
      quantity: 5,
      uom_id: 'uom-uuid',
      price_unit: 100,
    };

    it('should add line to draft order', async () => {
      const draftOrder = createMockSalesOrder({ status: 'draft' });
      const newLine = createMockSalesOrderLine();

      // findById: queryOne for order, query for lines
      // addLine: queryOne for INSERT, query for updateTotals
      mockQueryOne
        .mockResolvedValueOnce(draftOrder) // findById - get order
        .mockResolvedValueOnce(newLine); // INSERT line
      mockQuery
        .mockResolvedValueOnce([]) // findById - get lines
        .mockResolvedValueOnce([]); // updateTotals

      const result = await ordersService.addLine('order-uuid-1', lineDto, tenantId, userId);

      expect(result.id).toBe(newLine.id);
    });

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

      await expect(
        ordersService.addLine('order-uuid-1', lineDto, tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('removeLine', () => {
    it('should remove line from draft order', async () => {
      const draftOrder = createMockSalesOrder({ status: 'draft' });
      mockQueryOne.mockResolvedValue(draftOrder);
      mockQuery.mockResolvedValue([]);

      await ordersService.removeLine('order-uuid-1', 'line-uuid', tenantId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('DELETE FROM sales.sales_order_lines'),
        expect.any(Array)
      );
    });

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

      await expect(
        ordersService.removeLine('order-uuid-1', 'line-uuid', tenantId)
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('confirm', () => {
    const mockClient = {
      query: jest.fn(),
      release: jest.fn(),
    };

    beforeEach(() => {
      mockGetClient.mockResolvedValue(mockClient);
    });

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

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

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

      const result = await ordersService.confirm('order-uuid-1', tenantId, userId);

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

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

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

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

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

    it('should rollback on error', async () => {
      const order = createMockSalesOrder({
        status: 'draft',
        company_id: 'company-uuid',
        lines: [createMockSalesOrderLine()],
      });

      mockQueryOne.mockResolvedValue(order);
      mockQuery.mockResolvedValue([createMockSalesOrderLine()]);
      mockClient.query
        .mockResolvedValueOnce({ rows: [] }) // BEGIN
        .mockResolvedValueOnce({ rows: [{ location_id: 'stock-loc-uuid', warehouse_id: 'wh-uuid' }] }) // stock location
        .mockResolvedValueOnce({ rows: [{ id: 'customer-loc-uuid' }] }) // customer location
        .mockRejectedValueOnce(new Error('DB Error')); // INSERT picking fails

      await expect(
        ordersService.confirm('order-uuid-1', tenantId, userId)
      ).rejects.toThrow('DB Error');

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

  describe('cancel', () => {
    const mockClientCancel = {
      query: jest.fn(),
      release: jest.fn(),
    };

    beforeEach(() => {
      mockGetClient.mockResolvedValue(mockClientCancel);
    });

    it('should cancel draft order', async () => {
      const draftOrder = createMockSalesOrder({
        status: 'draft',
        delivery_status: 'pending',
        invoice_status: 'pending',
      });
      mockQueryOne
        .mockResolvedValueOnce(draftOrder) // findById
        .mockResolvedValueOnce({ ...draftOrder, status: 'cancelled' }); // findById after cancel
      mockQuery.mockResolvedValue([]);

      // Mock client.query for cancel flow (draft orders don't need stock release)
      mockClientCancel.query
        .mockResolvedValueOnce({ rows: [] }) // BEGIN
        .mockResolvedValueOnce({ rows: [] }) // UPDATE status
        .mockResolvedValueOnce({ rows: [] }); // COMMIT

      await ordersService.cancel('order-uuid-1', tenantId, userId);

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

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

      await expect(
        ordersService.cancel('order-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

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

      await expect(
        ordersService.cancel('order-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError when order has deliveries', async () => {
      const orderWithDeliveries = createMockSalesOrder({
        status: 'sent',
        delivery_status: 'partial',
      });
      mockQueryOne.mockResolvedValue(orderWithDeliveries);
      mockQuery.mockResolvedValue([]);

      await expect(
        ordersService.cancel('order-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError when order has invoices', async () => {
      const orderWithInvoices = createMockSalesOrder({
        status: 'sent',
        invoice_status: 'partial',
      });
      mockQueryOne.mockResolvedValue(orderWithInvoices);
      mockQuery.mockResolvedValue([]);

      await expect(
        ordersService.cancel('order-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('createInvoice', () => {
    const mockClient = {
      query: jest.fn(),
      release: jest.fn(),
    };

    beforeEach(() => {
      mockGetClient.mockResolvedValue(mockClient);
    });

    it('should create invoice from confirmed order', async () => {
      const order = createMockSalesOrder({
        status: 'sent',
        invoice_status: 'pending',
        invoice_policy: 'order',
        lines: [createMockSalesOrderLine({ quantity: 10, qty_invoiced: 0 })],
      });

      mockQueryOne.mockResolvedValue(order);
      mockQuery.mockResolvedValue([createMockSalesOrderLine({ quantity: 10, qty_invoiced: 0 })]);
      mockClient.query
        .mockResolvedValueOnce(undefined) // BEGIN
        .mockResolvedValueOnce({ rows: [{ next_num: 1 }] }) // sequence
        .mockResolvedValueOnce({ rows: [{ id: 'invoice-uuid' }] }) // INSERT invoice
        .mockResolvedValueOnce(undefined) // INSERT line
        .mockResolvedValueOnce(undefined) // UPDATE qty_invoiced
        .mockResolvedValueOnce(undefined) // UPDATE invoice totals
        .mockResolvedValueOnce(undefined) // UPDATE order status
        .mockResolvedValueOnce(undefined); // COMMIT

      const result = await ordersService.createInvoice('order-uuid-1', tenantId, userId);

      expect(result.invoiceId).toBe('invoice-uuid');
      expect(mockClient.query).toHaveBeenCalledWith('BEGIN');
      expect(mockClient.query).toHaveBeenCalledWith('COMMIT');
    });

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

      await expect(
        ordersService.createInvoice('order-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError when order is fully invoiced', async () => {
      const fullyInvoicedOrder = createMockSalesOrder({
        status: 'sent',
        invoice_status: 'invoiced',
      });
      mockQueryOne.mockResolvedValue(fullyInvoicedOrder);
      mockQuery.mockResolvedValue([]);

      await expect(
        ordersService.createInvoice('order-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError when no lines to invoice', async () => {
      const order = createMockSalesOrder({
        status: 'sent',
        invoice_status: 'pending',
        invoice_policy: 'order',
        lines: [createMockSalesOrderLine({ quantity: 10, qty_invoiced: 10 })],
      });

      mockQueryOne.mockResolvedValue(order);
      mockQuery.mockResolvedValue([createMockSalesOrderLine({ quantity: 10, qty_invoiced: 10 })]);

      await expect(
        ordersService.createInvoice('order-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should rollback on error', async () => {
      const order = createMockSalesOrder({
        status: 'sent',
        invoice_status: 'pending',
        invoice_policy: 'order',
        lines: [createMockSalesOrderLine({ quantity: 10, qty_invoiced: 0 })],
      });

      mockQueryOne.mockResolvedValue(order);
      mockQuery.mockResolvedValue([createMockSalesOrderLine({ quantity: 10, qty_invoiced: 0 })]);
      mockClient.query
        .mockResolvedValueOnce(undefined) // BEGIN
        .mockRejectedValueOnce(new Error('DB Error')); // sequence fails

      await expect(
        ordersService.createInvoice('order-uuid-1', tenantId, userId)
      ).rejects.toThrow('DB Error');

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