<?php
namespace App\Tests\Unit\Service\Constraint;

use App\Entity\Access\Access;
use App\Entity\Organization\Organization;
use App\Entity\Organization\Parameters;
use App\Repository\Access\AccessRepository;
use App\Service\Constraint\DateTimeConstraint;
use App\Service\Organization\Utils as OrganizationUtils;
use App\Tests\Unit\TestToolsTrait;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

class TestableDateTimeConstraint extends DateTimeConstraint {
    public function hasCustomPeriods($historical): bool { return parent::hasCustomPeriods($historical); }
    public function addConstraint(array $contraints, array $newContraint): array { return parent::addConstraint($contraints, $newContraint); }
    public function cleanConstraints(array $constraints): array { return parent::cleanConstraints($constraints); }

    public function getCustomPeriods(string $dateStart, string $dateEnd): array { return parent::getCustomPeriods($dateStart, $dateEnd); }
    public function getPeriods(Access $access): array { return parent::getPeriods($access); }
    public function presentConstraint(array $periods): array { return parent::presentConstraint($periods); }
    public function pastConstraint($periods): array { return parent::pastConstraint($periods); }
    public function futureConstraint($periods): array { return parent::futureConstraint($periods); }
}


class DateTimeConstraintTest extends TestCase
{
   use TestToolsTrait;

   private MockObject | OrganizationUtils $organizationUtils;
   private MockObject | EntityManagerInterface $entityManager;

   private array $periods;

   public function setUp(): void
   {
       $this->entityManager = $this->getMockBuilder(EntityManagerInterface::class)->disableOriginalConstructor()->getMock();
       $this->organizationUtils = $this->getMockBuilder(OrganizationUtils::class)->disableOriginalConstructor()->getMock();

       $this->periods = [
           'dateStart' => '2021-12-20',
           'dateEnd' => '2022-08-31'
       ];
   }

   /**
    * @see DateTimeConstraint::invoke()
    */
   public function testInvokePresentNoCustomPeriods(): void {
       $dateTimeConstraint = $this->getMockBuilder(TestableDateTimeConstraint::class)
           ->setConstructorArgs([$this->entityManager, $this->organizationUtils])
           ->setMethodsExcept(['invoke'])
           ->getMock();

       $access = $this->getMockBuilder(Access::class)->getMock();

       $accessRepository = $this->getMockBuilder(AccessRepository::class)->disableOriginalConstructor()->getMock();
       $accessRepository->method('find')->with(123)->willReturn($access);

       $this->entityManager->method('getRepository')->with(Access::class)->willReturn($accessRepository);

       $historical = ["future" => false,"past" => false,"present" => true,"dateStart" => null,"dateEnd" => null];
       $access->method('getHistorical')->willReturn($historical);

       $dateTimeConstraint->method('hasCustomPeriods')->with($historical)->willReturn(false);

       $periods = ['dateStart' => '2020-01-01', 'dateEnd' => '2020-12-31'];
       $dateTimeConstraint->method('getPeriods')->with($access)->willReturn($periods);

       $constraint = ['foo'];
       $dateTimeConstraint->method('presentConstraint')->with($periods)->willReturn($constraint);

       $dateTimeConstraint
           ->expects(self::once())
           ->method('addConstraint')
           ->with(['start' => [], 'end' => []] , $constraint)
           ->willReturn(['bar']);

       $dateTimeConstraint
           ->expects(self::once())
           ->method('cleanConstraints')
           ->with(['bar']);

       $dateTimeConstraint->invoke(123);
   }

