<?php
/**
 * iCalcreator, the PHP class package managing iCal (rfc2445/rfc5445) calendar information.
 *
 * This file is a part of iCalcreator.
 *
 * @author    Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
 * @copyright 2007-2022 Kjell-Inge Gustafsson, kigkonsult, All rights reserved
 * @link      https://kigkonsult.se
 * @license   Subject matter of licence is the software iCalcreator.
 *            The above copyright, link, package and version notices,
 *            this licence notice and the invariant [rfc5545] PRODID result use
 *            as implemented and invoked in iCalcreator shall be included in
 *            all copies or substantial portions of the iCalcreator.
 *
 *            iCalcreator is free software: you can redistribute it and/or modify
 *            it under the terms of the GNU Lesser General Public License as
 *            published by the Free Software Foundation, either version 3 of
 *            the License, or (at your option) any later version.
 *
 *            iCalcreator is distributed in the hope that it will be useful,
 *            but WITHOUT ANY WARRANTY; without even the implied warranty of
 *            MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *            GNU Lesser General Public License for more details.
 *
 *            You should have received a copy of the GNU Lesser General Public License
 *            along with iCalcreator. If not, see <https://www.gnu.org/licenses/>.
 */
namespace Kigkonsult\Icalcreator;

use Kigkonsult\Icalcreator\Util\DateTimeFactory;
use Kigkonsult\Icalcreator\Util\RecurFactory2;
use DateTime;
use Exception;

/**
 * class RecurTest, testing selectComponents
 *
 * @since  2.27.20 - 2019-05-20
 */
class RecurYearTest extends RecurBaseTest
{
    /**
     * recurYearlyTest111x provider
     *
     * @return mixed[]
     * @throws Exception
     */
    public function recurYearlyTest111xProvider() : array
    {
        $dataArr = [];
        $dataSetNo = 0;
        $DATASET = 'DATASET';

        $interval = 1;
        $count = 10;
        for( $ix = 111; $ix <= 112; $ix++ ) {
            $time = microtime( true );
            $start = DateTimeFactory::factory( '20190101T0900', 'Europe/Stockholm' );
            $year = (int)$start->format( 'Y' );
            $month = (int)$start->format( 'm' );
            $day = (int)$start->format( 'd' );
            $end = ( clone $start )->modify( '20 years' );
            $expects = [];
            $x = 1;
            while( $x < $count ) {
                $year += $interval;
                $Ymd = sprintf( '%04d%02d%02d', $year, $month, $day );
                $expects[] = $Ymd;
                ++$x;
            }
            $execTime = microtime( true ) - $time;
            $dataArr[] = [
                $ix . '-' . $interval,
                $start,
                $end,
                [
                    IcalInterface::FREQ => IcalInterface::YEARLY,
                    IcalInterface::INTERVAL => $interval,
                    IcalInterface::COUNT => $count,
                    $DATASET => $dataSetNo++
                ],
                $expects,
                $execTime
            ];
            ++$interval;
        }
        return $dataArr;
    }

    /**
     * Testing recur2date Yearly simple
     *
     * @test
     * @dataProvider recurYearlyTest111xProvider
     * @param string $case
     * @param DateTime $start
     * @param DateTime|mixed[] $end
     * @param mixed[]  $recur
     * @param mixed[]  $expects
     * @param float $prepTime
     * @throws Exception
     */
    public function recurYearlyTest111x(
        string           $case,
        DateTime         $start,
        DateTime | array $end,
        array            $recur,
        array            $expects,
        float            $prepTime
    ) : void
    {
        $this->recurYearlyTest1X( $case, $start, $end, $recur, $expects, $prepTime );
    }

