/**
 * Copyright 2016-2017 Linagora, Université Joseph Fourier, Floralis
 *
 * The present code is developed in the scope of the joint LINAGORA -
 * Université Joseph Fourier - Floralis research program and is designated
 * as a "Result" pursuant to the terms and conditions of the LINAGORA
 * - Université Joseph Fourier - Floralis research program. Each copyright
 * holder of Results enumerated here above fully & independently holds complete
 * ownership of the complete Intellectual Property rights applicable to the whole
 * of said Results, and may freely exploit it in any manner which does not infringe
 * the moral rights of the other copyright holders.
 *
 * Licensed 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 net.roboconf.dm.scheduler.internal;

import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.Mockito;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerKey;
import org.quartz.impl.matchers.GroupMatcher;

import net.roboconf.core.internal.tests.TestApplication;
import net.roboconf.core.model.beans.Application;
import net.roboconf.core.model.runtime.ScheduledJob;
import net.roboconf.core.utils.Utils;
import net.roboconf.dm.internal.test.TestManagerWrapper;
import net.roboconf.dm.management.ManagedApplication;
import net.roboconf.dm.management.Manager;
import net.roboconf.dm.management.events.IDmListener;

/**
 * @author Vincent Zurczak - Linagora
 */
public class RoboconfSchedulerTest {

	@Rule
	public TemporaryFolder folder = new TemporaryFolder();
	private RoboconfScheduler scheduler;
	private Manager manager;


	@Before
	public void prepare() throws Exception {

		// Prepare the manager
		this.manager = Mockito.spy( new Manager());
		this.manager.configurationMngr().setWorkingDirectory( this.folder.newFolder());

		this.scheduler = new RoboconfScheduler();
		this.scheduler.manager = this.manager;

		// Configure applications and commands
		TestManagerWrapper wrapper = new TestManagerWrapper( this.manager );
		Application app = new TestApplication().name( "app" );
		app.setDirectory( this.folder.newFolder());
		wrapper.addManagedApplication( new ManagedApplication( app ));

		// Register commands
		this.manager.commandsMngr().createOrUpdateCommand( app, "cmd", "" );

		// Ignore all the manager's invocations until now
		Mockito.reset( this.manager );
	}


	@After
	public void clean() throws Exception {
		this.scheduler.stop();
	}


	@Test
	public void testStartAndStop() throws Exception {

		Assert.assertNull( this.scheduler.dmListener );
		Assert.assertNull( this.scheduler.scheduler );

		Mockito.verifyZeroInteractions( this.manager );
		this.scheduler.start();

		Assert.assertNotNull( this.scheduler.dmListener );
		Assert.assertNotNull( this.scheduler.scheduler );

		Assert.assertTrue( this.scheduler.scheduler.isStarted());
		Mockito.verify( this.manager, Mockito.times( 1 )).listenerAppears( this.scheduler.dmListener );
		Mockito.verify( this.manager, Mockito.atLeast( 1 )).configurationMngr();

		String dmPath = this.manager.configurationMngr().getWorkingDirectory().getAbsolutePath();
		String schedulerPath = this.scheduler.getSchedulerDirectory().getAbsolutePath();
		Assert.assertTrue( schedulerPath.startsWith( dmPath ));

		this.scheduler.stop();
		Mockito.verify( this.manager, Mockito.times( 1 )).listenerAppears( Mockito.any( IDmListener.class ));
		Mockito.verify( this.manager, Mockito.times( 1 )).listenerDisappears( Mockito.any( IDmListener.class ));

		Assert.assertNull( this.scheduler.dmListener );
		Assert.assertNull( this.scheduler.scheduler );
	}


	@Test
	public void testStop_noManager() throws Exception {

		this.scheduler.manager = null;
		this.scheduler.stop();

		Assert.assertNull( this.scheduler.dmListener );
		Assert.assertNull( this.scheduler.scheduler );
	}


