import { jest, describe, it, expect, beforeEach } from '@jest/globals';
import { createMockLead } 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 { leadsService } from '../leads.service.js';
import { NotFoundError, ValidationError, ConflictError } from '../../../shared/errors/index.js';

describe('LeadsService', () => {
  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 leads with pagination', async () => {
      const mockLeads = [
        createMockLead({ id: '1', name: 'Lead 1' }),
        createMockLead({ id: '2', name: 'Lead 2' }),
      ];

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

      const result = await leadsService.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 leadsService.findAll(tenantId, { status: 'new' });

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

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

      await leadsService.findAll(tenantId, { stage_id: 'stage-uuid' });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('l.stage_id = $'),
        expect.arrayContaining([tenantId, 'stage-uuid'])
      );
    });

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

      await leadsService.findAll(tenantId, { source: 'website' });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('l.source = $'),
        expect.arrayContaining([tenantId, 'website'])
      );
    });

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

      await leadsService.findAll(tenantId, { search: 'John' });

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

  describe('findById', () => {
    it('should return lead when found', async () => {
      const mockLead = createMockLead();
      mockQueryOne.mockResolvedValue(mockLead);

      const result = await leadsService.findById('lead-uuid-1', tenantId);

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

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

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

  describe('create', () => {
    const createDto = {
      company_id: 'company-uuid',
      name: 'New Lead',
      contact_name: 'Jane Doe',
      email: 'jane@test.com',
    };

    it('should create lead successfully', async () => {
      const createdLead = createMockLead({ ...createDto });
      mockQueryOne
        .mockResolvedValueOnce(createdLead) // INSERT
        .mockResolvedValueOnce(createdLead); // findById

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

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

  describe('update', () => {
    it('should update lead successfully', async () => {
      const existingLead = createMockLead({ status: 'new' });
      mockQueryOne.mockResolvedValue(existingLead);
      mockQuery.mockResolvedValue([]);

      await leadsService.update(
        'lead-uuid-1',
        { name: 'Updated Lead' },
        tenantId,
        userId
      );

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

    it('should throw ValidationError when lead is converted', async () => {
      const convertedLead = createMockLead({ status: 'converted' });
      mockQueryOne.mockResolvedValue(convertedLead);

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

    it('should throw ValidationError when lead is lost', async () => {
      const lostLead = createMockLead({ status: 'lost' });
      mockQueryOne.mockResolvedValue(lostLead);

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

  describe('moveStage', () => {
    it('should move lead to new stage', async () => {
      const lead = createMockLead({ status: 'new' });
      mockQueryOne.mockResolvedValue(lead);
      mockQuery.mockResolvedValue([]);

      await leadsService.moveStage('lead-uuid-1', 'new-stage-uuid', tenantId, userId);

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

    it('should throw ValidationError when lead is converted', async () => {
      const convertedLead = createMockLead({ status: 'converted' });
      mockQueryOne.mockResolvedValue(convertedLead);

      await expect(
        leadsService.moveStage('lead-uuid-1', 'new-stage-uuid', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('convert', () => {
    it('should convert lead to opportunity', async () => {
      const lead = createMockLead({ status: 'qualified', email: 'test@example.com' });

      mockQueryOne.mockResolvedValue(lead);
      mockClient.query
        .mockResolvedValueOnce(undefined) // BEGIN
        .mockResolvedValueOnce({ rows: [] }) // existing partner check
        .mockResolvedValueOnce({ rows: [{ id: 'new-partner-uuid' }] }) // create partner
        .mockResolvedValueOnce({ rows: [{ id: 'stage-uuid' }] }) // get default stage
        .mockResolvedValueOnce({ rows: [{ id: 'opportunity-uuid' }] }) // create opportunity
        .mockResolvedValueOnce(undefined) // update lead
        .mockResolvedValueOnce(undefined); // COMMIT

      const result = await leadsService.convert('lead-uuid-1', tenantId, userId);

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

    it('should throw ValidationError when lead is already converted', async () => {
      const convertedLead = createMockLead({ status: 'converted' });
      mockQueryOne.mockResolvedValue(convertedLead);

      await expect(
        leadsService.convert('lead-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError when lead is lost', async () => {
      const lostLead = createMockLead({ status: 'lost' });
      mockQueryOne.mockResolvedValue(lostLead);

      await expect(
        leadsService.convert('lead-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should rollback on error', async () => {
      const lead = createMockLead({ status: 'qualified', email: 'test@example.com' });
      mockQueryOne.mockResolvedValue(lead);
      mockClient.query
        .mockResolvedValueOnce(undefined) // BEGIN
        .mockRejectedValueOnce(new Error('DB Error'));

      await expect(
        leadsService.convert('lead-uuid-1', tenantId, userId)
      ).rejects.toThrow('DB Error');

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

  describe('markLost', () => {
    it('should mark lead as lost', async () => {
      const lead = createMockLead({ status: 'qualified' });
      mockQueryOne.mockResolvedValue(lead);
      mockQuery.mockResolvedValue([]);

      await leadsService.markLost('lead-uuid-1', 'reason-uuid', 'Too expensive', tenantId, userId);

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

    it('should throw ValidationError when lead is converted', async () => {
      const convertedLead = createMockLead({ status: 'converted' });
      mockQueryOne.mockResolvedValue(convertedLead);

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

    it('should throw ValidationError when lead is already lost', async () => {
      const lostLead = createMockLead({ status: 'lost' });
      mockQueryOne.mockResolvedValue(lostLead);

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

  describe('delete', () => {
    it('should delete lead without opportunity', async () => {
      const lead = createMockLead({ opportunity_id: null });
      mockQueryOne.mockResolvedValue(lead);
      mockQuery.mockResolvedValue([]);

      await leadsService.delete('lead-uuid-1', tenantId);

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

    it('should throw ConflictError when lead has opportunity', async () => {
      const lead = createMockLead({ opportunity_id: 'opportunity-uuid' });
      mockQueryOne.mockResolvedValue(lead);

      await expect(
        leadsService.delete('lead-uuid-1', tenantId)
      ).rejects.toThrow(ConflictError);
    });
  });
});
