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

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

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

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

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

  describe('findAll', () => {
    it('should return RFQs with pagination', async () => {
      const mockRfqs = [
        createMockRfq({ id: '1', name: 'RFQ-000001' }),
        createMockRfq({ id: '2', name: 'RFQ-000002' }),
      ];

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

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

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

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

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

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

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

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

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

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

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

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

  describe('findById', () => {
    it('should return RFQ with lines when found', async () => {
      const mockRfq = createMockRfq({ partner_ids: ['partner-1'] });
      const mockPartners = [{ id: 'partner-1', name: 'Partner 1' }];
      const mockLines = [createMockRfqLine()];

      mockQueryOne.mockResolvedValue(mockRfq);
      mockQuery
        .mockResolvedValueOnce(mockPartners) // partner names
        .mockResolvedValueOnce(mockLines); // lines

      const result = await rfqsService.findById('rfq-uuid-1', tenantId);

      expect(result.lines).toEqual(mockLines);
      expect(result.partner_names).toEqual(['Partner 1']);
    });

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

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

  describe('create', () => {
    const createDto = {
      company_id: 'company-uuid',
      partner_ids: ['supplier-uuid-1'],
      lines: [
        {
          product_id: 'product-uuid',
          description: 'Test product',
          quantity: 10,
          uom_id: 'uom-uuid',
        },
      ],
    };

    it('should create RFQ with auto-generated number', async () => {
      const mockRfq = createMockRfq({ name: 'RFQ-000001' });
      const mockLines = [createMockRfqLine()];

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

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

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

      expect(result.name).toBe('RFQ-000001');
      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(
        rfqsService.create(emptyDto, tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

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

      await expect(
        rfqsService.create(noSuppliersDto, tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

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

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

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

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

      await rfqsService.update(
        'rfq-uuid-1',
        { partner_ids: ['new-supplier-uuid'] },
        tenantId,
        userId
      );

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('UPDATE purchase.rfqs SET'),
        expect.any(Array)
      );
    });

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

      await expect(
        rfqsService.update('rfq-uuid-1', { partner_ids: ['new-supplier'] }, tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

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

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

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

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

    it('should add line to draft RFQ', async () => {
      const draftRfq = createMockRfq({ status: 'draft' });
      const newLine = createMockRfqLine();

      mockQueryOne
        .mockResolvedValueOnce(draftRfq) // findById
        .mockResolvedValueOnce(newLine); // INSERT line
      mockQuery.mockResolvedValue([]);

      const result = await rfqsService.addLine('rfq-uuid-1', lineDto, tenantId);

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

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

      await expect(
        rfqsService.addLine('rfq-uuid-1', lineDto, tenantId)
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('updateLine', () => {
    it('should update line in draft RFQ', async () => {
      const existingLine = createMockRfqLine({ id: 'line-uuid' });
      const draftRfq = createMockRfq({ status: 'draft' });
      const updatedLine = { ...existingLine, quantity: 20 };

      // findById: queryOne for rfq, query for partners (if partner_ids has values), query for lines
      // updateLine: queryOne for UPDATE ... RETURNING
      mockQueryOne
        .mockResolvedValueOnce(draftRfq) // findById - get rfq
        .mockResolvedValueOnce(updatedLine); // UPDATE line RETURNING
      mockQuery
        .mockResolvedValueOnce([{ id: 'supplier-uuid-1', name: 'Test Supplier' }]) // findById - get partners
        .mockResolvedValueOnce([existingLine]); // findById - get lines (must return existing line)

      const result = await rfqsService.updateLine('rfq-uuid-1', 'line-uuid', { quantity: 20 }, tenantId);

      expect(result.quantity).toBe(20);
    });

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

      await expect(
        rfqsService.updateLine('rfq-uuid-1', 'line-uuid', { quantity: 20 }, tenantId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw NotFoundError when line not found', async () => {
      const draftRfq = createMockRfq({ status: 'draft', lines: [] });
      mockQueryOne.mockResolvedValue(draftRfq);
      mockQuery.mockResolvedValue([]);

      await expect(
        rfqsService.updateLine('rfq-uuid-1', 'nonexistent-line', { quantity: 20 }, tenantId)
      ).rejects.toThrow(NotFoundError);
    });
  });

  describe('removeLine', () => {
    it('should remove line from draft RFQ', async () => {
      const line1 = createMockRfqLine({ id: 'line-1' });
      const line2 = createMockRfqLine({ id: 'line-2' });
      const draftRfq = createMockRfq({ status: 'draft', lines: [line1, line2] });

      mockQueryOne.mockResolvedValue(draftRfq);
      mockQuery.mockResolvedValue([line1, line2]);

      await rfqsService.removeLine('rfq-uuid-1', 'line-1', tenantId);

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

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

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

    it('should throw NotFoundError when line not found', async () => {
      const draftRfq = createMockRfq({ status: 'draft', lines: [] });
      mockQueryOne.mockResolvedValue(draftRfq);
      mockQuery.mockResolvedValue([]);

      await expect(
        rfqsService.removeLine('rfq-uuid-1', 'nonexistent-line', tenantId)
      ).rejects.toThrow(NotFoundError);
    });

    it('should throw ValidationError when trying to remove the last line', async () => {
      const line = createMockRfqLine({ id: 'line-uuid' });
      const draftRfq = createMockRfq({ status: 'draft', lines: [line] });

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

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

  describe('send', () => {
    it('should send draft RFQ with lines', async () => {
      const draftRfq = createMockRfq({
        status: 'draft',
        lines: [createMockRfqLine()],
      });
      mockQueryOne.mockResolvedValue(draftRfq);
      mockQuery.mockResolvedValue([createMockRfqLine()]);

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

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

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

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

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

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

  describe('markResponded', () => {
    it('should mark sent RFQ as responded', async () => {
      const sentRfq = createMockRfq({ status: 'sent' });
      mockQueryOne.mockResolvedValue(sentRfq);
      mockQuery.mockResolvedValue([]);

      await rfqsService.markResponded('rfq-uuid-1', tenantId, userId);

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

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

      await expect(
        rfqsService.markResponded('rfq-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('accept', () => {
    it('should accept sent RFQ', async () => {
      const sentRfq = createMockRfq({ status: 'sent' });
      mockQueryOne.mockResolvedValue(sentRfq);
      mockQuery.mockResolvedValue([]);

      await rfqsService.accept('rfq-uuid-1', tenantId, userId);

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

    it('should accept responded RFQ', async () => {
      const respondedRfq = createMockRfq({ status: 'responded' });
      mockQueryOne.mockResolvedValue(respondedRfq);
      mockQuery.mockResolvedValue([]);

      await rfqsService.accept('rfq-uuid-1', tenantId, userId);

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

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

      await expect(
        rfqsService.accept('rfq-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('reject', () => {
    it('should reject sent RFQ', async () => {
      const sentRfq = createMockRfq({ status: 'sent' });
      mockQueryOne.mockResolvedValue(sentRfq);
      mockQuery.mockResolvedValue([]);

      await rfqsService.reject('rfq-uuid-1', tenantId, userId);

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

    it('should reject responded RFQ', async () => {
      const respondedRfq = createMockRfq({ status: 'responded' });
      mockQueryOne.mockResolvedValue(respondedRfq);
      mockQuery.mockResolvedValue([]);

      await rfqsService.reject('rfq-uuid-1', tenantId, userId);

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

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

      await expect(
        rfqsService.reject('rfq-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });
  });

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

      await rfqsService.cancel('rfq-uuid-1', tenantId, userId);

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

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

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

    it('should throw ValidationError when RFQ is accepted', async () => {
      const acceptedRfq = createMockRfq({ status: 'accepted' });
      mockQueryOne.mockResolvedValue(acceptedRfq);
      mockQuery.mockResolvedValue([]);

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

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

      await rfqsService.delete('rfq-uuid-1', tenantId);

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

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

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