    /**
     * recurYearlyTest23 provider
     *
     * @return mixed[]
     * @throws Exception
     */
    public function recurYearlyTest23Provider() : array
    {
        $dataArr   = [];
        $dataSetNo = 0;
        $DATASET   = 'DATASET';

        // rfc example 23 - exact (no interval)
        $dataArr[] = [
            '19-23-0-0-0',
            DateTimeFactory::factory( '19970610T0900', 'Europe/Stockholm' ),
            DateTimeFactory::factory( '20040610T0900', 'Europe/Stockholm' ),
            [
                IcalInterface::FREQ     => IcalInterface::YEARLY,
                IcalInterface::COUNT    => 10,
                IcalInterface::BYMONTH  => [ 6, 7 ],
                $DATASET                => $dataSetNo,
                'MRANGE'                => [ 6, 7 ]
            ],
            [ 19970710, 19980610, 19980710, 19990610, 19990710, 20000610, 20000710, 20010610, 20010710 ],
            0,0,
        ];

        // rfc example 23 - with interval for-loop '19-23-...'
        $count    = 10;
        $mRange   = [];
        for( $ix1 = 1; $ix1 < 5; $ix1++ ) {
            $interval = 0;
            for( $ix2 = 1; $ix2 <= 10; $ix2++ ) {
                ++$interval;
                $mRange[] = array_rand( array_flip( range( 1, 12 )));
                sort( $mRange );
                $mRange   = array_unique( $mRange, SORT_NUMERIC );
                $time     = microtime( true );
//              $start    = DateTimeFactory::factory( '20190101T0900', 'Europe/Stockholm' );
                $start    = DateTimeFactory::factory( '19970610T0900', 'Europe/Stockholm' );
                $startYmd = $start->format( 'Ymd' );
                $end      = ( clone $start )->setDate(
                    ((int) $start->format( 'Y' ) + ( 10 + $interval )),
                    (int) $start->format( 'm' ),
                    (int) $start->format( 'd' )
                );
                $endYmd   = $end->format( 'Ymd' );
                $expects  = [];
                $x        = 1;
                $wDate    = clone $start;
                $wDate    = $wDate->setDate(
                    (int) $wDate->format( 'Y' ),
                    1,
                    (int) $wDate->format( 'd' )
                );
                $currYear = (int) $wDate->format( 'Y' );
                while( $x < $count ) {
                    if( $currYear !== (int) $wDate->format( 'Y' )) {
                        $wDate   = $wDate->setDate(
                            ((int) $wDate->format( 'Y' ) + $interval ),
                            1,
                            (int) $wDate->format( 'd' )
                        );
                        $currYear = (int) $wDate->format( 'Y' );
                    }
                    if( $endYmd < $wDate->format( 'Ymd' )) {
                        break;
                    }
                    if( $startYmd < $wDate->format( 'Ymd' )) {
                        if( in_array( (int)$wDate->format( 'm' ), $mRange, true ) ) {
                            $expects[] = $wDate->format( 'Ymd' );
                            ++$x;
                        }
                        if( 12 === (int) $wDate->format( 'm' )) {
                            $currYear = -1;
                            continue;
                        }
                    }
                    $wDate = $wDate->setDate(
                        (int)  $wDate->format( 'Y' ),
                        ((int) $wDate->format( 'm' ) + 1 ),
                        (int)  $wDate->format( 'd' )
                    );
                } // end while
                $execTime  = microtime( true ) - $time;
                $dataArr[] = [
                    '19-23-' . $ix1 . '-' . $ix2 . '-' . $interval,
                    $start,
                    $end,
                    [
                        IcalInterface::FREQ     => IcalInterface::YEARLY,
                        IcalInterface::INTERVAL => $interval,
                        IcalInterface::COUNT    => $count,
                        IcalInterface::BYMONTH  => $mRange,
                        $DATASET            => $dataSetNo++,
                        'MRANGE'            => implode( ',', $mRange )
                    ],
                    $expects,
                    $execTime,
                ];
            } // end for... $x2
        } // end for... $x1

        return $dataArr;
    }

    /**
     * Testing recur2date Yearly , rfc example 23 - with interval for-loop '19-23-...'
     *
     * Test RecurFactory::recurYearly1
     *
     * @test
     * @dataProvider recurYearlyTest23Provider
     * @param string $case
     * @param DateTime $start
     * @param DateTime|mixed[] $end
     * @param mixed[]  $recur
     * @param mixed[]  $expects
     * @param float $prepTime
     * @throws Exception
     */
    public function recurYearlyTest23(
        string           $case,
        DateTime         $start,
        DateTime | array $end,
        array            $recur,
        array            $expects,
        float            $prepTime
    ) : void
    {
        $this->recurYearlyTest1X( $case, $start, $end, $recur, $expects, $prepTime );
    }

