<?php

/** @noinspection PhpUnhandledExceptionInspection */

namespace App\Tests\Unit\Service\OnChange\Organization;

use App\Entity\Access\Access;
use App\Entity\Booking\Course;
use App\Entity\Education\EducationCurriculum;
use App\Entity\Education\EducationNotationConfig;
use App\Entity\Organization\Organization;
use App\Entity\Organization\Parameters;
use App\Enum\Education\AdvancedEducationNotationTypeEnum;
use App\Message\Command\Parameters\AverageChange;
use App\Message\Command\Typo3\Typo3DeleteCommand;
use App\Message\Command\Typo3\Typo3UndeleteCommand;
use App\Message\Command\Typo3\Typo3UpdateCommand;
use App\Repository\Booking\CourseRepository;
use App\Service\Network\Utils as NetworkUtils;
use App\Service\OnChange\OnChangeContext;
use App\Service\OnChange\Organization\OnParametersChange;
use App\Service\Organization\Utils as OrganizationUtils;
use AssertionError;
use Doctrine\Common\Collections\ArrayCollection;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;

class OnParametersChangeTest extends TestCase
{
    private CourseRepository $courseRepository;
    private NetworkUtils $networkUtils;
    private MessageBusInterface $messageBus;
    private OrganizationUtils $organizationUtils;

    public function setUp(): void
    {
        $this->courseRepository = $this->getMockBuilder(CourseRepository::class)->disableOriginalConstructor()->getMock();
        $this->networkUtils = $this->getMockBuilder(NetworkUtils::class)->disableOriginalConstructor()->getMock();
        $this->organizationUtils = $this->getMockBuilder(OrganizationUtils::class)->disableOriginalConstructor()->getMock();
        $this->messageBus = $this->getMockBuilder(MessageBusInterface::class)->disableOriginalConstructor()->getMock();
    }

    /**
     * @see OnParametersChange::validate()
     */
    public function testValidateValid(): void
    {
        $onParametersChange = $this->getMockBuilder(OnParametersChange::class)
            ->setConstructorArgs([$this->courseRepository, $this->networkUtils, $this->organizationUtils, $this->messageBus])
            ->setMethodsExcept(['validate'])
            ->getMock();

        $context = $this->getMockBuilder(OnChangeContext::class)->disableOriginalConstructor()->getMock();

        $consecutiveParams = [
            [false, true],  // Is CMF and site web enabled
            [true, false],  // Is not CMF and site web disabled
            [false, false]  // Is not CMF and site web enabled
        ];

        foreach ($consecutiveParams as $params) {
            $organization = $this->getMockBuilder(Organization::class)->getMock();
            $parameters = $this->getMockBuilder(Parameters::class)->getMock();
            $parameters->method('getOrganization')->willReturn($organization);

            $parameters->expects(self::once())->method('getDesactivateOpentalentSiteWeb')->willReturn($params[0]);

            $this->networkUtils = $this->getMockBuilder(NetworkUtils::class)->getMock();
            $this->networkUtils->method('isCMFAndActiveNow')->with($organization)->willReturn($params[1]);

            $onParametersChange->validate($parameters, $context);
        }
    }

    /**
     * @see OnParametersChange::validate()
     */
    public function testValidateInvalid(): void
    {
        $onParametersChange = $this->getMockBuilder(OnParametersChange::class)
            ->setConstructorArgs([$this->courseRepository, $this->networkUtils, $this->organizationUtils, $this->messageBus])
            ->setMethodsExcept(['validate'])
            ->getMock();

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

        $context = $this->getMockBuilder(OnChangeContext::class)->disableOriginalConstructor()->getMock();

        // Is CMF and site web disabled
        $parameters->expects(self::once())->method('getDesactivateOpentalentSiteWeb')->willReturn(true);
        $this->networkUtils->expects(self::once())->method('isCMFAndActiveNow')->with($organization)->willReturn(true);

        $this->expectException(\RuntimeException::class);

        $onParametersChange->validate($parameters, $context);
    }

