/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.gobblin.scheduler;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import com.google.common.base.Function;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.ServiceManager;

import org.apache.gobblin.configuration.ConfigurationKeys;
import org.apache.gobblin.testing.AssertWithBackoff;


/**
 * Unit tests for the job configuration file monitor in {@link org.apache.gobblin.scheduler.JobScheduler}.
 *
 * @author Yinan Li
 */
@Test(enabled=false, groups = {"gobblin.scheduler"})
public class JobConfigFileMonitorTest {

  private static final String JOB_CONFIG_FILE_DIR = "gobblin-test/resource/job-conf";

  private String jobConfigDir;
  private ServiceManager serviceManager;
  private JobScheduler jobScheduler;
  private File newJobConfigFile;

  private class GetNumScheduledJobs implements Function<Void, Integer> {

    @Override
    public Integer apply(Void input) {
      return JobConfigFileMonitorTest.this.jobScheduler.getScheduledJobs().size();
    }
  }

  @BeforeClass
  public void setUp() throws Exception {
    this.jobConfigDir =
        Files.createTempDirectory(String.format("gobblin-test_%s_job-conf", this.getClass().getSimpleName()))
            .toString();

    FileUtils.forceDeleteOnExit(new File(this.jobConfigDir));
    FileUtils.copyDirectory(new File(JOB_CONFIG_FILE_DIR), new File(jobConfigDir));

    Properties properties = new Properties();
    try (Reader schedulerPropsReader = new FileReader("gobblin-test/resource/gobblin.test.properties")) {
      properties.load(schedulerPropsReader);
    }
    properties.setProperty(ConfigurationKeys.JOB_CONFIG_FILE_DIR_KEY, jobConfigDir);
    properties.setProperty(ConfigurationKeys.JOB_CONFIG_FILE_GENERAL_PATH_KEY, jobConfigDir);
    properties.setProperty(ConfigurationKeys.JOB_CONFIG_FILE_MONITOR_POLLING_INTERVAL_KEY, "1000");
    properties.setProperty(ConfigurationKeys.METRICS_ENABLED_KEY, "false");

    SchedulerService quartzService = new SchedulerService(new Properties());
    this.jobScheduler = new JobScheduler(properties, quartzService);
    this.serviceManager = new ServiceManager(Lists.newArrayList(quartzService, this.jobScheduler));
    this.serviceManager.startAsync().awaitHealthy(10, TimeUnit.SECONDS);;
  }

  @Test (enabled=false)
  public void testAddNewJobConfigFile() throws Exception {
    final Logger log = LoggerFactory.getLogger("testAddNewJobConfigFile");
    log.info("testAddNewJobConfigFile: start");
    AssertWithBackoff assertWithBackoff = AssertWithBackoff.create().logger(log).timeoutMs(15000);
    assertWithBackoff.assertEquals(new GetNumScheduledJobs(), 3, "3 scheduled jobs");

    /* Set a time gap, to let the monitor recognize the "3-file" status as old status,
    so that new added file can be discovered */
    Thread.sleep(1000);

    // Create a new job configuration file by making a copy of an existing
    // one and giving a different job name
    Properties jobProps = new Properties();
    jobProps.load(new FileReader(new File(this.jobConfigDir, "GobblinTest1.pull")));
    jobProps.setProperty(ConfigurationKeys.JOB_NAME_KEY, "Gobblin-test-new");
    this.newJobConfigFile = new File(this.jobConfigDir, "Gobblin-test-new.pull");
    jobProps.store(new FileWriter(this.newJobConfigFile), null);

    assertWithBackoff.assertEquals(new GetNumScheduledJobs(), 4, "4 scheduled jobs");

    Set<String> jobNames = Sets.newHashSet(this.jobScheduler.getScheduledJobs());
    Set<String> expectedJobNames =
        ImmutableSet.<String>builder()
          .add("GobblinTest1", "GobblinTest2", "GobblinTest3", "Gobblin-test-new")
          .build();

    Assert.assertEquals(jobNames, expectedJobNames);
    log.info("testAddNewJobConfigFile: end");
  }

  @Test(enabled=false, dependsOnMethods = {"testAddNewJobConfigFile"})
  public void testChangeJobConfigFile()
      throws Exception {
    final Logger log = LoggerFactory.getLogger("testChangeJobConfigFile");
    log.info("testChangeJobConfigFile: start");
    Assert.assertEquals(this.jobScheduler.getScheduledJobs().size(), 4);

    // Make a change to the new job configuration file
    Properties jobProps = new Properties();
    jobProps.load(new FileReader(this.newJobConfigFile));
    jobProps.setProperty(ConfigurationKeys.JOB_COMMIT_POLICY_KEY, "partial");
    jobProps.setProperty(ConfigurationKeys.JOB_NAME_KEY, "Gobblin-test-new2");
    jobProps.store(new FileWriter(this.newJobConfigFile), null);

    AssertWithBackoff.create()
        .logger(log)
        .timeoutMs(30000)
        .assertEquals(new GetNumScheduledJobs(), 4, "4 scheduled jobs");

    final Set<String> expectedJobNames =
        ImmutableSet.<String>builder()
          .add("GobblinTest1", "GobblinTest2", "GobblinTest3", "Gobblin-test-new2")
          .build();
    AssertWithBackoff.create()
      .logger(log)
      .timeoutMs(30000)
      .assertEquals(new Function<Void, Set<String>>() {
          @Override public Set<String> apply(Void input) {
            return Sets.newHashSet(JobConfigFileMonitorTest.this.jobScheduler.getScheduledJobs());
          }
        }, expectedJobNames, "Job change detected");
    log.info("testChangeJobConfigFile: end");
  }

  @Test(enabled=false, dependsOnMethods = {"testChangeJobConfigFile"})
  public void testUnscheduleJob()
      throws Exception {
    final Logger log = LoggerFactory.getLogger("testUnscheduleJob");
    log.info("testUnscheduleJob: start");
    Assert.assertEquals(this.jobScheduler.getScheduledJobs().size(), 4);

    // Disable the new job by setting job.disabled=true
    Properties jobProps = new Properties();
    jobProps.load(new FileReader(this.newJobConfigFile));
    jobProps.setProperty(ConfigurationKeys.JOB_DISABLED_KEY, "true");
    jobProps.store(new FileWriter(this.newJobConfigFile), null);

    AssertWithBackoff.create()
        .logger(log)
        .timeoutMs(7500)
        .assertEquals(new GetNumScheduledJobs(), 3, "3 scheduled jobs");

    Set<String> jobNames = Sets.newHashSet(this.jobScheduler.getScheduledJobs());
    Assert.assertEquals(jobNames.size(), 3);
    Assert.assertTrue(jobNames.contains("GobblinTest1"));
    Assert.assertTrue(jobNames.contains("GobblinTest2"));
    Assert.assertTrue(jobNames.contains("GobblinTest3"));
    log.info("testUnscheduleJob: end");
  }

  @AfterClass
  public void tearDown()
      throws TimeoutException, IOException {
    if (jobConfigDir != null) {
      FileUtils.forceDelete(new File(jobConfigDir));
    }
    this.serviceManager.stopAsync().awaitStopped(30, TimeUnit.SECONDS);
  }
}
