<?php
/**
 * Piwik - Open source web analytics
 *
 * @link http://piwik.org
 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 */
use Piwik\EventDispatcher;
use Piwik\TaskScheduler;
use Piwik\ScheduledTask;

class TaskSchedulerTest extends PHPUnit_Framework_TestCase
{
    private static function getTestTimetable()
    {
        return array(
            'Piwik_CoreAdminHome.purgeOutdatedArchives' => 1355529607,
            'Piwik_PrivacyManager.deleteReportData_1'   => 1322229607,
        );
    }

    /**
     * Dataprovider for testGetTimetableFromOptionValue
     */
    public function getTimetableFromOptionValueTestCases()
    {
        return array(

            // invalid option values should return a fresh array
            array(array(), false),
            array(array(), null),
            array(array(), 1),
            array(array(), ''),
            array(array(), 'test'),

            // valid serialized array
            array(
                array(
                    'Piwik_CoreAdminHome.purgeOutdatedArchives' => 1355529607,
                    'Piwik_PrivacyManager.deleteReportData'     => 1355529607,
                ),
                'a:2:{s:41:"Piwik_CoreAdminHome.purgeOutdatedArchives";i:1355529607;s:37:"Piwik_PrivacyManager.deleteReportData";i:1355529607;}'
            ),
        );
    }

    /**
     * @group Core
     * @group TaskScheduler
     * @dataProvider getTimetableFromOptionValueTestCases
     */
    public function testGetTimetableFromOptionValue($expectedTimetable, $option)
    {
        $getTimetableFromOptionValue = new ReflectionMethod(
            '\Piwik\TaskScheduler', 'getTimetableFromOptionValue'
        );
        $getTimetableFromOptionValue->setAccessible(true);

        $this->assertEquals($expectedTimetable, $getTimetableFromOptionValue->invoke(new TaskScheduler(), $option));
    }

    /**
     * Dataprovider for testTaskHasBeenScheduledOnce
     */
    public function taskHasBeenScheduledOnceTestCases()
    {
        $timetable = self::getTestTimetable();

        return array(
            array(true, 'Piwik_CoreAdminHome.purgeOutdatedArchives', $timetable),
            array(true, 'Piwik_PrivacyManager.deleteReportData_1', $timetable),
            array(false, 'Piwik_PDFReports.weeklySchedule"', $timetable)
        );
    }

    /**
     * @group Core
     * @group TaskScheduler
     * @dataProvider taskHasBeenScheduledOnceTestCases
     */
    public function testTaskHasBeenScheduledOnce($expectedDecision, $taskName, $timetable)
    {
        $taskHasBeenScheduledOnce = new ReflectionMethod(
            '\Piwik\TaskScheduler', 'taskHasBeenScheduledOnce'
        );
        $taskHasBeenScheduledOnce->setAccessible(true);

        $this->assertEquals($expectedDecision, $taskHasBeenScheduledOnce->invoke(new TaskScheduler(), $taskName, $timetable));
    }

    /**
     * Dataprovider for testGetScheduledTimeForMethod
     */
    public function getScheduledTimeForMethodTestCases()
    {
        $timetable = serialize(self::getTestTimetable());

        return array(
            array(1355529607, 'Piwik_CoreAdminHome', 'purgeOutdatedArchives', null, $timetable),
            array(1322229607, 'Piwik_PrivacyManager', 'deleteReportData', 1, $timetable),
            array(false, 'Piwik_PDFReports', 'weeklySchedule', null, $timetable)
        );
    }

    /**
     * @group Core
     * @group TaskScheduler
     * @dataProvider getScheduledTimeForMethodTestCases
     */
    public function testGetScheduledTimeForMethod($expectedTime, $className, $methodName, $methodParameter, $timetable)
    {
        self::stubPiwikOption($timetable);

        $this->assertEquals($expectedTime, TaskScheduler::getScheduledTimeForMethod($className, $methodName, $methodParameter));

        self::resetPiwikOption();
    }

