<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Magento\Cron\Test\Unit\Model;

use Magento\Cron\Model\Schedule;

/**
 * Class \Magento\Cron\Test\Unit\Model\ObserverTest
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 */
class ScheduleTest extends \PHPUnit\Framework\TestCase
{
    /**
     * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager
     */
    protected $helper;

    protected $resourceJobMock;

    protected function setUp()
    {
        $this->helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);

        $this->resourceJobMock = $this->getMockBuilder(\Magento\Cron\Model\ResourceModel\Schedule::class)
            ->disableOriginalConstructor()
            ->setMethods(['trySetJobUniqueStatusAtomic', '__wakeup', 'getIdFieldName'])
            ->getMockForAbstractClass();

        $this->resourceJobMock->expects($this->any())
            ->method('getIdFieldName')
            ->will($this->returnValue('id'));
    }

    /**
     * @param string $cronExpression
     * @param array $expected
     * @dataProvider setCronExprDataProvider
     */
    public function testSetCronExpr($cronExpression, $expected)
    {
        // 1. Create mocks
        /** @var \Magento\Cron\Model\Schedule $model */
        $model = $this->helper->getObject(\Magento\Cron\Model\Schedule::class);

        // 2. Run tested method
        $model->setCronExpr($cronExpression);

        // 3. Compare actual result with expected result
        $result = $model->getCronExprArr();
        $this->assertEquals($result, $expected);
    }

    /**
     * Data provider
     *
     * Here is a list of allowed characters and values for Cron expression
     * http://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm
     *
     * @return array
     */
    public function setCronExprDataProvider()
    {
        return [
            ['1 2 3 4 5', [1, 2, 3, 4, 5]],
            ['1 2 3 4 5 6', [1, 2, 3, 4, 5, 6]],
            ['a b c d e', ['a', 'b', 'c', 'd', 'e']],   //should fail if validation will be added
            ['* * * * *', ['*', '*', '*', '*', '*']],

            ['0 * * * *', ['0', '*', '*', '*', '*']],
            ['59 * * * *', ['59', '*', '*', '*', '*']],
            [', * * * *', [',', '*', '*', '*', '*']],
            ['1-2 * * * *', ['1-2', '*', '*', '*', '*']],
            ['0/5 * * * *', ['0/5', '*', '*', '*', '*']],

            ['* 0 * * *', ['*', '0', '*', '*', '*']],
            ['* 59 * * *', ['*', '59', '*', '*', '*']],
            ['* , * * *', ['*', ',', '*', '*', '*']],
            ['* 1-2 * * *', ['*', '1-2', '*', '*', '*']],
            ['* 0/5 * * *', ['*', '0/5', '*', '*', '*']],

            ['* * 0 * *', ['*', '*', '0', '*', '*']],
            ['* * 23 * *', ['*', '*', '23', '*', '*']],
            ['* * , * *', ['*', '*', ',', '*', '*']],
            ['* * 1-2 * *', ['*', '*', '1-2', '*', '*']],
            ['* * 0/5 * *', ['*', '*', '0/5', '*', '*']],

            ['* * * 1 *', ['*', '*', '*', '1', '*']],
            ['* * * 31 *', ['*', '*', '*', '31', '*']],
            ['* * * , *', ['*', '*', '*', ',', '*']],
            ['* * * 1-2 *', ['*', '*', '*', '1-2', '*']],
            ['* * * 0/5 *', ['*', '*', '*', '0/5', '*']],
            ['* * * ? *', ['*', '*', '*', '?', '*']],
            ['* * * L *', ['*', '*', '*', 'L', '*']],
            ['* * * W *', ['*', '*', '*', 'W', '*']],
            ['* * * C *', ['*', '*', '*', 'C', '*']],

            ['* * * * 0', ['*', '*', '*', '*', '0']],
            ['* * * * 11', ['*', '*', '*', '*', '11']],
            ['* * * * ,', ['*', '*', '*', '*', ',']],
            ['* * * * 1-2', ['*', '*', '*', '*', '1-2']],
            ['* * * * 0/5', ['*', '*', '*', '*', '0/5']],
            ['* * * * JAN', ['*', '*', '*', '*', 'JAN']],
            ['* * * * DEC', ['*', '*', '*', '*', 'DEC']],
            ['* * * * JAN-DEC', ['*', '*', '*', '*', 'JAN-DEC']],

            ['* * * * * 1', ['*', '*', '*', '*', '*', '1']],
            ['* * * * * 7', ['*', '*', '*', '*', '*', '7']],
            ['* * * * * ,', ['*', '*', '*', '*', '*', ',']],
            ['* * * * * 1-2', ['*', '*', '*', '*', '*', '1-2']],
            ['* * * * * 0/5', ['*', '*', '*', '*', '*', '0/5']],
            ['* * * * * ?', ['*', '*', '*', '*', '*', '?']],
            ['* * * * * L', ['*', '*', '*', '*', '*', 'L']],
            ['* * * * * 6#3', ['*', '*', '*', '*', '*', '6#3']],
            ['* * * * * SUN', ['*', '*', '*', '*', '*', 'SUN']],
            ['* * * * * SAT', ['*', '*', '*', '*', '*', 'SAT']],
            ['* * * * * SUN-SAT', ['*', '*', '*', '*', '*', 'SUN-SAT']],
        ];
    }

    /**
     * @param string $cronExpression
     * @expectedException \Magento\Framework\Exception\CronException
     * @dataProvider setCronExprExceptionDataProvider
     */
    public function testSetCronExprException($cronExpression)
    {
        // 1. Create mocks
        /** @var \Magento\Cron\Model\Schedule $model */
        $model = $this->helper->getObject(\Magento\Cron\Model\Schedule::class);

        // 2. Run tested method
        $model->setCronExpr($cronExpression);
    }

    /**
     * Here is a list of allowed characters and values for Cron expression
     * http://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm
     *
     * @return array
     */
    public function setCronExprExceptionDataProvider()
    {
        return [
            [''],
            [null],
            [false],
            ['1 2 3 4'],
            ['1 2 3 4 5 6 7']
        ];
    }

    /**
     * @param int $scheduledAt
     * @param array $cronExprArr
     * @param $expected
     * @dataProvider tryScheduleDataProvider
     */
    public function testTrySchedule($scheduledAt, $cronExprArr, $expected)
    {
        // 1. Create mocks
        /** @var \Magento\Cron\Model\Schedule $model */
        $model = $this->helper->getObject(
            \Magento\Cron\Model\Schedule::class
        );

        // 2. Set fixtures
        $model->setScheduledAt($scheduledAt);
        $model->setCronExprArr($cronExprArr);

        // 3. Run tested method
        $result = $model->trySchedule();

        // 4. Compare actual result with expected result
        $this->assertEquals($expected, $result);
    }

    public function testTryScheduleWithConversionToAdminStoreTime()
    {
        $scheduledAt = '2011-12-13 14:15:16';
        $cronExprArr = ['*', '*', '*', '*', '*'];

        // 1. Create mocks
        $timezoneConverter = $this->createMock(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class);
        $timezoneConverter->expects($this->once())
            ->method('date')
            ->with($scheduledAt)
            ->willReturn(new \DateTime($scheduledAt));

        /** @var \Magento\Cron\Model\Schedule $model */
        $model = $this->helper->getObject(
            \Magento\Cron\Model\Schedule::class,
            ['timezoneConverter' => $timezoneConverter]
        );

        // 2. Set fixtures
        $model->setScheduledAt($scheduledAt);
        $model->setCronExprArr($cronExprArr);

        // 3. Run tested method
        $result = $model->trySchedule();

        // 4. Compare actual result with expected result
        $this->assertTrue($result);
    }

    /**
     * @return array
     */
    public function tryScheduleDataProvider()
    {
        $date = '2011-12-13 14:15:16';
        return [
            [$date, [], false],
            [$date, null, false],
            [$date, false, false],
            [$date, [], false],
            [$date, null, false],
            [$date, false, false],
            [strtotime($date), ['*', '*', '*', '*', '*'], true],
            [strtotime($date), ['15', '*', '*', '*', '*'], true],
            [strtotime($date), ['*', '14', '*', '*', '*'], true],
            [strtotime($date), ['*', '*', '13', '*', '*'], true],
            [strtotime($date), ['*', '*', '*', '12', '*'], true],
            [strtotime('Monday'), ['*', '*', '*', '*', '1'], true],
        ];
    }

    /**
     * @param string $cronExpressionPart
     * @param int $dateTimePart
     * @param bool $expectedResult
     * @dataProvider matchCronExpressionDataProvider
     */
    public function testMatchCronExpression($cronExpressionPart, $dateTimePart, $expectedResult)
    {
        // 1. Create mocks
        /** @var \Magento\Cron\Model\Schedule $model */
        $model = $this->helper->getObject(\Magento\Cron\Model\Schedule::class);

        // 2. Run tested method
        $result = $model->matchCronExpression($cronExpressionPart, $dateTimePart);

        // 3. Compare actual result with expected result
        $this->assertEquals($expectedResult, $result);
    }

    /**
     * @return array
     */
    public function matchCronExpressionDataProvider()
    {
        return [
            ['*', 0, true],
            ['*', 1, true],
            ['*', 59, true],

            ['0,1,20', 0, true],
            ['0,1,20', 1, true],
            ['0,1,20', 20, true],
            ['0,1,22', 2, false],
            ['0,1,*', 2, true],

            ['0-20', 0, true],
            ['0-20', 1, true],
            ['0-20', 20, true],
            ['0-20', 21, false],

            ['*/2', 0, true],
            ['*/2', 2, true],
            ['*/2', 4, true],
            ['*/2', 3, false],
            ['*/20', 40, true],

            ['0-20/5', 0, true],
            ['0-20/5', 5, true],
            ['0-20/5', 10, true],
            ['0-20/5', 21, false],
            ['0-20/5', 25, false],

            ['1/5', 5, false],
            ['5/5', 5, true],
            ['10/5', 10, true],
        ];
    }

    /**
     * @param string $cronExpressionPart
     * @expectedException \Magento\Framework\Exception\CronException
     * @dataProvider matchCronExpressionExceptionDataProvider
     */
    public function testMatchCronExpressionException($cronExpressionPart)
    {
        $dateTimePart = 10;

        // 1 Create mocks
        /** @var \Magento\Cron\Model\Schedule $model */
        $model = $this->helper->getObject(\Magento\Cron\Model\Schedule::class);

        // 2. Run tested method
        $model->matchCronExpression($cronExpressionPart, $dateTimePart);
    }

    /**
     * @return array
     */
    public function matchCronExpressionExceptionDataProvider()
    {
        return [
            ['1/2/3'],    //Invalid cron expression, expecting 'match/modulus': 1/2/3
            ['1/'],       //Invalid cron expression, expecting numeric modulus: 1/
            ['-'],        //Invalid cron expression
            ['1-2-3'],    //Invalid cron expression, expecting 'from-to' structure: 1-2-3
        ];
    }

    /**
     * @param mixed $param
     * @param int $expectedResult
     * @dataProvider getNumericDataProvider
     */
    public function testGetNumeric($param, $expectedResult)
    {
        // 1. Create mocks
        /** @var \Magento\Cron\Model\Schedule $model */
        $model = $this->helper->getObject(\Magento\Cron\Model\Schedule::class);

        // 2. Run tested method
        $result = $model->getNumeric($param);

        // 3. Compare actual result with expected result
        $this->assertEquals($expectedResult, $result);
    }

    /**
     * @return array
     */
    public function getNumericDataProvider()
    {
        return [
            [null, false],
            ['', false],
            ['0', 0],
            [0, 0],
            [1, 1],
            [PHP_INT_MAX, PHP_INT_MAX],
            [1.1, 1.1],

            ['feb', 2],
            ['Feb', 2],
            ['FEB', 2],
            ['february', 2],
            ['febXXX', 2],

            ['wed', 3],
            ['Wed', 3],
            ['WED', 3],
            ['Wednesday', 3],
            ['wedXXX', 3],
        ];
    }

    public function testTryLockJobSuccess()
    {
        $scheduleId = 1;

        $this->resourceJobMock->expects($this->once())
            ->method('trySetJobUniqueStatusAtomic')
            ->with($scheduleId, Schedule::STATUS_RUNNING, Schedule::STATUS_PENDING)
            ->will($this->returnValue(true));

        /** @var \Magento\Cron\Model\Schedule $model */
        $model = $this->helper->getObject(
            \Magento\Cron\Model\Schedule::class,
            [
                'resource' => $this->resourceJobMock
            ]
        );
        $model->setId($scheduleId);
        $this->assertEquals(0, $model->getStatus());

        $model->tryLockJob();

        $this->assertEquals(Schedule::STATUS_RUNNING, $model->getStatus());
    }

    public function testTryLockJobFailure()
    {
        $scheduleId = 1;

        $this->resourceJobMock->expects($this->once())
            ->method('trySetJobUniqueStatusAtomic')
            ->with($scheduleId, Schedule::STATUS_RUNNING, Schedule::STATUS_PENDING)
            ->will($this->returnValue(false));

        /** @var \Magento\Cron\Model\Schedule $model */
        $model = $this->helper->getObject(
            \Magento\Cron\Model\Schedule::class,
            [
                'resource' => $this->resourceJobMock
            ]
        );
        $model->setId($scheduleId);
        $this->assertEquals(0, $model->getStatus());

        $model->tryLockJob();

        $this->assertEquals(0, $model->getStatus());
    }
}