package com.thinkbiganalytics.metadata.sla.spi.core;

/*-
 * #%L
 * thinkbig-sla-metrics-default
 * %%
 * Copyright (C) 2017 ThinkBig Analytics
 * %%
 * 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.
 * #L%
 */

import com.thinkbiganalytics.metadata.api.MetadataAccess;
import com.thinkbiganalytics.metadata.api.MetadataAction;
import com.thinkbiganalytics.metadata.api.MetadataCommand;
import com.thinkbiganalytics.metadata.api.MetadataRollbackAction;
import com.thinkbiganalytics.metadata.api.MetadataRollbackCommand;
import com.thinkbiganalytics.metadata.api.jobrepo.job.BatchJobExecution;
import com.thinkbiganalytics.metadata.api.jobrepo.job.BatchJobExecutionProvider;
import com.thinkbiganalytics.metadata.sla.api.AssessmentResult;
import com.thinkbiganalytics.metadata.sla.api.Metric;
import com.thinkbiganalytics.metadata.sla.api.core.FeedOnTimeArrivalMetric;
import com.thinkbiganalytics.metadata.sla.api.core.FeedOnTimeArrivalMetricAssessor;
import com.thinkbiganalytics.metadata.sla.spi.MetricAssessmentBuilder;
import com.thinkbiganalytics.scheduler.util.CronExpressionUtil;

import org.joda.time.DateTime;
import org.joda.time.Period;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.BDDMockito;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.quartz.CronExpression;
import org.testng.Assert;

import java.security.Principal;
import java.text.ParseException;

import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;

@RunWith(PowerMockRunner.class)
@PrepareForTest({DateTime.class, CronExpressionUtil.class})
public class FeedOnTimeArrivalMetricAssessorTest {

    private DateTime lateTime;
    private FeedOnTimeArrivalMetric metric;

    @Mock
    private BatchJobExecutionProvider jobExecutionProvider;

    @Mock
    private MetricAssessmentBuilder builder;

    private MetadataAccess metadataAccess = new MockMetadataAccess();

    @InjectMocks
    private FeedOnTimeArrivalMetricAssessor assessor = new FeedOnTimeArrivalMetricAssessor();

    private int lateTimeGracePeriod = 4;


    @Before
    public void setUp() throws Exception {
        initMocks(this);

        when(this.builder.message(any(String.class))).thenReturn(this.builder);
        when(this.builder.metric(any(Metric.class))).thenReturn(this.builder);
        when(this.builder.result(any(AssessmentResult.class))).thenReturn(this.builder);

        this.assessor.setMetadataAccess(this.metadataAccess);

        CronExpression cron = new CronExpression("0 0 12 1/1 * ? *");  // Noon every day

        this.lateTime = new DateTime(CronExpressionUtil.getPreviousFireTime(cron)).plusHours(4);
        this.metric = new FeedOnTimeArrivalMetric("feed",
                                                  cron,
                                                  Period.hours(lateTimeGracePeriod));
    }

    @Test
    public void testMinuteBeforeLate() throws ParseException {
        DateTime feedEnd = this.lateTime.minusMinutes(1);
        BatchJobExecution feed = createFeedJobExecution(feedEnd);
        when(this.jobExecutionProvider.findLatestCompletedJobForFeed("feed")).thenReturn(feed);

        this.assessor.assess(metric, this.builder);

        verify(this.builder).result(AssessmentResult.SUCCESS);
    }

    @Test
    /**
     * test use case where the date window is still valid, but still no data has been found for the Feed.
     * This will return no Assessment result as it still could process data
     */
    public void testNoDataButStillBeforeLateTime() throws ParseException {

        PowerMockito.mockStatic(DateTime.class);
        PowerMockito.mockStatic(CronExpressionUtil.class);

        DateTime now = new DateTime().minusHours(2);

        //Some Cron Expression .. Mockito will overwrite
        CronExpression cron = new CronExpression("0 0 0/2 1/1 * ? *");

        //set the current time to a known time
        BDDMockito.given(DateTime.now()).willReturn(now);
        //set the previous fire date to a known time in the past,but within the window
        DateTime previousFireTime = new DateTime(now).minusHours(3);
        BDDMockito.given(CronExpressionUtil.getPreviousFireTime(cron)).willReturn(previousFireTime.toDate());
        //window is = (now - 3)  - (now -3) + lateTime)
        //Some Feed End Time to a time not within this window
        DateTime lastFeedTime = new DateTime().minusWeeks(2);
        BatchJobExecution feed = createFeedJobExecution(lastFeedTime);
        when(this.jobExecutionProvider.findLatestCompletedJobForFeed("feed")).thenReturn(feed);

        this.metric = new FeedOnTimeArrivalMetric("feed", cron, Period.hours(lateTimeGracePeriod));

        this.assessor.assess(metric, this.builder);

        //assert values
        DateTime lateTime = previousFireTime.plusHours(lateTimeGracePeriod);
        Assert.assertTrue(now.isBefore(lateTime) && !(lastFeedTime.isAfter(previousFireTime) && lastFeedTime.isBefore(lateTime)));
        verify(this.builder);
    }