    /**
     * @see OnParametersChange::beforeChange()
     */
    public function testBeforeChange(): void
    {
        $onParametersChange = $this
            ->getMockBuilder(OnParametersChange::class)
            ->setConstructorArgs([$this->courseRepository, $this->networkUtils, $this->organizationUtils, $this->messageBus])
            ->setMethodsExcept(['beforeChange'])
            ->getMock();

        $onParametersChange->expects(self::once())->method('onAdvancedEducationNotationTypeChange');

        $onParametersChange->expects(self::once())->method('onMusicalDateChange');

        $musicalDate = new \DateTime('2022-01-01');

        $previousParameters = $this->getMockBuilder(Parameters::class)->getMock();
        $previousParameters->method('getAdvancedEducationNotationType')->willReturn('BY_EDUCATION');
        $previousParameters->method('getMusicalDate')->willReturn($musicalDate);

        $context = $this->getMockBuilder(OnChangeContext::class)->disableOriginalConstructor()->getMock();
        $context->method('previousData')->willReturn($previousParameters);

        $parameters = $this->getMockBuilder(Parameters::class)->getMock();
        $parameters->method('getAdvancedEducationNotationType')->willReturn('SOMETHING_ELSE');
        $parameters->method('getMusicalDate')->willReturn(new \DateTime('2023-01-01'));

        // Both mocked methods should be called once here
        $onParametersChange->beforeChange($parameters, $context);
    }

    /**
     * @see OnParametersChange::beforeChange()
     */
    public function testBeforeChangeNoChange(): void
    {
        $onParametersChange = $this
            ->getMockBuilder(OnParametersChange::class)
            ->setConstructorArgs([$this->courseRepository, $this->networkUtils, $this->organizationUtils, $this->messageBus])
            ->setMethodsExcept(['beforeChange'])
            ->getMock();

        $onParametersChange->expects(self::never())->method('onAdvancedEducationNotationTypeChange');

        $onParametersChange->expects(self::never())->method('onMusicalDateChange');

        $musicalDate = new \DateTime('2022-01-01');

        $previousParameters = $this->getMockBuilder(Parameters::class)->getMock();
        $previousParameters->method('getAdvancedEducationNotationType')->willReturn('BY_EDUCATION');
        $previousParameters->method('getMusicalDate')->willReturn($musicalDate);

        $context = $this->getMockBuilder(OnChangeContext::class)->disableOriginalConstructor()->getMock();
        $context->method('previousData')->willReturn($previousParameters);

        $parameters = $this->getMockBuilder(Parameters::class)->getMock();
        $parameters->method('getId')->willReturn(1);
        $parameters->method('getAdvancedEducationNotationType')->willReturn('BY_EDUCATION');
        $parameters->method('getMusicalDate')->willReturn($musicalDate);

        // None of the mocked methods should be called again here
        $onParametersChange->beforeChange($parameters, $context);
    }

    /**
     * @see OnParametersChange::onChange()
     */
    public function testOnChangeNoChange(): void
    {
        $onParametersChange = $this
            ->getMockBuilder(OnParametersChange::class)
            ->setConstructorArgs([$this->courseRepository, $this->networkUtils, $this->organizationUtils, $this->messageBus])
            ->setMethodsExcept(['onChange'])
            ->getMock();

        $this->messageBus->expects($this->never())->method('dispatch');

        $previousParameters = $this->getMockBuilder(Parameters::class)->getMock();
        $previousParameters->method('getId')->willReturn(1);
        $previousParameters->method('getAverage')->willReturn(20);
        $previousParameters->method('getDesactivateOpentalentSiteWeb')->willReturn(false);
        $previousParameters->method('getCustomDomain')->willReturn(null);

        $context = $this->getMockBuilder(OnChangeContext::class)->disableOriginalConstructor()->getMock();
        $context->method('previousData')->willReturn($previousParameters);

        $parameters = $this->getMockBuilder(Parameters::class)->getMock();
        $parameters->method('getId')->willReturn(1);
        $parameters->method('getAverage')->willReturn(20);
        $parameters->method('getDesactivateOpentalentSiteWeb')->willReturn(false);
        $parameters->method('getCustomDomain')->willReturn(null);

        $onParametersChange->onChange($parameters, $context);
    }

