<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\CoreBundle\Tests\Entity;

use Ferienpass\CoreBundle\Entity\AccessCodeStrategy;
use Ferienpass\CoreBundle\Entity\ConsentForm;
use Ferienpass\CoreBundle\Entity\ConversationItem;
use Ferienpass\CoreBundle\Entity\Edition;
use Ferienpass\CoreBundle\Entity\EditionTask;
use Ferienpass\CoreBundle\Exception\AmbiguousHolidayTaskException;
use PHPUnit\Framework\TestCase;
use Symfony\Component\String\Slugger\AsciiSlugger;

final class EditionTest extends TestCase
{
    public function testGetIdIsNullByDefault(): void
    {
        $edition = new Edition();

        self::assertNull($edition->getId());
    }

    public function testSetIdAndGetId(): void
    {
        $edition = new Edition();
        $edition->setId(42);

        self::assertSame(42, $edition->getId());
    }

    public function testGetNameIsNullByDefault(): void
    {
        $edition = new Edition();

        self::assertNull($edition->getName());
    }

    public function testSetNameAndGetName(): void
    {
        $edition = new Edition();
        $edition->setName('Summer 2024');

        self::assertSame('Summer 2024', $edition->getName());
    }

    public function testGetAliasIsNullByDefault(): void
    {
        $edition = new Edition();

        self::assertNull($edition->getAlias());
    }

    public function testSetAliasAndGetAlias(): void
    {
        $edition = new Edition();
        $edition->setAlias('summer-2024');

        self::assertSame('summer-2024', $edition->getAlias());
    }

    public function testGetCreatedAtIsSetOnConstruct(): void
    {
        $edition = new Edition();

        self::assertInstanceOf(\DateTimeInterface::class, $edition->getCreatedAt());
    }

    public function testIsArchivedReturnsFalseByDefault(): void
    {
        $edition = new Edition();

        self::assertFalse($edition->isArchived());
    }

    public function testSetArchivedUpdatesValue(): void
    {
        $edition = new Edition();
        $edition->setArchived(true);

        self::assertTrue($edition->isArchived());
    }

    public function testGetTasksIsEmptyByDefault(): void
    {
        $edition = new Edition();

        self::assertCount(0, $edition->getTasks());
    }

    public function testAddTaskAddsToCollection(): void
    {
        $edition = new Edition();
        $task = new EditionTask($edition);
        $task->setType('holiday');
        $edition->addTask($task);

        self::assertCount(1, $edition->getTasks());
        self::assertSame($edition, $task->getEdition());
    }

    public function testRemoveTaskRemovesFromCollection(): void
    {
        $edition = new Edition();
        $task = new EditionTask($edition);
        $edition->addTask($task);
        $edition->removeTask($task);

        self::assertCount(0, $edition->getTasks());
    }

    public function testGetHolidayReturnsNullWhenNoTasks(): void
    {
        $edition = new Edition();

        self::assertNull($edition->getHoliday());
    }

    public function testGetHolidayReturnsHolidayTask(): void
    {
        $edition = new Edition();
        $task = new EditionTask($edition);
        $task->setType('holiday');
        $edition->addTask($task);

        self::assertSame($task, $edition->getHoliday());
    }

    public function testGetHolidayThrowsWhenMultipleHolidays(): void
    {
        $edition = new Edition();

        $task1 = new EditionTask($edition);
        $task1->setType('holiday');
        $edition->addTask($task1);

        $task2 = new EditionTask($edition);
        $task2->setType('holiday');
        $edition->addTask($task2);

        $this->expectException(AmbiguousHolidayTaskException::class);
        $edition->getHoliday();
    }

    public function testGetTasksOfTypeFiltersCorrectly(): void
    {
        $edition = new Edition();

        $task1 = new EditionTask($edition);
        $task1->setType('allocation');
        $edition->addTask($task1);

        $task2 = new EditionTask($edition);
        $task2->setType('firstcome');
        $edition->addTask($task2);

        $result = $edition->getTasksOfType('allocation');
        self::assertCount(1, $result);
    }

    public function testIsParticipantListReleasedWhenNoPlanListTask(): void
    {
        $edition = new Edition();

        // No publish_lists task → always released
        self::assertTrue($edition->isParticipantListReleased());
    }