    /**
     * recurYearlyTest23e provider, rfc example 23 -Extended, both byMonth and byMonthDay
     *
     * @return mixed[]
     * @throws Exception
     */
    public function recurYearlyTest23eProvider() : array
    {
        $dataArr   = [];
        $dataSetNo = 0;
        $DATASET   = 'DATASET';

        // rfc example 23 -Extended, both byMonth and byMonthDay
        $start    = DateTimeFactory::factory( '20190101T0900', 'Europe/Stockholm' );
        $end      = ( clone $start )->modify('+10 years' );
        $count    = 20;
        $mRange   = [ 1 ]; // month
        $dRange   = [];    // days in month
        $baseDays = [ 4, 8, 12, 16, -16, -12, -8, -4 ];
        for( $ix1 = 1; $ix1 < 5; $ix1++ ) {
            $interval = 1;
            for( $ix2 = 1; $ix2 <= 2; $ix2++ ) {
                $mRange[] = array_rand( array_flip( range( 4, 12 )));
                sort( $mRange );
                $mRange   = array_values( array_unique( $mRange ));
                $dKey     = array_rand( $baseDays );
                $dRange[] = $baseDays[$dKey];
                sort( $dRange );
                $dRange   = array_values( array_unique( $dRange ));
                $time     = microtime( true );
                $startYmd = $start->format( 'Ymd' );
                $startYm  = $start->format( 'Ym' );
                $endYmd   = $end->format( 'Ymd' );
                $expects  = [];
                $x        = 1;
                $wDate    = clone $start;
                $currYear = $year = (int) $wDate->format( 'Y' );
                $mx       = 0;
                $month    = $mRange[$mx];
                $day      = (int) $wDate->format( 'd' );
                $wDate->setDate( $year, $month, $day );
                $currMonth = $month;
                while(( $x < $count ) && ( $endYmd > $wDate->format( 'Ymd' ))) {
//                    if( 4000 < ++$y ) break;
                    if( $currYear !== (int) $wDate->format( 'Y' )) {
                        $year    += $interval;
                        $currYear = $year;
                        $mx        = 0;
                        $currMonth = $month = $mRange[$mx];
                    } // end if
                    if( $currMonth !== $month ) {
                        ++$mx;
                        if( ! isset( $mRange[$mx] )) {
                            $currYear  = -1;
                            continue;
                        }
                        $currMonth = $month = $mRange[$mx];
                    } // end if
                    $wDate->setDate( $year, $month, $day );
                    if( $endYmd < $wDate->format( 'Ymd' )) {
                        break;
                    }
                    if( $startYm > $wDate->format( 'Ym' )) {
                        $currMonth    = -1;
                        continue;
                    }
                    if( in_array( $month, $mRange, true ) ) { // bort ??
                        $xDate = clone $wDate;
                        foreach( RecurFactory2::getMonthDaysFromByMonthDayList(
                            (int) $wDate->format( 't' ),
                            $dRange
                        ) as $monthDay ) {
                            if( $x >= $count ) {
                                break 2;
                            }
                            $xDate->setDate(
                                (int) $wDate->format( 'Y' ),
                                (int) $wDate->format( 'm' ),
                                $monthDay
                            );
                            $Ymd = $xDate->format( 'Ymd' );
                            if( $startYmd >= $Ymd ) {
                                continue;
                            }
                            if( $endYmd < $Ymd ) {
                                break 2;
                            }
                            $expects[] = $Ymd;
                            ++$x;
                        } // end foreach
                    } // end if ... in mRange
                    $currMonth = -1;
                } // end while
                $execTime  = microtime( true ) - $time;
                $dataArr[] = [
                    '19-23e' . $ix1 . '-' . $ix2 . '-' . $interval,
                    $start,
                    $end,
                    [
                        IcalInterface::FREQ       => IcalInterface::YEARLY,
                        IcalInterface::INTERVAL   => $interval,
                        IcalInterface::COUNT      => $count,
                        IcalInterface::BYMONTH    => $mRange,
                        IcalInterface::BYMONTHDAY => $dRange,
                        $DATASET              => $dataSetNo++,
                        'MRANGE'              => implode( ',', $mRange )
                    ],
                    $expects,
                    $execTime,
                ];
            } // end for... $x2
        } // end for... $x1

        return $dataArr;
    }

    /**
     * Testing recur2date Yearly , rfc example 23 -Extended, both byMonth and byMonthDay
     *
     * @test
     * @dataProvider recurYearlyTest23eProvider
     * @param string $case
     * @param DateTime $start
     * @param DateTime|mixed[] $end
     * @param mixed[]  $recur
     * @param mixed[]  $expects
     * @param float $prepTime
     * @throws Exception
     */
    public function recurYearlyTest23e(
        string           $case,
        DateTime         $start,
        DateTime | array $end,
        array            $recur,
        array            $expects,
        float            $prepTime
    ) : void
    {
        $this->recurYearlyTest1X( $case, $start, $end, $recur, $expects, $prepTime );
    }