    /**
     * @see OnParametersChange::onChange()
     */
    public function testOnChangeAverageChanged(): void
    {
        $onParametersChange = $this
            ->getMockBuilder(OnParametersChange::class)
            ->setConstructorArgs([$this->courseRepository, $this->networkUtils, $this->organizationUtils, $this->messageBus])
            ->setMethodsExcept(['onChange'])
            ->getMock();

        $this->messageBus
            ->expects(self::once())
            ->method('dispatch')
            ->with(self::isInstanceOf(AverageChange::class))
            ->willReturn(new Envelope(new AverageChange(1)));

        $previousParameters = $this->getMockBuilder(Parameters::class)->getMock();
        $previousParameters->method('getId')->willReturn(1);
        $previousParameters->expects(self::once())->method('getAverage')->willReturn(20);
        $previousParameters->method('getDesactivateOpentalentSiteWeb')->willReturn(false);
        $previousParameters->method('getCustomDomain')->willReturn(null);

        $context = $this->getMockBuilder(OnChangeContext::class)->disableOriginalConstructor()->getMock();
        $context->method('previousData')->willReturn($previousParameters);

        $parameters = $this->getMockBuilder(Parameters::class)->getMock();
        $parameters->method('getId')->willReturn(1);
        $parameters->method('getDesactivateOpentalentSiteWeb')->willReturn(false);
        $parameters->method('getCustomDomain')->willReturn(null);
        $parameters->expects(self::once())->method('getAverage')->willReturn(30);

        $onParametersChange->onChange($parameters, $context);
    }

    /**
     * @see OnParametersChange::onChange()
     */
    public function testOnChangeCustomDomainChanged(): void
    {
        $onParametersChange = $this
            ->getMockBuilder(OnParametersChange::class)
            ->setConstructorArgs([$this->courseRepository, $this->networkUtils, $this->organizationUtils, $this->messageBus])
            ->setMethodsExcept(['onChange'])
            ->getMock();

        $this->messageBus
            ->expects(self::once())
            ->method('dispatch')
            ->with(self::isInstanceOf(Typo3UpdateCommand::class))
            ->willReturn(new Envelope(new Typo3UpdateCommand(1)));

        $previousParameters = $this->getMockBuilder(Parameters::class)->getMock();
        $previousParameters->method('getId')->willReturn(1);
        $previousParameters->method('getAverage')->willReturn(20);
        $previousParameters->method('getDesactivateOpentalentSiteWeb')->willReturn(false);
        $previousParameters->expects(self::once())->method('getCustomDomain')->willReturn(null);

        $context = $this->getMockBuilder(OnChangeContext::class)->disableOriginalConstructor()->getMock();
        $context->method('previousData')->willReturn($previousParameters);

        $organization = $this->getMockBuilder(Organization::class)->getMock();
        $organization->method('getId')->willReturn(1);

        $parameters = $this->getMockBuilder(Parameters::class)->getMock();
        $parameters->method('getId')->willReturn(1);
        $parameters->method('getOrganization')->willReturn($organization);
        $parameters->method('getDesactivateOpentalentSiteWeb')->willReturn(false);
        $parameters->expects(self::once())->method('getCustomDomain')->willReturn('custom');
        $parameters->method('getAverage')->willReturn(20);

        $onParametersChange->onChange($parameters, $context);
    }

    /**
     * @see OnParametersChange::onChange()
     */
    public function testOnChangeWebsiteDisabled(): void
    {
        $onParametersChange = $this
            ->getMockBuilder(OnParametersChange::class)
            ->setConstructorArgs([$this->courseRepository, $this->networkUtils, $this->organizationUtils, $this->messageBus])
            ->setMethodsExcept(['onChange'])
            ->getMock();

        $this->messageBus
            ->expects(self::once())
            ->method('dispatch')
            ->with(self::isInstanceOf(Typo3DeleteCommand::class))
            ->willReturn(new Envelope(new Typo3DeleteCommand(1)));

        $previousParameters = $this->getMockBuilder(Parameters::class)->getMock();
        $previousParameters->method('getId')->willReturn(1);
        $previousParameters->method('getAverage')->willReturn(20);
        $previousParameters->expects(self::once())->method('getDesactivateOpentalentSiteWeb')->willReturn(false);
        $previousParameters->method('getCustomDomain')->willReturn(null);

        $context = $this->getMockBuilder(OnChangeContext::class)->disableOriginalConstructor()->getMock();
        $context->method('previousData')->willReturn($previousParameters);

        $organization = $this->getMockBuilder(Organization::class)->getMock();
        $organization->method('getId')->willReturn(1);

        $parameters = $this->getMockBuilder(Parameters::class)->getMock();
        $parameters->method('getId')->willReturn(1);
        $parameters->method('getOrganization')->willReturn($organization);
        $parameters->method('getDesactivateOpentalentSiteWeb')->willReturn(true);
        $parameters->method('getCustomDomain')->willReturn(null);
        $parameters->method('getAverage')->willReturn(20);

        $onParametersChange->onChange($parameters, $context);
    }