    public function testIsParticipantListReleasedWhenActivePublishListTask(): void
    {
        $edition = new Edition();
        $task = new EditionTask($edition);
        $task->setType('publish_lists');
        $task->setPeriodBegin(new \DateTimeImmutable('-1 hour'));
        $task->setPeriodEnd(new \DateTimeImmutable('+1 hour'));
        $edition->addTask($task);

        self::assertTrue($edition->isParticipantListReleased());
    }

    public function testIsParticipantListNotReleasedWhenInactivePublishListTask(): void
    {
        $edition = new Edition();
        $task = new EditionTask($edition);
        $task->setType('publish_lists');
        $task->setPeriodBegin(new \DateTimeImmutable('+1 hour'));
        $task->setPeriodEnd(new \DateTimeImmutable('+2 hours'));
        $edition->addTask($task);

        self::assertFalse($edition->isParticipantListReleased());
    }

    public function testIsOnlineWhenNoShowOffersTask(): void
    {
        $edition = new Edition();

        self::assertTrue($edition->isOnline());
    }

    public function testIsOnlineWhenActiveShowOffersTask(): void
    {
        $edition = new Edition();
        $task = new EditionTask($edition);
        $task->setType('show_offers');
        $task->setPeriodBegin(new \DateTimeImmutable('-1 hour'));
        $task->setPeriodEnd(new \DateTimeImmutable('+1 hour'));
        $edition->addTask($task);

        self::assertTrue($edition->isOnline());
    }

    public function testIsNotOnlineWhenInactiveShowOffersTask(): void
    {
        $edition = new Edition();
        $task = new EditionTask($edition);
        $task->setType('show_offers');
        $task->setPeriodBegin(new \DateTimeImmutable('+1 hour'));
        $task->setPeriodEnd(new \DateTimeImmutable('+2 hours'));
        $edition->addTask($task);

        self::assertFalse($edition->isOnline());
    }

    public function testIsEditableForHostsWhenNoHostEditingStages(): void
    {
        $edition = new Edition();

        // No host_editing_stage tasks → always editable
        self::assertTrue($edition->isEditableForHosts());
    }

    public function testIsEditableForHostsWhenActiveHostEditingStage(): void
    {
        $edition = new Edition();
        $task = new EditionTask($edition);
        $task->setType('host_editing_stage');
        $task->setPeriodBegin(new \DateTimeImmutable('-1 hour'));
        $task->setPeriodEnd(new \DateTimeImmutable('+1 hour'));
        $edition->addTask($task);

        self::assertTrue($edition->isEditableForHosts());
    }

    public function testIsNotEditableForHostsWhenOnlyInactiveStage(): void
    {
        $edition = new Edition();
        $task = new EditionTask($edition);
        $task->setType('host_editing_stage');
        $task->setPeriodBegin(new \DateTimeImmutable('+1 hour'));
        $task->setPeriodEnd(new \DateTimeImmutable('+2 hours'));
        $edition->addTask($task);

        self::assertFalse($edition->isEditableForHosts());
    }

    public function testIsCompletedReturnsFalseWhenNoHoliday(): void
    {
        $edition = new Edition();

        self::assertFalse($edition->isCompleted());
    }

    public function testIsCompletedReturnsTrueWhenArchived(): void
    {
        $edition = new Edition();
        $edition->setArchived(true);

        self::assertTrue($edition->isCompleted());
    }

    public function testIsCompletedReturnsTrueWhenHolidayPeriodEnded(): void
    {
        $edition = new Edition();
        $task = new EditionTask($edition);
        $task->setType('holiday');
        $task->setPeriodBegin(new \DateTimeImmutable('-2 days'));
        $task->setPeriodEnd(new \DateTimeImmutable('-1 day'));
        $edition->addTask($task);

        self::assertTrue($edition->isCompleted());
    }

    public function testIsCompletedReturnsFalseWhenHolidayOngoing(): void
    {
        $edition = new Edition();
        $task = new EditionTask($edition);
        $task->setType('holiday');
        $task->setPeriodBegin(new \DateTimeImmutable('-1 day'));
        $task->setPeriodEnd(new \DateTimeImmutable('+1 day'));
        $edition->addTask($task);

        self::assertFalse($edition->isCompleted());
    }

