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

// Helper factory
function createMockContract(overrides: Record<string, any> = {}) {
  return {
    id: 'contract-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',
    name: 'Contract John Doe 2026',
    reference: 'CON-001',
    contract_type: 'permanent',
    status: 'draft',
    job_position_id: 'position-uuid-1',
    job_position_name: 'Developer',
    department_id: 'dept-uuid-1',
    department_name: 'Engineering',
    date_start: new Date('2026-01-01'),
    date_end: null,
    trial_date_end: new Date('2026-04-01'),
    wage: 50000,
    wage_type: 'monthly',
    currency_id: 'currency-uuid-1',
    currency_code: 'MXN',
    hours_per_week: 40,
    vacation_days: 12,
    christmas_bonus_days: 15,
    document_url: null,
    notes: null,
    created_at: new Date(),
    ...overrides,
  };
}

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

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

  describe('findAll', () => {
    it('should return contracts with pagination', async () => {
      const mockContracts = [
        createMockContract({ id: '1', name: 'Contract 1' }),
        createMockContract({ id: '2', name: 'Contract 2' }),
      ];

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

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

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

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

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

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

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

      await contractsService.findAll(tenantId, { status: 'active' });

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

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

      await contractsService.findAll(tenantId, { contract_type: 'temporary' });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('c.contract_type = $'),
        expect.arrayContaining([tenantId, 'temporary'])
      );
    });

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

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

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

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

      await contractsService.findAll(tenantId, { page: 3, limit: 10 });

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

  describe('findById', () => {
    it('should return contract when found', async () => {
      const mockContract = createMockContract();
      mockQueryOne.mockResolvedValue(mockContract);

      const result = await contractsService.findById('contract-uuid-1', tenantId);

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

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

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

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

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

  describe('create', () => {
    const createDto = {
      company_id: 'company-uuid',
      employee_id: 'employee-uuid-1',
      name: 'New Contract',
      contract_type: 'permanent' as const,
      date_start: '2026-01-15',
      wage: 45000,
    };

    it('should create contract successfully', async () => {
      const newContract = createMockContract({ ...createDto, id: 'new-uuid' });

      // No active contract exists
      mockQueryOne.mockResolvedValueOnce(null);
      // INSERT returns new contract
      mockQueryOne.mockResolvedValueOnce({ id: 'new-uuid' });
      // findById for return value
      mockQueryOne.mockResolvedValueOnce(newContract);

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

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

    it('should throw ValidationError when employee already has active contract', async () => {
      mockQueryOne.mockResolvedValue({ id: 'existing-active-contract' });

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

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

      await expect(
        contractsService.create(createDto, tenantId, userId)
      ).rejects.toThrow('El empleado ya tiene un contrato activo');
    });

    it('should create contract with all optional fields', async () => {
      const fullDto = {
        company_id: 'company-uuid',
        employee_id: 'employee-uuid-1',
        name: 'Full Contract',
        reference: 'REF-001',
        contract_type: 'temporary' as const,
        job_position_id: 'position-uuid',
        department_id: 'dept-uuid',
        date_start: '2026-01-15',
        date_end: '2026-12-31',
        trial_date_end: '2026-04-15',
        wage: 45000,
        wage_type: 'monthly',
        currency_id: 'currency-uuid',
        hours_per_week: 48,
        vacation_days: 12,
        christmas_bonus_days: 15,
        document_url: 'https://docs.example.com/contract.pdf',
        notes: 'Test notes',
      };

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

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

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

  describe('update', () => {
    it('should update contract in draft status', async () => {
      const existingContract = createMockContract({ status: 'draft' });
      const updatedContract = createMockContract({ status: 'draft', wage: 55000 });

      mockQueryOne.mockResolvedValueOnce(existingContract); // findById
      mockQuery.mockResolvedValue([]); // UPDATE
      mockQueryOne.mockResolvedValueOnce(updatedContract); // findById after update

      const result = await contractsService.update(
        'contract-uuid-1',
        { wage: 55000 },
        tenantId,
        userId
      );

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

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

      await expect(
        contractsService.update('nonexistent-id', { wage: 50000 }, tenantId, userId)
      ).rejects.toThrow(NotFoundError);
    });

    it('should throw ValidationError when contract is not in draft status', async () => {
      const activeContract = createMockContract({ status: 'active' });
      mockQueryOne.mockResolvedValue(activeContract);

      await expect(
        contractsService.update('contract-uuid-1', { wage: 50000 }, tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError with correct message for non-draft contract', async () => {
      const activeContract = createMockContract({ status: 'active' });
      mockQueryOne.mockResolvedValue(activeContract);

      await expect(
        contractsService.update('contract-uuid-1', { wage: 50000 }, tenantId, userId)
      ).rejects.toThrow('Solo se pueden editar contratos en estado borrador');
    });

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

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

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

  describe('activate', () => {
    it('should activate draft contract', async () => {
      const draftContract = createMockContract({ status: 'draft' });
      const activatedContract = createMockContract({ status: 'active' });

      mockQueryOne.mockResolvedValueOnce(draftContract); // findById
      mockQueryOne.mockResolvedValueOnce(null); // no other active contract
      mockQuery.mockResolvedValueOnce([]); // UPDATE status
      mockQuery.mockResolvedValueOnce([]); // UPDATE employee
      mockQueryOne.mockResolvedValueOnce(activatedContract); // findById after update

      const result = await contractsService.activate('contract-uuid-1', tenantId, userId);

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

    it('should throw ValidationError when contract is not draft', async () => {
      const activeContract = createMockContract({ status: 'active' });
      mockQueryOne.mockResolvedValue(activeContract);

      await expect(
        contractsService.activate('contract-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

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

      await expect(
        contractsService.activate('contract-uuid-1', tenantId, userId)
      ).rejects.toThrow('Solo se pueden activar contratos en estado borrador');
    });

    it('should throw ValidationError when employee has another active contract', async () => {
      const draftContract = createMockContract({ status: 'draft' });

      mockQueryOne.mockResolvedValueOnce(draftContract); // findById
      mockQueryOne.mockResolvedValueOnce({ id: 'other-contract-uuid' }); // active contract exists

      await expect(
        contractsService.activate('contract-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError with correct message for existing active contract', async () => {
      const draftContract = createMockContract({ status: 'draft' });

      mockQueryOne.mockResolvedValueOnce(draftContract);
      mockQueryOne.mockResolvedValueOnce({ id: 'other-contract-uuid' });

      await expect(
        contractsService.activate('contract-uuid-1', tenantId, userId)
      ).rejects.toThrow('El empleado ya tiene otro contrato activo');
    });

    it('should update employee department and position when specified', async () => {
      const draftContract = createMockContract({
        status: 'draft',
        department_id: 'dept-uuid',
        job_position_id: 'position-uuid',
      });

      mockQueryOne.mockResolvedValueOnce(draftContract);
      mockQueryOne.mockResolvedValueOnce(null);
      mockQuery.mockResolvedValueOnce([]);
      mockQuery.mockResolvedValueOnce([]);
      mockQueryOne.mockResolvedValueOnce(createMockContract({ status: 'active' }));

      await contractsService.activate('contract-uuid-1', tenantId, userId);

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

  describe('terminate', () => {
    it('should terminate active contract', async () => {
      const activeContract = createMockContract({ status: 'active' });
      const terminatedContract = createMockContract({ status: 'terminated' });

      mockQueryOne.mockResolvedValueOnce(activeContract); // findById
      mockQuery.mockResolvedValue([]); // UPDATE
      mockQueryOne.mockResolvedValueOnce(terminatedContract); // findById after update

      const result = await contractsService.terminate(
        'contract-uuid-1',
        '2026-12-31',
        tenantId,
        userId
      );

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

    it('should throw ValidationError when contract is not active', async () => {
      const draftContract = createMockContract({ status: 'draft' });
      mockQueryOne.mockResolvedValue(draftContract);

      await expect(
        contractsService.terminate('contract-uuid-1', '2026-12-31', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

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

      await expect(
        contractsService.terminate('contract-uuid-1', '2026-12-31', tenantId, userId)
      ).rejects.toThrow('Solo se pueden terminar contratos activos');
    });

    it('should set the termination date', async () => {
      const activeContract = createMockContract({ status: 'active' });

      mockQueryOne.mockResolvedValueOnce(activeContract);
      mockQuery.mockResolvedValue([]);
      mockQueryOne.mockResolvedValueOnce(createMockContract({ status: 'terminated' }));

      await contractsService.terminate('contract-uuid-1', '2026-06-15', tenantId, userId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('date_end = $'),
        expect.arrayContaining(['2026-06-15'])
      );
    });
  });

  describe('cancel', () => {
    it('should cancel draft contract', async () => {
      const draftContract = createMockContract({ status: 'draft' });
      const cancelledContract = createMockContract({ status: 'cancelled' });

      mockQueryOne.mockResolvedValueOnce(draftContract); // findById
      mockQuery.mockResolvedValue([]); // UPDATE
      mockQueryOne.mockResolvedValueOnce(cancelledContract); // findById after update

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

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

    it('should throw ValidationError when contract is active', async () => {
      const activeContract = createMockContract({ status: 'active' });
      mockQueryOne.mockResolvedValue(activeContract);

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

    it('should throw ValidationError when contract is terminated', async () => {
      const terminatedContract = createMockContract({ status: 'terminated' });
      mockQueryOne.mockResolvedValue(terminatedContract);

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

    it('should throw ValidationError with correct message', async () => {
      const activeContract = createMockContract({ status: 'active' });
      mockQueryOne.mockResolvedValue(activeContract);

      await expect(
        contractsService.cancel('contract-uuid-1', tenantId, userId)
      ).rejects.toThrow('No se puede cancelar un contrato activo o terminado');
    });

    it('should allow cancelling expired contract', async () => {
      const expiredContract = createMockContract({ status: 'expired' });
      const cancelledContract = createMockContract({ status: 'cancelled' });

      mockQueryOne.mockResolvedValueOnce(expiredContract);
      mockQuery.mockResolvedValue([]);
      mockQueryOne.mockResolvedValueOnce(cancelledContract);

      await contractsService.cancel('contract-uuid-1', tenantId, userId);

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

  describe('delete', () => {
    it('should delete draft contract', async () => {
      const draftContract = createMockContract({ status: 'draft' });

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

      await contractsService.delete('contract-uuid-1', tenantId);

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

    it('should delete cancelled contract', async () => {
      const cancelledContract = createMockContract({ status: 'cancelled' });

      mockQueryOne.mockResolvedValueOnce(cancelledContract);
      mockQuery.mockResolvedValue([]);

      await contractsService.delete('contract-uuid-1', tenantId);

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

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

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

    it('should throw ValidationError when contract is active', async () => {
      const activeContract = createMockContract({ status: 'active' });
      mockQueryOne.mockResolvedValue(activeContract);

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

    it('should throw ValidationError when contract is terminated', async () => {
      const terminatedContract = createMockContract({ status: 'terminated' });
      mockQueryOne.mockResolvedValue(terminatedContract);

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

    it('should throw ValidationError with correct message', async () => {
      const activeContract = createMockContract({ status: 'active' });
      mockQueryOne.mockResolvedValue(activeContract);

      await expect(
        contractsService.delete('contract-uuid-1', tenantId)
      ).rejects.toThrow('Solo se pueden eliminar contratos en borrador o cancelados');
    });
  });
});