    /**
     * @see OnParametersChange::onChange()
     */
    public function testOnChangeWebsiteEnabled(): void
    {
        $onParametersChange = $this
            ->getMockBuilder(OnParametersChange::class)
            ->setConstructorArgs([$this->courseRepository, $this->networkUtils, $this->organizationUtils, $this->messageBus])
            ->setMethodsExcept(['onChange'])
            ->getMock();

        $this->messageBus
            ->expects(self::exactly(2))
            ->method('dispatch')
            ->willReturnCallback(function ($message) {
                if ($message instanceof Typo3UndeleteCommand) {
                    return new Envelope(new Typo3UndeleteCommand(1));
                }
                if($message instanceof Typo3UpdateCommand) {
                    return new Envelope(new Typo3UpdateCommand(1));
                }
                throw new AssertionError('unexpected message : ' . $message::class);
            });

        $previousParameters = $this->getMockBuilder(Parameters::class)->getMock();
        $previousParameters->method('getId')->willReturn(1);
        $previousParameters->method('getAverage')->willReturn(20);
        $previousParameters->expects(self::once())->method('getDesactivateOpentalentSiteWeb')->willReturn(true);
        $previousParameters->method('getCustomDomain')->willReturn(null);

        $context = $this->getMockBuilder(OnChangeContext::class)->disableOriginalConstructor()->getMock();
        $context->method('previousData')->willReturn($previousParameters);

        $organization = $this->getMockBuilder(Organization::class)->getMock();
        $organization->method('getId')->willReturn(1);

        $parameters = $this->getMockBuilder(Parameters::class)->getMock();
        $parameters->method('getId')->willReturn(1);
        $parameters->method('getOrganization')->willReturn($organization);
        $parameters->method('getDesactivateOpentalentSiteWeb')->willReturn(false);
        $parameters->method('getCustomDomain')->willReturn(null);
        $parameters->method('getAverage')->willReturn(20);

        $onParametersChange->onChange($parameters, $context);
    }

    /**
     * @see OnParametersChange::onAdvancedEducationNotationTypeChange()
     */
    public function testOnAdvancedEducationNotationTypeByTeachersChange(): void
    {
        $onParametersChange = $this
            ->getMockBuilder(OnParametersChange::class)
            ->setConstructorArgs([$this->courseRepository, $this->networkUtils, $this->organizationUtils, $this->messageBus])
            ->setMethodsExcept(['onAdvancedEducationNotationTypeChange'])
            ->getMock();

        $educationCurriculum = $this->getMockBuilder(EducationCurriculum::class)->getMock();

        $educationNotationConfig = $this->getMockBuilder(EducationNotationConfig::class)->getMock();
        $educationNotationConfig->method('getEducationCurriculums')->willReturn(new ArrayCollection([$educationCurriculum]));

        $organization = $this->getMockBuilder(Organization::class)->getMock();
        $organization->method('getEducationNotationConfigs')->willReturn(new ArrayCollection([$educationNotationConfig]));

        $parameters = $this->getMockBuilder(Parameters::class)->getMock();
        $parameters->method('getOrganization')->willReturn($organization);
        $parameters->method('getAdvancedEducationNotationType')->willReturn(AdvancedEducationNotationTypeEnum::BY_TEACHER()->getValue());

        $educationCurriculum->expects(self::once())->method('setEducationNotationConfig')->with(null);

        $onParametersChange->onAdvancedEducationNotationTypeChange($parameters);
    }