    public function testGenerateAliasCreatesSlugFromName(): void
    {
        $edition = new Edition();
        $edition->setName('My Edition 2024');
        $edition->generateAlias(new AsciiSlugger());

        self::assertSame('my-edition-2024', $edition->getAlias());
    }

    public function testHostsCanAssignDefaultsFalse(): void
    {
        $edition = new Edition();

        self::assertFalse($edition->hostsCanAssign());
    }

    public function testSetHostsCanAssignUpdatesValue(): void
    {
        $edition = new Edition();
        $edition->setHostsCanAssign(true);

        self::assertTrue($edition->hostsCanAssign());
    }

    public function testUsePaymentsDefaultsTrue(): void
    {
        $edition = new Edition();

        self::assertTrue($edition->usePayments());
    }

    public function testSetUsePaymentsUpdatesValue(): void
    {
        $edition = new Edition();
        $edition->setUsePayments(false);

        self::assertFalse($edition->usePayments());
    }

    public function testHostsCanPublishDefaultsFalse(): void
    {
        $edition = new Edition();

        self::assertFalse($edition->hostsCanPublish());
    }

    public function testSetHostsCanPublishUpdatesValue(): void
    {
        $edition = new Edition();
        $edition->setHostsCanPublish(true);

        self::assertTrue($edition->hostsCanPublish());
    }

    public function testGetPaymentsIsNullByDefault(): void
    {
        $edition = new Edition();

        self::assertNull($edition->getPayments());
    }

    public function testSetPaymentsAndGetPayments(): void
    {
        $edition = new Edition();
        $edition->setPayments('stripe');

        self::assertSame('stripe', $edition->getPayments());
    }

    public function testAddConsentFormAddsToCollection(): void
    {
        $edition = new Edition();

        // Manually init consentForms collection via reflection since it's not initialized in constructor
        $prop = new \ReflectionProperty(Edition::class, 'consentForms');
        $prop->setValue($edition, new \Doctrine\Common\Collections\ArrayCollection());

        $form = new ConsentForm();
        $edition->addConsentForm($form);

        self::assertCount(1, $edition->getConsentForms());
        self::assertSame($edition, $form->getEdition());
    }

    public function testGetActivityIsEmptyByDefault(): void
    {
        $edition = new Edition();

        self::assertCount(0, $edition->getActivity());
    }

    public function testGetConversationReturnsConversationItems(): void
    {
        $edition = new Edition();

        $edition->getConversation()->add(new ConversationItem($edition, null, 'A comment'));

        self::assertCount(1, $edition->getConversation());
    }

    public function testGetSurveyWithApplicationIsNullByDefault(): void
    {
        $edition = new Edition();

        self::assertNull($edition->getSurveyWithApplication());
    }

    public function testSetSurveyWithApplication(): void
    {
        $edition = new Edition();
        $edition->setSurveyWithApplication('survey_type_a');

        self::assertSame('survey_type_a', $edition->getSurveyWithApplication());
    }

    public function testGetSurveyWithoutApplicationIsNullByDefault(): void
    {
        $edition = new Edition();

        self::assertNull($edition->getSurveyWithoutApplication());
    }

    public function testSetSurveyWithoutApplication(): void
    {
        $edition = new Edition();
        $edition->setSurveyWithoutApplication('survey_type_b');

        self::assertSame('survey_type_b', $edition->getSurveyWithoutApplication());
    }

    public function testFindPreviousTaskReturnsNullWhenNoMatchingTask(): void
    {
        $edition = new Edition();

        self::assertNull($edition->findPreviousTask('allocation'));
    }

    public function testFindPreviousTaskReturnsPastTask(): void
    {
        $edition = new Edition();
        $task = new EditionTask($edition);
        $task->setType('allocation');
        $task->setPeriodBegin(new \DateTimeImmutable('-3 days'));
        $task->setPeriodEnd(new \DateTimeImmutable('-1 day'));
        $edition->addTask($task);

        self::assertSame($task, $edition->findPreviousTask('allocation'));
    }

