<?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 Doctrine\Common\Collections\ArrayCollection;
use Ferienpass\CoreBundle\Entity\Edition;
use Ferienpass\CoreBundle\Entity\EditionTask;
use Ferienpass\CoreBundle\Fixtures\Factory\EditionTaskFactory;
use PHPUnit\Framework\TestCase;
use Zenstruck\Foundry\Test\Factories;

final class EditionTaskTest extends TestCase
{
    use Factories;

    public function testIsAnApplicationSystem(): void
    {
        $task = EditionTaskFactory::new()->ofTypeFirstComeApplicationSystem()->create();

        self::assertTrue($task->isAnApplicationSystem());
    }

    public function testIsNotAnApplicationSystemForOtherType(): void
    {
        $task = EditionTaskFactory::new()->ofTypeHostEditingStage()->create();

        self::assertFalse($task->isAnApplicationSystem());
    }

    public function testGetApplicationSystem(): void
    {
        $task = EditionTaskFactory::new()->ofTypeFirstComeApplicationSystem()->create();

        self::assertSame('firstcome', $task->getApplicationSystem());
    }

    public function testGetType(): void
    {
        $task = EditionTaskFactory::new()->ofTypeHostEditingStage()->create();

        self::assertSame('host_editing_stage', $task->getType());
    }

    public function testAlwaysActiveDefaultsFalse(): void
    {
        $task = EditionTaskFactory::new()->create();

        self::assertFalse($task->isAlwaysActive());
    }

    public function testSetAlwaysActive(): void
    {
        $task = EditionTaskFactory::new()->create();
        $task->setAlwaysActive(true);

        self::assertTrue($task->isAlwaysActive());
    }

    public function testIsActiveWhenAlwaysActive(): void
    {
        $task = EditionTaskFactory::new()
            ->isInPast()
            ->create();
        $task->setAlwaysActive(true);

        // Even past tasks are active when alwaysActive is true
        self::assertTrue($task->isActive());
    }