    /**
     * @see OnParametersChange::onAdvancedEducationNotationTypeChange()
     */
    public function testOnAdvancedEducationNotationTypeByEducationChange(): void
    {
        $onParametersChange = $this
            ->getMockBuilder(OnParametersChange::class)
            ->setConstructorArgs([$this->courseRepository, $this->networkUtils, $this->organizationUtils, $this->messageBus])
            ->setMethodsExcept(['onAdvancedEducationNotationTypeChange'])
            ->getMock();

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

        $educationNotationConfig = $this->getMockBuilder(EducationNotationConfig::class)->getMock();
        $educationNotationConfig->method('getTeachers')->willReturn(new ArrayCollection([$teacher]));

        $organization = $this->getMockBuilder(Organization::class)->getMock();
        $organization->method('getEducationNotationConfigs')->willReturn(new ArrayCollection([$educationNotationConfig]));

        $parameters = $this->getMockBuilder(Parameters::class)->getMock();
        $parameters->method('getOrganization')->willReturn($organization);
        $parameters->method('getAdvancedEducationNotationType')->willReturn(AdvancedEducationNotationTypeEnum::BY_EDUCATION()->getValue());

        $teacher->expects(self::once())->method('setEducationNotationConfig')->with(null);

        $onParametersChange->onAdvancedEducationNotationTypeChange($parameters);
    }

    /**
     * Un cours qui débute le 02/09/2022, si l'année musical passe du 05/09 au 01/09 alors le cours passe de l'année 2021/2022 à 2022/2023
     * @throws \Exception
     * @see OnParametersChange::onMusicalDateChange()
     */
    public function testOnMusicalDateChangeToPast(): void
    {
        $onParametersChange = $this
            ->getMockBuilder(OnParametersChange::class)
            ->setConstructorArgs([$this->courseRepository, $this->networkUtils, $this->organizationUtils, $this->messageBus])
            ->setMethodsExcept(['onMusicalDateChange'])
            ->getMock();

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

        $parameters->method('getMusicalDate')->willReturn(new \DateTime('2022-09-01'));

        $startDate = new \DateTime('2022-09-02');

        $this->organizationUtils
            ->expects(self::once())
            ->method('getActivityYearSwitchDate')
            ->with($organization, $startDate)
            ->willReturn(2022);

        $course = $this->getMockBuilder(Course::class)->getMock();
        $course->method('getStartYear')->willReturn(2021);
        $course->method('getEndYear')->willReturn(2022);
        $course->method('getDatetimeStart')->willReturn($startDate);

        $this->courseRepository->method('getCoursesToFrom')->willReturn([$course]);

        $course->expects(self::once())->method('setStartYear')->with(2022);
        $course->expects(self::once())->method('setEndYear')->with(2023);

        $onParametersChange->onMusicalDateChange($parameters, new \DateTime('2022-09-05'));
    }

    /**
     * Un cours qui débute le 02/09/2022, si l'année musical passe du 01/09 au 05/09 alors le cours passe de l'année 2022/2023 à 2021/2022
     *
     * @throws \Exception
     * @see OnParametersChange::onMusicalDateChange()
     */
    public function testOnMusicalDateChangeToFuture(): void
    {
        $onParametersChange = $this
            ->getMockBuilder(OnParametersChange::class)
            ->setConstructorArgs([$this->courseRepository, $this->networkUtils, $this->organizationUtils, $this->messageBus])
            ->setMethodsExcept(['onMusicalDateChange'])
            ->getMock();

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

        $parameters->method('getMusicalDate')->willReturn(new \DateTime('2022-09-05'));

        $startDate = new \DateTime('2022-09-02');

        $this->organizationUtils
            ->expects(self::once())
            ->method('getActivityYearSwitchDate')
            ->with($organization, $startDate)
            ->willReturn(2021);

        $course = $this->getMockBuilder(Course::class)->getMock();
        $course->method('getStartYear')->willReturn(2022);
        $course->method('getEndYear')->willReturn(2023);
        $course->method('getDatetimeStart')->willReturn($startDate);

        $this->courseRepository
            ->method('getCoursesToFrom')
            ->willReturn([$course])
        ;

        $course->expects(self::once())->method('setStartYear')->with(2021);
        $course->expects(self::once())->method('setEndYear')->with(2022);

        $onParametersChange->onMusicalDateChange($parameters, new \DateTime('2022-09-01'));
    }
}