    /**
     * @see DateTimeConstraint::invoke()
     */
    public function testInvokePastNoCustomPeriods(): void
    {
        $dateTimeConstraint = $this->getMockBuilder(TestableDateTimeConstraint::class)
            ->setConstructorArgs([$this->entityManager, $this->organizationUtils])
            ->setMethodsExcept(['invoke'])
            ->getMock();

        $access = $this->getMockBuilder(Access::class)->getMock();

        $accessRepository = $this->getMockBuilder(AccessRepository::class)->disableOriginalConstructor()->getMock();
        $accessRepository->method('find')->with(123)->willReturn($access);

        $this->entityManager->method('getRepository')->with(Access::class)->willReturn($accessRepository);

        $historical = ["future" => false,"past" => true,"present" => false,"dateStart" => null,"dateEnd" => null];

        $access->method('getHistorical')->willReturn($historical);

        $dateTimeConstraint->method('hasCustomPeriods')->with($historical)->willReturn(false);

        $periods = ['dateStart' => '2020-01-01', 'dateEnd' => '2020-12-31'];
        $dateTimeConstraint->method('getPeriods')->with($access)->willReturn($periods);

        $constraint = ['foo'];
        $dateTimeConstraint->method('pastConstraint')->with($periods)->willReturn($constraint);

        $dateTimeConstraint
            ->expects(self::once())
            ->method('addConstraint')
            ->with(['start' => [], 'end' => []] , $constraint)
            ->willReturn(['bar']);

        $dateTimeConstraint
            ->expects(self::once())
            ->method('cleanConstraints')
            ->with(['bar']);

        $dateTimeConstraint->invoke(123);
    }

    /**
     * @see DateTimeConstraint::invoke()
     */
    public function testInvokeFutureNoCustomPeriods(): void
    {
        $dateTimeConstraint = $this->getMockBuilder(TestableDateTimeConstraint::class)
            ->setConstructorArgs([$this->entityManager, $this->organizationUtils])
            ->setMethodsExcept(['invoke'])
            ->getMock();

        $access = $this->getMockBuilder(Access::class)->getMock();

        $accessRepository = $this->getMockBuilder(AccessRepository::class)->disableOriginalConstructor()->getMock();
        $accessRepository->method('find')->with(123)->willReturn($access);

        $this->entityManager->method('getRepository')->with(Access::class)->willReturn($accessRepository);

        $historical = ["future" => true,"past" => false,"present" => false,"dateStart" => null,"dateEnd" => null];

        $access->method('getHistorical')->willReturn($historical);

        $dateTimeConstraint->method('hasCustomPeriods')->with($historical)->willReturn(false);

        $periods = ['dateStart' => '2020-01-01', 'dateEnd' => '2020-12-31'];
        $dateTimeConstraint->method('getPeriods')->with($access)->willReturn($periods);

        $constraint = ['foo'];
        $dateTimeConstraint->method('futureConstraint')->with($periods)->willReturn($constraint);

        $dateTimeConstraint
            ->expects(self::once())
            ->method('addConstraint')
            ->with(['start' => [], 'end' => []] , $constraint)
            ->willReturn(['bar']);

        $dateTimeConstraint
            ->expects(self::once())
            ->method('cleanConstraints')
            ->with(['bar']);

        $dateTimeConstraint->invoke(123);
    }

    /**
     * @see DateTimeConstraint::invoke()
     */
    public function testInvokeMultiplePeriodsNoCustom(): void
    {
        $dateTimeConstraint = $this->getMockBuilder(TestableDateTimeConstraint::class)
            ->setConstructorArgs([$this->entityManager, $this->organizationUtils])
            ->setMethodsExcept(['invoke'])
            ->getMock();

        $access = $this->getMockBuilder(Access::class)->getMock();

        $accessRepository = $this->getMockBuilder(AccessRepository::class)->disableOriginalConstructor()->getMock();
        $accessRepository->method('find')->with(123)->willReturn($access);

        $this->entityManager->method('getRepository')->with(Access::class)->willReturn($accessRepository);

        $historical = ["future" => true,"past" => true,"present" => true,"dateStart" => null,"dateEnd" => null];

        $access->method('getHistorical')->willReturn($historical);
        $dateTimeConstraint->method('getPeriods')->with($access)->willReturn(['dateStart' => '2020-01-01', 'dateEnd' => '2020-12-31']);

        $dateTimeConstraint->method('hasCustomPeriods')->with($historical)->willReturn(false);

        $dateTimeConstraint->method('pastConstraint')->willReturn(['pastConstraint']);
        $dateTimeConstraint->method('presentConstraint')->willReturn(['presentConstraint']);
        $dateTimeConstraint->method('futureConstraint')->willReturn(['futureConstraint']);

        $dateTimeConstraint
            ->expects(self::exactly(3))
            ->method('addConstraint')
            ->withConsecutive(
                [['start' => [], 'end' => []] , ['presentConstraint']],
                [['start' => [], 'end' => []] , ['pastConstraint']],
                [['start' => [], 'end' => []] , ['futureConstraint']]
            )->willReturn(['start' => [], 'end' => []]);

        $dateTimeConstraint
            ->expects(self::once())
            ->method('cleanConstraints')
            ->with(['start' => [], 'end' => []]);

        $dateTimeConstraint->invoke(123);
    }