    @Test
    public void testSuccess() throws ParseException {

        PowerMockito.mockStatic(DateTime.class);
        PowerMockito.mockStatic(CronExpressionUtil.class);

        DateTime now = new DateTime().minusHours(2);

        //set the current time to a known time
        BDDMockito.given(DateTime.now()).willReturn(now);
        //set the previous fire date to a known time in the past,but within the window
        DateTime previousFireTime = new DateTime(now).minusHours(3);

        //Some Cron Expression .. Mockito will overwrite
        CronExpression cron = new CronExpression("0 0 0/2 1/1 * ? *");
        BDDMockito.given(CronExpressionUtil.getPreviousFireTime(cron)).willReturn(previousFireTime.toDate());
        //window is = (now - 3)  - (now -3) + lateTime)
        //Some Feed End Time to a time within this window
        DateTime lastFeedTime = new DateTime(previousFireTime).plus(lateTimeGracePeriod - 1);
        BatchJobExecution feed = createFeedJobExecution(lastFeedTime);
        when(this.jobExecutionProvider.findLatestCompletedJobForFeed("feed")).thenReturn(feed);
        this.metric = new FeedOnTimeArrivalMetric("feed", cron, Period.hours(lateTimeGracePeriod));

        this.assessor.assess(metric, this.builder);

        //assert values
        DateTime lateTime = previousFireTime.plusHours(lateTimeGracePeriod);
        Assert.assertTrue((lastFeedTime.isAfter(previousFireTime) && lastFeedTime.isBefore(lateTime)));
        verify(this.builder.result(AssessmentResult.SUCCESS));
    }

    @Test
    public void testFailure() throws ParseException {

        PowerMockito.mockStatic(DateTime.class);
        PowerMockito.mockStatic(CronExpressionUtil.class);

        DateTime now = new DateTime().minusHours(2);

        //Some Cron Expression .. Mockito will overwrite
        CronExpression cron = new CronExpression("0 0 0/2 1/1 * ? *");

        //set the current time to a known time
        BDDMockito.given(DateTime.now()).willReturn(now);
        //set the previous fire date to a known time in the past
        DateTime previousFireTime = new DateTime(now).minusHours(4);
        BDDMockito.given(CronExpressionUtil.getPreviousFireTime(cron)).willReturn(previousFireTime.toDate());
        //window is = (now - 4)  - (now -4) + lateTimeGracePeriod)
        //Some Feed End Time to a time outside the window
        DateTime lastFeedTime = new DateTime(previousFireTime).minusHours(lateTimeGracePeriod + 1);
        BatchJobExecution feed = createFeedJobExecution(lastFeedTime);
        when(this.jobExecutionProvider.findLatestCompletedJobForFeed("feed")).thenReturn(feed);
        this.metric = new FeedOnTimeArrivalMetric("feed", cron, Period.hours(lateTimeGracePeriod));

        this.assessor.assess(metric, this.builder);

        //assert values
        verify(this.builder.result(AssessmentResult.FAILURE));
    }

    @Test
    public void testMinuteAfterLate() throws ParseException {
        DateTime now = this.lateTime.plusMinutes(2);
        DateTime feedEnd = this.lateTime.plusMinutes(1);
        BatchJobExecution feed = createFeedJobExecution(feedEnd);

        PowerMockito.mockStatic(DateTime.class);
        BDDMockito.given(DateTime.now()).willReturn(now);
        when(this.jobExecutionProvider.findLatestCompletedJobForFeed("feed")).thenReturn(feed);

        this.assessor.assess(metric, this.builder);

        verify(this.builder).result(AssessmentResult.FAILURE);
    }


    @Test
    public void testFeedNotFound() throws ParseException {
        when(this.jobExecutionProvider.findLatestCompletedJobForFeed("feed")).thenReturn(null);
        this.assessor.assess(metric, this.builder);

        verify(this.builder).result(AssessmentResult.WARNING);
    }


    private BatchJobExecution createFeedJobExecution(DateTime endTime) {
        BatchJobExecution feed = mock(BatchJobExecution.class);
        when(feed.getEndTime()).thenReturn(endTime);
        return feed;
    }


    public class MockMetadataAccess implements MetadataAccess {

        public MockMetadataAccess() {

        }

        @Override
        public <R> R commit(MetadataCommand<R> cmd, Principal... principals) {
            try {
                return cmd.execute();
            } catch (Exception e) {

            }
            return null;
        }

        @Override
        public <R> R commit(MetadataCommand<R> cmd, MetadataRollbackCommand rollbackCmd, Principal... principals) {
            try {
                return cmd.execute();
            } catch (Exception e) {

            }
            return null;
        }

        @Override
        public void commit(MetadataAction cmd, Principal... principals) {
            try {
                cmd.execute();
            } catch (Exception e) {

            }
        }

        @Override
        public void commit(MetadataAction cmd, MetadataRollbackAction rollbackAction, Principal... principals) {
            try {
                cmd.execute();
            } catch (Exception e) {

            }
        }

        @Override
        public <R> R read(MetadataCommand<R> cmd, Principal... principals) {
            try {
                return cmd.execute();
            } catch (Exception e) {

            }
            return null;
        }

        @Override
        public void read(MetadataAction cmd, Principal... principals) {
            try {
                cmd.execute();
            } catch (Exception e) {

            }
        }
    }

}
