import { jest, describe, it, expect, beforeEach } from '@jest/globals';
import { createMockQuotation, createMockQuotationLine } 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,
    })),
  },
}));

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

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

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

  describe('findAll', () => {
    it('should return quotations with pagination', async () => {
      const mockQuotations = [
        createMockQuotation({ id: '1', name: 'QUO-000001' }),
        createMockQuotation({ id: '2', name: 'QUO-000002' }),
      ];

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

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

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

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

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

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

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

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

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

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

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

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

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

      await quotationsService.findAll(tenantId, { search: 'Test' });

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

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

      await quotationsService.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 quotation with lines when found', async () => {
      const mockQuotation = createMockQuotation();
      const mockLines = [createMockQuotationLine()];

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

      const result = await quotationsService.findById('quotation-uuid-1', tenantId);

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

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

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

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

    it('should create quotation with auto-generated number', async () => {
      mockQueryOne
        .mockResolvedValueOnce({ next_num: 1 }) // sequence
        .mockResolvedValueOnce(createMockQuotation({ name: 'QUO-000001' })); // INSERT

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

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

    it('should use provided quotation_date', async () => {
      mockQueryOne
        .mockResolvedValueOnce({ next_num: 2 })
        .mockResolvedValueOnce(createMockQuotation());

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

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

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

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

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

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

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

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

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

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

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

      await quotationsService.delete('quotation-uuid-1', tenantId);

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

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

      await expect(
        quotationsService.delete('quotation-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 quotation', async () => {
      const draftQuotation = createMockQuotation({ status: 'draft' });
      const newLine = createMockQuotationLine();

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

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

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

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

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

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

      await quotationsService.removeLine('quotation-uuid-1', 'line-uuid', tenantId);

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

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

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

  describe('send', () => {
    it('should send draft quotation with lines', async () => {
      const draftQuotation = createMockQuotation({ status: 'draft' });
      const mockLines = [createMockQuotationLine()];

      // findById: queryOne for quotation, query for lines
      // send: query for UPDATE
      mockQueryOne.mockResolvedValue(draftQuotation);
      mockQuery
        .mockResolvedValueOnce(mockLines) // findById - get lines
        .mockResolvedValueOnce([]); // UPDATE status

      await quotationsService.send('quotation-uuid-1', tenantId, userId);

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

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

      await expect(
        quotationsService.send('quotation-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

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

      await expect(
        quotationsService.send('quotation-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });
  });

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

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

    it('should confirm quotation and create sales order', async () => {
      const quotation = createMockQuotation({
        status: 'draft',
        lines: [createMockQuotationLine()],
      });

      mockQueryOne.mockResolvedValue(quotation);
      mockQuery.mockResolvedValue([createMockQuotationLine()]);
      mockClient.query
        .mockResolvedValueOnce(undefined) // BEGIN
        .mockResolvedValueOnce({ rows: [{ next_num: 1 }] }) // sequence
        .mockResolvedValueOnce({ rows: [{ id: 'order-uuid' }] }) // INSERT order
        .mockResolvedValueOnce(undefined) // INSERT lines
        .mockResolvedValueOnce(undefined) // UPDATE quotation
        .mockResolvedValueOnce(undefined); // COMMIT

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

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

    it('should throw ValidationError when quotation status is invalid', async () => {
      const cancelledQuotation = createMockQuotation({ status: 'cancelled' });
      mockQueryOne.mockResolvedValue(cancelledQuotation);
      mockQuery.mockResolvedValue([]);

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

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

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

    it('should rollback on error', async () => {
      const quotation = createMockQuotation({
        status: 'draft',
        lines: [createMockQuotationLine()],
      });

      mockQueryOne.mockResolvedValue(quotation);
      mockQuery.mockResolvedValue([createMockQuotationLine()]);
      mockClient.query
        .mockResolvedValueOnce(undefined) // BEGIN
        .mockRejectedValueOnce(new Error('DB Error')); // sequence fails

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

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

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

      await quotationsService.cancel('quotation-uuid-1', tenantId, userId);

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

    it('should cancel sent quotation', async () => {
      const sentQuotation = createMockQuotation({ status: 'sent' });
      mockQueryOne.mockResolvedValue(sentQuotation);
      mockQuery.mockResolvedValue([]);

      await quotationsService.cancel('quotation-uuid-1', tenantId, userId);

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

    it('should throw ValidationError when quotation is confirmed', async () => {
      const confirmedQuotation = createMockQuotation({ status: 'confirmed' });
      mockQueryOne.mockResolvedValue(confirmedQuotation);
      mockQuery.mockResolvedValue([]);

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

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

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