	@Test
	public void testSaveAndLoadJobs() throws Exception {

		// Start the scheduler and halts triggers
		this.scheduler.start();
		this.scheduler.scheduler.standby();
		File schedulerDirectory = this.scheduler.getSchedulerDirectory();

		Set<JobKey> jobKeys = this.scheduler.scheduler.getJobKeys( GroupMatcher.anyJobGroup());
		Assert.assertEquals( 0, jobKeys.size());
		Assert.assertEquals( 0, Utils.listAllFiles( schedulerDirectory ).size());

		// Create several jobs
		final int max = 5;
		String[] jobIds = new String[ max ];
		for( int i=0; i<max; i++ ) {
			jobIds[ i ] = this.scheduler.saveJob( null, "job " + i, "cmd", "0 0 0 ? 1 *", "app" );
		}

		jobKeys = this.scheduler.scheduler.getJobKeys( GroupMatcher.anyJobGroup());
		Assert.assertEquals( max, jobKeys.size());
		Assert.assertEquals( max, Utils.listAllFiles( schedulerDirectory ).size());

		// Delete a job
		this.scheduler.deleteJob( jobIds[ 3 ]);
		jobKeys = this.scheduler.scheduler.getJobKeys( GroupMatcher.anyJobGroup());
		Assert.assertEquals( max - 1, jobKeys.size());
		Assert.assertEquals( max - 1, Utils.listAllFiles( schedulerDirectory ).size());

		// Find job properties
		ScheduledJob job = this.scheduler.findJobProperties( jobIds[ 1 ]);
		Assert.assertNotNull( job );
		Assert.assertEquals( "app", job.getAppName());
		Assert.assertEquals( "cmd", job.getCmdName());
		Assert.assertEquals( "0 0 0 ? 1 *", job.getCron());
		Assert.assertEquals( "job 1", job.getJobName());

		// Find the properties of a non-existing job
		job = this.scheduler.findJobProperties( "job M" );
		Assert.assertNull( job );
	}


	@Test
	public void testSaveAndLoadJobs_invalidCron() throws Exception {

		// Start the scheduler and halts triggers
		this.scheduler.start();
		this.scheduler.scheduler.standby();
		File schedulerDirectory = this.scheduler.getSchedulerDirectory();

		Assert.assertEquals( 0, this.scheduler.listJobs().size());
		Set<JobKey> jobKeys = this.scheduler.scheduler.getJobKeys( GroupMatcher.anyJobGroup());
		Assert.assertEquals( 0, jobKeys.size());
		Assert.assertEquals( 0, Utils.listAllFiles( schedulerDirectory ).size());

		// Create an invalid job
		try {
			this.scheduler.saveJob( null, "job", "cmd", "0 0 0 ? invalid *", "app" );
			Assert.fail( "An I/O exception was expected." );

		} catch( IOException e ) {
			// nothing
		}

		// There should not be any job: in the scheduler...
		jobKeys = this.scheduler.scheduler.getJobKeys( GroupMatcher.anyJobGroup());
		Assert.assertEquals( 0, jobKeys.size());
		Assert.assertEquals( 0, Utils.listAllFiles( schedulerDirectory ).size());

		// ... and listed by Roboconf
		Assert.assertEquals( 0, this.scheduler.listJobs().size());
	}


	@Test
	public void testDeleteJob_inexisting() throws Exception {

		// Start the scheduler and halts triggers
		this.scheduler.start();
		this.scheduler.scheduler.standby();
		File schedulerDirectory = this.scheduler.getSchedulerDirectory();

		Set<JobKey> jobKeys = this.scheduler.scheduler.getJobKeys( GroupMatcher.anyJobGroup());
		Assert.assertEquals( 0, jobKeys.size());
		Assert.assertEquals( 0, Utils.listAllFiles( schedulerDirectory ).size());

		// Delete a job
		this.scheduler.deleteJob( "job 3" );
		jobKeys = this.scheduler.scheduler.getJobKeys( GroupMatcher.anyJobGroup());
		Assert.assertEquals( 0, jobKeys.size());
		Assert.assertEquals( 0, Utils.listAllFiles( schedulerDirectory ).size());
	}


	@Test( expected = IllegalArgumentException.class )
	public void testSaveJob_invalidApplicationName() throws Exception {

		// Start the scheduler and halts triggers
		this.scheduler.start();
		this.scheduler.scheduler.standby();

		// Create a job
		this.scheduler.saveJob( null, "my job", "cmd", "0 0 0 ? 1 *", "app that does nto exist" );
	}


	@Test( expected = IllegalArgumentException.class )
	public void testSaveJob_invalidCommandName() throws Exception {

		// Start the scheduler and halts triggers
		this.scheduler.start();
		this.scheduler.scheduler.standby();

		// Create a job
		try {
			Assert.assertNotNull( this.scheduler.saveJob( null, "my job", "cmd", "0 0 0 ? 1 *", "app" ));

		} catch( Exception e ) {
			Assert.fail( "No failure was expected here, the application was registered in the setup method." );
		}

		// This one will fail
		this.scheduler.saveJob( null, "my job", "cmd that does not exist", "0 0 0 ? 1 *", "app" );
	}


