import { Test, TestingModule } from '@nestjs/testing';
import {
  Configuration,
  ConfigurationNotFoundException,
} from '../../configuration.entity';
import { EEnvVariable } from '../../enums/env-variable.enum';
import { EConfigurationProperty } from '../../helpers/configuration-type.helper';
import {
  ConfigurationValidationService,
  OPTIONAL_CONFIGURATION_PROPERTIES,
  OPTIONAL_ENV_PROPERTIES,
} from './configuration-validation.service';

describe('ConfigurationValidationService', () => {
  let service: ConfigurationValidationService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [ConfigurationValidationService],
    }).compile();

    service = module.get<ConfigurationValidationService>(
      ConfigurationValidationService
    );
  });

  afterEach(() => {
    for (const property of Object.values(EEnvVariable)) {
      delete process.env[property];
    }
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  it('should have defined validators for every configuration property', () => {
    for (const property of Object.values(EConfigurationProperty)) {
      expect(service.VALIDATIONS[property]).toBeDefined();
      expect(typeof service.VALIDATIONS[property]).toBe('function');
    }
  });

  it('should have defined validators for every environment variable', () => {
    for (const property of Object.values(EEnvVariable)) {
      expect(service.ENV_VALIDATIONS[property]).toBeDefined();
      expect(typeof service.ENV_VALIDATIONS[property]).toBe('function');
    }
  });

  it('should call validation for every property', async () => {
    for (const validation in service.VALIDATIONS) {
      service.VALIDATIONS[validation] = jest.fn().mockReturnValue('');
    }

    Configuration.get = jest.fn().mockResolvedValue('');

    await service.validate();

    for (const property of Object.values(EConfigurationProperty)) {
      expect(service.VALIDATIONS[property]).toBeCalledTimes(1);
    }
  });

  it('should call validation for every environment variable', async () => {
    for (const validation in service.ENV_VALIDATIONS) {
      service.ENV_VALIDATIONS[validation] = jest.fn().mockReturnValue('');
    }

    for (const property of Object.values(EEnvVariable)) {
      process.env[property] = '';
    }

    await service.validateEnv();

    for (const property of Object.values(EEnvVariable)) {
      expect(service.ENV_VALIDATIONS[property]).toBeCalledTimes(1);
    }
  });

  it('should allow missing or null value for optional property', async () => {
    for (const validation in service.VALIDATIONS) {
      service.VALIDATIONS[validation] = jest.fn().mockReturnValue('');
    }

    Configuration.get = jest
      .fn()
      .mockImplementation((property: EConfigurationProperty) => {
        return OPTIONAL_CONFIGURATION_PROPERTIES.includes(property) ? null : '';
      });
    await expect(service.validate()).resolves.not.toThrow();

    Configuration.get = jest
      .fn()
      .mockImplementation((property: EConfigurationProperty) => {
        if (OPTIONAL_CONFIGURATION_PROPERTIES.includes(property)) {
          throw new ConfigurationNotFoundException();
        }
      });
    await expect(service.validate()).resolves.not.toThrow();
  });

  it('should allow missing or null value for optional environment variable', async () => {
    for (const validation in service.ENV_VALIDATIONS) {
      service.ENV_VALIDATIONS[validation] = jest.fn().mockReturnValue('');
    }

    for (const property of Object.values(EEnvVariable)) {
      process.env[property] = OPTIONAL_ENV_PROPERTIES.includes(property)
        ? undefined
        : '';
    }

    await expect(service.validateEnv()).resolves.not.toThrow();
  });

  it('should NOT allow missing or null value for required properties', async () => {
    for (const validation in service.VALIDATIONS) {
      service.VALIDATIONS[validation] = jest.fn().mockReturnValue('');
    }

    Configuration.get = jest
      .fn()
      .mockImplementation((property: EConfigurationProperty) => {
        return !OPTIONAL_CONFIGURATION_PROPERTIES.includes(property)
          ? null
          : '';
      });
    await expect(service.validate()).rejects.toThrow();

    Configuration.get = jest
      .fn()
      .mockImplementation((property: EConfigurationProperty) => {
        if (!OPTIONAL_CONFIGURATION_PROPERTIES.includes(property)) {
          throw new ConfigurationNotFoundException();
        }
      });
    await expect(service.validate()).rejects.toThrow();
  });

  it('should NOT allow missing or null value for required environment variable', async () => {
    for (const validation in service.ENV_VALIDATIONS) {
      service.ENV_VALIDATIONS[validation] = jest.fn().mockReturnValue('');
    }

    for (const property of Object.values(EEnvVariable)) {
      if (OPTIONAL_ENV_PROPERTIES.includes(property)) {
        process.env[property] = '';
      }
    }

    await expect(service.validateEnv()).rejects.toThrow();
  });

  it('MOTD_TIME validator should allow valid values', async () => {
    const generateValidateFn = (value: string) =>
      ConfigurationValidationService.validateMotdTime(value);

    await expect(
      generateValidateFn('2021-07-11T13:00:00.000Z|2021-07-11T13:00:00.000Z')
    ).resolves.not.toThrow();
    await expect(
      generateValidateFn('|2021-07-11T13:00:00.000Z')
    ).resolves.not.toThrow();
    await expect(
      generateValidateFn('2021-07-11T13:00:00.000Z|')
    ).resolves.not.toThrow();
    await expect(generateValidateFn('|')).resolves.not.toThrow();
  });

  it('MOTD_TIME validator should NOT allow invalid values', async () => {
    const generateValidateFn = (value: string) => () =>
      ConfigurationValidationService.validateMotdTime(value);

    await expect(generateValidateFn('')).rejects.toThrow();
    await expect(generateValidateFn('123|123')).rejects.toThrow();
    await expect(generateValidateFn('132|')).rejects.toThrow();
    await expect(generateValidateFn('|123')).rejects.toThrow();
    await expect(
      generateValidateFn('2021-07-11T13:00:00.000Z|123')
    ).rejects.toThrow();
    await expect(
      generateValidateFn('123|2021-07-11T13:00:00.000Z')
    ).rejects.toThrow();
  });
});
