/**
 * InspectionService Unit Tests
 * Module: Quality
 * Project: erp-construccion
 */

import { Repository } from 'typeorm';
import { InspectionService, CreateInspectionDto, RecordResultDto } from '../services/inspection.service';
import { Inspection, InspectionStatus } from '../entities/inspection.entity';
import { InspectionResult } from '../entities/inspection-result.entity';
import { NonConformity } from '../entities/non-conformity.entity';
import { Checklist } from '../entities/checklist.entity';

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

describe('InspectionService', () => {
  let service: InspectionService;
  let inspectionRepository: jest.Mocked<Repository<Inspection>>;
  let resultRepository: jest.Mocked<Repository<InspectionResult>>;
  let nonConformityRepository: jest.Mocked<Repository<NonConformity>>;
  let checklistRepository: jest.Mocked<Repository<Checklist>>;

  const mockTenantId = 'tenant-123';
  const mockUserId = 'user-456';
  const mockCtx = { tenantId: mockTenantId, userId: mockUserId };

  const mockChecklist: Partial<Checklist> = {
    id: 'checklist-1',
    tenantId: mockTenantId,
    name: 'Quality Checklist',
    isActive: true,
    deletedAt: null,
    items: [
      { id: 'item-1', isActive: true } as any,
      { id: 'item-2', isActive: true } as any,
    ],
  };

  const mockInspection: Partial<Inspection> = {
    id: 'inspection-1',
    tenantId: mockTenantId,
    checklistId: 'checklist-1',
    loteId: 'lote-1',
    inspectionNumber: 'INS-2601-ABC123',
    inspectionDate: new Date('2026-02-03'),
    inspectorId: 'inspector-1',
    status: 'pending' as InspectionStatus,
    totalItems: 2,
  };

  beforeEach(() => {
    inspectionRepository = createMockRepository<Inspection>();
    resultRepository = createMockRepository<InspectionResult>();
    nonConformityRepository = createMockRepository<NonConformity>();
    checklistRepository = createMockRepository<Checklist>();

    service = new InspectionService(
      inspectionRepository,
      resultRepository,
      nonConformityRepository,
      checklistRepository
    );
  });

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

  describe('create', () => {
    const createDto: CreateInspectionDto = {
      checklistId: 'checklist-1',
      loteId: 'lote-1',
      inspectionDate: new Date('2026-02-03'),
      inspectorId: 'inspector-1',
      notes: 'Test inspection',
    };

    it('should create inspection successfully', async () => {
      checklistRepository.findOne.mockResolvedValue(mockChecklist as Checklist);
      inspectionRepository.create.mockReturnValue(mockInspection as Inspection);
      inspectionRepository.save.mockResolvedValue(mockInspection as Inspection);

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

      expect(checklistRepository.findOne).toHaveBeenCalledWith({
        where: expect.objectContaining({
          id: createDto.checklistId,
          tenantId: mockTenantId,
          isActive: true,
          deletedAt: null,
        }),
        relations: ['items'],
      });
      expect(inspectionRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          tenantId: mockTenantId,
          checklistId: createDto.checklistId,
          loteId: createDto.loteId,
          status: 'pending',
        })
      );
      expect(result).toEqual(mockInspection);
    });

    it('should throw error when checklist not found', async () => {
      checklistRepository.findOne.mockResolvedValue(null);

      await expect(service.create(mockCtx, createDto)).rejects.toThrow(
        'Checklist not found or inactive'
      );
    });

    it('should calculate total items from active checklist items', async () => {
      checklistRepository.findOne.mockResolvedValue(mockChecklist as Checklist);
      inspectionRepository.create.mockReturnValue(mockInspection as Inspection);
      inspectionRepository.save.mockResolvedValue(mockInspection as Inspection);

      await service.create(mockCtx, createDto);

      expect(inspectionRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          totalItems: 2,
        })
      );
    });
  });

  describe('findById', () => {
    it('should return inspection when found', async () => {
      inspectionRepository.findOne.mockResolvedValue(mockInspection as Inspection);

      const result = await service.findById(mockCtx, 'inspection-1');

      expect(result).toEqual(mockInspection);
      expect(inspectionRepository.findOne).toHaveBeenCalledWith({
        where: { id: 'inspection-1', tenantId: mockTenantId },
      });
    });

    it('should return null when inspection not found', async () => {
      inspectionRepository.findOne.mockResolvedValue(null);

      const result = await service.findById(mockCtx, 'non-existent');

      expect(result).toBeNull();
    });
  });

  describe('startInspection', () => {
    it('should start pending inspection', async () => {
      const pendingInspection = { ...mockInspection, status: 'pending' as InspectionStatus };
      const startedInspection = { ...pendingInspection, status: 'in_progress' as InspectionStatus };

      inspectionRepository.findOne.mockResolvedValue(pendingInspection as Inspection);
      inspectionRepository.save.mockResolvedValue(startedInspection as Inspection);

      const result = await service.startInspection(mockCtx, 'inspection-1');

      expect(result?.status).toBe('in_progress');
      expect(inspectionRepository.save).toHaveBeenCalled();
    });

    it('should throw error when inspection is not pending', async () => {
      const inProgressInspection = { ...mockInspection, status: 'in_progress' as InspectionStatus };
      inspectionRepository.findOne.mockResolvedValue(inProgressInspection as Inspection);

      await expect(service.startInspection(mockCtx, 'inspection-1')).rejects.toThrow(
        'Can only start pending inspections'
      );
    });

    it('should return null when inspection not found', async () => {
      inspectionRepository.findOne.mockResolvedValue(null);

      const result = await service.startInspection(mockCtx, 'non-existent');

      expect(result).toBeNull();
    });
  });

  describe('recordResult', () => {
    const resultDto: RecordResultDto = {
      checklistItemId: 'item-1',
      result: 'passed',
      observations: 'Looks good',
    };

    it('should record passed result', async () => {
      const inProgressInspection = { ...mockInspection, status: 'in_progress' as InspectionStatus };
      const mockResult: Partial<InspectionResult> = {
        id: 'result-1',
        tenantId: mockTenantId,
        inspectionId: 'inspection-1',
        checklistItemId: 'item-1',
        result: 'passed',
      };

      inspectionRepository.findOne.mockResolvedValue(inProgressInspection as Inspection);
      resultRepository.findOne.mockResolvedValue(null);
      resultRepository.create.mockReturnValue(mockResult as InspectionResult);
      resultRepository.save.mockResolvedValue(mockResult as InspectionResult);

      const result = await service.recordResult(mockCtx, 'inspection-1', resultDto);

      expect(result.result).toBe('passed');
      expect(nonConformityRepository.create).not.toHaveBeenCalled();
    });

    it('should create non-conformity when result is failed', async () => {
      const failedDto: RecordResultDto = {
        checklistItemId: 'item-1',
        result: 'failed',
        observations: 'Defect found',
      };
      const inProgressInspection = { ...mockInspection, status: 'in_progress' as InspectionStatus };
      const mockResult: Partial<InspectionResult> = {
        id: 'result-1',
        result: 'failed',
      };
      const mockNC: Partial<NonConformity> = {
        id: 'nc-1',
        ncNumber: 'NC-2601-ABCD',
        status: 'open',
      };

      inspectionRepository.findOne.mockResolvedValue(inProgressInspection as Inspection);
      resultRepository.findOne.mockResolvedValue(null);
      resultRepository.create.mockReturnValue(mockResult as InspectionResult);
      resultRepository.save.mockResolvedValue(mockResult as InspectionResult);
      nonConformityRepository.create.mockReturnValue(mockNC as NonConformity);
      nonConformityRepository.save.mockResolvedValue(mockNC as NonConformity);

      const result = await service.recordResult(mockCtx, 'inspection-1', failedDto);

      expect(result.result).toBe('failed');
      expect(nonConformityRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          tenantId: mockTenantId,
          inspectionId: 'inspection-1',
          severity: 'minor',
          status: 'open',
        })
      );
    });

    it('should update existing result instead of creating new', async () => {
      const existingResult: Partial<InspectionResult> = {
        id: 'result-1',
        tenantId: mockTenantId,
        inspectionId: 'inspection-1',
        checklistItemId: 'item-1',
        result: 'failed',
      };
      const inProgressInspection = { ...mockInspection, status: 'in_progress' as InspectionStatus };

      inspectionRepository.findOne.mockResolvedValue(inProgressInspection as Inspection);
      resultRepository.findOne.mockResolvedValue(existingResult as InspectionResult);
      resultRepository.save.mockResolvedValue({ ...existingResult, result: 'passed' } as InspectionResult);

      await service.recordResult(mockCtx, 'inspection-1', resultDto);

      expect(resultRepository.create).not.toHaveBeenCalled();
      expect(resultRepository.save).toHaveBeenCalled();
    });

    it('should throw error when inspection not found', async () => {
      inspectionRepository.findOne.mockResolvedValue(null);

      await expect(service.recordResult(mockCtx, 'non-existent', resultDto)).rejects.toThrow(
        'Inspection not found'
      );
    });

    it('should throw error when inspection is not in progress', async () => {
      const pendingInspection = { ...mockInspection, status: 'pending' as InspectionStatus };
      inspectionRepository.findOne.mockResolvedValue(pendingInspection as Inspection);

      await expect(service.recordResult(mockCtx, 'inspection-1', resultDto)).rejects.toThrow(
        'Can only record results for in-progress inspections'
      );
    });
  });

  describe('completeInspection', () => {
    it('should complete inspection and calculate pass rate', async () => {
      const inProgressInspection = {
        ...mockInspection,
        status: 'in_progress' as InspectionStatus,
        results: [
          { result: 'passed' },
          { result: 'passed' },
          { result: 'failed' },
          { result: 'not_applicable' },
        ],
      };
      const completedInspection = {
        ...inProgressInspection,
        status: 'completed' as InspectionStatus,
        passedItems: 2,
        failedItems: 1,
        passRate: 66.67,
      };

      inspectionRepository.findOne.mockResolvedValue(inProgressInspection as Inspection);
      inspectionRepository.save.mockResolvedValue(completedInspection as unknown as Inspection);

      const result = await service.completeInspection(mockCtx, 'inspection-1');

      expect(result?.status).toBe('completed');
      expect(result?.passedItems).toBe(2);
      expect(result?.failedItems).toBe(1);
      expect(inspectionRepository.save).toHaveBeenCalled();
    });

    it('should throw error when inspection is not in progress', async () => {
      const pendingInspection = { ...mockInspection, status: 'pending' as InspectionStatus };
      inspectionRepository.findOne.mockResolvedValue(pendingInspection as Inspection);

      await expect(service.completeInspection(mockCtx, 'inspection-1')).rejects.toThrow(
        'Can only complete in-progress inspections'
      );
    });
  });

  describe('approveInspection', () => {
    it('should approve completed inspection without critical NCs', async () => {
      const completedInspection = {
        ...mockInspection,
        status: 'completed' as InspectionStatus,
        nonConformities: [
          { severity: 'minor', status: 'verified' },
        ],
      };
      const approvedInspection = {
        ...completedInspection,
        status: 'approved' as InspectionStatus,
        approvedById: mockUserId,
      };

      inspectionRepository.findOne.mockResolvedValue(completedInspection as unknown as Inspection);
      inspectionRepository.save.mockResolvedValue(approvedInspection as unknown as Inspection);

      const result = await service.approveInspection(mockCtx, 'inspection-1');

      expect(result?.status).toBe('approved');
      expect(result?.approvedById).toBe(mockUserId);
    });

    it('should throw error when critical NCs are open', async () => {
      const completedInspection = {
        ...mockInspection,
        status: 'completed' as InspectionStatus,
        nonConformities: [
          { severity: 'critical', status: 'open' },
        ],
      };

      inspectionRepository.findOne.mockResolvedValue(completedInspection as unknown as Inspection);

      await expect(service.approveInspection(mockCtx, 'inspection-1')).rejects.toThrow(
        'Cannot approve inspection with open critical non-conformities'
      );
    });

    it('should throw error when inspection is not completed', async () => {
      const inProgressInspection = { ...mockInspection, status: 'in_progress' as InspectionStatus };
      inspectionRepository.findOne.mockResolvedValue(inProgressInspection as Inspection);

      await expect(service.approveInspection(mockCtx, 'inspection-1')).rejects.toThrow(
        'Can only approve completed inspections'
      );
    });
  });

  describe('rejectInspection', () => {
    it('should reject completed inspection with reason', async () => {
      const completedInspection = {
        ...mockInspection,
        status: 'completed' as InspectionStatus,
      };
      const rejectedInspection = {
        ...completedInspection,
        status: 'rejected' as InspectionStatus,
        rejectionReason: 'Does not meet standards',
      };

      inspectionRepository.findOne.mockResolvedValue(completedInspection as Inspection);
      inspectionRepository.save.mockResolvedValue(rejectedInspection as unknown as Inspection);

      const result = await service.rejectInspection(mockCtx, 'inspection-1', 'Does not meet standards');

      expect(result?.status).toBe('rejected');
      expect(result?.rejectionReason).toBe('Does not meet standards');
    });

    it('should throw error when inspection is not completed', async () => {
      const inProgressInspection = { ...mockInspection, status: 'in_progress' as InspectionStatus };
      inspectionRepository.findOne.mockResolvedValue(inProgressInspection as Inspection);

      await expect(service.rejectInspection(mockCtx, 'inspection-1', 'reason')).rejects.toThrow(
        'Can only reject completed inspections'
      );
    });

    it('should return null when inspection not found', async () => {
      inspectionRepository.findOne.mockResolvedValue(null);

      const result = await service.rejectInspection(mockCtx, 'non-existent', 'reason');

      expect(result).toBeNull();
    });
  });

  describe('findWithFilters', () => {
    it('should return paginated results with filters', async () => {
      const mockQueryBuilder = {
        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().mockResolvedValue([[mockInspection], 1]),
      };

      inspectionRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);

      const result = await service.findWithFilters(
        mockCtx,
        { status: 'pending' as InspectionStatus },
        1,
        20
      );

      expect(result.data).toHaveLength(1);
      expect(result.total).toBe(1);
      expect(result.page).toBe(1);
      expect(result.limit).toBe(20);
      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('insp.status = :status', { status: 'pending' });
    });
  });
});