    public function testFindCurrentTaskReturnsNullWhenNoActiveTask(): void
    {
        $edition = new Edition();

        self::assertNull($edition->findCurrentTask('allocation'));
    }

    public function testFindCurrentTaskReturnsActiveTask(): void
    {
        $edition = new Edition();
        $task = new EditionTask($edition);
        $task->setType('allocation');
        $task->setPeriodBegin(new \DateTimeImmutable('-1 hour'));
        $task->setPeriodEnd(new \DateTimeImmutable('+1 hour'));
        $edition->addTask($task);

        self::assertSame($task, $edition->findCurrentTask('allocation'));
    }

    public function testFindUpcomingTaskReturnsNullWhenNoFutureTask(): void
    {
        $edition = new Edition();

        self::assertNull($edition->findUpcomingTask('allocation'));
    }

    public function testFindUpcomingTaskReturnsFutureTask(): void
    {
        $edition = new Edition();
        $task = new EditionTask($edition);
        $task->setType('allocation');
        $task->setPeriodBegin(new \DateTimeImmutable('+1 hour'));
        $task->setPeriodEnd(new \DateTimeImmutable('+2 hours'));
        $edition->addTask($task);

        self::assertSame($task, $edition->findUpcomingTask('allocation'));
    }

    public function testGetAccessCodeStrategyReturnsNullWhenNoActiveTasks(): void
    {
        $edition = new Edition();

        self::assertNull($edition->getAccessCodeStrategy());
    }

    public function testRemoveConsentForm(): void
    {
        $edition = new Edition();

        $prop = new \ReflectionProperty(Edition::class, 'consentForms');
        $prop->setValue($edition, new \Doctrine\Common\Collections\ArrayCollection());

        $consentForm = new ConsentForm();
        $edition->addConsentForm($consentForm);
        $edition->removeConsentForm($consentForm);

        self::assertCount(0, $edition->getConsentForms());
    }

    public function testGetHostEditingStagesReturnsFilteredTasks(): void
    {
        $edition = new Edition();
        $task = new EditionTask($edition);
        $task->setType('host_editing_stage');
        $edition->addTask($task);

        $stages = $edition->getHostEditingStages();

        self::assertCount(1, $stages);
    }

    public function testFindPreviousTaskWithApplicationSystem(): void
    {
        $edition = new Edition();
        $task = new EditionTask($edition);
        $task->setType('application_system');
        $task->setApplicationSystem('lot');
        $task->setPeriodBegin(new \DateTimeImmutable('-2 months'));
        $task->setPeriodEnd(new \DateTimeImmutable('-1 month'));
        $edition->addTask($task);

        $found = $edition->findPreviousTask('application_system', 'lot');
        self::assertSame($task, $found);

        $notFound = $edition->findPreviousTask('application_system', 'other');
        self::assertNull($notFound);
    }

    public function testFindCurrentTaskWithApplicationSystem(): void
    {
        $edition = new Edition();
        $task = new EditionTask($edition);
        $task->setType('application_system');
        $task->setApplicationSystem('lot');
        $task->setPeriodBegin(new \DateTimeImmutable('-1 hour'));
        $task->setPeriodEnd(new \DateTimeImmutable('+1 hour'));
        $edition->addTask($task);

        $found = $edition->findCurrentTask('application_system', 'lot');
        self::assertSame($task, $found);

        $notFound = $edition->findCurrentTask('application_system', 'other');
        self::assertNull($notFound);
    }

    public function testFindUpcomingTaskWithApplicationSystem(): void
    {
        $edition = new Edition();
        $task = new EditionTask($edition);
        $task->setType('application_system');
        $task->setApplicationSystem('lot');
        $task->setPeriodBegin(new \DateTimeImmutable('+1 month'));
        $task->setPeriodEnd(new \DateTimeImmutable('+2 months'));
        $edition->addTask($task);

        $found = $edition->findUpcomingTask('application_system', 'lot');
        self::assertSame($task, $found);

        $notFound = $edition->findUpcomingTask('application_system', 'other');
        self::assertNull($notFound);
    }