    /**
     * recurYearlyTest23l provider, rfc example 23 changed date and limited by INTERVAL 2 and byMonthDay [ -20, -10 ]
     *
     * @return mixed[]
     * @throws Exception
     */
    public function recurYearlyTest23lProvider() : array
    {
        $dataArr   = [];
        $dataSetNo = 0;
        $DATASET   = 'DATASET';

        // rfc example 23 changed date and limited by INTERVAL 2 and byMonthDay [ -20, -10 ]
        $start    = DateTimeFactory::factory( '20200801T0900', 'Europe/Stockholm' );
        $end      = ( clone $start )->modify('+12 years' );
        $dataArr[] = [
            '19-23l',
            $start,
            $end,
            [
                IcalInterface::FREQ       => IcalInterface::YEARLY,
                IcalInterface::INTERVAL   => 2,
                IcalInterface::COUNT      => 10,
                IcalInterface::BYMONTHDAY => [ -15, -2 ],
                $DATASET              => $dataSetNo++,
            ],
            [ 20200817, 20200830, 20220817, 20220830, 20240817, 20240830, 20260817, 20260830, 20280817 ],
            0.0,
        ];

        return $dataArr;
    }

    /**
     * Testing recur2date Yearly, rfc example 23 changed date and limited by INTERVAL 2 and byMonthDay [ -20, -10 ]
     *
     * @test
     * @dataProvider recurYearlyTest23lProvider
     * @param string $case
     * @param DateTime $start
     * @param DateTime|mixed[] $end
     * @param mixed[]  $recur
     * @param mixed[]  $expects
     * @param float $prepTime
     * @throws Exception
     */
    public function recurYearlyTest23l(
        string           $case,
        DateTime         $start,
        DateTime | array $end,
        array            $recur,
        array            $expects,
        float            $prepTime
    ) : void
    {
        $this->recurYearlyTest1X( $case, $start, $end, $recur, $expects, $prepTime );
    }

    /**
     * Testing recur2date Yearly
     *
     * @param string $case
     * @param DateTime $start
     * @param DateTime|mixed[] $end
     * @param mixed[]  $recur
     * @param mixed[]  $expects
     * @param float $prepTime
     * @throws Exception
     */
    public function recurYearlyTest1X(
        string           $case,
        DateTime         $start,
        DateTime | array $end,
        array            $recur,
        array            $expects,
        float            $prepTime
    ) : void
    {
        $saveStartDate = clone $start;

        if( ! isset( $recur[IcalInterface::INTERVAL] )) {
            $recur[IcalInterface::INTERVAL] = 1;
        }

//      error_log('' ); // test ###
//      error_log( __FUNCTION__ . ' start ' . $case ); // test ###

        $strCase   = str_pad( $case, 12 );

        if( '19-23l' === $case ) {
            $result = array_flip( $expects );
            if( defined( 'DISPRECUR' ) && ( '1' === DISPRECUR )) {
                echo $strCase . 'expects                  : ' .
                    implode( ' - ', array_keys( $result ) ) . PHP_EOL;        // test ###
            }
        }
        else {
            $result = $this->recur2dateTest( // return old
                $case,
                $start,
                $end,
                $recur,
                $expects,
                $prepTime
            );
        }
        $recurDisp = str_replace( [PHP_EOL, ' ' ], '', var_export( $recur, true ));
        if( ! RecurFactory2::isRecurYearly1( $recur )) {
            if( defined( 'DISPRECUR' ) && ( '1' === DISPRECUR )) {
                echo $strCase . ' NOT isRecurYearly1 ' . $recurDisp . PHP_EOL;
            }
            $this->fail();
        }
        $time     = microtime( true );
        $resultX  = RecurFactory2::recurYearly1( $recur, $start, clone $start, $end );
        $execTime = microtime( true ) - $time;
        if( defined( 'DISPRECUR' ) && ( '1' === DISPRECUR )) {
            echo $strCase . 'year smpl1 time:' . number_format( $execTime, 6 ) . ' : ' .
                implode( ' - ', array_keys( $resultX ) ) . PHP_EOL;        // test ###
            echo $recurDisp . ' start ' . $start->format( 'Ymd' ) . ' end ' . $end->format( 'Ymd' ) . PHP_EOL; // test ###
        }
        $endFormat = is_array( $end )
            ? implode( '-', $end )
            : $end->format( 'Ymd' );
        $this->assertEquals(
            array_keys( $result ),  // exp, old
            array_keys( $resultX ), // new, actual
            sprintf(
                self::$ERRFMT,
                __FUNCTION__,
                $case . ' old -> new  ',
                $saveStartDate->format( 'Ymd' ),
                $endFormat,
                $recurDisp
            )
        );
    }

