<?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\Workflow;

use Doctrine\Common\Collections\ArrayCollection;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\OfferDate;
use Ferienpass\CoreBundle\Workflow\OfferGuard;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Workflow\Event\GuardEvent;
use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\Transition;
use Symfony\Component\Workflow\WorkflowInterface;

final class OfferGuardTest extends TestCase
{
    public function testCancelBlockedWhenNotPublished(): void
    {
        $guard = new OfferGuard();
        $offer = $this->makeOffer(['published' => false]);
        $event = $this->makeEvent($offer);

        $guard->cancel($event);

        self::assertTrue($event->isBlocked());
    }

    public function testCancelAllowedWhenPublishedAndNotCancelled(): void
    {
        $guard = new OfferGuard();
        $offer = $this->makeOffer(['published' => true, 'cancelled' => false]);
        $event = $this->makeEvent($offer);

        $guard->cancel($event);

        self::assertFalse($event->isBlocked());
    }

    public function testCancelBlockedWhenAlreadyCancelled(): void
    {
        $guard = new OfferGuard();
        $offer = $this->makeOffer(['published' => true, 'cancelled' => true]);
        $event = $this->makeEvent($offer);

        $guard->cancel($event);

        self::assertTrue($event->isBlocked());
    }

    public function testCancelBlockedWhenImmutable(): void
    {
        $guard = new OfferGuard();

        $date = $this->createMock(OfferDate::class);
        $date->method('getEnd')->willReturn(new \DateTime('-1 hour'));

        $offer = $this->makeOffer(['published' => true, 'cancelled' => false, 'dates' => [$date]]);
        $event = $this->makeEvent($offer);

        $guard->cancel($event);

        self::assertTrue($event->isBlocked());
    }

    public function testRelaunchAllowedWhenCancelledAndNotImmutable(): void
    {
        $guard = new OfferGuard();
        $offer = $this->makeOffer(['cancelled' => true]);
        $event = $this->makeEvent($offer);

        $guard->relaunch($event);

        self::assertFalse($event->isBlocked());
    }

    public function testRelaunchBlockedWhenNotCancelled(): void
    {
        $guard = new OfferGuard();
        $offer = $this->makeOffer(['cancelled' => false]);
        $event = $this->makeEvent($offer);

        $guard->relaunch($event);

        self::assertTrue($event->isBlocked());
    }

    public function testCompleteBlockedWhenAlreadyCompleted(): void
    {
        $guard = new OfferGuard();
        $offer = $this->makeOffer(['completed' => true]);
        $event = $this->makeEvent($offer);

        $guard->complete($event);

        self::assertTrue($event->isBlocked());
    }

    public function testCompleteBlockedWhenNoDates(): void
    {
        $guard = new OfferGuard();
        $offer = $this->makeOffer(['completed' => false]);
        $event = $this->makeEvent($offer);

        $guard->complete($event);

        self::assertTrue($event->isBlocked());
    }

    public function testCompleteBlockedWhenEndInFuture(): void
    {
        $guard = new OfferGuard();

        $date = $this->createMock(OfferDate::class);
        $date->method('getEnd')->willReturn(new \DateTime('+1 hour'));

        $offer = $this->makeOffer(['completed' => false, 'dates' => [$date]]);
        $event = $this->makeEvent($offer);

        $guard->complete($event);

        self::assertTrue($event->isBlocked());
    }

    public function testCompleteAllowedWhenEndInPast(): void
    {
        $guard = new OfferGuard();

        $date = $this->createMock(OfferDate::class);
        $date->method('getEnd')->willReturn(new \DateTime('-1 hour'));

        $offer = $this->makeOffer(['completed' => false, 'draft' => false, 'finalized' => false, 'reviewed' => false, 'dates' => [$date]]);
        $event = $this->makeEvent($offer);

        $guard->complete($event);

        self::assertFalse($event->isBlocked());
    }

    public function testApproveAllowedWhenNotCompleted(): void
    {
        $guard = new OfferGuard();
        $offer = $this->makeOffer(['completed' => false]);
        $event = $this->makeEvent($offer);

        $guard->approve($event);

        self::assertFalse($event->isBlocked());
    }

    public function testApproveBlockedWhenCompleted(): void
    {
        $guard = new OfferGuard();
        $offer = $this->makeOffer(['completed' => true]);
        $event = $this->makeEvent($offer);

        $guard->approve($event);

        self::assertTrue($event->isBlocked());
    }

    public function testCancelThrowsForNonOffer(): void
    {
        $guard = new OfferGuard();
        $event = $this->makeEvent(new \stdClass());

        $this->expectException(\RuntimeException::class);
        $guard->cancel($event);
    }

    public function testRelaunchThrowsForNonOffer(): void
    {
        $guard = new OfferGuard();
        $event = $this->makeEvent(new \stdClass());

        $this->expectException(\RuntimeException::class);
        $guard->relaunch($event);
    }

    public function testCompleteThrowsForNonOffer(): void
    {
        $guard = new OfferGuard();
        $event = $this->makeEvent(new \stdClass());

        $this->expectException(\RuntimeException::class);
        $guard->complete($event);
    }

    public function testApproveThrowsForNonOffer(): void
    {
        $guard = new OfferGuard();
        $event = $this->makeEvent(new \stdClass());

        $this->expectException(\RuntimeException::class);
        $guard->approve($event);
    }

    public function testCancelBlockedWhenOfferIsCompleted(): void
    {
        // isImmutable() returns true when isCompleted() is true
        $guard = new OfferGuard();
        $offer = $this->makeOffer(['published' => true, 'cancelled' => false, 'completed' => true]);
        $event = $this->makeEvent($offer);

        $guard->cancel($event);

        self::assertTrue($event->isBlocked());
    }

    public function testRelaunchBlockedWhenOfferIsCompleted(): void
    {
        // isImmutable() returns true when completed → relaunch blocked
        $guard = new OfferGuard();
        $offer = $this->makeOffer(['cancelled' => true, 'completed' => true]);
        $event = $this->makeEvent($offer);

        $guard->relaunch($event);

        self::assertTrue($event->isBlocked());
    }

    private function makeOffer(array $config = []): OfferInterface
    {
        $offer = $this->createMock(OfferInterface::class);
        $offer->method('isPublished')->willReturn($config['published'] ?? false);
        $offer->method('isCancelled')->willReturn($config['cancelled'] ?? false);
        $offer->method('isCompleted')->willReturn($config['completed'] ?? false);
        $offer->method('isDraft')->willReturn($config['draft'] ?? false);
        $offer->method('isFinalized')->willReturn($config['finalized'] ?? false);
        $offer->method('isReviewed')->willReturn($config['reviewed'] ?? false);

        $dates = $config['dates'] ?? [];
        $offer->method('getDates')->willReturn(new ArrayCollection($dates));

        return $offer;
    }

    private function makeEvent(object $subject): GuardEvent
    {
        $workflow = $this->createMock(WorkflowInterface::class);
        $transition = new Transition('test', 'from', 'to');
        $marking = new Marking();

        return new GuardEvent($subject, $marking, $transition, $workflow);
    }
}
