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

// Helper factories
function createMockDepartment(overrides: Record<string, any> = {}) {
  return {
    id: 'dept-uuid-1',
    tenant_id: 'test-tenant-uuid',
    company_id: 'company-uuid-1',
    company_name: 'Test Company',
    name: 'Engineering',
    code: 'ENG',
    parent_id: null,
    parent_name: null,
    manager_id: null,
    manager_name: null,
    description: 'Engineering department',
    color: '#3B82F6',
    active: true,
    employee_count: 5,
    created_at: new Date(),
    ...overrides,
  };
}

function createMockJobPosition(overrides: Record<string, any> = {}) {
  return {
    id: 'position-uuid-1',
    tenant_id: 'test-tenant-uuid',
    name: 'Senior Developer',
    department_id: 'dept-uuid-1',
    department_name: 'Engineering',
    description: 'Senior software developer position',
    requirements: 'Experience with TypeScript',
    responsibilities: 'Code review, mentoring',
    min_salary: 50000,
    max_salary: 80000,
    active: true,
    employee_count: 3,
    created_at: new Date(),
    ...overrides,
  };
}

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

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

  // ========== DEPARTMENTS ==========

  describe('findAll', () => {
    it('should return departments with pagination', async () => {
      const mockDepartments = [
        createMockDepartment({ id: '1', name: 'Engineering' }),
        createMockDepartment({ id: '2', name: 'Marketing' }),
      ];

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

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

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

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

      await departmentsService.findAll(tenantId, { company_id: 'company-uuid' });

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

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

      await departmentsService.findAll(tenantId, { active: true });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('d.active = $'),
        expect.arrayContaining([tenantId, true])
      );
    });

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

      await departmentsService.findAll(tenantId, { search: 'Eng' });

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

    it('should apply pagination correctly', async () => {
      mockQueryOne.mockResolvedValue({ count: '100' });
      mockQuery.mockResolvedValue([]);

      await departmentsService.findAll(tenantId, { page: 3, limit: 20 });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('LIMIT'),
        expect.arrayContaining([20, 40]) // limit=20, offset=40 (page 3)
      );
    });

    it('should return empty result when no departments found', async () => {
      mockQueryOne.mockResolvedValue({ count: '0' });
      mockQuery.mockResolvedValue([]);

      const result = await departmentsService.findAll(tenantId, {});

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

  describe('findById', () => {
    it('should return department when found', async () => {
      const mockDepartment = createMockDepartment();
      mockQueryOne.mockResolvedValue(mockDepartment);

      const result = await departmentsService.findById('dept-uuid-1', tenantId);

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

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

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

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

      await expect(
        departmentsService.findById('nonexistent-id', tenantId)
      ).rejects.toThrow('Departamento no encontrado');
    });
  });

  describe('create', () => {
    const createDto = {
      company_id: 'company-uuid',
      name: 'New Department',
      code: 'NEW',
      description: 'A new department',
    };

    it('should create department successfully', async () => {
      const newDept = createMockDepartment({ ...createDto, id: 'new-uuid' });

      // No existing department with same name
      mockQueryOne.mockResolvedValueOnce(null);
      // INSERT returns new department
      mockQueryOne.mockResolvedValueOnce({ id: 'new-uuid' });
      // findById for return value
      mockQueryOne.mockResolvedValueOnce(newDept);

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

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

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

      await expect(
        departmentsService.create(createDto, tenantId, userId)
      ).rejects.toThrow(ConflictError);
    });

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

      await expect(
        departmentsService.create(createDto, tenantId, userId)
      ).rejects.toThrow('Ya existe un departamento con ese nombre en esta empresa');
    });

    it('should create department with optional fields', async () => {
      const fullDto = {
        company_id: 'company-uuid',
        name: 'Full Department',
        code: 'FULL',
        parent_id: 'parent-uuid',
        manager_id: 'manager-uuid',
        description: 'Full description',
        color: '#FF0000',
      };

      mockQueryOne.mockResolvedValueOnce(null);
      mockQueryOne.mockResolvedValueOnce({ id: 'new-uuid' });
      mockQueryOne.mockResolvedValueOnce(createMockDepartment({ ...fullDto }));

      const result = await departmentsService.create(fullDto, tenantId, userId);

      expect(result.name).toBe('Full Department');
    });
  });

  describe('update', () => {
    it('should update department successfully', async () => {
      const existingDept = createMockDepartment();
      const updatedDept = createMockDepartment({ name: 'Updated Name' });

      mockQueryOne.mockResolvedValueOnce(existingDept); // findById
      mockQueryOne.mockResolvedValueOnce(null); // no name conflict
      mockQuery.mockResolvedValue([]); // UPDATE
      mockQueryOne.mockResolvedValueOnce(updatedDept); // findById after update

      const result = await departmentsService.update(
        'dept-uuid-1',
        { name: 'Updated Name' },
        tenantId
      );

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

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

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

    it('should return unchanged department when no fields to update', async () => {
      const existingDept = createMockDepartment();
      mockQueryOne.mockResolvedValue(existingDept);

      const result = await departmentsService.update(
        'dept-uuid-1',
        {},
        tenantId
      );

      expect(result).toEqual(existingDept);
      expect(mockQuery).not.toHaveBeenCalled();
    });

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

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

      await expect(
        departmentsService.update('dept-uuid-1', { name: 'Existing Name' }, tenantId)
      ).rejects.toThrow(ConflictError);
    });

    it('should update multiple fields at once', async () => {
      const existingDept = createMockDepartment();
      const updatedDept = createMockDepartment({
        name: 'New Name',
        code: 'NEW',
        active: false,
      });

      mockQueryOne.mockResolvedValueOnce(existingDept);
      mockQueryOne.mockResolvedValueOnce(null); // no name conflict
      mockQuery.mockResolvedValue([]);
      mockQueryOne.mockResolvedValueOnce(updatedDept);

      const result = await departmentsService.update(
        'dept-uuid-1',
        { name: 'New Name', code: 'NEW', active: false },
        tenantId
      );

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

  describe('delete', () => {
    it('should delete department without employees or subdepartments', async () => {
      const department = createMockDepartment({ employee_count: 0 });

      mockQueryOne
        .mockResolvedValueOnce(department) // findById
        .mockResolvedValueOnce({ count: '0' }) // hasEmployees
        .mockResolvedValueOnce({ count: '0' }); // hasChildren
      mockQuery.mockResolvedValue([]);

      await departmentsService.delete('dept-uuid-1', tenantId);

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

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

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

    it('should throw ConflictError when department has employees', async () => {
      const department = createMockDepartment();

      mockQueryOne
        .mockResolvedValueOnce(department) // findById
        .mockResolvedValueOnce({ count: '5' }); // hasEmployees

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

    it('should throw ConflictError with correct message for employees', async () => {
      const department = createMockDepartment();

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

      await expect(
        departmentsService.delete('dept-uuid-1', tenantId)
      ).rejects.toThrow('No se puede eliminar un departamento con empleados asociados');
    });

    it('should throw ConflictError when department has subdepartments', async () => {
      const department = createMockDepartment();

      mockQueryOne
        .mockResolvedValueOnce(department) // findById
        .mockResolvedValueOnce({ count: '0' }) // hasEmployees
        .mockResolvedValueOnce({ count: '3' }); // hasChildren

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

    it('should throw ConflictError with correct message for subdepartments', async () => {
      const department = createMockDepartment();

      mockQueryOne
        .mockResolvedValueOnce(department)
        .mockResolvedValueOnce({ count: '0' })
        .mockResolvedValueOnce({ count: '3' });

      await expect(
        departmentsService.delete('dept-uuid-1', tenantId)
      ).rejects.toThrow('No se puede eliminar un departamento con subdepartamentos');
    });
  });

  // ========== JOB POSITIONS ==========

  describe('getJobPositions', () => {
    it('should return active job positions by default', async () => {
      const mockPositions = [
        createMockJobPosition({ id: '1', name: 'Developer' }),
        createMockJobPosition({ id: '2', name: 'Designer' }),
      ];

      mockQuery.mockResolvedValue(mockPositions);

      const result = await departmentsService.getJobPositions(tenantId);

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

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

      await departmentsService.getJobPositions(tenantId, true);

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

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

      const result = await departmentsService.getJobPositions(tenantId);

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

  describe('getJobPositionById', () => {
    it('should return job position when found', async () => {
      const mockPosition = createMockJobPosition();
      mockQueryOne.mockResolvedValue(mockPosition);

      const result = await departmentsService.getJobPositionById('position-uuid-1', tenantId);

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

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

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

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

      await expect(
        departmentsService.getJobPositionById('nonexistent-id', tenantId)
      ).rejects.toThrow('Puesto no encontrado');
    });
  });

  describe('createJobPosition', () => {
    const createDto = {
      name: 'New Position',
      department_id: 'dept-uuid-1',
      description: 'New position description',
    };

    it('should create job position successfully', async () => {
      const newPosition = createMockJobPosition({ ...createDto, id: 'new-uuid' });

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

      const result = await departmentsService.createJobPosition(createDto, tenantId);

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

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

      await expect(
        departmentsService.createJobPosition(createDto, tenantId)
      ).rejects.toThrow(ConflictError);
    });

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

      await expect(
        departmentsService.createJobPosition(createDto, tenantId)
      ).rejects.toThrow('Ya existe un puesto con ese nombre');
    });

    it('should create position with salary range', async () => {
      const dtoWithSalary = {
        name: 'Senior Developer',
        min_salary: 50000,
        max_salary: 80000,
      };

      mockQueryOne.mockResolvedValueOnce(null);
      mockQueryOne.mockResolvedValueOnce(createMockJobPosition(dtoWithSalary));

      const result = await departmentsService.createJobPosition(dtoWithSalary, tenantId);

      expect(result.min_salary).toBe(50000);
      expect(result.max_salary).toBe(80000);
    });
  });

  describe('updateJobPosition', () => {
    it('should update job position successfully', async () => {
      const existingPosition = createMockJobPosition();
      const updatedPosition = createMockJobPosition({ name: 'Updated Position' });

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

      const result = await departmentsService.updateJobPosition(
        'position-uuid-1',
        { name: 'Updated Position' },
        tenantId
      );

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

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

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

    it('should return unchanged position when no fields to update', async () => {
      const existingPosition = createMockJobPosition();
      mockQueryOne.mockResolvedValue(existingPosition);

      const result = await departmentsService.updateJobPosition(
        'position-uuid-1',
        {},
        tenantId
      );

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

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

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

      await expect(
        departmentsService.updateJobPosition('position-uuid-1', { name: 'Existing Name' }, tenantId)
      ).rejects.toThrow(ConflictError);
    });

    it('should update salary fields', async () => {
      const existingPosition = createMockJobPosition();
      const updatedPosition = createMockJobPosition({
        min_salary: 60000,
        max_salary: 90000,
      });

      mockQueryOne.mockResolvedValueOnce(existingPosition);
      mockQuery.mockResolvedValue([]);
      mockQueryOne.mockResolvedValueOnce(updatedPosition);

      await departmentsService.updateJobPosition(
        'position-uuid-1',
        { min_salary: 60000, max_salary: 90000 },
        tenantId
      );

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

  describe('deleteJobPosition', () => {
    it('should delete position without employees', async () => {
      const position = createMockJobPosition({ employee_count: 0 });

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

      await departmentsService.deleteJobPosition('position-uuid-1', tenantId);

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

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

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

    it('should throw ConflictError when position has employees', async () => {
      const position = createMockJobPosition();

      mockQueryOne
        .mockResolvedValueOnce(position) // getById
        .mockResolvedValueOnce({ count: '5' }); // hasEmployees

      await expect(
        departmentsService.deleteJobPosition('position-uuid-1', tenantId)
      ).rejects.toThrow(ConflictError);
    });

    it('should throw ConflictError with correct message for assigned employees', async () => {
      const position = createMockJobPosition();

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

      await expect(
        departmentsService.deleteJobPosition('position-uuid-1', tenantId)
      ).rejects.toThrow('No se puede eliminar un puesto con empleados asociados');
    });
  });
});