    /**
     * recurYearlyTest2X provider
     *
     * @return mixed[]
     * @throws Exception
     */
    public function recurYearlyTest2XProvider() : array
    {
        $dataArr   = [];
        $dataSetNo = 0;
        $DATASET   = 'DATASET';

        // yearly in june, third TU/WE/TH in month, forever
        $start   = DateTimeFactory::factory( '20200101T090000', 'Europe/Stockholm');
        $wDate   = clone $start;
        $dataArr[] = [
            '2001',
            $start,
            $wDate->modify(  10 . ' year' ), // end
            [
                IcalInterface::FREQ      => IcalInterface::YEARLY,
                IcalInterface::BYMONTH   => 6,
                IcalInterface::BYDAY     => [
                    [ IcalInterface::DAY => IcalInterface::TU ],
                    [ IcalInterface::DAY => IcalInterface::WE ],
                    [ IcalInterface::DAY => IcalInterface::TH ],
                ],
                IcalInterface::BYSETPOS  => -3,
                $DATASET             => $dataSetNo++
            ],
            [ 20200624,20210624,20220628,20230627,20240625,20250624,20260624,20270624,20280627,20290626 ]
        ];

        // neotsn Thanksgiving event - 4th Thursday of every November - Yearly - same in recurMonthly2Test by MONTHLY
        $start   = DateTimeFactory::factory( '20201126T113000', 'America/Chicago');
        $wDate   = clone $start;
        $dataArr[] = [
            'neotsn',
            $start,
            $wDate->modify(  10 . ' year' ), // end
            [
                IcalInterface::FREQ      => IcalInterface::YEARLY,
                IcalInterface::BYMONTH   => 11,
                IcalInterface::BYDAY     => [
                    [ IcalInterface::DAY => IcalInterface::TH ],
                ],
                IcalInterface::BYSETPOS  => 4,
                $DATASET             => $dataSetNo++
            ],
            [ 20211125,20221124,20231123,20241128,20251127,20261126,20271125,20281123,20291122 ]
        ];

        return $dataArr;
    }

    /**
     * Testing recurMonthlyYearly3 - YEARLY + BYMONTH + BYDAY
     *
     * @test
     * @dataProvider recurYearlyTest2XProvider
     * @param string   $case
     * @param DateTime $start
     * @param DateTime|mixed[] $end
     * @param mixed[]  $recur
     * @param mixed[]  $expects
     * @throws Exception
     */
    public function recurYearly2XTest(
        string           $case,
        DateTime         $start,
        DateTime | array $end,
        array            $recur,
        array            $expects
    ) : void
    {
        $saveStartDate = clone $start;

        if( ! isset( $recur[IcalInterface::INTERVAL] )) {
            $recur[IcalInterface::INTERVAL] = 1;
        }
        $strCase   = str_pad( $case, 12 );
        $recurDisp = str_replace( [PHP_EOL, ' ' ], '', var_export( $recur, true ));
        if( ! RecurFactory2::isRecurYearly2( $recur )) {
            if( defined( 'DISPRECUR' ) && ( '1' === DISPRECUR )) {
                echo $strCase . ' NOT isRecurYearly2 ' . $recurDisp . PHP_EOL;
            }
            $this->fail();
        } // end if
        $time     = microtime( true );
        $resultX  = RecurFactory2::recurMonthlyYearly3( $recur, $start, clone $start, $end );
        $execTime = microtime( true ) - $time;
        if( defined( 'DISPRECUR' ) && ( '1' === DISPRECUR )) {
            echo $strCase . 'year smpl2 time:' . number_format( $execTime, 6 ) . ' : ' .
                implode( ' - ', array_keys( $resultX ) ) . PHP_EOL; // test ###
            echo $recurDisp . PHP_EOL; // test ###
        }
        $this->assertEquals(
            $expects,
            array_keys( $resultX ),
            sprintf( self::$ERRFMT, __FUNCTION__, $case . '-21',
                $saveStartDate->format( 'Ymd' ),
                $end->format( 'Ymd' ),
                PHP_EOL . $recurDisp .
                PHP_EOL . 'exp : ' . implode( ',', $expects ) .
                PHP_EOL . 'got : ' . implode( ',', array_keys( $resultX ))
            )
        );
    }
}