content: /*
   * 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.cloudstack.vm.schedule;
  
  import com.cloud.event.ActionEventUtils;
  import com.cloud.user.User;
  import com.cloud.uservm.UserVm;
  import com.cloud.utils.component.ComponentContext;
  import com.cloud.vm.UserVmManager;
  import com.cloud.vm.VirtualMachine;
  import org.apache.cloudstack.api.ApiCommandResourceType;
  import org.apache.cloudstack.api.command.user.vm.RebootVMCmd;
  import org.apache.cloudstack.api.command.user.vm.StartVMCmd;
  import org.apache.cloudstack.api.command.user.vm.StopVMCmd;
  import org.apache.cloudstack.framework.jobs.AsyncJobDispatcher;
  import org.apache.cloudstack.framework.jobs.AsyncJobManager;
  import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO;
  import org.apache.cloudstack.vm.schedule.dao.VMScheduleDao;
  import org.apache.cloudstack.vm.schedule.dao.VMScheduledJobDao;
  import org.apache.commons.lang.time.DateUtils;
  import org.junit.After;
  import org.junit.Assert;
  import org.junit.Before;
  import org.junit.Test;
  import org.junit.runner.RunWith;
  import org.mockito.InjectMocks;
  import org.mockito.Mock;
  import org.mockito.MockedStatic;
  import org.mockito.Mockito;
  import org.mockito.MockitoAnnotations;
  import org.mockito.Spy;
  import org.mockito.junit.MockitoJUnitRunner;
  import org.springframework.test.util.ReflectionTestUtils;
  
  import javax.persistence.EntityExistsException;
  import java.time.ZonedDateTime;
  import java.util.Calendar;
  import java.util.Date;
  import java.util.EnumMap;
  import java.util.HashMap;
  import java.util.Map;
  import java.util.TimeZone;
  
  import static org.mockito.Mockito.when;
  
  @RunWith(MockitoJUnitRunner.class)
  public class VMSchedulerImplTest {
      @Spy
      @InjectMocks
      private VMSchedulerImpl vmScheduler = new VMSchedulerImpl();
  
      @Mock
      private UserVmManager userVmManager;
  
      @Mock
      private VMScheduleDao vmScheduleDao;
  
      @Mock
      private VMScheduledJobDao vmScheduledJobDao;
  
      @Mock
      private EnumMap<VMSchedule.Action, String> actionEventMap;
  
      @Mock
      private AsyncJobManager asyncJobManager;
  
      @Mock
      private AsyncJobDispatcher asyncJobDispatcher;
  
      private AutoCloseable closeable;
  
      private MockedStatic<ActionEventUtils> actionEventUtilsMocked;
  
      @Before
      public void setUp() throws Exception {
          closeable = MockitoAnnotations.openMocks(this);
          actionEventUtilsMocked = Mockito.mockStatic(ActionEventUtils.class);
          Mockito.when(ActionEventUtils.onScheduledActionEvent(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(),
                         Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyBoolean(),
                         Mockito.anyLong()))
                 .thenReturn(1L);
          Mockito.when(ActionEventUtils.onCompletedActionEvent(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(),
                  Mockito.anyString(), Mockito.anyBoolean(),
                  Mockito.anyString(),
                  Mockito.anyLong(), Mockito.anyString(), Mockito.anyLong())).thenReturn(1L);
      }
  
      @After
      public void tearDown() throws Exception {
          actionEventUtilsMocked.close();
          closeable.close();
      }
  
      @Test
      public void testProcessJobRunning() {
          executeProcessJobWithVMStateAndActionNonSkipped(VirtualMachine.State.Running, VMSchedule.Action.STOP);
          executeProcessJobWithVMStateAndActionNonSkipped(VirtualMachine.State.Running, VMSchedule.Action.FORCE_STOP);
          executeProcessJobWithVMStateAndActionNonSkipped(VirtualMachine.State.Running, VMSchedule.Action.REBOOT);
          executeProcessJobWithVMStateAndActionNonSkipped(VirtualMachine.State.Running, VMSchedule.Action.FORCE_REBOOT);
          executeProcessJobWithVMStateAndActionNonSkipped(VirtualMachine.State.Stopped, VMSchedule.Action.START);
      }
  
      private void executeProcessJobWithVMStateAndActionNonSkipped(VirtualMachine.State state, VMSchedule.Action action) {
          VMScheduledJob vmScheduledJob = Mockito.mock(VMScheduledJob.class);
          VirtualMachine vm = Mockito.mock(VirtualMachine.class);
  
          Long expectedValue = 1L;
  
          prepareMocksForProcessJob(vm, vmScheduledJob, state, action, expectedValue);
  
          Long jobId = vmScheduler.processJob(vmScheduledJob, vm);
  
          actionEventUtilsMocked.verify(() -> ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, vm.getAccountId(), null,
                  actionEventMap.get(action), true,
                  String.format("Executing action (%s) for VM: %s", vmScheduledJob.getAction(), vm),
                  vm.getId(), ApiCommandResourceType.VirtualMachine.toString(), 0));
          Assert.assertEquals(expectedValue, jobId);
      }
  
      private void prepareMocksForProcessJob(VirtualMachine vm, VMScheduledJob vmScheduledJob,
                                             VirtualMachine.State vmState, VMSchedule.Action action,
                                             Long executeJobReturnValue) {
          Mockito.when(vm.getState()).thenReturn(vmState);
          Mockito.when(vmScheduledJob.getAction()).thenReturn(action);
  
          if (executeJobReturnValue != null) {
              Mockito.doReturn(executeJobReturnValue).when(vmScheduler).executeStartVMJob(
                      Mockito.any(VirtualMachine.class), Mockito.anyLong());
              Mockito.doReturn(executeJobReturnValue).when(vmScheduler).executeStopVMJob(
                      Mockito.any(VirtualMachine.class), Mockito.anyBoolean(), Mockito.anyLong());
              Mockito.doReturn(executeJobReturnValue).when(vmScheduler).executeRebootVMJob(
                      Mockito.any(VirtualMachine.class), Mockito.anyBoolean(), Mockito.anyLong());
          }
      }
  
      @Test
      public void testProcessJobInvalidAction() {
          VMScheduledJob vmScheduledJob = Mockito.mock(VMScheduledJob.class);
          VirtualMachine vm = Mockito.mock(VirtualMachine.class);
  
          prepareMocksForProcessJob(vm, vmScheduledJob, VirtualMachine.State.Running, VMSchedule.Action.START, null);
  
          Long jobId = vmScheduler.processJob(vmScheduledJob, vm);
  
          Assert.assertNull(jobId);
      }
  
      @Test
      public void testProcessJobVMInInvalidState() {
          VMScheduledJob vmScheduledJob = Mockito.mock(VMScheduledJob.class);
          VirtualMachine vm = Mockito.mock(VirtualMachine.class);
  
          prepareMocksForProcessJob(vm, vmScheduledJob, VirtualMachine.State.Unknown, VMSchedule.Action.START, null);
  
          Long jobId = vmScheduler.processJob(vmScheduledJob, vm);
  
          Assert.assertNull(jobId);
      }
  
      @Test
      public void testScheduleNextJobScheduleScheduleExists() {
          Date now = DateUtils.setSeconds(new Date(), 0);
          Date startDate = DateUtils.addDays(now, 1);
          Date expectedScheduledTime = DateUtils.round(DateUtils.addMinutes(startDate, 1), Calendar.MINUTE);
          UserVm vm = Mockito.mock(UserVm.class);
  
          VMScheduleVO vmSchedule = Mockito.mock(VMScheduleVO.class);
          Mockito.when(vmSchedule.getEnabled()).thenReturn(true);
          Mockito.when(vmSchedule.getSchedule()).thenReturn("* * * * *");
          Mockito.when(vmSchedule.getTimeZoneId()).thenReturn(TimeZone.getTimeZone("UTC").toZoneId());
          Mockito.when(vmSchedule.getStartDate()).thenReturn(startDate);
          Mockito.when(userVmManager.getUserVm(Mockito.anyLong())).thenReturn(vm);
          Mockito.when(vmScheduledJobDao.persist(Mockito.any())).thenThrow(EntityExistsException.class);
          Date actualScheduledTime = vmScheduler.scheduleNextJob(vmSchedule, new Date());
  
          Assert.assertEquals(expectedScheduledTime, actualScheduledTime);
      }
  
      @Test
      public void testScheduleNextJobScheduleFutureSchedule() {
          Date now = DateUtils.setSeconds(new Date(), 0);
          Date startDate = DateUtils.addDays(now, 1);
          Date expectedScheduledTime = DateUtils.round(DateUtils.addMinutes(startDate, 1), Calendar.MINUTE);
          UserVm vm = Mockito.mock(UserVm.class);
  
          VMScheduleVO vmSchedule = Mockito.mock(VMScheduleVO.class);
          Mockito.when(vmSchedule.getEnabled()).thenReturn(true);
          Mockito.when(vmSchedule.getSchedule()).thenReturn("* * * * *");
          Mockito.when(vmSchedule.getTimeZoneId()).thenReturn(TimeZone.getTimeZone("UTC").toZoneId());
          Mockito.when(vmSchedule.getStartDate()).thenReturn(startDate);
          Mockito.when(userVmManager.getUserVm(Mockito.anyLong())).thenReturn(vm);
          Date actualScheduledTime = vmScheduler.scheduleNextJob(vmSchedule, new Date());
  
          Assert.assertEquals(expectedScheduledTime, actualScheduledTime);
      }
  
      @Test
      public void testScheduleNextJobScheduleFutureScheduleWithTimeZoneChecks() throws Exception {
          // Ensure that Date vmSchedulerImpl.scheduleNextJob(VMScheduleVO vmSchedule) generates
          // the correct scheduled time on basis of schedule(cron), start date
          // and the timezone of the user. The system running the test can have any timezone.
          String cron = "30 5 * * *";
  
          Date now = DateUtils.setSeconds(new Date(), 0);
          Date startDate = DateUtils.addDays(now, 1);
  
          UserVm vm = Mockito.mock(UserVm.class);
  
          VMScheduleVO vmSchedule = Mockito.mock(VMScheduleVO.class);
          Mockito.when(vmSchedule.getEnabled()).thenReturn(true);
          Mockito.when(vmSchedule.getSchedule()).thenReturn(cron);
          Mockito.when(vmSchedule.getTimeZoneId()).thenReturn(TimeZone.getTimeZone("EST").toZoneId());
          Mockito.when(vmSchedule.getStartDate()).thenReturn(startDate);
          Mockito.when(userVmManager.getUserVm(Mockito.anyLong())).thenReturn(vm);
  
          // The timezone of the user is EST. The cron expression is 30 5 * * *.
          // The start date is 1 day from now. The expected scheduled time is 5:30 AM EST.
          // The actual scheduled time is 10:30 AM UTC.
          // The actual scheduled time is 5:30 AM EST.
          ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(startDate.toInstant(), vmSchedule.getTimeZoneId());
          zonedDateTime = zonedDateTime.withHour(5).withMinute(30).withSecond(0).withNano(0);
          Date expectedScheduledTime = Date.from(zonedDateTime.toInstant());
  
          if (expectedScheduledTime.before(startDate)) {
              expectedScheduledTime = DateUtils.addDays(expectedScheduledTime, 1);
          }
  
          Date actualScheduledTime = vmScheduler.scheduleNextJob(vmSchedule, new Date());
          Assert.assertEquals(expectedScheduledTime, actualScheduledTime);
      }
  
      @Test
      public void testScheduleNextJobScheduleCurrentSchedule() {
          Date now = DateUtils.setSeconds(new Date(), 0);
          Date expectedScheduledTime = DateUtils.round(DateUtils.addMinutes(now, 1), Calendar.MINUTE);
          UserVm vm = Mockito.mock(UserVm.class);
  
          VMScheduleVO vmSchedule = Mockito.mock(VMScheduleVO.class);
          Mockito.when(vmSchedule.getEnabled()).thenReturn(true);
          Mockito.when(vmSchedule.getSchedule()).thenReturn("* * * * *");
          Mockito.when(vmSchedule.getTimeZoneId()).thenReturn(TimeZone.getTimeZone("UTC").toZoneId());
          Mockito.when(vmSchedule.getStartDate()).thenReturn(DateUtils.addDays(now, -1));
          Mockito.when(userVmManager.getUserVm(Mockito.anyLong())).thenReturn(vm);
          Date actualScheduledTime = vmScheduler.scheduleNextJob(vmSchedule, new Date());
  
          Assert.assertEquals(expectedScheduledTime, actualScheduledTime);
      }
  
      @Test
      public void testScheduleNextJobScheduleCurrentScheduleWithTimeZoneChecks() throws Exception {
          // Ensure that Date vmSchedulerImpl.scheduleNextJob(VMScheduleVO vmSchedule) generates
          // the correct scheduled time on basis of schedule(cron), start date
          // and the timezone of the user. The system running the test can have any timezone.
          String cron = "30 5 * * *";
  
          Date now = DateUtils.setSeconds(new Date(), 0);
  
          UserVm vm = Mockito.mock(UserVm.class);
  
          VMScheduleVO vmSchedule = Mockito.mock(VMScheduleVO.class);
          Mockito.when(vmSchedule.getEnabled()).thenReturn(true);
          Mockito.when(vmSchedule.getSchedule()).thenReturn(cron);
          Mockito.when(vmSchedule.getTimeZoneId()).thenReturn(TimeZone.getTimeZone("EST").toZoneId());
          Mockito.when(vmSchedule.getStartDate()).thenReturn(DateUtils.addDays(now, -1));
          Mockito.when(userVmManager.getUserVm(Mockito.anyLong())).thenReturn(vm);
  
          // The timezone of the user is EST. The cron expression is 30 5 * * *.
          // The start date is 1 day ago. The expected scheduled time is 5:30 AM EST.
          // The actual scheduled time is 10:30 AM UTC.
          // The actual scheduled time is 5:30 AM EST.
          ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(now.toInstant(), vmSchedule.getTimeZoneId());
          zonedDateTime = zonedDateTime.withHour(5).withMinute(30).withSecond(0).withNano(0);
          Date expectedScheduledTime = Date.from(zonedDateTime.toInstant());
  
          if (expectedScheduledTime.before(now)) {
              expectedScheduledTime = DateUtils.addDays(expectedScheduledTime, 1);
          }
  
          Date actualScheduledTime = vmScheduler.scheduleNextJob(vmSchedule, new Date());
          Assert.assertEquals(expectedScheduledTime, actualScheduledTime);
      }
  
      @Test
      public void testScheduleNextJobScheduleExpired() {
          VMScheduleVO vmSchedule = Mockito.mock(VMScheduleVO.class);
          Mockito.when(vmSchedule.getEndDate()).thenReturn(DateUtils.addMinutes(new Date(), -5));
          Mockito.when(vmSchedule.getEnabled()).thenReturn(true);
          Date actualDate = vmScheduler.scheduleNextJob(vmSchedule, new Date());
          Assert.assertNull(actualDate);
      }
  
      @Test
      public void testScheduleNextJobScheduleDisabled() {
          VMScheduleVO vmSchedule = Mockito.mock(VMScheduleVO.class);
          Mockito.when(vmSchedule.getEnabled()).thenReturn(false);
          Date actualDate = vmScheduler.scheduleNextJob(vmSchedule, new Date());
          Assert.assertNull(actualDate);
      }
  
  
      @Test
      public void testExecuteJobs() {
          /*
           Test VMSchedulerImpl.executeJobs() method with a map of VMScheduledJob objects
           covering all the possible scenarios
           1. When the job is executed successfully
           2. When the job is skipped (processJob returns null)
          */
  
          VMScheduledJobVO job1 = Mockito.mock(VMScheduledJobVO.class);
          VMScheduledJobVO job2 = Mockito.mock(VMScheduledJobVO.class);
  
          UserVm vm1 = Mockito.mock(UserVm.class);
          UserVm vm2 = Mockito.mock(UserVm.class);
  
          Mockito.when(job2.getVmId()).thenReturn(2L);
  
          Mockito.when(userVmManager.getUserVm(2L)).thenReturn(vm2);
  
          Mockito.doReturn(null).when(vmScheduler).processJob(job2, vm2);
  
          Mockito.when(vmScheduledJobDao.acquireInLockTable(job1.getId())).thenReturn(job1);
          Mockito.when(vmScheduledJobDao.acquireInLockTable(job2.getId())).thenReturn(job2);
  
          Map<Long, VMScheduledJob> jobs = new HashMap<>();
          jobs.put(1L, job1);
          jobs.put(2L, job2);
  
          ReflectionTestUtils.setField(vmScheduler, "currentTimestamp", new Date());
  
          vmScheduler.executeJobs(jobs);
  
          Mockito.verify(vmScheduler, Mockito.times(2)).processJob(Mockito.any(), Mockito.any());
          Mockito.verify(vmScheduledJobDao, Mockito.times(2)).acquireInLockTable(Mockito.anyLong());
      }
  
      @Test
      public void testExecuteStopVMJob() {
          VirtualMachine vm = Mockito.mock(VirtualMachine.class);
          Mockito.when(asyncJobManager.submitAsyncJob(Mockito.any(AsyncJobVO.class))).thenReturn(1L);
          try (MockedStatic<ComponentContext> ignored = Mockito.mockStatic(ComponentContext.class)) {
              when(ComponentContext.inject(StopVMCmd.class)).thenReturn(Mockito.mock(StopVMCmd.class));
              long jobId = vmScheduler.executeStopVMJob(vm, false, 1L);
  
              Assert.assertEquals(1L, jobId);
          }
      }
  
      @Test
      public void testExecuteRebootVMJob() {
          VirtualMachine vm = Mockito.mock(VirtualMachine.class);
          Mockito.when(asyncJobManager.submitAsyncJob(Mockito.any(AsyncJobVO.class))).thenReturn(1L);
          try (MockedStatic<ComponentContext> ignored = Mockito.mockStatic(ComponentContext.class)) {
              when(ComponentContext.inject(RebootVMCmd.class)).thenReturn(Mockito.mock(RebootVMCmd.class));
              long jobId = vmScheduler.executeRebootVMJob(vm, false, 1L);
  
              Assert.assertEquals(1L, jobId);
          }
      }
  
      @Test
      public void testExecuteStartVMJob() {
          VirtualMachine vm = Mockito.mock(VirtualMachine.class);
          Mockito.when(asyncJobManager.submitAsyncJob(Mockito.any(AsyncJobVO.class))).thenReturn(1L);
          try (MockedStatic<ComponentContext> ignored = Mockito.mockStatic(ComponentContext.class)) {
              when(ComponentContext.inject(StartVMCmd.class)).thenReturn(Mockito.mock(StartVMCmd.class));
              long jobId = vmScheduler.executeStartVMJob(vm, 1L);
  
              Assert.assertEquals(1L, jobId);
          }
      }
  }
  