import { jest, describe, it, expect, beforeEach } from '@jest/globals';

// 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 { leavesService } from '../leaves.service.js';
import { NotFoundError, ValidationError, ConflictError } from '../../../shared/errors/index.js';

// Helper factories
function createMockLeaveType(overrides: Record<string, any> = {}) {
  return {
    id: 'leave-type-uuid-1',
    tenant_id: 'test-tenant-uuid',
    name: 'Vacaciones',
    code: 'VAC',
    leave_type: 'vacation',
    requires_approval: true,
    max_days: 30,
    is_paid: true,
    color: '#3B82F6',
    active: true,
    created_at: new Date(),
    ...overrides,
  };
}

function createMockLeave(overrides: Record<string, any> = {}) {
  return {
    id: 'leave-uuid-1',
    tenant_id: 'test-tenant-uuid',
    company_id: 'company-uuid-1',
    company_name: 'Test Company',
    employee_id: 'employee-uuid-1',
    employee_name: 'John Doe',
    employee_number: 'EMP-001',
    leave_type_id: 'leave-type-uuid-1',
    leave_type_name: 'Vacaciones',
    name: 'Vacation Request',
    date_from: new Date('2026-02-01'),
    date_to: new Date('2026-02-05'),
    number_of_days: 5,
    status: 'draft',
    description: 'Vacation time',
    approved_by: null,
    approved_by_name: null,
    approved_at: null,
    rejection_reason: null,
    created_at: new Date(),
    ...overrides,
  };
}

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

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

  // ========== LEAVE TYPES ==========

  describe('getLeaveTypes', () => {
    it('should return active leave types by default', async () => {
      const mockLeaveTypes = [
        createMockLeaveType({ id: '1', name: 'Vacaciones' }),
        createMockLeaveType({ id: '2', name: 'Enfermedad' }),
      ];

      mockQuery.mockResolvedValue(mockLeaveTypes);

      const result = await leavesService.getLeaveTypes(tenantId);

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

    it('should include inactive types when flag is true', async () => {
      mockQuery.mockResolvedValue([]);

      await leavesService.getLeaveTypes(tenantId, true);

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

    it('should return empty array when no types found', async () => {
      mockQuery.mockResolvedValue([]);

      const result = await leavesService.getLeaveTypes(tenantId);

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

  describe('getLeaveTypeById', () => {
    it('should return leave type when found', async () => {
      const mockLeaveType = createMockLeaveType();
      mockQueryOne.mockResolvedValue(mockLeaveType);

      const result = await leavesService.getLeaveTypeById('leave-type-uuid-1', tenantId);

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

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

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

    it('should throw NotFoundError with correct message', async () => {
      mockQueryOne.mockResolvedValue(null);

      await expect(
        leavesService.getLeaveTypeById('nonexistent-id', tenantId)
      ).rejects.toThrow('Tipo de ausencia no encontrado');
    });
  });

  describe('createLeaveType', () => {
    const createDto = {
      name: 'New Leave Type',
      code: 'NEW',
      leave_type: 'personal' as const,
    };

    it('should create leave type successfully', async () => {
      const newLeaveType = createMockLeaveType({ ...createDto, id: 'new-uuid' });

      mockQueryOne.mockResolvedValueOnce(null); // No existing
      mockQueryOne.mockResolvedValueOnce(newLeaveType); // INSERT

      const result = await leavesService.createLeaveType(createDto, tenantId);

      expect(result.name).toBe('New Leave Type');
    });

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

      await expect(
        leavesService.createLeaveType(createDto, tenantId)
      ).rejects.toThrow(ConflictError);
    });

    it('should throw ConflictError with correct message', async () => {
      mockQueryOne.mockResolvedValue({ id: 'existing-uuid' });

      await expect(
        leavesService.createLeaveType(createDto, tenantId)
      ).rejects.toThrow('Ya existe un tipo de ausencia con ese nombre');
    });

    it('should create leave type with all optional fields', async () => {
      const fullDto = {
        name: 'Full Leave Type',
        code: 'FULL',
        leave_type: 'vacation' as const,
        requires_approval: false,
        max_days: 15,
        is_paid: false,
        color: '#FF0000',
      };

      mockQueryOne.mockResolvedValueOnce(null);
      mockQueryOne.mockResolvedValueOnce(createMockLeaveType(fullDto));

      const result = await leavesService.createLeaveType(fullDto, tenantId);

      expect(result.name).toBe('Full Leave Type');
    });

    it('should use default values for optional fields', async () => {
      const minimalDto = {
        name: 'Minimal Type',
        leave_type: 'other' as const,
      };

      mockQueryOne.mockResolvedValueOnce(null);
      mockQueryOne.mockResolvedValueOnce(createMockLeaveType({
        ...minimalDto,
        requires_approval: true,
        is_paid: true,
      }));

      const result = await leavesService.createLeaveType(minimalDto, tenantId);

      expect(result.requires_approval).toBe(true);
      expect(result.is_paid).toBe(true);
    });
  });

  describe('updateLeaveType', () => {
    it('should update leave type successfully', async () => {
      const existingType = createMockLeaveType();
      const updatedType = createMockLeaveType({ name: 'Updated Name' });

      mockQueryOne.mockResolvedValueOnce(existingType); // getById
      mockQueryOne.mockResolvedValueOnce(null); // no name conflict
      mockQuery.mockResolvedValue([]); // UPDATE
      mockQueryOne.mockResolvedValueOnce(updatedType); // getById after update

      const result = await leavesService.updateLeaveType(
        'leave-type-uuid-1',
        { name: 'Updated Name' },
        tenantId
      );

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('UPDATE hr.leave_types SET'),
        expect.any(Array)
      );
    });

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

      await expect(
        leavesService.updateLeaveType('nonexistent-id', { name: 'Test' }, tenantId)
      ).rejects.toThrow(NotFoundError);
    });

    it('should return unchanged type when no fields to update', async () => {
      const existingType = createMockLeaveType();
      mockQueryOne.mockResolvedValue(existingType);

      const result = await leavesService.updateLeaveType(
        'leave-type-uuid-1',
        {},
        tenantId
      );

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

    it('should throw ConflictError when updating to existing name', async () => {
      const existingType = createMockLeaveType({ name: 'Original' });

      mockQueryOne.mockResolvedValueOnce(existingType); // getById
      mockQueryOne.mockResolvedValueOnce({ id: 'other-uuid' }); // name check

      await expect(
        leavesService.updateLeaveType('leave-type-uuid-1', { name: 'Existing Name' }, tenantId)
      ).rejects.toThrow(ConflictError);
    });
  });

  describe('deleteLeaveType', () => {
    it('should delete leave type not in use', async () => {
      const leaveType = createMockLeaveType();

      mockQueryOne
        .mockResolvedValueOnce(leaveType) // getById
        .mockResolvedValueOnce({ count: '0' }); // inUse
      mockQuery.mockResolvedValue([]);

      await leavesService.deleteLeaveType('leave-type-uuid-1', tenantId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('DELETE FROM hr.leave_types'),
        expect.any(Array)
      );
    });

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

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

    it('should throw ConflictError when type has associated leaves', async () => {
      const leaveType = createMockLeaveType();

      mockQueryOne
        .mockResolvedValueOnce(leaveType)
        .mockResolvedValueOnce({ count: '5' });

      await expect(
        leavesService.deleteLeaveType('leave-type-uuid-1', tenantId)
      ).rejects.toThrow(ConflictError);
    });

    it('should throw ConflictError with correct message', async () => {
      const leaveType = createMockLeaveType();

      mockQueryOne
        .mockResolvedValueOnce(leaveType)
        .mockResolvedValueOnce({ count: '5' });

      await expect(
        leavesService.deleteLeaveType('leave-type-uuid-1', tenantId)
      ).rejects.toThrow('No se puede eliminar un tipo de ausencia que esta en uso');
    });
  });

  // ========== LEAVES ==========

  describe('findAll (leaves)', () => {
    it('should return leaves with pagination', async () => {
      const mockLeaves = [
        createMockLeave({ id: '1', name: 'Leave 1' }),
        createMockLeave({ id: '2', name: 'Leave 2' }),
      ];

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

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

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

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

      await leavesService.findAll(tenantId, { employee_id: 'employee-uuid' });

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

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

      await leavesService.findAll(tenantId, { leave_type_id: 'type-uuid' });

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

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

      await leavesService.findAll(tenantId, { status: 'approved' });

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

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

      await leavesService.findAll(tenantId, { date_from: '2026-01-01' });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('l.date_from >= $'),
        expect.arrayContaining([tenantId, '2026-01-01'])
      );
    });

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

      await leavesService.findAll(tenantId, { date_to: '2026-12-31' });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('l.date_to <= $'),
        expect.arrayContaining([tenantId, '2026-12-31'])
      );
    });

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

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

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

  describe('findById (leave)', () => {
    it('should return leave when found', async () => {
      const mockLeave = createMockLeave();
      mockQueryOne.mockResolvedValue(mockLeave);

      const result = await leavesService.findById('leave-uuid-1', tenantId);

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

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

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

    it('should throw NotFoundError with correct message', async () => {
      mockQueryOne.mockResolvedValue(null);

      await expect(
        leavesService.findById('nonexistent-id', tenantId)
      ).rejects.toThrow('Solicitud de ausencia no encontrada');
    });
  });

  describe('create (leave)', () => {
    const createDto = {
      company_id: 'company-uuid',
      employee_id: 'employee-uuid-1',
      leave_type_id: 'leave-type-uuid-1',
      date_from: '2026-02-01',
      date_to: '2026-02-05',
    };

    it('should create leave successfully', async () => {
      const leaveType = createMockLeaveType({ max_days: 30 });
      const newLeave = createMockLeave({ ...createDto, id: 'new-uuid' });

      mockQueryOne.mockResolvedValueOnce(leaveType); // getLeaveTypeById
      mockQueryOne.mockResolvedValueOnce({ count: '0' }); // overlap check
      mockQueryOne.mockResolvedValueOnce({ id: 'new-uuid' }); // INSERT
      mockQueryOne.mockResolvedValueOnce(newLeave); // findById

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

      expect(result.employee_id).toBe('employee-uuid-1');
    });

    it('should calculate days correctly for same date (1 day)', async () => {
      const leaveType = createMockLeaveType({ max_days: 30 });
      const sameDayDto = {
        ...createDto,
        date_from: '2026-02-05',
        date_to: '2026-02-05',
      };
      const newLeave = createMockLeave({ ...sameDayDto, number_of_days: 1 });

      mockQueryOne.mockResolvedValueOnce(leaveType); // getLeaveTypeById
      mockQueryOne.mockResolvedValueOnce({ count: '0' }); // overlap check
      mockQueryOne.mockResolvedValueOnce({ id: 'new-uuid' }); // INSERT
      mockQueryOne.mockResolvedValueOnce(newLeave); // findById

      const result = await leavesService.create(sameDayDto, tenantId, userId);

      expect(result.number_of_days).toBe(1);
    });

    it('should throw ValidationError when exceeding max days', async () => {
      const leaveType = createMockLeaveType({ max_days: 3 });
      const longDto = {
        ...createDto,
        date_from: '2026-02-01',
        date_to: '2026-02-10', // 10 days > 3 max
      };

      mockQueryOne.mockResolvedValueOnce(leaveType);

      await expect(
        leavesService.create(longDto, tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError with max days message', async () => {
      const leaveType = createMockLeaveType({ max_days: 3 });
      const longDto = {
        ...createDto,
        date_from: '2026-02-01',
        date_to: '2026-02-10',
      };

      mockQueryOne.mockResolvedValueOnce(leaveType);

      await expect(
        leavesService.create(longDto, tenantId, userId)
      ).rejects.toThrow('Este tipo de ausencia tiene un maximo de 3 dias');
    });

    it('should throw ValidationError when overlapping leaves exist', async () => {
      const leaveType = createMockLeaveType({ max_days: null });

      mockQueryOne.mockResolvedValueOnce(leaveType);
      mockQueryOne.mockResolvedValueOnce({ count: '1' }); // overlap exists

      await expect(
        leavesService.create(createDto, tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError with overlap message', async () => {
      const leaveType = createMockLeaveType({ max_days: null });

      mockQueryOne.mockResolvedValueOnce(leaveType);
      mockQueryOne.mockResolvedValueOnce({ count: '1' });

      await expect(
        leavesService.create(createDto, tenantId, userId)
      ).rejects.toThrow('Ya existe una solicitud de ausencia para estas fechas');
    });
  });

  describe('update (leave)', () => {
    it('should update draft leave', async () => {
      const existingLeave = createMockLeave({ status: 'draft' });
      const updatedLeave = createMockLeave({ status: 'draft', description: 'Updated' });

      mockQueryOne.mockResolvedValueOnce(existingLeave); // findById
      mockQuery.mockResolvedValue([]); // UPDATE
      mockQueryOne.mockResolvedValueOnce(updatedLeave); // findById after update

      const result = await leavesService.update(
        'leave-uuid-1',
        { description: 'Updated' },
        tenantId,
        userId
      );

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('UPDATE hr.leaves SET'),
        expect.any(Array)
      );
    });

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

      await expect(
        leavesService.update('nonexistent-id', { description: 'Test' }, tenantId, userId)
      ).rejects.toThrow(NotFoundError);
    });

    it('should throw ValidationError when leave is not draft', async () => {
      const submittedLeave = createMockLeave({ status: 'submitted' });
      mockQueryOne.mockResolvedValue(submittedLeave);

      await expect(
        leavesService.update('leave-uuid-1', { description: 'Test' }, tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError with correct message for non-draft', async () => {
      const submittedLeave = createMockLeave({ status: 'submitted' });
      mockQueryOne.mockResolvedValue(submittedLeave);

      await expect(
        leavesService.update('leave-uuid-1', { description: 'Test' }, tenantId, userId)
      ).rejects.toThrow('Solo se pueden editar solicitudes en borrador');
    });

    it('should return unchanged leave when no fields to update', async () => {
      const existingLeave = createMockLeave({ status: 'draft' });
      mockQueryOne.mockResolvedValue(existingLeave);

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

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

    it('should recalculate days when dates change', async () => {
      const existingLeave = createMockLeave({ status: 'draft' });
      const updatedLeave = createMockLeave({
        status: 'draft',
        date_from: new Date('2026-03-01'),
        date_to: new Date('2026-03-10'),
        number_of_days: 10,
      });

      mockQueryOne.mockResolvedValueOnce(existingLeave);
      mockQuery.mockResolvedValue([]);
      mockQueryOne.mockResolvedValueOnce(updatedLeave);

      await leavesService.update(
        'leave-uuid-1',
        { date_from: '2026-03-01', date_to: '2026-03-10' },
        tenantId,
        userId
      );

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

  describe('submit', () => {
    it('should submit draft leave', async () => {
      const draftLeave = createMockLeave({ status: 'draft' });
      const submittedLeave = createMockLeave({ status: 'submitted' });

      mockQueryOne.mockResolvedValueOnce(draftLeave); // findById
      mockQuery.mockResolvedValue([]); // UPDATE
      mockQueryOne.mockResolvedValueOnce(submittedLeave); // findById after update

      const result = await leavesService.submit('leave-uuid-1', tenantId, userId);

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

    it('should throw ValidationError when leave is not draft', async () => {
      const submittedLeave = createMockLeave({ status: 'submitted' });
      mockQueryOne.mockResolvedValue(submittedLeave);

      await expect(
        leavesService.submit('leave-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError with correct message', async () => {
      const submittedLeave = createMockLeave({ status: 'submitted' });
      mockQueryOne.mockResolvedValue(submittedLeave);

      await expect(
        leavesService.submit('leave-uuid-1', tenantId, userId)
      ).rejects.toThrow('Solo se pueden enviar solicitudes en borrador');
    });
  });

  describe('approve', () => {
    it('should approve submitted leave', async () => {
      const submittedLeave = createMockLeave({
        status: 'submitted',
        date_from: new Date('2099-01-01'),
        date_to: new Date('2099-01-05'),
      });
      const approvedLeave = createMockLeave({
        status: 'approved',
        approved_by: userId,
      });

      mockQueryOne.mockResolvedValueOnce(submittedLeave); // findById
      mockQuery.mockResolvedValueOnce([]); // UPDATE status
      mockQueryOne.mockResolvedValueOnce(approvedLeave); // findById after update

      const result = await leavesService.approve('leave-uuid-1', tenantId, userId);

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

    it('should throw ValidationError when leave is not submitted', async () => {
      const draftLeave = createMockLeave({ status: 'draft' });
      mockQueryOne.mockResolvedValue(draftLeave);

      await expect(
        leavesService.approve('leave-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError with correct message', async () => {
      const draftLeave = createMockLeave({ status: 'draft' });
      mockQueryOne.mockResolvedValue(draftLeave);

      await expect(
        leavesService.approve('leave-uuid-1', tenantId, userId)
      ).rejects.toThrow('Solo se pueden aprobar solicitudes enviadas');
    });

    it('should record approver information', async () => {
      const submittedLeave = createMockLeave({
        status: 'submitted',
        date_from: new Date('2099-01-01'),
        date_to: new Date('2099-01-05'),
      });

      mockQueryOne.mockResolvedValueOnce(submittedLeave);
      mockQuery.mockResolvedValue([]);
      mockQueryOne.mockResolvedValueOnce(createMockLeave({ status: 'approved' }));

      await leavesService.approve('leave-uuid-1', tenantId, userId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('approved_by = $'),
        expect.arrayContaining([userId])
      );
    });
  });

  describe('reject', () => {
    it('should reject submitted leave with reason', async () => {
      const submittedLeave = createMockLeave({ status: 'submitted' });
      const rejectedLeave = createMockLeave({
        status: 'rejected',
        rejection_reason: 'Project deadline',
      });

      mockQueryOne.mockResolvedValueOnce(submittedLeave); // findById
      mockQuery.mockResolvedValue([]); // UPDATE
      mockQueryOne.mockResolvedValueOnce(rejectedLeave); // findById after update

      const result = await leavesService.reject(
        'leave-uuid-1',
        'Project deadline',
        tenantId,
        userId
      );

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

    it('should throw ValidationError when leave is not submitted', async () => {
      const draftLeave = createMockLeave({ status: 'draft' });
      mockQueryOne.mockResolvedValue(draftLeave);

      await expect(
        leavesService.reject('leave-uuid-1', 'Reason', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError with correct message', async () => {
      const draftLeave = createMockLeave({ status: 'draft' });
      mockQueryOne.mockResolvedValue(draftLeave);

      await expect(
        leavesService.reject('leave-uuid-1', 'Reason', tenantId, userId)
      ).rejects.toThrow('Solo se pueden rechazar solicitudes enviadas');
    });

    it('should store rejection reason', async () => {
      const submittedLeave = createMockLeave({ status: 'submitted' });

      mockQueryOne.mockResolvedValueOnce(submittedLeave);
      mockQuery.mockResolvedValue([]);
      mockQueryOne.mockResolvedValueOnce(createMockLeave({ status: 'rejected' }));

      await leavesService.reject('leave-uuid-1', 'Team needs coverage', tenantId, userId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('rejection_reason = $'),
        expect.arrayContaining(['Team needs coverage'])
      );
    });
  });

  describe('cancel', () => {
    it('should cancel draft leave', async () => {
      const draftLeave = createMockLeave({ status: 'draft' });
      const cancelledLeave = createMockLeave({ status: 'cancelled' });

      mockQueryOne.mockResolvedValueOnce(draftLeave); // findById
      mockQuery.mockResolvedValue([]); // UPDATE
      mockQueryOne.mockResolvedValueOnce(cancelledLeave); // findById after update

      const result = await leavesService.cancel('leave-uuid-1', tenantId, userId);

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

    it('should cancel submitted leave', async () => {
      const submittedLeave = createMockLeave({ status: 'submitted' });
      const cancelledLeave = createMockLeave({ status: 'cancelled' });

      mockQueryOne.mockResolvedValueOnce(submittedLeave);
      mockQuery.mockResolvedValue([]);
      mockQueryOne.mockResolvedValueOnce(cancelledLeave);

      await leavesService.cancel('leave-uuid-1', tenantId, userId);

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

    it('should cancel approved leave', async () => {
      const approvedLeave = createMockLeave({ status: 'approved' });
      const cancelledLeave = createMockLeave({ status: 'cancelled' });

      mockQueryOne.mockResolvedValueOnce(approvedLeave);
      mockQuery.mockResolvedValue([]);
      mockQueryOne.mockResolvedValueOnce(cancelledLeave);

      await leavesService.cancel('leave-uuid-1', tenantId, userId);

      expect(mockQuery).toHaveBeenCalled();
    });

    it('should throw ValidationError when leave is already cancelled', async () => {
      const cancelledLeave = createMockLeave({ status: 'cancelled' });
      mockQueryOne.mockResolvedValue(cancelledLeave);

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

    it('should throw ValidationError with correct message for already cancelled', async () => {
      const cancelledLeave = createMockLeave({ status: 'cancelled' });
      mockQueryOne.mockResolvedValue(cancelledLeave);

      await expect(
        leavesService.cancel('leave-uuid-1', tenantId, userId)
      ).rejects.toThrow('La solicitud ya esta cancelada');
    });

    it('should throw ValidationError when leave is rejected', async () => {
      const rejectedLeave = createMockLeave({ status: 'rejected' });
      mockQueryOne.mockResolvedValue(rejectedLeave);

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

    it('should throw ValidationError with correct message for rejected', async () => {
      const rejectedLeave = createMockLeave({ status: 'rejected' });
      mockQueryOne.mockResolvedValue(rejectedLeave);

      await expect(
        leavesService.cancel('leave-uuid-1', tenantId, userId)
      ).rejects.toThrow('No se puede cancelar una solicitud rechazada');
    });
  });

  describe('delete (leave)', () => {
    it('should delete draft leave', async () => {
      const draftLeave = createMockLeave({ status: 'draft' });

      mockQueryOne.mockResolvedValueOnce(draftLeave); // findById
      mockQuery.mockResolvedValue([]); // DELETE

      await leavesService.delete('leave-uuid-1', tenantId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('DELETE FROM hr.leaves'),
        expect.any(Array)
      );
    });

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

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

    it('should throw ValidationError when leave is not draft', async () => {
      const submittedLeave = createMockLeave({ status: 'submitted' });
      mockQueryOne.mockResolvedValue(submittedLeave);

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

    it('should throw ValidationError with correct message', async () => {
      const submittedLeave = createMockLeave({ status: 'submitted' });
      mockQueryOne.mockResolvedValue(submittedLeave);

      await expect(
        leavesService.delete('leave-uuid-1', tenantId)
      ).rejects.toThrow('Solo se pueden eliminar solicitudes en borrador');
    });

    it('should throw ValidationError for approved leave', async () => {
      const approvedLeave = createMockLeave({ status: 'approved' });
      mockQueryOne.mockResolvedValue(approvedLeave);

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

    it('should throw ValidationError for cancelled leave', async () => {
      const cancelledLeave = createMockLeave({ status: 'cancelled' });
      mockQueryOne.mockResolvedValue(cancelledLeave);

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