	@Test
	public void testSaveJobAndUpdateJob() throws Exception {

		// Start the scheduler and halts triggers
		this.scheduler.start();
		this.scheduler.scheduler.standby();
		File schedulerDirectory = this.scheduler.getSchedulerDirectory();

		Set<JobKey> jobKeys = this.scheduler.scheduler.getJobKeys( GroupMatcher.anyJobGroup());
		Assert.assertEquals( 0, jobKeys.size());
		Assert.assertEquals( 0, Utils.listAllFiles( schedulerDirectory ).size());

		// Create a job
		String jobId = this.scheduler.saveJob( null, "my job", "cmd", "0 0 0 ? 1 *", "app" );

		jobKeys = this.scheduler.scheduler.getJobKeys( GroupMatcher.anyJobGroup());
		Assert.assertEquals( 1, jobKeys.size());
		Assert.assertEquals( 1, Utils.listAllFiles( schedulerDirectory ).size());

		// Find job properties
		ScheduledJob readJob = this.scheduler.findJobProperties( jobId );
		Assert.assertNotNull( readJob );
		Assert.assertEquals( "app", readJob.getAppName());
		Assert.assertEquals( "cmd", readJob.getCmdName());
		Assert.assertEquals( "0 0 0 ? 1 *", readJob.getCron());
		Assert.assertEquals( "my job", readJob.getJobName());

		// Rename the initial job and the application's name.
		// We also need to make sure the application exists...
		TestManagerWrapper wrapper = new TestManagerWrapper( this.manager );
		Application app = new TestApplication().name( "new app" );
		app.setDirectory( this.folder.newFolder());
		wrapper.addManagedApplication( new ManagedApplication( app ));

		// ... as well as the command.
		this.manager.commandsMngr().createOrUpdateCommand( app, "cmd", "" );

		// Update the job
		jobId = this.scheduler.saveJob( jobId, "my new job", "cmd", "0 0 0 ? 1 *", app.getName());

		// Find job properties
		readJob = this.scheduler.findJobProperties( jobId );
		Assert.assertNotNull( readJob );
		Assert.assertEquals( app.getName(), readJob.getAppName());
		Assert.assertEquals( "cmd", readJob.getCmdName());
		Assert.assertEquals( "0 0 0 ? 1 *", readJob.getCron());
		Assert.assertEquals( "my new job", readJob.getJobName());

		// Delete the job
		this.scheduler.deleteJob( jobId );
		jobKeys = this.scheduler.scheduler.getJobKeys( GroupMatcher.anyJobGroup());
		Assert.assertEquals( 0, jobKeys.size());
		Assert.assertEquals( 0, Utils.listAllFiles( schedulerDirectory ).size());
	}


	@Test
	public void testDeleteJob_noAppNameInJobProperties() throws Exception {

		this.scheduler.scheduler = Mockito.mock( Scheduler.class );

		Properties props = new Properties();
		props.put( RoboconfScheduler.JOB_NAME, "job" );
		props.put( RoboconfScheduler.CMD_NAME, "cmd" );
		props.put( RoboconfScheduler.CRON, "0 0 0 ? 1 *" );

		try {
			File f = this.scheduler.getJobFile( "job" );
			Assert.assertTrue( f.getParentFile().mkdirs());
			Utils.writePropertiesFile( props, f );

		} catch( Exception e ) {
			Assert.fail( "No exception was expected here." );
		}

		this.scheduler.deleteJob( "job" );
		Mockito.verifyZeroInteractions( this.scheduler.scheduler );
	}


