package io.digdag.core.schedule;

import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import io.digdag.client.DigdagClient;
import io.digdag.client.config.Config;
import io.digdag.client.config.ConfigFactory;
import io.digdag.core.acroute.DummyAccountRoutingFactory;
import io.digdag.core.database.ThreadLocalTransactionManager;
import io.digdag.core.database.TransactionManager;
import io.digdag.core.repository.ProjectStoreManager;
import io.digdag.core.repository.StoredProject;
import io.digdag.core.repository.StoredWorkflowDefinitionWithProject;
import io.digdag.core.session.SessionStore;
import io.digdag.core.session.SessionStoreManager;
import io.digdag.core.session.StoredSessionAttemptWithSession;
import io.digdag.core.workflow.AttemptBuilder;
import io.digdag.core.workflow.WorkflowExecutor;
import io.digdag.spi.AccountRouting;
import io.digdag.spi.ScheduleTime;
import io.digdag.spi.Scheduler;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;

import javax.sql.DataSource;

import java.time.Instant;

import static java.time.ZoneOffset.UTC;
import static org.junit.Assert.assertFalse;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.spy;

@RunWith(MockitoJUnitRunner.class)
public class ScheduleExecutorTest
{
    private static final int SCHEDULE_ID = 13;
    private static final int PROJECT_ID = 9;
    private static final int SITE_ID = 7;
    private static final long WORKFLOW_DEFINITION_ID = 17;
    private static final String WORKFLOW_NAME = "wfwf";

    private static final ConfigFactory CONFIG_FACTORY = new ConfigFactory(DigdagClient.objectMapper());

    @Mock ProjectStoreManager projectStoreManager;
    @Mock ScheduleStoreManager scheduleStoreManager;
    @Mock SchedulerManager schedulerManager;
    TransactionManager transactionManager;
    @Mock SessionStore sessionStore;
    @Mock SessionStoreManager sessionStoreManager;
    @Mock Scheduler scheduler;
    @Mock ScheduleControlStore scs;
    @Mock StoredSchedule schedule;
    @Mock StoredProject project;
    @Mock StoredWorkflowDefinitionWithProject workflowDefinition;
    @Mock StoredSessionAttemptWithSession attempt;
    @Mock ScheduleTime nextScheduleTime;
    @Mock AttemptBuilder attemptBuilder;
    @Mock WorkflowExecutor workflowExecutor;
    @Mock DataSource dataSource;
    @Mock ScheduleConfig scheduleConfig;

    private ScheduleExecutor scheduleExecutor;

    private Instant now;
    private Config workflowConfig;

    @Before
    public void setUp()
            throws Exception
    {
        transactionManager = new ThreadLocalTransactionManager(dataSource);
        scheduleExecutor = spy(
                new ScheduleExecutor(
                        projectStoreManager,
                        scheduleStoreManager,
                        schedulerManager,
                        transactionManager,
                        sessionStoreManager,
                        attemptBuilder,
                        workflowExecutor,
                        CONFIG_FACTORY,
                        scheduleConfig,
                        new DummyAccountRoutingFactory()
                ));

        now = Instant.now();

        when(project.getId()).thenReturn(PROJECT_ID);
        when(project.getSiteId()).thenReturn(SITE_ID);

        workflowConfig = CONFIG_FACTORY.create();

        when(workflowDefinition.getTimeZone()).thenReturn(UTC);
        when(workflowDefinition.getId()).thenReturn(WORKFLOW_DEFINITION_ID);
        when(workflowDefinition.getName()).thenReturn(WORKFLOW_NAME);
        when(workflowDefinition.getProject()).thenReturn(project);
        when(workflowDefinition.getConfig()).thenReturn(workflowConfig);

        when(scheduler.nextScheduleTime(now)).thenReturn(nextScheduleTime);
        when(schedulerManager.getScheduler(workflowDefinition)).thenReturn(scheduler);

        when(schedule.getId()).thenReturn(SCHEDULE_ID);
        when(schedule.getWorkflowDefinitionId()).thenReturn(WORKFLOW_DEFINITION_ID);
        when(schedule.getNextScheduleTime()).thenReturn(now);
        when(schedule.getNextRunTime()).thenReturn(now);

        when(sessionStoreManager.getSessionStore(SITE_ID)).thenReturn(sessionStore);
        when(projectStoreManager.getWorkflowDetailsById(WORKFLOW_DEFINITION_ID)).thenReturn(workflowDefinition);

        doAnswer(invocation -> {
            ScheduleStoreManager.ScheduleAction func = invocation.getArgumentAt(3, ScheduleStoreManager.ScheduleAction.class);
            func.schedule(scs, schedule);
            return null;
        }).when(scheduleStoreManager).lockReadySchedules(any(Instant.class), eq(1), any(AccountRouting.class), any(ScheduleStoreManager.ScheduleAction.class));
    }