    public function testIsCompletedWhenPeriodEndIsInPast(): void
    {
        $edition = $this->createStub(Edition::class);
        $edition->method('getTasks')->willReturn(new ArrayCollection());

        $task = EditionTaskFactory::new()->isInPast()->with(['edition' => $edition])->create();

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

    public function testIsNotCompletedWhenPeriodEndIsInFuture(): void
    {
        $task = EditionTaskFactory::new()
            ->with(['periodEnd' => new \DateTimeImmutable('+1 month')])
            ->create();

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

    public function testSetType(): void
    {
        $task = EditionTaskFactory::new()->create();
        $task->setType('holiday');

        self::assertSame('holiday', $task->getType());
    }

    public function testSetMaxApplications(): void
    {
        $task = EditionTaskFactory::new()->create();
        $task->setMaxApplications(5);

        self::assertSame(5, $task->getMaxApplications());
    }

    public function testMaxApplicationsNullByDefault(): void
    {
        $task = EditionTaskFactory::new()->create();

        self::assertNull($task->getMaxApplications());
    }

    public function testSetMaxWaitlist(): void
    {
        $task = EditionTaskFactory::new()->create();
        $task->setMaxWaitlist(10);

        self::assertSame(10, $task->getMaxWaitlist());
    }

    public function testAllowAnonymousDefaultsFalse(): void
    {
        $task = EditionTaskFactory::new()->create();

        self::assertFalse($task->isAllowAnonymous());
    }

    public function testSetAllowAnonymous(): void
    {
        $task = EditionTaskFactory::new()->create();
        $task->setAllowAnonymous(true);

        self::assertTrue($task->isAllowAnonymous());
    }

    public function testSkipMaxApplicationsDefaultsFalse(): void
    {
        $task = EditionTaskFactory::new()->create();

        self::assertFalse($task->isSkipMaxApplications());
    }

    public function testSetSkipMaxApplications(): void
    {
        $task = EditionTaskFactory::new()->create();
        $task->setSkipMaxApplications(true);

        self::assertTrue($task->isSkipMaxApplications());
    }

    public function testSetDescription(): void
    {
        $task = EditionTaskFactory::new()->create();
        $task->setDescription('Test description');

        self::assertSame('Test description', $task->getDescription());
    }

    public function testSetMaxApplicationsDay(): void
    {
        $task = EditionTaskFactory::new()->create();
        $task->setMaxApplicationsDay(3);

        self::assertSame(3, $task->getMaxApplicationsDay());
    }

    public function testMaxApplicationsDayNullByDefault(): void
    {
        $task = EditionTaskFactory::new()->create();

        self::assertNull($task->getMaxApplicationsDay());
    }

    public function testSetTitle(): void
    {
        $task = EditionTaskFactory::new()->create();
        $task->setTitle('Application Window');

        self::assertSame('Application Window', $task->getTitle());
    }

    public function testSetPeriodBegin(): void
    {
        $task = EditionTaskFactory::new()->create();
        $date = new \DateTimeImmutable('2024-01-01 08:00:00');
        $task->setPeriodBegin($date);

        self::assertSame($date, $task->getPeriodBegin());
    }

    public function testSetPeriodEnd(): void
    {
        $task = EditionTaskFactory::new()->create();
        $date = new \DateTimeImmutable('2024-12-31 23:59:59');
        $task->setPeriodEnd($date);

        self::assertSame($date, $task->getPeriodEnd());
    }

    public function testSetApplicationSystem(): void
    {
        $task = EditionTaskFactory::new()->create();
        $task->setApplicationSystem('firstcome');

        self::assertSame('firstcome', $task->getApplicationSystem());
    }

    public function testSetAllowAnonymousFee(): void
    {
        $task = EditionTaskFactory::new()->create();
        $task->setAllowAnonymousFee(true);

        self::assertTrue($task->isAllowAnonymousFee());
    }

    public function testAllowAnonymousFeeDefaultsFalse(): void
    {
        $task = EditionTaskFactory::new()->create();

        self::assertFalse($task->isAllowAnonymousFee());
    }

    public function testIsActiveWhenWithinPeriod(): void
    {
        $edition = $this->createStub(Edition::class);
        $edition->method('getTasks')->willReturn(new ArrayCollection());

        $task = EditionTaskFactory::new()
            ->with([
                'periodBegin' => new \DateTimeImmutable('-1 day'),
                'periodEnd' => new \DateTimeImmutable('+1 day'),
                'edition' => $edition,
            ])
            ->create();

        self::assertTrue($task->isActive());
    }

    public function testIsNotActiveWhenPeriodNotStarted(): void
    {
        $task = EditionTaskFactory::new()
            ->with([
                'periodBegin' => new \DateTimeImmutable('+1 week'),
                'periodEnd' => new \DateTimeImmutable('+2 weeks'),
            ])
            ->create();

        self::assertFalse($task->isActive());
    }

    public function testIsNotActiveWhenPeriodEnded(): void
    {
        $task = EditionTaskFactory::new()->isInPast()->create();

        self::assertFalse($task->isActive());
    }

    public function testSetId(): void
    {
        $task = EditionTaskFactory::new()->create();
        $task->setId(42);

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

    public function testGetEdition(): void
    {
        $edition = $this->createStub(Edition::class);
        $edition->method('getTasks')->willReturn(new ArrayCollection());

        $task = EditionTaskFactory::new()->with(['edition' => $edition])->create();

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

    public function testSetEdition(): void
    {
        $task = EditionTaskFactory::new()->create();
        $edition = $this->createStub(Edition::class);
        $task->setEdition($edition);

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

    public function testGetProgressReturns100WhenCompleted(): void
    {
        $edition = $this->createStub(Edition::class);
        $edition->method('getTasks')->willReturn(new ArrayCollection());

        $task = EditionTaskFactory::new()->isInPast()->with(['edition' => $edition])->create();

        self::assertSame(100, $task->getProgress());
    }

    public function testGetProgressReturns0WhenNotActive(): void
    {
        $task = EditionTaskFactory::new()
            ->with([
                'periodBegin' => new \DateTimeImmutable('+1 week'),
                'periodEnd' => new \DateTimeImmutable('+2 weeks'),
            ])
            ->create();

        self::assertSame(0, $task->getProgress());
    }

    public function testGetProgressReturnsBetween0And100WhenActive(): void
    {
        $edition = $this->createStub(Edition::class);
        $edition->method('getTasks')->willReturn(new ArrayCollection());

        $task = EditionTaskFactory::new()
            ->with([
                'periodBegin' => new \DateTimeImmutable('-5 days'),
                'periodEnd' => new \DateTimeImmutable('+5 days'),
                'edition' => $edition,
            ])
            ->create();

        $progress = $task->getProgress();
        self::assertGreaterThan(0, $progress);
        self::assertLessThan(100, $progress);
    }

    public function testGetAccessCodeStrategyNullByDefault(): void
    {
        $task = EditionTaskFactory::new()->create();

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

    public function testSetAccessCodeStrategy(): void
    {
        $task = EditionTaskFactory::new()->create();
        $strategy = $this->createStub(\Ferienpass\CoreBundle\Entity\AccessCodeStrategy::class);
        $task->setAccessCodeStrategy($strategy);

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

    public function testSetAccessCodeStrategyNull(): void
    {
        $task = EditionTaskFactory::new()->create();
        $strategy = $this->createStub(\Ferienpass\CoreBundle\Entity\AccessCodeStrategy::class);
        $task->setAccessCodeStrategy($strategy);
        $task->setAccessCodeStrategy(null);

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

    public function testIsUpcomingWhenNotActiveAndNotCompleted(): void
    {
        $edition = $this->createStub(Edition::class);
        $edition->method('getTasks')->willReturn(new ArrayCollection());

        $task = EditionTaskFactory::new()
            ->with([
                'periodBegin' => new \DateTimeImmutable('+1 week'),
                'periodEnd' => new \DateTimeImmutable('+2 weeks'),
                'edition' => $edition,
            ])
            ->create();

        self::assertTrue($task->isUpcoming());
        self::assertFalse($task->isActive());
        self::assertFalse($task->isCompleted());
    }

    public function testGetCreatedAtIsSetOnConstruct(): void
    {
        $edition = $this->createStub(Edition::class);
        $task = new EditionTask($edition);

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

    public function testGetDependenciesDefaultIsEmpty(): void
    {
        $edition = $this->createStub(Edition::class);
        $edition->method('getTasks')->willReturn(new ArrayCollection());

        $task = EditionTaskFactory::new()->with(['type' => 'holiday', 'edition' => $edition])->create();

        self::assertCount(0, $task->getDependencies());
    }

    public function testGetDependenciesForAllocationTypeFiltersLotSystems(): void
    {
        $lotTask = EditionTaskFactory::new()
            ->with(['type' => 'application_system', 'applicationSystem' => 'lot'])
            ->create();
        $firstcomeTask = EditionTaskFactory::new()
            ->with(['type' => 'application_system', 'applicationSystem' => 'firstcome'])
            ->create();

        $edition = $this->createStub(Edition::class);
        $edition->method('getTasks')->willReturn(new ArrayCollection([$lotTask, $firstcomeTask]));

        $allocationTask = EditionTaskFactory::new()->with(['type' => 'allocation', 'edition' => $edition])->create();

        $deps = $allocationTask->getDependencies();
        self::assertCount(1, $deps);
        self::assertSame($lotTask, $deps->first());
    }

    public function testGetDependenciesForFirstcomeFiltersAllocationTasks(): void
    {
        $allocationTask = EditionTaskFactory::new()->with(['type' => 'allocation'])->create();
        $holidayTask = EditionTaskFactory::new()->with(['type' => 'holiday'])->create();

        $edition = $this->createStub(Edition::class);
        $edition->method('getTasks')->willReturn(new ArrayCollection([$allocationTask, $holidayTask]));

        $firstcomeTask = EditionTaskFactory::new()
            ->with(['type' => 'application_system', 'applicationSystem' => 'firstcome', 'edition' => $edition])
            ->create();

        $deps = $firstcomeTask->getDependencies();
        self::assertCount(1, $deps);
        self::assertSame($allocationTask, $deps->first());
    }

    public function testGetDependenciesForPublishListsFiltersAllocationTasks(): void
    {
        $allocationTask = EditionTaskFactory::new()->with(['type' => 'allocation'])->create();

        $edition = $this->createStub(Edition::class);
        $edition->method('getTasks')->willReturn(new ArrayCollection([$allocationTask]));

        $publishTask = EditionTaskFactory::new()->with(['type' => 'publish_lists', 'edition' => $edition])->create();

        $deps = $publishTask->getDependencies();
        self::assertCount(1, $deps);
        self::assertSame($allocationTask, $deps->first());
    }

    public function testIsCompletedReturnsTrueForAllocationTypeWhenPastEnd(): void
    {
        $edition = $this->createStub(Edition::class);
        $edition->method('getTasks')->willReturn(new ArrayCollection());

        $task = EditionTaskFactory::new()
            ->with(['type' => 'allocation', 'edition' => $edition])
            ->isInPast()
            ->create();

        // waiting is hardcoded to 0, so 0 === 0 → true
        self::assertTrue($task->isCompleted());
    }

    public function testIsCompletedReturnsFalseWhenDependencyNotCompleted(): void
    {
        // A firstcome task depends on allocation tasks
        $allocationTask = EditionTaskFactory::new()
            ->with(['type' => 'allocation'])
            ->create(); // periodEnd is in future → not completed

        $edition = $this->createStub(Edition::class);
        $edition->method('getTasks')->willReturn(new ArrayCollection([$allocationTask]));

        $firstcomeTask = EditionTaskFactory::new()
            ->with([
                'type' => 'application_system',
                'applicationSystem' => 'firstcome',
                'edition' => $edition,
                'periodEnd' => new \DateTimeImmutable('-1 second'),
            ])
            ->create();

        // allocationTask is not completed (periodEnd > now), so firstcomeTask also not completed
        self::assertFalse($firstcomeTask->isCompleted());
    }

    public function testIsCompletedReturnsTrueWhenAllDependenciesCompleted(): void
    {
        $allocationTask = EditionTaskFactory::new()
            ->with(['type' => 'allocation'])
            ->isInPast()
            ->create(); // completed: past + waiting=0

        // Allocation task itself depends on empty tasks → isActive check: allocation uses getDependencies → empty ArrayCollection
        // But wait, allocation needs getTasks on edition. Let's use a standalone mock.
        $innerEdition = $this->createStub(Edition::class);
        $innerEdition->method('getTasks')->willReturn(new ArrayCollection());
        $prop = new \ReflectionProperty($allocationTask, 'edition');
        $prop->setValue($allocationTask, $innerEdition);

        $outerEdition = $this->createStub(Edition::class);
        $outerEdition->method('getTasks')->willReturn(new ArrayCollection([$allocationTask]));

        $firstcomeTask = EditionTaskFactory::new()
            ->with([
                'type' => 'application_system',
                'applicationSystem' => 'firstcome',
                'edition' => $outerEdition,
                'periodEnd' => new \DateTimeImmutable('-1 second'),
            ])
            ->create();

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

    public function testIsActiveReturnsFalseWhenDependencyNotCompleted(): void
    {
        // A firstcome task is active in period but its allocation dependency is not completed
        $allocationTask = EditionTaskFactory::new()
            ->with(['type' => 'allocation'])
            ->create(); // periodEnd > now → not completed

        $edition = $this->createStub(Edition::class);
        $edition->method('getTasks')->willReturn(new ArrayCollection([$allocationTask]));

        $firstcomeTask = EditionTaskFactory::new()
            ->with([
                'type' => 'application_system',
                'applicationSystem' => 'firstcome',
                'edition' => $edition,
                'periodBegin' => new \DateTimeImmutable('-1 day'),
                'periodEnd' => new \DateTimeImmutable('+1 day'),
            ])
            ->create();

        // allocation is not completed (periodEnd in future) → firstcome task not active
        self::assertFalse($firstcomeTask->isActive());
    }

    public function testGetProgressForAllocationTypeWithNoOffersReturnsZero(): void
    {
        $edition = $this->createStub(Edition::class);
        $edition->method('getTasks')->willReturn(new ArrayCollection());
        $edition->method('getOffers')->willReturn(new ArrayCollection());

        $task = EditionTaskFactory::new()
            ->with([
                'type' => 'allocation',
                'edition' => $edition,
                'periodBegin' => new \DateTimeImmutable('-1 day'),
                'periodEnd' => new \DateTimeImmutable('+1 day'),
            ])
            ->create();

        // isCompleted = false (periodEnd > now), isActive = true (in period, no deps)
        // Allocation branch: no offers → all = 0 → return 0
        self::assertSame(0, $task->getProgress());
    }

    public function testGetProgressForAllocationTypeWithOffersCalculatesPercentage(): void
    {
        $waitingAttendance = $this->createStub(\Ferienpass\CoreBundle\Entity\Attendance::class);
        $waitingAttendance->method('isWaiting')->willReturn(true);

        $confirmedAttendance = $this->createStub(\Ferienpass\CoreBundle\Entity\Attendance::class);
        $confirmedAttendance->method('isWaiting')->willReturn(false);

        // Offer with 2 attendances: 1 waiting, 1 confirmed
        $attendancesAll = new ArrayCollection([$waitingAttendance, $confirmedAttendance]);
        $offer = $this->createStub(\Ferienpass\CoreBundle\Entity\Offer\OfferInterface::class);
        $offer->method('getAttendances')->willReturn($attendancesAll);

        $edition = $this->createStub(Edition::class);
        $edition->method('getTasks')->willReturn(new ArrayCollection());
        $edition->method('getOffers')->willReturn(new ArrayCollection([$offer]));

        $task = EditionTaskFactory::new()
            ->with([
                'type' => 'allocation',
                'edition' => $edition,
                'periodBegin' => new \DateTimeImmutable('-1 day'),
                'periodEnd' => new \DateTimeImmutable('+1 day'),
            ])
            ->create();

        // 1 waiting out of 2 total → 50% progress
        self::assertSame(50, $task->getProgress());
    }
}
