import { jest, describe, it, expect, beforeEach } from '@jest/globals';
import { createMockStage, createMockLostReason } from '../../../__tests__/helpers.js';

// Mock query functions
const mockQuery = jest.fn();
const mockQueryOne = jest.fn();

jest.mock('../../../config/database.js', () => ({
  query: (...args: any[]) => mockQuery(...args),
  queryOne: (...args: any[]) => mockQueryOne(...args),
}));

// Import after mocking
import { stagesService } from '../stages.service.js';
import { NotFoundError, ConflictError } from '../../../shared/errors/index.js';

describe('StagesService', () => {
  const tenantId = 'test-tenant-uuid';

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

  describe('Lead Stages', () => {
    describe('getLeadStages', () => {
      it('should return active lead stages', async () => {
        const mockStages = [
          createMockStage({ id: '1', name: 'New' }),
          createMockStage({ id: '2', name: 'Qualified' }),
        ];
        mockQuery.mockResolvedValue(mockStages);

        const result = await stagesService.getLeadStages(tenantId);

        expect(result).toHaveLength(2);
        expect(mockQuery).toHaveBeenCalledWith(
          expect.stringContaining('active = TRUE'),
          [tenantId]
        );
      });

      it('should include inactive stages when requested', async () => {
        mockQuery.mockResolvedValue([]);

        await stagesService.getLeadStages(tenantId, true);

        expect(mockQuery).toHaveBeenCalledWith(
          expect.not.stringContaining('active = TRUE'),
          [tenantId]
        );
      });
    });

    describe('getLeadStageById', () => {
      it('should return stage when found', async () => {
        const mockStage = createMockStage();
        mockQueryOne.mockResolvedValue(mockStage);

        const result = await stagesService.getLeadStageById('stage-uuid', tenantId);

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

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

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

    describe('createLeadStage', () => {
      it('should create lead stage successfully', async () => {
        const newStage = createMockStage({ name: 'New Stage' });
        mockQueryOne
          .mockResolvedValueOnce(null) // unique check
          .mockResolvedValueOnce(newStage); // INSERT

        const result = await stagesService.createLeadStage({ name: 'New Stage' }, tenantId);

        expect(result.name).toBe('New Stage');
      });

      it('should throw ConflictError when name exists', async () => {
        mockQueryOne.mockResolvedValue({ id: 'existing-uuid' });

        await expect(
          stagesService.createLeadStage({ name: 'Existing Stage' }, tenantId)
        ).rejects.toThrow(ConflictError);
      });
    });

    describe('updateLeadStage', () => {
      it('should update lead stage successfully', async () => {
        const existingStage = createMockStage();
        mockQueryOne
          .mockResolvedValueOnce(existingStage) // getById
          .mockResolvedValueOnce(null) // unique name check
          .mockResolvedValueOnce({ ...existingStage, name: 'Updated' }); // getById after update
        mockQuery.mockResolvedValue([]);

        const result = await stagesService.updateLeadStage('stage-uuid', { name: 'Updated' }, tenantId);

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

      it('should throw ConflictError when name exists for another stage', async () => {
        const existingStage = createMockStage();
        mockQueryOne
          .mockResolvedValueOnce(existingStage) // getById
          .mockResolvedValueOnce({ id: 'other-uuid' }); // name exists

        await expect(
          stagesService.updateLeadStage('stage-uuid', { name: 'Duplicate' }, tenantId)
        ).rejects.toThrow(ConflictError);
      });
    });

    describe('deleteLeadStage', () => {
      it('should delete stage without leads', async () => {
        const stage = createMockStage();
        mockQueryOne
          .mockResolvedValueOnce(stage) // getById
          .mockResolvedValueOnce({ count: '0' }); // in use check
        mockQuery.mockResolvedValue([]);

        await stagesService.deleteLeadStage('stage-uuid', tenantId);

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

      it('should throw ConflictError when stage has leads', async () => {
        const stage = createMockStage();
        mockQueryOne
          .mockResolvedValueOnce(stage) // getById
          .mockResolvedValueOnce({ count: '5' }); // in use

        await expect(
          stagesService.deleteLeadStage('stage-uuid', tenantId)
        ).rejects.toThrow(ConflictError);
      });
    });
  });

  describe('Opportunity Stages', () => {
    describe('getOpportunityStages', () => {
      it('should return active opportunity stages', async () => {
        const mockStages = [
          createMockStage({ id: '1', name: 'Qualification' }),
          createMockStage({ id: '2', name: 'Proposal' }),
        ];
        mockQuery.mockResolvedValue(mockStages);

        const result = await stagesService.getOpportunityStages(tenantId);

        expect(result).toHaveLength(2);
      });
    });

    describe('createOpportunityStage', () => {
      it('should create opportunity stage successfully', async () => {
        const newStage = createMockStage({ name: 'New Stage' });
        mockQueryOne
          .mockResolvedValueOnce(null) // unique check
          .mockResolvedValueOnce(newStage); // INSERT

        const result = await stagesService.createOpportunityStage({ name: 'New Stage' }, tenantId);

        expect(result.name).toBe('New Stage');
      });
    });

    describe('deleteOpportunityStage', () => {
      it('should delete stage without opportunities', async () => {
        const stage = createMockStage();
        mockQueryOne
          .mockResolvedValueOnce(stage) // getById
          .mockResolvedValueOnce({ count: '0' }); // in use check
        mockQuery.mockResolvedValue([]);

        await stagesService.deleteOpportunityStage('stage-uuid', tenantId);

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

      it('should throw ConflictError when stage has opportunities', async () => {
        const stage = createMockStage();
        mockQueryOne
          .mockResolvedValueOnce(stage) // getById
          .mockResolvedValueOnce({ count: '3' }); // in use

        await expect(
          stagesService.deleteOpportunityStage('stage-uuid', tenantId)
        ).rejects.toThrow(ConflictError);
      });
    });
  });

  describe('Lost Reasons', () => {
    describe('getLostReasons', () => {
      it('should return active lost reasons', async () => {
        const mockReasons = [
          createMockLostReason({ id: '1', name: 'Too expensive' }),
          createMockLostReason({ id: '2', name: 'Competitor' }),
        ];
        mockQuery.mockResolvedValue(mockReasons);

        const result = await stagesService.getLostReasons(tenantId);

        expect(result).toHaveLength(2);
      });
    });

    describe('createLostReason', () => {
      it('should create lost reason successfully', async () => {
        const newReason = createMockLostReason({ name: 'New Reason' });
        mockQueryOne
          .mockResolvedValueOnce(null) // unique check
          .mockResolvedValueOnce(newReason); // INSERT

        const result = await stagesService.createLostReason({ name: 'New Reason' }, tenantId);

        expect(result.name).toBe('New Reason');
      });

      it('should throw ConflictError when name exists', async () => {
        mockQueryOne.mockResolvedValue({ id: 'existing-uuid' });

        await expect(
          stagesService.createLostReason({ name: 'Existing' }, tenantId)
        ).rejects.toThrow(ConflictError);
      });
    });

    describe('deleteLostReason', () => {
      it('should delete reason not in use', async () => {
        const reason = createMockLostReason();
        mockQueryOne
          .mockResolvedValueOnce(reason) // getById
          .mockResolvedValueOnce({ count: '0' }) // leads check
          .mockResolvedValueOnce({ count: '0' }); // opportunities check
        mockQuery.mockResolvedValue([]);

        await stagesService.deleteLostReason('reason-uuid', tenantId);

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

      it('should throw ConflictError when reason is in use by leads', async () => {
        const reason = createMockLostReason();
        mockQueryOne
          .mockResolvedValueOnce(reason) // getById
          .mockResolvedValueOnce({ count: '2' }); // leads check

        await expect(
          stagesService.deleteLostReason('reason-uuid', tenantId)
        ).rejects.toThrow(ConflictError);
      });

      it('should throw ConflictError when reason is in use by opportunities', async () => {
        const reason = createMockLostReason();
        mockQueryOne
          .mockResolvedValueOnce(reason) // getById
          .mockResolvedValueOnce({ count: '0' }) // leads check
          .mockResolvedValueOnce({ count: '3' }); // opportunities check

        await expect(
          stagesService.deleteLostReason('reason-uuid', tenantId)
        ).rejects.toThrow(ConflictError);
      });
    });
  });
});
