import { jest, describe, it, expect, beforeEach } from '@jest/globals';
import { createMockOpportunity, createMockStage } 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 { opportunitiesService } from '../opportunities.service.js';
import { NotFoundError, ValidationError } from '../../../shared/errors/index.js';

describe('OpportunitiesService', () => {
  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 opportunities with pagination', async () => {
      const mockOpportunities = [
        createMockOpportunity({ id: '1', name: 'Opp 1' }),
        createMockOpportunity({ id: '2', name: 'Opp 2' }),
      ];

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

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

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

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

      await opportunitiesService.findAll(tenantId, { status: 'open' });

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

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

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

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

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

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

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

  describe('findById', () => {
    it('should return opportunity when found', async () => {
      const mockOpp = createMockOpportunity();
      mockQueryOne.mockResolvedValue(mockOpp);

      const result = await opportunitiesService.findById('opp-uuid-1', tenantId);

      expect(result).toEqual(mockOpp);
    });

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

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

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

    it('should create opportunity successfully', async () => {
      const createdOpp = createMockOpportunity({ ...createDto });
      mockQueryOne
        .mockResolvedValueOnce(createdOpp) // INSERT
        .mockResolvedValueOnce(createdOpp); // findById

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

      expect(result.name).toBe(createDto.name);
    });
  });

  describe('update', () => {
    it('should update opportunity successfully', async () => {
      const existingOpp = createMockOpportunity({ status: 'open' });
      mockQueryOne.mockResolvedValue(existingOpp);
      mockQuery.mockResolvedValue([]);

      await opportunitiesService.update(
        'opp-uuid-1',
        { name: 'Updated Opportunity' },
        tenantId,
        userId
      );

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('UPDATE crm.opportunities SET'),
        expect.any(Array)
      );
    });

    it('should throw ValidationError when opportunity is not open', async () => {
      const wonOpp = createMockOpportunity({ status: 'won' });
      mockQueryOne.mockResolvedValue(wonOpp);

      await expect(
        opportunitiesService.update('opp-uuid-1', { name: 'Test' }, tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('moveStage', () => {
    it('should move opportunity to new stage', async () => {
      const opp = createMockOpportunity({ status: 'open' });
      const stage = createMockStage({ id: 'new-stage-uuid', probability: 50 });

      mockQueryOne
        .mockResolvedValueOnce(opp) // findById
        .mockResolvedValueOnce(stage); // get stage
      mockQuery.mockResolvedValue([]);

      await opportunitiesService.moveStage('opp-uuid-1', 'new-stage-uuid', tenantId, userId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('stage_id = $1'),
        expect.any(Array)
      );
    });

    it('should throw ValidationError when opportunity is not open', async () => {
      const wonOpp = createMockOpportunity({ status: 'won' });
      mockQueryOne.mockResolvedValue(wonOpp);

      await expect(
        opportunitiesService.moveStage('opp-uuid-1', 'stage-uuid', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw NotFoundError when stage not found', async () => {
      const opp = createMockOpportunity({ status: 'open' });
      mockQueryOne
        .mockResolvedValueOnce(opp) // findById
        .mockResolvedValueOnce(null); // stage not found

      await expect(
        opportunitiesService.moveStage('opp-uuid-1', 'nonexistent-stage', tenantId, userId)
      ).rejects.toThrow(NotFoundError);
    });
  });

  describe('markWon', () => {
    it('should mark opportunity as won', async () => {
      const opp = createMockOpportunity({ status: 'open' });
      mockQueryOne.mockResolvedValue(opp);
      mockQuery.mockResolvedValue([]);

      await opportunitiesService.markWon('opp-uuid-1', tenantId, userId);

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

    it('should throw ValidationError when opportunity is not open', async () => {
      const lostOpp = createMockOpportunity({ status: 'lost' });
      mockQueryOne.mockResolvedValue(lostOpp);

      await expect(
        opportunitiesService.markWon('opp-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('markLost', () => {
    it('should mark opportunity as lost', async () => {
      const opp = createMockOpportunity({ status: 'open' });
      mockQueryOne.mockResolvedValue(opp);
      mockQuery.mockResolvedValue([]);

      await opportunitiesService.markLost('opp-uuid-1', 'reason-uuid', 'Notes', tenantId, userId);

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

    it('should throw ValidationError when opportunity is not open', async () => {
      const wonOpp = createMockOpportunity({ status: 'won' });
      mockQueryOne.mockResolvedValue(wonOpp);

      await expect(
        opportunitiesService.markLost('opp-uuid-1', 'reason-uuid', 'Notes', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('createQuotation', () => {
    it('should create quotation from opportunity', async () => {
      const opp = createMockOpportunity({ status: 'open', quotation_id: null });

      mockQueryOne.mockResolvedValue(opp);
      mockClient.query
        .mockResolvedValueOnce(undefined) // BEGIN
        .mockResolvedValueOnce({ rows: [{ next_num: 1 }] }) // sequence
        .mockResolvedValueOnce({ rows: [{ id: 'currency-uuid' }] }) // currency
        .mockResolvedValueOnce({ rows: [{ id: 'quotation-uuid' }] }) // create quotation
        .mockResolvedValueOnce(undefined) // update opportunity
        .mockResolvedValueOnce(undefined); // COMMIT

      const result = await opportunitiesService.createQuotation('opp-uuid-1', tenantId, userId);

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

    it('should throw ValidationError when opportunity is not open', async () => {
      const wonOpp = createMockOpportunity({ status: 'won' });
      mockQueryOne.mockResolvedValue(wonOpp);

      await expect(
        opportunitiesService.createQuotation('opp-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError when quotation already exists', async () => {
      const opp = createMockOpportunity({ status: 'open', quotation_id: 'existing-quotation' });
      mockQueryOne.mockResolvedValue(opp);

      await expect(
        opportunitiesService.createQuotation('opp-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should rollback on error', async () => {
      const opp = createMockOpportunity({ status: 'open', quotation_id: null });
      mockQueryOne.mockResolvedValue(opp);
      mockClient.query
        .mockResolvedValueOnce(undefined) // BEGIN
        .mockRejectedValueOnce(new Error('DB Error'));

      await expect(
        opportunitiesService.createQuotation('opp-uuid-1', tenantId, userId)
      ).rejects.toThrow('DB Error');

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

  describe('delete', () => {
    it('should delete opportunity without quotation or order', async () => {
      const opp = createMockOpportunity({ quotation_id: null, order_id: null, lead_id: null });
      mockQueryOne.mockResolvedValue(opp);
      mockQuery.mockResolvedValue([]);

      await opportunitiesService.delete('opp-uuid-1', tenantId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('DELETE FROM crm.opportunities'),
        expect.any(Array)
      );
    });

    it('should throw ValidationError when has quotation', async () => {
      const opp = createMockOpportunity({ quotation_id: 'quotation-uuid' });
      mockQueryOne.mockResolvedValue(opp);

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

    it('should throw ValidationError when has order', async () => {
      const opp = createMockOpportunity({ order_id: 'order-uuid' });
      mockQueryOne.mockResolvedValue(opp);

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

    it('should update lead when deleting opportunity with lead', async () => {
      const opp = createMockOpportunity({ quotation_id: null, order_id: null, lead_id: 'lead-uuid' });
      mockQueryOne.mockResolvedValue(opp);
      mockQuery.mockResolvedValue([]);

      await opportunitiesService.delete('opp-uuid-1', tenantId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('UPDATE crm.leads SET opportunity_id = NULL'),
        expect.any(Array)
      );
    });
  });

  describe('getPipeline', () => {
    it('should return pipeline with stages and opportunities', async () => {
      const mockStages = [
        createMockStage({ id: '1', name: 'Qualification', sequence: 1 }),
        createMockStage({ id: '2', name: 'Proposal', sequence: 2 }),
      ];

      const mockOpps = [
        createMockOpportunity({ id: '1', stage_id: '1', expected_revenue: 5000 }),
        createMockOpportunity({ id: '2', stage_id: '2', expected_revenue: 10000 }),
      ];

      mockQuery
        .mockResolvedValueOnce(mockStages) // stages
        .mockResolvedValueOnce(mockOpps); // opportunities

      const result = await opportunitiesService.getPipeline(tenantId);

      expect(result.stages).toHaveLength(2);
      expect(result.totals.total_opportunities).toBe(2);
    });
  });
});
