/*
 * Copyright 2015 jmrozanec
 * 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 com.cronutils.parser;

import java.time.ZonedDateTime;
import java.util.Locale;
import java.util.Optional;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import com.cronutils.builder.CronBuilder;
import com.cronutils.descriptor.CronDescriptor;
import com.cronutils.model.Cron;
import com.cronutils.model.CronType;
import com.cronutils.model.definition.CronDefinitionBuilder;
import com.cronutils.model.field.expression.FieldExpressionFactory;
import com.cronutils.model.time.ExecutionTime;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;

public class CronParserQuartzIntegrationTest {

    private static final String LAST_EXECUTION_NOT_PRESENT_ERROR = "last execution was not present";
    private CronParser parser;

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    @Before
    public void setUp() {
        parser = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ));
    }

    /**
     * Corresponds to issue#11
     * https://github.com/jmrozanec/cron-utils/issues/11
     * Reported case:
     * when parsing: "* * * * $ ?"
     * we receive: NumberFormatException
     * Expected: throw IllegalArgumentException notifying invalid char was used
     */
    @Test(expected = IllegalArgumentException.class)
    public void testInvalidCharsDetected() {
        parser.parse("* * * * $ ?");
    }

    @Test(expected = IllegalArgumentException.class)
    public void testInvalidCharsDetectedWithSingleSpecialChar() {
        parser.parse("* * * * $W ?");
    }

    @Test(expected = IllegalArgumentException.class)
    public void testInvalidCharsDetectedWithHashExpression1() {
        parser.parse("* * * * $#3 ?");
    }

    @Test(expected = IllegalArgumentException.class)
    public void testInvalidCharsDetectedWithHashExpression2() {
        parser.parse("* * * * 3#$ ?");
    }

    /**
     * Issue #15: we should support L in range (ex.: L-3)
     */
    @Test
    public void testLSupportedInDoMRange() {
        parser.parse("* * * L-3 * ?");
    }

    /**
     * Issue #15: we should support L in range (ex.: L-3), but not other special chars
     */
    @Test(expected = IllegalArgumentException.class)
    public void testLSupportedInRange() {
        parser.parse("* * * W-3 * ?");
    }

    @Test
    public void testNLSupported() {
        parser.parse("* * * 3L * ?");
    }

    /**
     * Issue #23: we should support L in DoM.
     */
    @Test
    public void testLSupportedInDoM() {
        parser.parse("0 0/10 22 L * ?");
    }

    /**
     * Issue #27: month ranges string mapping.
     */
    @Test
    public void testMonthRangeStringMapping() {
        parser.parse("0 0 0 * JUL-AUG ? *");
        parser.parse("0 0 0 * JAN-FEB ? *");
    }

    /**
     * Issue #27: month string mapping.
     */
    @Test
    public void testSingleMonthStringMapping() {
        parser.parse("0 0 0 * JAN ? *");
    }

    /**
     * Issue #27: day of week string ranges mapping.
     */
    @Test
    public void testDoWRangeStringMapping() {
        parser.parse("0 0 0 ? * MON-FRI *");
    }

    /**
     * Issue #27: day of week string mapping.
     */
    @Test
    public void testSingleDoWStringMapping() {
        parser.parse("0 0 0 ? * MON *");
    }

    /**
     * Issue #27: July month as string is parsed as some special char occurrence.
     */
    @Test
    public void testJulyMonthAsStringConsideredSpecialChar() {
        assertNotNull(parser.parse("0 0 0 * JUL ? *"));
    }

    /**
     * Issue #35: A>B in range considered invalid expression for Quartz.
     */
    @Test
    public void testSunToSat() {
        // FAILS SUN-SAT: SUN = 7 and SAT = 6
        parser.parse("0 0 12 ? * SUN-SAT");
    }

    /**
     * Issue #39: reported issue about exception being raised on parse.
     */
    @Test
    public void testParseExpressionWithQuestionMarkAndWeekdays() {
        parser.parse("0 0 0 ? * MON,TUE *");
    }

    /**
     * Issue #39: reported issue about exception being raised on parse.
     */
    @Test
    public void testDescribeExpressionWithQuestionMarkAndWeekdays() {
        final Cron quartzCron = parser.parse("0 0 0 ? * MON,TUE *");
        final CronDescriptor descriptor = CronDescriptor.instance(Locale.ENGLISH);
        descriptor.describe(quartzCron);
    }

    /**
     * Issue #60: Parser exception when parsing cron.
     */
    @Test
    public void testDescribeExpression() {
        final String expression = "0 * * ? * 1,5";
        final Cron c = parser.parse(expression);
        CronDescriptor.instance(Locale.GERMAN).describe(c);
    }

    /**
     * Issue #63: Parser exception when parsing cron.
     */
    @Test(expected = IllegalArgumentException.class)
    public void testDoMAndDoWParametersInvalidForQuartz() {
        parser.parse("0 30 17 4 1 * 2016");
    }

    /**
     * Issue #78: ExecutionTime.forCron fails on intervals
     */
    @Test
    public void testIntervalSeconds() {
        final ExecutionTime executionTime = ExecutionTime.forCron(parser.parse("0/20 * * * * ?"));
        final ZonedDateTime now = ZonedDateTime.parse("2005-08-09T18:32:42Z");
        final Optional<ZonedDateTime> lastExecution = executionTime.lastExecution(now);
        if (lastExecution.isPresent()) {
            final ZonedDateTime assertDate = ZonedDateTime.parse("2005-08-09T18:32:40Z");
            assertEquals(assertDate, lastExecution.get());
        } else {
            fail(LAST_EXECUTION_NOT_PRESENT_ERROR);
        }
    }

    /**
     * Issue #78: ExecutionTime.forCron fails on intervals
     */
    @Test
    public void testIntervalMinutes() {
        final ExecutionTime executionTime = ExecutionTime.forCron(parser.parse("0 0/7 * * * ?"));
        final ZonedDateTime now = ZonedDateTime.parse("2005-08-09T18:32:42Z");
        final Optional<ZonedDateTime> lastExecution = executionTime.lastExecution(now);
        if (lastExecution.isPresent()) {
            final ZonedDateTime assertDate = ZonedDateTime.parse("2005-08-09T18:28:00Z");
            assertEquals(assertDate, lastExecution.get());
        } else {
            fail(LAST_EXECUTION_NOT_PRESENT_ERROR);
        }
    }

    /**
     * Issue #89: regression - NumberFormatException: For input string: "$".
     */
    @Test
    public void testRegressionDifferentMessageForException() {
        thrown.expect(IllegalArgumentException.class);
        thrown.expectMessage("Invalid chars in expression! Expression: $ Invalid chars: $");
        assertNotNull(ExecutionTime.forCron(parser.parse("* * * * $ ?")));
    }

    /**
     * Issue #90: Reported error contains other expression than the one provided.
     */
    @Test
    public void testReportedErrorContainsSameExpressionAsProvided() {
        thrown.expect(IllegalArgumentException.class);
        thrown.expectMessage(
                "Invalid cron expression: 0/1 * * * * *. Both, a day-of-week AND a day-of-month parameter, are not supported.");
        assertNotNull(ExecutionTime.forCron(parser.parse("0/1 * * * * *")));
    }

    /**
     * Issue #109: Missing expression and invalid chars in error message
     * https://github.com/jmrozanec/cron-utils/issues/109
     */
    @Test
    public void testMissingExpressionAndInvalidCharsInErrorMessage() {
        thrown.expect(IllegalArgumentException.class);
        final String cronexpression = "* * -1 * * ?";
        thrown.expectMessage(
                String.format("Failed to parse '%s'. Invalid expression! Expression: -1 does not describe a range. Negative numbers are not allowed.",
                        cronexpression));
        assertNotNull(ExecutionTime.forCron(parser.parse(cronexpression)));
    }

    /**
     * Issue #148: Cron Builder/Parser fails on Every X years.
     */
    @Test
    public void testEveryXYears() {
        CronBuilder.cron(CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ)).withDoM(FieldExpressionFactory.on(1))
                .withDoW(FieldExpressionFactory.questionMark())
                .withYear(FieldExpressionFactory.every(FieldExpressionFactory.between(1970, 2099), 4))
                .withMonth(FieldExpressionFactory.on(1))
                .withHour(FieldExpressionFactory.on(0))
                .withMinute(FieldExpressionFactory.on(0))
                .withSecond(FieldExpressionFactory.on(0));
    }

    @Test
    public void testRejectIllegalMonthArgument() {
        thrown.expect(IllegalArgumentException.class);
        CronBuilder.cron(CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ)).withMonth(FieldExpressionFactory.on(0));
    }

    /**
     * Issue #151: L-7 in day of month should work to find the day 7 days prior to the last day of the month.
     */
    @Test
    public void testLSupportedInDoMRangeNextExecutionCalculation() {
        final ExecutionTime executionTime = ExecutionTime.forCron(parser.parse("0 15 10 L-7 * ?"));
        final ZonedDateTime now = ZonedDateTime.parse("2017-01-31T10:00:00Z");
        final Optional<ZonedDateTime> nextExecution = executionTime.nextExecution(now);
        if (nextExecution.isPresent()) {
            final ZonedDateTime assertDate = ZonedDateTime.parse("2017-02-21T10:15:00Z");
            assertEquals(assertDate, nextExecution.get());
        } else {
            fail("next execution was not present");
        }
    }

    /**
     * Issue #154: Quartz Cron Year Pattern is not fully supported - i.e. increments on years are not supported
     * https://github.com/jmrozanec/cron-utils/issues/154
     * Duplicate of #148
     */
    @Test
    public void supportQuartzCronExpressionIncrementsOnYears() {
        final String[] sampleCronExpressions = {
                "0 0 0 1 * ? 2017/2",
                "0 0 0 1 * ? 2017/3",
                "0 0 0 1 * ? 2017/10",
                "0 0 0 1 * ? 2017-2047/2",
        };

        final CronParser quartzCronParser = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ));
        for (final String cronExpression : sampleCronExpressions) {
            final Cron quartzCron = quartzCronParser.parse(cronExpression);
            quartzCron.validate();
        }
    }

    @Test
    public void testErrorAbout2Parts() {
        thrown.expect(IllegalArgumentException.class);
        thrown.expectMessage("Cron expression contains 2 parts but we expect one of [6, 7]");
        assertNotNull(ExecutionTime.forCron(parser.parse("* *")));
    }

    @Test
    public void testErrorAboutMissingSteps() {
        thrown.expect(IllegalArgumentException.class);
        thrown.expectMessage("Missing steps for expression: */");
        assertNotNull(ExecutionTime.forCron(parser.parse("*/ * * * * ?")));
    }

    /**
     * Issue #375: Quartz Last Day of Week pattern does not support Day of Week as text
     * https://github.com/jmrozanec/cron-utils/issues/375
     */
    @Test
    public void testLastDayOfWeek() {
        final String cronExpression = "0 0 1 ? 1/1 MONL *";
        final CronParser quartzCronParser = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ));
        quartzCronParser.parse(cronExpression);
    }
}