    public function testSetTasksReplacesCollection(): void
    {
        $edition = new Edition();
        $task = new EditionTask($edition);
        $collection = new \Doctrine\Common\Collections\ArrayCollection([$task]);
        $edition->setTasks($collection);

        self::assertSame($collection, $edition->getTasks());
    }

    public function testGetOffersIsEmptyByDefault(): void
    {
        $edition = new Edition();

        self::assertInstanceOf(\Doctrine\Common\Collections\Collection::class, $edition->getOffers());
        self::assertEmpty($edition->getOffers());
    }

    public function testGetActiveTasksReturnsMatchingTasks(): void
    {
        $edition = new Edition();

        $task = new EditionTask($edition);
        $task->setType('application_system');
        $task->setPeriodBegin(new \DateTimeImmutable('-1 hour'));
        $task->setPeriodEnd(new \DateTimeImmutable('+1 hour'));
        $edition->addTask($task);

        $result = $edition->getActiveTasks('application_system');

        self::assertCount(1, $result);
    }

    public function testGetAccessCodeStrategyReturnsStrategyFromActiveTask(): void
    {
        $edition = new Edition();

        $strategy = new AccessCodeStrategy();

        $task = new EditionTask($edition);
        $task->setType('application_system');
        $task->setPeriodBegin(new \DateTimeImmutable('-1 hour'));
        $task->setPeriodEnd(new \DateTimeImmutable('+1 hour'));
        $task->setAccessCodeStrategy($strategy);
        $edition->addTask($task);

        self::assertSame($strategy, $edition->getAccessCodeStrategy());
    }

    public function testGetCurrentHostEditingStageReturnsMostRecentWhenMultiple(): void
    {
        $edition = new Edition();

        $task1 = new EditionTask($edition);
        $task1->setType('host_editing_stage');
        $task1->setPeriodBegin(new \DateTimeImmutable('-2 hours'));
        $task1->setPeriodEnd(new \DateTimeImmutable('+2 hours'));
        $edition->addTask($task1);

        $task2 = new EditionTask($edition);
        $task2->setType('host_editing_stage');
        $task2->setPeriodBegin(new \DateTimeImmutable('-1 hour'));
        $task2->setPeriodEnd(new \DateTimeImmutable('+1 hour'));
        $edition->addTask($task2);

        // When multiple tasks are active, getCurrentHostEditingStage returns first
        $result = $edition->getCurrentHostEditingStage();
        self::assertNotNull($result);
    }

    public function testHostAgreementDefaults(): void
    {
        $edition = new Edition();
        $prop = new \ReflectionProperty(Edition::class, 'consentForms');
        $prop->setValue($edition, new \Doctrine\Common\Collections\ArrayCollection());

        self::assertNull($edition->getHostConsentForm());
        self::assertFalse($edition->isHostAgreementActive());
    }

    public function testHostConsentFormIsExcludedFromParticipantConsentForms(): void
    {
        $edition = new Edition();
        $prop = new \ReflectionProperty(Edition::class, 'consentForms');
        $prop->setValue($edition, new \Doctrine\Common\Collections\ArrayCollection());

        $participant = new ConsentForm();
        $edition->addConsentForm($participant);

        $host = new ConsentForm();
        $host->setSubject('host');
        $host->setText('Confidential.');
        $edition->addConsentForm($host);

        self::assertCount(1, $edition->getConsentForms(), 'host declaration is filtered out of participant consents');
        self::assertSame($participant, $edition->getConsentForms()->first());
        self::assertSame($host, $edition->getHostConsentForm());
    }

    public function testHostAgreementActiveRequiresText(): void
    {
        $edition = new Edition();
        $prop = new \ReflectionProperty(Edition::class, 'consentForms');
        $prop->setValue($edition, new \Doctrine\Common\Collections\ArrayCollection());

        $host = new ConsentForm();
        $host->setSubject('host');
        $edition->addConsentForm($host);
        self::assertFalse($edition->isHostAgreementActive(), 'host declaration without text is not active');

        $host->setText('   ');
        self::assertFalse($edition->isHostAgreementActive(), 'whitespace-only text is not active');

        $host->setText('Confidential.');
        self::assertTrue($edition->isHostAgreementActive());
    }
}