    /**
     * Dataprovider for testTaskShouldBeExecuted
     */
    public function taskShouldBeExecutedTestCases()
    {
        $timetable = self::getTestTimetable();

        // set a date in the future (should not run)
        $timetable['Piwik_CoreAdminHome.purgeOutdatedArchives'] = time() + 60000;

        // set now (should run)
        $timetable['Piwik_PrivacyManager.deleteReportData_1'] = time();

        return array(
            array(false, 'Piwik_CoreAdminHome.purgeOutdatedArchives', $timetable),
            array(true, 'Piwik_PrivacyManager.deleteReportData_1', $timetable),
            array(false, 'Piwik_PDFReports.weeklySchedule"', $timetable)
        );
    }

    /**
     * @group Core
     * @group TaskScheduler
     * @dataProvider taskShouldBeExecutedTestCases
     */
    public function testTaskShouldBeExecuted($expectedDecision, $taskName, $timetable)
    {
        $taskShouldBeExecuted = new ReflectionMethod(
            '\Piwik\TaskScheduler', 'taskShouldBeExecuted'
        );
        $taskShouldBeExecuted->setAccessible(true);

        $this->assertEquals($expectedDecision, $taskShouldBeExecuted->invoke(new TaskScheduler(), $taskName, $timetable));
    }

    /**
     * Dataprovider for testExecuteTask
     */
    public function executeTaskTestCases()
    {
        return array(
            array('scheduledTaskOne', null),
            array('scheduledTaskTwo', 'parameterValue'),
            array('scheduledTaskTwo', 1),
        );
    }

    /**
     * @group Core
     * @group TaskScheduler
     * @dataProvider executeTaskTestCases
     */
    public function testExecuteTask($methodName, $parameterValue)
    {
        // assert the scheduled method is executed once with the correct parameter
        $mock = $this->getMock('TaskSchedulerTest', array($methodName));
        $mock->expects($this->once())->method($methodName)->with($this->equalTo($parameterValue));

        $executeTask = new ReflectionMethod('\Piwik\TaskScheduler', 'executeTask');
        $executeTask->setAccessible(true);

        $this->assertNotEmpty($executeTask->invoke(
            new TaskScheduler(),
            new ScheduledTask ($mock, $methodName, $parameterValue, new Piwik_ScheduledTime_Daily())
        ));
    }

    /**
     * Dataprovider for testRunTasks
     */
    public function testRunTasksTestCases()
    {
        $systemTime = time();

        $dailySchedule = $this->getMock('Piwik_ScheduledTime_Daily', array('getTime'));
        $dailySchedule->expects($this->any())
            ->method('getTime')
            ->will($this->returnValue($systemTime));

        $scheduledTaskOne = new ScheduledTask ($this, 'scheduledTaskOne', null, $dailySchedule);
        $scheduledTaskTwo = new ScheduledTask ($this, 'scheduledTaskTwo', 1, $dailySchedule);
        $scheduledTaskThree = new ScheduledTask ($this, 'scheduledTaskThree', null, $dailySchedule);

        $caseOneExpectedTable = array(
            'TaskSchedulerTest.scheduledTaskOne'   => $scheduledTaskOne->getRescheduledTime(),
            'TaskSchedulerTest.scheduledTaskTwo_1' => $systemTime + 60000,
            'TaskSchedulerTest.scheduledTaskThree' => $scheduledTaskThree->getRescheduledTime(),
        );

        $caseTwoTimetableBeforeExecution = $caseOneExpectedTable;
        $caseTwoTimetableBeforeExecution['TaskSchedulerTest.scheduledTaskThree'] = $systemTime; // simulate elapsed time between case 1 and 2

        return array(

            // case 1) contains :
            // - scheduledTaskOne: already scheduled before, should be executed and rescheduled
            // - scheduledTaskTwo: already scheduled before, should not be executed and therefore not rescheduled
            // - scheduledTaskThree: not already scheduled before, should be scheduled but not executed
            array(
                $caseOneExpectedTable,

                // methods that should be executed
                array(
                    'TaskSchedulerTest.scheduledTaskOne'
                ),

                // timetable before task execution
                array(
                    'TaskSchedulerTest.scheduledTaskOne'   => $systemTime,
                    'TaskSchedulerTest.scheduledTaskTwo_1' => $systemTime + 60000,
                ),
                // configured tasks
                array(
                    $scheduledTaskOne,
                    $scheduledTaskTwo,
                    $scheduledTaskThree,
                )
            ),

            // case 2) follows case 1) with :
            // - scheduledTaskOne: already scheduled before, should not be executed and therefore not rescheduled
            // - scheduledTaskTwo: not configured for execution anymore, should be removed from the timetable
            // - scheduledTaskThree: already scheduled before, should be executed and rescheduled
            array(
                // expected timetable
                array(
                    'TaskSchedulerTest.scheduledTaskOne'   => $scheduledTaskOne->getRescheduledTime(),
                    'TaskSchedulerTest.scheduledTaskThree' => $scheduledTaskThree->getRescheduledTime()
                ),

                // methods that should be executed
                array(
                    'TaskSchedulerTest.scheduledTaskThree'
                ),

                // timetable before task execution
                $caseTwoTimetableBeforeExecution,

                // configured tasks
                array(
                    $scheduledTaskOne,
//					$scheduledTaskTwo, Not configured anymore (ie. not returned after TaskScheduler::GET_TASKS_EVENT is issued)
                    $scheduledTaskThree,
                )
            ),
        );
    }