    /**
     * @see DateTimeConstraint::invoke()
     */
    public function testInvokeWithCustom(): void {
        $dateTimeConstraint = $this->getMockBuilder(TestableDateTimeConstraint::class)
            ->setConstructorArgs([$this->entityManager, $this->organizationUtils])
            ->setMethodsExcept(['invoke'])
            ->getMock();

        $access = $this->getMockBuilder(Access::class)->getMock();

        $accessRepository = $this->getMockBuilder(AccessRepository::class)->disableOriginalConstructor()->getMock();
        $accessRepository->method('find')->with(123)->willReturn($access);

        $this->entityManager->method('getRepository')->with(Access::class)->willReturn($accessRepository);

        $historical = ["future" => false,"past" => false,"present" => true,"dateStart" => '2020-01-01',"dateEnd" => '2020-02-01'];

        $access->method('getHistorical')->willReturn($historical);

        $dateTimeConstraint->method('hasCustomPeriods')->with($historical)->willReturn(true);

        $dateTimeConstraint->expects(self::never())->method('pastConstraint');
        $dateTimeConstraint->expects(self::never())->method('futureConstraint');

        $dateTimeConstraint
            ->method('getCustomPeriods')
            ->with('2020-01-01', '2020-02-01')
            ->willReturn(['dateStart' => 2020, 'dateEnd' => 2020]);

        $dateTimeConstraint->expects(self::once())
            ->method('presentConstraint')
            ->with(['dateStart' => 2020, 'dateEnd' => 2020])
            ->willReturn(['bar']);

        $dateTimeConstraint
            ->expects(self::once())
            ->method('addConstraint')
            ->with(['start' => [], 'end' => []], ['bar'])
            ->willReturn(['start' => [], 'end' => []]);

        $dateTimeConstraint
            ->expects(self::once())
            ->method('cleanConstraints')
            ->with(['start' => [], 'end' => []]);

        $dateTimeConstraint->invoke(123);
    }

    /**
     * @see DateTimeConstraint::getCustomPeriods()
     */
   public function testGetCustomPeriods(): void {
       $dateTimeConstraint = $this->getMockBuilder(TestableDateTimeConstraint::class)
           ->setConstructorArgs([$this->entityManager, $this->organizationUtils])
           ->setMethodsExcept(['getCustomPeriods'])
           ->getMock();

       $this->assertEquals(
           ['dateStart' => '2020-01-01', 'dateEnd' => '2020-12-31'],
           $dateTimeConstraint->getCustomPeriods('2020-01-01', '2020-12-31')
       );
   }

   /**
    * @see DateTimeConstraint::presentConstraint()
    */
   public function testPresentConstraint(): void
   {
       $expected = [
           'start' => [
               '2022-08-31' => 4
           ],
           'end' => [
               '2021-12-20' => 8,
               'NULL' => 0
           ]
       ];

       $dateTimeConstraint = $this->getMockBuilder(TestableDateTimeConstraint::class)
           ->setConstructorArgs([$this->entityManager, $this->organizationUtils])
           ->setMethodsExcept(['presentConstraint'])
           ->getMock();

       $result = $this->invokeMethod($dateTimeConstraint, 'presentConstraint', [$this->periods]);

       $this->assertEquals($expected, $result);
   }

    /**
     * @see DateTimeConstraint::pastConstraint()
     */
    public function testPastConstraint(): void
    {
        $expected = [
            'end' => [
                '2021-12-20' => 1
            ]
        ];

        $dateTimeConstraint = $this->getMockBuilder(TestableDateTimeConstraint::class)
            ->setConstructorArgs([$this->entityManager, $this->organizationUtils])
            ->setMethodsExcept(['pastConstraint'])
            ->getMock();

        $result = $this->invokeMethod($dateTimeConstraint, 'pastConstraint', [$this->periods]);

        $this->assertEquals($expected, $result);
    }

