/**
 * ToolLoanService Unit Tests
 * Module: Assets (MAE-015)
 * GAP: GAP-002
 */

import { Repository, SelectQueryBuilder } from 'typeorm';
import { ToolLoanService, CreateToolLoanDto, ReturnToolDto } from '../services/tool-loan.service';
import { ToolLoan } from '../entities/tool-loan.entity';
import { Asset } from '../entities/asset.entity';

const createMockRepository = <T>(): jest.Mocked<Repository<T>> => ({
  create: jest.fn(),
  save: jest.fn(),
  findOne: jest.fn(),
  find: jest.fn(),
  count: jest.fn(),
  update: jest.fn(),
  createQueryBuilder: jest.fn(),
} as unknown as jest.Mocked<Repository<T>>);

const createMockQueryBuilder = <T>(): jest.Mocked<SelectQueryBuilder<T>> => ({
  leftJoinAndSelect: jest.fn().mockReturnThis(),
  where: jest.fn().mockReturnThis(),
  andWhere: jest.fn().mockReturnThis(),
  orderBy: jest.fn().mockReturnThis(),
  skip: jest.fn().mockReturnThis(),
  take: jest.fn().mockReturnThis(),
  getManyAndCount: jest.fn(),
} as unknown as jest.Mocked<SelectQueryBuilder<T>>);

describe('ToolLoanService', () => {
  let service: ToolLoanService;
  let loanRepository: jest.Mocked<Repository<ToolLoan>>;
  let assetRepository: jest.Mocked<Repository<Asset>>;

  const mockTenantId = 'tenant-123';
  const mockUserId = 'user-456';

  const mockTool: Partial<Asset> = {
    id: 'tool-1',
    tenantId: mockTenantId,
    name: 'Taladro Industrial',
    assetType: 'tool',
    status: 'available',
  };

  const mockLoan: Partial<ToolLoan> = {
    id: 'loan-1',
    tenantId: mockTenantId,
    toolId: 'tool-1',
    employeeId: 'emp-1',
    employeeName: 'Juan Perez',
    status: 'active',
    loanDate: new Date('2026-01-15'),
    expectedReturnDate: new Date('2026-01-30'),
  };

  beforeEach(() => {
    loanRepository = createMockRepository<ToolLoan>();
    assetRepository = createMockRepository<Asset>();

    const mockDataSource = {
      getRepository: jest.fn().mockImplementation((entity) => {
        if (entity === ToolLoan) return loanRepository;
        if (entity === Asset) return assetRepository;
        return createMockRepository();
      }),
    };

    service = new ToolLoanService(mockDataSource as any);
  });

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

  describe('create', () => {
    const createDto: CreateToolLoanDto = {
      toolId: 'tool-1',
      employeeId: 'emp-1',
      employeeName: 'Juan Perez',
      loanDate: new Date('2026-01-15'),
      expectedReturnDate: new Date('2026-01-30'),
    };

    it('should create a new tool loan', async () => {
      assetRepository.findOne.mockResolvedValue(mockTool as Asset);
      loanRepository.findOne.mockResolvedValue(null); // No active loan
      loanRepository.create.mockReturnValue(mockLoan as ToolLoan);
      loanRepository.save.mockResolvedValue(mockLoan as ToolLoan);
      assetRepository.update.mockResolvedValue({ affected: 1 } as any);

      const result = await service.create(mockTenantId, createDto, mockUserId);

      expect(loanRepository.save).toHaveBeenCalled();
      expect(assetRepository.update).toHaveBeenCalled(); // Tool status updated
      expect(result).toEqual(mockLoan);
    });

    it('should throw error if tool not found', async () => {
      assetRepository.findOne.mockResolvedValue(null);

      await expect(service.create(mockTenantId, createDto, mockUserId))
        .rejects.toThrow('Tool not found');
    });

    it('should throw error if tool already has active loan', async () => {
      assetRepository.findOne.mockResolvedValue(mockTool as Asset);
      loanRepository.findOne.mockResolvedValue(mockLoan as ToolLoan);

      await expect(service.create(mockTenantId, createDto, mockUserId))
        .rejects.toThrow('Tool already has an active loan');
    });
  });

  describe('returnTool', () => {
    const returnDto: ReturnToolDto = {
      actualReturnDate: new Date('2026-01-28'),
      conditionIn: 'good',
      status: 'returned',
    };

    it('should return tool and update status', async () => {
      loanRepository.findOne.mockResolvedValue(mockLoan as ToolLoan);
      loanRepository.save.mockResolvedValue({ ...mockLoan, status: 'returned' } as ToolLoan);
      assetRepository.update.mockResolvedValue({ affected: 1 } as any);

      const result = await service.returnTool(mockTenantId, 'loan-1', returnDto, mockUserId);

      expect(loanRepository.save).toHaveBeenCalled();
      expect(assetRepository.update).toHaveBeenCalledWith(
        { id: mockLoan.toolId, tenantId: mockTenantId },
        expect.objectContaining({ status: 'available' })
      );
      expect(result?.status).toBe('returned');
    });

    it('should mark tool as in_maintenance if returned damaged', async () => {
      loanRepository.findOne.mockResolvedValue(mockLoan as ToolLoan);
      loanRepository.save.mockResolvedValue({ ...mockLoan, status: 'damaged' } as ToolLoan);
      assetRepository.update.mockResolvedValue({ affected: 1 } as any);

      await service.returnTool(mockTenantId, 'loan-1', { ...returnDto, status: 'damaged' }, mockUserId);

      expect(assetRepository.update).toHaveBeenCalledWith(
        { id: mockLoan.toolId, tenantId: mockTenantId },
        expect.objectContaining({ status: 'in_maintenance' })
      );
    });
  });

  describe('markAsLost', () => {
    it('should mark loan as lost and tool as inactive', async () => {
      loanRepository.findOne.mockResolvedValue(mockLoan as ToolLoan);
      loanRepository.save.mockResolvedValue({ ...mockLoan, status: 'lost' } as ToolLoan);
      assetRepository.update.mockResolvedValue({ affected: 1 } as any);

      const result = await service.markAsLost(mockTenantId, 'loan-1', 'Extraviado en obra', mockUserId);

      expect(result?.status).toBe('lost');
      expect(assetRepository.update).toHaveBeenCalledWith(
        { id: mockLoan.toolId, tenantId: mockTenantId },
        { status: 'inactive' }
      );
    });
  });

  describe('getStatistics', () => {
    it('should return loan statistics', async () => {
      loanRepository.count.mockResolvedValueOnce(5)  // active
        .mockResolvedValueOnce(20) // returned
        .mockResolvedValueOnce(2)  // overdue
        .mockResolvedValueOnce(1)  // lost
        .mockResolvedValueOnce(3); // damaged

      const stats = await service.getStatistics(mockTenantId);

      expect(stats).toEqual({
        active: 5,
        returned: 20,
        overdue: 2,
        lost: 1,
        damaged: 3,
      });
    });
  });
});