	@Test
	public void testSaveJob_invalidProperties_noCron() throws Exception {

		// Start the scheduler and halts triggers
		this.scheduler.start();
		this.scheduler.scheduler.standby();
		File schedulerDirectory = this.scheduler.getSchedulerDirectory();

		Set<JobKey> jobKeys = this.scheduler.scheduler.getJobKeys( GroupMatcher.anyJobGroup());
		Assert.assertEquals( 0, jobKeys.size());
		Assert.assertEquals( 0, Utils.listAllFiles( schedulerDirectory ).size());

		// Save an invalid job (no cron)
		this.scheduler.saveJob( null, "job", "cmd", null, "app" );

		jobKeys = this.scheduler.scheduler.getJobKeys( GroupMatcher.anyJobGroup());
		Assert.assertEquals( 0, jobKeys.size());
		Assert.assertEquals( 0, Utils.listAllFiles( schedulerDirectory ).size());

		// Save an invalid job (no job name)
		this.scheduler.saveJob( null, null, "cmd", "0 0 0 ? 1 *", "app" );

		jobKeys = this.scheduler.scheduler.getJobKeys( GroupMatcher.anyJobGroup());
		Assert.assertEquals( 0, jobKeys.size());
		Assert.assertEquals( 0, Utils.listAllFiles( schedulerDirectory ).size());

		// Save an invalid job (no command name)
		this.scheduler.saveJob( null, "job", null, "0 0 0 ? 1 *", "app" );

		jobKeys = this.scheduler.scheduler.getJobKeys( GroupMatcher.anyJobGroup());
		Assert.assertEquals( 0, jobKeys.size());
		Assert.assertEquals( 0, Utils.listAllFiles( schedulerDirectory ).size());

		// Save an invalid job (no application name)
		this.scheduler.saveJob( null, "job", "cmd", "0 0 0 ? 1 *", null );

		jobKeys = this.scheduler.scheduler.getJobKeys( GroupMatcher.anyJobGroup());
		Assert.assertEquals( 0, jobKeys.size());
		Assert.assertEquals( 0, Utils.listAllFiles( schedulerDirectory ).size());

		// Save an invalid job (nothing at all)
		this.scheduler.saveJob( null, null, null, null, null );

		jobKeys = this.scheduler.scheduler.getJobKeys( GroupMatcher.anyJobGroup());
		Assert.assertEquals( 0, jobKeys.size());
		Assert.assertEquals( 0, Utils.listAllFiles( schedulerDirectory ).size());

		// Save an invalid job (an ID but nothing else)
		this.scheduler.saveJob( "some-id", null, null, null, null );

		jobKeys = this.scheduler.scheduler.getJobKeys( GroupMatcher.anyJobGroup());
		Assert.assertEquals( 0, jobKeys.size());
		Assert.assertEquals( 0, Utils.listAllFiles( schedulerDirectory ).size());
	}


	@Test
	public void testLoadAll_and_ListJobs() throws Exception {

		Utils.createDirectory( this.scheduler.getSchedulerDirectory());

		// Create several job FILES
		final int max = 5;
		for( int i=0; i<max; i++ ) {

			Properties props = new Properties();
			props.put( RoboconfScheduler.JOB_NAME, "same job name" );
			props.put( RoboconfScheduler.APP_NAME, "app" );
			props.put( RoboconfScheduler.CMD_NAME, "cmd" );
			props.put( RoboconfScheduler.CRON, "0 0 0 ? 1 *" );

			Utils.writePropertiesFile( props, this.scheduler.getJobFile( "my-id-" + i ));
		}

		// Start the scheduler and halts triggers
		this.scheduler.start();
		this.scheduler.scheduler.standby();
		File schedulerDirectory = this.scheduler.getSchedulerDirectory();

		Set<JobKey> jobKeys = this.scheduler.scheduler.getJobKeys( GroupMatcher.anyJobGroup());
		Assert.assertEquals( max, jobKeys.size());
		Assert.assertEquals( max, Utils.listAllFiles( schedulerDirectory ).size());

		// List the jobs
		List<ScheduledJob> jobs = this.scheduler.listJobs();
		Assert.assertEquals( max, jobs.size());

		Set<String> jobIds = new HashSet<> ();
		for( ScheduledJob job : jobs ) {
			Assert.assertEquals( "same job name", job.getJobName());
			Assert.assertEquals( "app", job.getAppName());
			Assert.assertEquals( "cmd", job.getCmdName());
			Assert.assertEquals( "0 0 0 ? 1 *", job.getCron());
			jobIds.add( job.getJobId());
		}

		Assert.assertEquals( max, jobIds.size());
	}


	@Test( expected = IOException.class )
	public void testSaveJob_exception() throws Exception {

		this.scheduler.scheduler = Mockito.mock( Scheduler.class );
		Mockito.when( this.scheduler.scheduler.scheduleJob(
				Mockito.any( JobDetail.class ),
				Mockito.any( Trigger.class ))).thenThrow( new SchedulerException( "For test" ));

		this.scheduler.saveJob( null, "job", "cmd", "0 0 0 ? 1 *", "app" );
	}


	@Test( expected = IOException.class )
	public void testDeleteJob_exception() throws Exception {

		this.scheduler.scheduler = Mockito.mock( Scheduler.class );
		Mockito
			.when( this.scheduler.scheduler.unscheduleJob( Mockito.any( TriggerKey.class )))
			.thenThrow( new SchedulerException( "For test" ));

		Properties props = new Properties();
		props.put( RoboconfScheduler.JOB_NAME, "job" );
		props.put( RoboconfScheduler.APP_NAME, "app" );
		props.put( RoboconfScheduler.CMD_NAME, "cmd" );
		props.put( RoboconfScheduler.CRON, "0 0 0 ? 1 *" );

		try {
			File f = this.scheduler.getJobFile( "job-id" );
			Assert.assertTrue( f.getParentFile().mkdirs());
			Utils.writePropertiesFile( props, f );

		} catch( Exception e ) {
			Assert.fail( "No exception was expected here." );
		}

		this.scheduler.deleteJob( "job-id" );
	}