    /**
     * @see DateTimeConstraint::futurConstraint()
     */
    public function testFutureConstraint(): void
    {
        $expected = [
            'start' => [
                '2022-08-31' => 5
            ]
        ];

        $dateTimeConstraint = $this->getMockBuilder(TestableDateTimeConstraint::class)
            ->setConstructorArgs([$this->entityManager, $this->organizationUtils])
            ->setMethodsExcept(['futureConstraint'])
            ->getMock();

        $result = $this->invokeMethod($dateTimeConstraint, 'futureConstraint', [$this->periods]);

        $this->assertEquals($expected, $result);
    }

    /**
     * Si l'année courante est l'année d'affichage choisie par l'utilisateur, alors la date de début est aujourd'hui
     *
     * @throws \ReflectionException
     * @see DateTimeConstraint::getPeriods()
     */
    public function testGetPeriodsToday(): void
    {
        $today = new \DateTime('now');

        $activityYear = (int)$today->format('Y');
        if ((int)$today->format('m') < 9) {
            $activityYear--;
        }

        $parameters = $this->getMockBuilder(Parameters::class)->disableOriginalConstructor()->getMock();
        $parameters->method('getMusicalDate')->willReturn(new \DateTime('2000-09-01'));

        $organization = $this->getMockBuilder(Organization::class)->disableOriginalConstructor()->getMock();
        $organization->method('getParameters')->willReturn($parameters);

        $access = $this->getMockBuilder(Access::class)->disableOriginalConstructor()->getMock();
        $access->method('getOrganization')->willReturn($organization);
        $access->method('getActivityYear')->willReturn(2020);

        $this->organizationUtils->method('getOrganizationCurrentActivityYear')->with($organization)->willReturn(2020);
        $this->organizationUtils
            ->method('getActivityPeriodsSwitchYear')
            ->with($organization, 2020)
            ->willReturn(['dateStart' => 'YEAR-09-01', 'dateEnd' => ($activityYear + 1) . '-08-31']); // dateStart will be overwritten

        $dateTimeConstraint = $this->getMockBuilder(TestableDateTimeConstraint::class)
            ->setConstructorArgs([$this->entityManager, $this->organizationUtils])
            ->setMethodsExcept(['getPeriods'])
            ->getMock();

        $periodExpected = ['dateStart' => $today->format('Y-m-d'), 'dateEnd' => ($activityYear + 1) . '-08-31'];

        $result = $this->invokeMethod($dateTimeConstraint, 'getPeriods', [$access]);

        $this->assertEquals($periodExpected, $result);
    }

    /**
     * @throws \ReflectionException
     * @see DateTimeConstraint::getPeriods()
     */
    public function testGetPeriodsNotToday(): void
    {
        $organization = $this->getMockBuilder(Organization::class)->disableOriginalConstructor()->getMock();

        $access = $this->getMockBuilder(Access::class)->disableOriginalConstructor()->getMock();
        $access->method('getOrganization')->willReturn($organization);
        $access->method('getActivityYear')->willReturn(2020);

        $this->organizationUtils->method('getOrganizationCurrentActivityYear')->with($organization)->willReturn(2022);
        $this->organizationUtils
            ->method('getActivityPeriodsSwitchYear')
            ->with($organization, 2020)
            ->willReturn(['dateStart' => '2020-09-01', 'dateEnd' => '2021-08-31']);

        $dateTimeConstraint = $this->getMockBuilder(TestableDateTimeConstraint::class)
            ->setConstructorArgs([$this->entityManager, $this->organizationUtils])
            ->setMethodsExcept(['getPeriods'])
            ->getMock();

        $periodExpected = ['dateStart' => '2020-09-01', 'dateEnd' => '2021-08-31'];
        $this->assertEquals($periodExpected, $this->invokeMethod($dateTimeConstraint, 'getPeriods', [$access]));
    }
}
