/**
 * KpiConfigService Unit Tests
 * Module: Reports (MAI-006)
 * GAP: GAP-001
 */

import { Repository, SelectQueryBuilder } from 'typeorm';
import { KpiConfigService, CreateKpiConfigDto, CreateKpiValueDto } from '../services/kpi-config.service';
import { KpiConfig } from '../entities/kpi-config.entity';
import { KpiValue } from '../entities/kpi-value.entity';

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

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

describe('KpiConfigService', () => {
  let service: KpiConfigService;
  let configRepository: jest.Mocked<Repository<KpiConfig>>;
  let valueRepository: jest.Mocked<Repository<KpiValue>>;

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

  const mockKpiConfig: Partial<KpiConfig> = {
    id: 'kpi-1',
    tenantId: mockTenantId,
    code: 'SPI',
    name: 'Schedule Performance Index',
    category: 'progress',
    module: 'construction',
    formula: 'earned_value / planned_value',
    formulaType: 'expression',
    targetValue: 1.0,
    thresholdGreen: 0.95,
    thresholdYellow: 0.85,
    isActive: true,
    isSystem: false,
  };

  beforeEach(() => {
    configRepository = createMockRepository<KpiConfig>();
    valueRepository = createMockRepository<KpiValue>();

    const mockDataSource = {
      getRepository: jest.fn().mockImplementation((entity) => {
        if (entity === KpiConfig) return configRepository;
        if (entity === KpiValue) return valueRepository;
        return createMockRepository();
      }),
    };

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

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

  describe('create', () => {
    const createDto: CreateKpiConfigDto = {
      code: 'SPI',
      name: 'Schedule Performance Index',
      category: 'progress',
      module: 'construction',
      formula: 'earned_value / planned_value',
      targetValue: 1.0,
      thresholdGreen: 0.95,
      thresholdYellow: 0.85,
    };

    it('should create a new KPI config', async () => {
      configRepository.findOne.mockResolvedValue(null);
      configRepository.create.mockReturnValue(mockKpiConfig as KpiConfig);
      configRepository.save.mockResolvedValue(mockKpiConfig as KpiConfig);

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

      expect(configRepository.findOne).toHaveBeenCalledWith({
        where: { tenantId: mockTenantId, code: createDto.code },
      });
      expect(configRepository.create).toHaveBeenCalled();
      expect(configRepository.save).toHaveBeenCalled();
      expect(result).toEqual(mockKpiConfig);
    });

    it('should throw error if KPI code already exists', async () => {
      configRepository.findOne.mockResolvedValue(mockKpiConfig as KpiConfig);

      await expect(service.create(mockTenantId, createDto, mockUserId))
        .rejects.toThrow(`KPI with code ${createDto.code} already exists`);
    });
  });

  describe('findById', () => {
    it('should return KPI config by ID', async () => {
      configRepository.findOne.mockResolvedValue(mockKpiConfig as KpiConfig);

      const result = await service.findById(mockTenantId, 'kpi-1');

      expect(configRepository.findOne).toHaveBeenCalledWith({
        where: { id: 'kpi-1', tenantId: mockTenantId, deletedAt: undefined },
      });
      expect(result).toEqual(mockKpiConfig);
    });

    it('should return null if not found', async () => {
      configRepository.findOne.mockResolvedValue(null);

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

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

  describe('findAll', () => {
    it('should return paginated KPI configs', async () => {
      const mockQueryBuilder = createMockQueryBuilder<KpiConfig>();
      mockQueryBuilder.getManyAndCount.mockResolvedValue([[mockKpiConfig as KpiConfig], 1]);
      configRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);

      const result = await service.findAll(mockTenantId, {}, { page: 1, limit: 20 });

      expect(result.data).toHaveLength(1);
      expect(result.total).toBe(1);
      expect(result.page).toBe(1);
      expect(result.totalPages).toBe(1);
    });

    it('should filter by category', async () => {
      const mockQueryBuilder = createMockQueryBuilder<KpiConfig>();
      mockQueryBuilder.getManyAndCount.mockResolvedValue([[], 0]);
      configRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);

      await service.findAll(mockTenantId, { category: 'progress' }, { page: 1, limit: 20 });

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'kpi.category = :category',
        { category: 'progress' }
      );
    });
  });

  describe('update', () => {
    it('should update KPI config', async () => {
      configRepository.findOne.mockResolvedValue(mockKpiConfig as KpiConfig);
      configRepository.save.mockResolvedValue({ ...mockKpiConfig, name: 'Updated' } as KpiConfig);

      const result = await service.update(mockTenantId, 'kpi-1', { name: 'Updated' }, mockUserId);

      expect(configRepository.save).toHaveBeenCalled();
      expect(result?.name).toBe('Updated');
    });

    it('should throw error when modifying system KPI', async () => {
      const systemKpi = { ...mockKpiConfig, isSystem: true };
      configRepository.findOne.mockResolvedValue(systemKpi as KpiConfig);

      await expect(service.update(mockTenantId, 'kpi-1', { isActive: false }, mockUserId))
        .rejects.toThrow('Cannot modify system KPIs');
    });
  });

  describe('delete', () => {
    it('should soft delete KPI config', async () => {
      configRepository.findOne.mockResolvedValue(mockKpiConfig as KpiConfig);
      configRepository.update.mockResolvedValue({ affected: 1 } as any);

      const result = await service.delete(mockTenantId, 'kpi-1', mockUserId);

      expect(configRepository.update).toHaveBeenCalled();
      expect(result).toBe(true);
    });

    it('should throw error when deleting system KPI', async () => {
      const systemKpi = { ...mockKpiConfig, isSystem: true };
      configRepository.findOne.mockResolvedValue(systemKpi as KpiConfig);

      await expect(service.delete(mockTenantId, 'kpi-1', mockUserId))
        .rejects.toThrow('Cannot delete system KPIs');
    });
  });

  describe('recordValue', () => {
    const createValueDto: CreateKpiValueDto = {
      kpiId: 'kpi-1',
      periodStart: new Date('2026-01-01'),
      periodEnd: new Date('2026-01-31'),
      value: 0.92,
      previousValue: 0.88,
    };

    it('should record KPI value with calculated status', async () => {
      configRepository.findOne.mockResolvedValue(mockKpiConfig as KpiConfig);

      const mockValue: Partial<KpiValue> = {
        id: 'value-1',
        tenantId: mockTenantId,
        kpiId: 'kpi-1',
        value: 0.92,
        status: 'yellow',
        isOnTarget: false,
      };
      valueRepository.create.mockReturnValue(mockValue as KpiValue);
      valueRepository.save.mockResolvedValue(mockValue as KpiValue);

      const result = await service.recordValue(mockTenantId, createValueDto, mockUserId);

      expect(valueRepository.create).toHaveBeenCalled();
      expect(valueRepository.save).toHaveBeenCalled();
      expect(result).toBeDefined();
    });

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

      await expect(service.recordValue(mockTenantId, createValueDto, mockUserId))
        .rejects.toThrow('KPI configuration not found');
    });
  });
});