	@Test
	public void testLoadAllJobs_invalidProperties() throws Exception {

		Utils.createDirectory( this.scheduler.getSchedulerDirectory());

		// Create several job FILES
		final int max = 5;
		for( int i=0; i<max; i++ ) {

			Properties props = new Properties();
			props.put( RoboconfScheduler.JOB_NAME, "same job name" );
			props.put( RoboconfScheduler.APP_NAME, "app" );
			props.put( RoboconfScheduler.CMD_NAME, "cmd" );

			// One job will have invalid properties
			if( i != 3 )
				props.put( RoboconfScheduler.CRON, "0 0 0 ? 1 *" );

			Utils.writePropertiesFile( props, this.scheduler.getJobFile( "job-id-" + i ));
		}

		// Start the scheduler and halts triggers
		this.scheduler.start();
		this.scheduler.scheduler.standby();
		File schedulerDirectory = this.scheduler.getSchedulerDirectory();

		Set<JobKey> jobKeys = this.scheduler.scheduler.getJobKeys( GroupMatcher.anyJobGroup());
		Assert.assertEquals( max - 1, jobKeys.size());
		Assert.assertEquals( max, Utils.listAllFiles( schedulerDirectory ).size());

		// List the jobs
		List<ScheduledJob> jobs = this.scheduler.listJobs();
		Assert.assertEquals( max, jobs.size());

		Set<String> jobIds = new HashSet<> ();
		for( ScheduledJob job : jobs ) {
			Assert.assertEquals( "app", job.getAppName());
			Assert.assertEquals( "cmd", job.getCmdName());
			Assert.assertEquals( "same job name", job.getJobName());
			jobIds.add( job.getJobId());
		}

		Assert.assertEquals( max, jobIds.size());
	}


	@Test
	public void testLoadAllJobs_exception() throws Exception {

		Utils.createDirectory( this.scheduler.getSchedulerDirectory());

		// Create several job FILES
		final int max = 5;
		for( int i=0; i<max; i++ ) {

			Properties props = new Properties();
			props.put( RoboconfScheduler.JOB_NAME, "job " + i );
			props.put( RoboconfScheduler.APP_NAME, "app" );
			props.put( RoboconfScheduler.CMD_NAME, "cmd" );
			props.put( RoboconfScheduler.CRON, "0 0 0 ? 1 *" );

			Utils.writePropertiesFile( props, this.scheduler.getJobFile( "job-id-" + i ));
		}

		// Throw an exception when a job is scheduled
		this.scheduler.scheduler = Mockito.mock( Scheduler.class );
		Mockito.when( this.scheduler.scheduler.scheduleJob(
				Mockito.any( JobDetail.class ),
				Mockito.any( Trigger.class ))).thenThrow( new SchedulerException( "For test" ));

		// Load the jobs...
		this.scheduler.loadJobs();
		File schedulerDirectory = this.scheduler.getSchedulerDirectory();

		Set<JobKey> jobKeys = this.scheduler.scheduler.getJobKeys( GroupMatcher.anyJobGroup());
		Assert.assertEquals( 0, jobKeys.size());
		Assert.assertEquals( max, Utils.listAllFiles( schedulerDirectory ).size());
	}


	@Test
	public void testListAllJobs_withEmptyProperties() throws Exception {

		Utils.createDirectory( this.scheduler.getSchedulerDirectory());

		Properties props = new Properties();
		props.put( RoboconfScheduler.JOB_NAME, "job1" );
		props.put( RoboconfScheduler.CMD_NAME, "cmd" );
		props.put( RoboconfScheduler.CRON, "0 0 0 ? 1 *" );

		Utils.writePropertiesFile( props, this.scheduler.getJobFile( "job1" ));
		Utils.writePropertiesFile( new Properties(), this.scheduler.getJobFile( "job2" ));

		List<ScheduledJob> jobs = this.scheduler.listJobs();
		Assert.assertEquals( 1, jobs.size());
		Assert.assertEquals( "job1", jobs.get( 0 ).getJobName());
	}


	@Test
	public void testFindJobProperties_inexitingJob() throws Exception {

		ScheduledJob job = this.scheduler.findJobProperties( "inexisting" );
		Assert.assertNull( job );
	}
}