    public function scheduledTaskOne()
    {
    } // nothing to do
    public function scheduledTaskTwo($param)
    {
    } // nothing to do
    public function scheduledTaskThree()
    {
    } // nothing to do

    /**
     * @group Core
     * @group TaskScheduler
     * @dataProvider testRunTasksTestCases
     */
    public function testRunTasks($expectedTimetable, $expectedExecutedTasks, $timetableBeforeTaskExecution, $configuredTasks)
    {
        // temporarily unload plugins
        $plugins = \Piwik\PluginsManager::getInstance()->getLoadedPlugins();
        $plugins = array_map(function ($p) { return $p->getPluginName(); }, $plugins);

        \Piwik\PluginsManager::getInstance()->unloadPlugins();
        
        // make sure the get tasks event returns our configured tasks
        Piwik_AddAction(TaskScheduler::GET_TASKS_EVENT, function(&$tasks) use($configuredTasks) {
            $tasks = $configuredTasks;
        });

        // stub the piwik option object to control the returned option value
        self::stubPiwikOption(serialize($timetableBeforeTaskExecution));

        // execute tasks
        $executionResults = TaskScheduler::runTasks();

        // assert methods are executed
        $executedTasks = array();
        foreach ($executionResults as $executionResult) {
            $executedTasks[] = $executionResult['task'];
            $this->assertNotEmpty($executionResult['output']);
        }
        $this->assertEquals($expectedExecutedTasks, $executedTasks);

        // assert the timetable is correctly updated
        $getTimetableFromOptionTable = new ReflectionMethod('\Piwik\TaskScheduler', 'getTimetableFromOptionTable');
        $getTimetableFromOptionTable->setAccessible(true);
        $this->assertEquals($expectedTimetable, $getTimetableFromOptionTable->invoke(new TaskScheduler()));

        // restore loaded plugins & piwik options
        EventDispatcher::getInstance()->clearObservers(TaskScheduler::GET_TASKS_EVENT);
        \Piwik\PluginsManager::getInstance()->loadPlugins($plugins);
        self::resetPiwikOption();
    }

    private static function stubPiwikOption($timetable)
    {
        self::getReflectedPiwikOptionInstance()->setValue(new MockPiwikOption($timetable));
    }

    private static function resetPiwikOption()
    {
        self::getReflectedPiwikOptionInstance()->setValue(null);
    }

    private static function getReflectedPiwikOptionInstance()
    {
        $piwikOptionInstance = new ReflectionProperty('\Piwik\Option', 'instance');
        $piwikOptionInstance->setAccessible(true);
        return $piwikOptionInstance;
    }
}