    @Test
    public void testSkipOnOvertime()
            throws Exception
    {
        // Enable skip_on_overtime
        workflowConfig.getNestedOrSetEmpty("schedule")
                .set("skip_on_overtime", true)
                .set("daily>", "12:00:00");

        // Indicate that there is an active attempt for this workflow
        when(sessionStore.getActiveAttemptsOfWorkflow(eq(PROJECT_ID), eq(WORKFLOW_NAME), anyInt(), any(Optional.class)))
                .thenReturn(ImmutableList.of(attempt));

        // Run the schedule executor...
        scheduleExecutor.runScheduleOnce(now);

        // Verify that another attempt was not started
        verify(scheduleExecutor, never()).startSchedule(any(StoredSchedule.class), any(Scheduler.class), any(StoredWorkflowDefinitionWithProject.class));

        // Verify that the schedule skipped to the next time
        verify(scs).updateNextScheduleTime(SCHEDULE_ID, nextScheduleTime);
    }

    @Test
    public void testDefaultConcurrentOvertimeExecution()
            throws Exception
    {
        // Leave skip_on_overtime disabled (default)
        workflowConfig.getNestedOrSetEmpty("schedule")
                .set("daily>", "12:00:00");

        // Indicate that there is an active attempt for this workflow
        when(sessionStore.getActiveAttemptsOfWorkflow(eq(PROJECT_ID), eq(WORKFLOW_NAME), anyInt(), any(Optional.class)))
                .thenReturn(ImmutableList.of(attempt));

        // Run the schedule executor...
        scheduleExecutor.runScheduleOnce(now);

        // Verify that another attempt was started
        verify(scheduleExecutor).startSchedule(any(StoredSchedule.class), any(Scheduler.class), any(StoredWorkflowDefinitionWithProject.class));

        // Verify that the schedule progressed to the next time
        verify(scs).updateNextScheduleTimeAndLastSessionTime(SCHEDULE_ID, nextScheduleTime, now);
    }

    @Test
    public void TestskipDelayedBy()
            throws Exception
    {
        // set skip_delayed_by 30
        workflowConfig.getNestedOrSetEmpty("schedule")
                .set("skip_delayed_by", "30s")
                .set("daily>", "12:00:00");

        // Indicate that there is no active attempt for this workflow
        when(sessionStore.getActiveAttemptsOfWorkflow(eq(PROJECT_ID), eq(WORKFLOW_NAME), anyInt(), any(Optional.class)))
                .thenReturn(ImmutableList.of());

        // Run the schedule executor at now + 31
        scheduleExecutor.runScheduleOnce(now.plusSeconds(31));

        // Verify the task was not started because it's over backfill_limit
        verify(scheduleExecutor, never()).startSchedule(any(StoredSchedule.class), any(Scheduler.class), any(StoredWorkflowDefinitionWithProject.class));

        // Verify that the schedule skipped to the next time
        verify(scs).updateNextScheduleTime(SCHEDULE_ID, nextScheduleTime);
    }

    @Test
    public void testRunWithinSkipDelayedBy()
            throws Exception
    {
        // set skip_delayed_by 30
        workflowConfig.getNestedOrSetEmpty("schedule")
                .set("skip_delayed_by", "30s")
                .set("daily>", "12:00:00");

        // Indicate that there is no active attempt for this workflow
        when(sessionStore.getActiveAttemptsOfWorkflow(eq(PROJECT_ID), eq(WORKFLOW_NAME), anyInt(), any(Optional.class)))
                .thenReturn(ImmutableList.of());

        // Run the schedule executor at now + 29
        scheduleExecutor.runScheduleOnce(now.plusSeconds(29));

        // Verify that task was started.
        verify(scheduleExecutor).startSchedule(any(StoredSchedule.class), any(Scheduler.class), any(StoredWorkflowDefinitionWithProject.class));

        // Verify that the schedule progressed to the next time
        verify(scs).updateNextScheduleTimeAndLastSessionTime(SCHEDULE_ID, nextScheduleTime, now);
    }

    @Test
    public void testNoSkipDelayedBy()
            throws Exception
    {
        // there is no skip_delayed_by.
        workflowConfig.getNestedOrSetEmpty("schedule")
                .set("daily>", "12:00:00");

        // Indicate that there is no active attempt for this workflow
        when(sessionStore.getActiveAttemptsOfWorkflow(eq(PROJECT_ID), eq(WORKFLOW_NAME), anyInt(), any(Optional.class)))
                .thenReturn(ImmutableList.of());

        // Run the schedule executor at now + 1
        scheduleExecutor.runScheduleOnce(now.plusSeconds(1));

        // Verify the task was started.
        verify(scheduleExecutor).startSchedule(any(StoredSchedule.class), any(Scheduler.class), any(StoredWorkflowDefinitionWithProject.class));

        // Verify that the schedule progressed to the next time
        verify(scs).updateNextScheduleTimeAndLastSessionTime(SCHEDULE_ID, nextScheduleTime, now);
    }

    @Test
    public void testDisabled()
            throws Exception
    {
        // Disable scheduler
        when(scheduleConfig.getEnabled()).thenReturn(false);

        // Trying to start the schedule executor.
        scheduleExecutor.start();

        // Executor is not started.
        assertFalse(scheduleExecutor.isStarted());
    }
}
