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

use Doctrine\ORM\EntityManagerInterface;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\Offer\Base;
use Ferienpass\CoreBundle\Entity\Participant\BaseParticipant;
use Ferienpass\CoreBundle\Entity\Payment;
use Ferienpass\CoreBundle\EventListener\Workflow\AttendanceConfirmTransactionListener;
use Ferienpass\CoreBundle\EventListener\Workflow\AttendanceStashListener;
use Ferienpass\CoreBundle\EventListener\Workflow\AttendanceStatusTransactionListener;
use Ferienpass\CoreBundle\EventListener\Workflow\AttendanceUnfulfilledTransactionListener;
use Ferienpass\CoreBundle\EventListener\Workflow\AttendanceWithdrawTransactionListener;
use Ferienpass\CoreBundle\EventListener\Workflow\OfferCancelTransactionListener;
use Ferienpass\CoreBundle\EventListener\Workflow\OfferCompleteTransactionListener;
use Ferienpass\CoreBundle\EventListener\Workflow\OfferFinalizeTransactionListener;
use Ferienpass\CoreBundle\EventListener\Workflow\OfferLogTransactionListener;
use Ferienpass\CoreBundle\EventListener\Workflow\OfferRelaunchTransactionListener;
use Ferienpass\CoreBundle\EventListener\Workflow\PaymentPayTransactionListener;
use Ferienpass\CoreBundle\Message\AttendanceCreated;
use Ferienpass\CoreBundle\Message\AttendanceStatusChanged;
use Ferienpass\CoreBundle\Message\OfferCompleted;
use Ferienpass\CoreBundle\Message\OfferFinalized;
use Ferienpass\CoreBundle\Payments\ReceiptNumberGenerator;
use Ferienpass\CoreBundle\Repository\AttendanceStashRepository;
use Ferienpass\CoreBundle\Settings\SystemSettingsManager;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Workflow\Event\AnnounceEvent;
use Symfony\Component\Workflow\Event\EnteredEvent;
use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\Transition;

final class WorkflowListenersTest extends TestCase
{
    // ---- AttendanceStashListener ----

    public function testAttendanceStashListenerSkipsNonAttendanceSubject(): void
    {
        $stashRepo = $this->createMock(AttendanceStashRepository::class);
        $stashRepo->expects(self::never())->method('findOneBy');

        $em = $this->createMock(EntityManagerInterface::class);
        $listener = new AttendanceStashListener($em, $stashRepo);

        $event = new EnteredEvent(new \stdClass(), new Marking());
        $listener($event);
    }

    public function testAttendanceStashListenerSkipsWhenCommitTrue(): void
    {
        $stashRepo = $this->createMock(AttendanceStashRepository::class);
        $stashRepo->expects(self::never())->method('findOneBy');

        $em = $this->createMock(EntityManagerInterface::class);
        $listener = new AttendanceStashListener($em, $stashRepo);

        $offer = new Base();
        $participant = new BaseParticipant();
        $attendance = new Attendance($offer, $participant);

        $event = new EnteredEvent($attendance, new Marking(), context: ['commit' => true]);
        $listener($event);
    }

    // ---- OfferLogTransactionListener ----

    public function testOfferLogTransactionListenerThrowsForNonOfferSubject(): void
    {
        $this->expectException(\RuntimeException::class);

        $security = $this->createMock(Security::class);
        $em = $this->createMock(EntityManagerInterface::class);
        $listener = new OfferLogTransactionListener($security, $em);

        $event = new AnnounceEvent(new \stdClass(), new Marking());
        $listener($event);
    }

    public function testOfferLogTransactionListenerSkipsWhenUserIsNotFerienpassUser(): void
    {
        $security = $this->createMock(Security::class);
        $security->method('getUser')->willReturn(null);

        $em = $this->createMock(EntityManagerInterface::class);
        $em->expects(self::never())->method('flush');

        $listener = new OfferLogTransactionListener($security, $em);

        $offer = new Base();
        $event = new AnnounceEvent($offer, new Marking());
        $listener($event);
    }

    // ---- AttendanceConfirmTransactionListener ----

    public function testAttendanceConfirmTransactionListenerThrowsForNonAttendanceSubject(): void
    {
        $this->expectException(\RuntimeException::class);

        $listener = new AttendanceConfirmTransactionListener();
        $event = new EnteredEvent(new \stdClass(), new Marking());
        $listener($event);
    }

    public function testAttendanceConfirmTransactionListenerDoesNothingWhenNotCommit(): void
    {
        $listener = new AttendanceConfirmTransactionListener();

        $offer = new Base();
        $participant = new BaseParticipant();
        $attendance = new Attendance($offer, $participant);

        $event = new EnteredEvent($attendance, new Marking(), context: ['commit' => false]);
        $listener($event);

        self::assertFalse($attendance->isPayable());
    }

    // ---- AttendanceWithdrawTransactionListener ----

    public function testAttendanceWithdrawTransactionListenerThrowsForNonAttendanceSubject(): void
    {
        $this->expectException(\RuntimeException::class);

        $listener = new AttendanceWithdrawTransactionListener(
            $this->createMock(MessageBusInterface::class),
            $this->createStub(SystemSettingsManager::class)
        );
        $event = new EnteredEvent(new \stdClass(), new Marking());
        $listener($event);
    }

    // ---- OfferCancelTransactionListener ----

    public function testOfferCancelTransactionListenerThrowsForNonOfferSubject(): void
    {
        $this->expectException(\RuntimeException::class);

        $listener = new OfferCancelTransactionListener($this->createMock(MessageBusInterface::class));
        $event = new EnteredEvent(new \stdClass(), new Marking());
        $listener($event);
    }

    // ---- OfferCompleteTransactionListener ----

    public function testOfferCompleteTransactionListenerThrowsForNonOfferSubject(): void
    {
        $this->expectException(\RuntimeException::class);

        $listener = new OfferCompleteTransactionListener($this->createMock(MessageBusInterface::class));
        $event = new EnteredEvent(new \stdClass(), new Marking());
        $listener($event);
    }

    // ---- OfferFinalizeTransactionListener ----

    public function testOfferFinalizeTransactionListenerThrowsForNonOfferSubject(): void
    {
        $this->expectException(\RuntimeException::class);

        $listener = new OfferFinalizeTransactionListener($this->createMock(MessageBusInterface::class));
        $event = new EnteredEvent(new \stdClass(), new Marking());
        $listener($event);
    }

    // ---- OfferRelaunchTransactionListener ----

    public function testOfferRelaunchTransactionListenerThrowsForNonOfferSubject(): void
    {
        $this->expectException(\RuntimeException::class);

        $listener = new OfferRelaunchTransactionListener($this->createMock(MessageBusInterface::class));
        $event = new EnteredEvent(new \stdClass(), new Marking());
        $listener($event);
    }

    public function testOfferRelaunchTransactionListenerSkipsWhenTransitionIsNotRelaunch(): void
    {
        $bus = $this->createMock(MessageBusInterface::class);
        $bus->expects(self::never())->method('dispatch');

        $listener = new OfferRelaunchTransactionListener($bus);
        $offer = new Base();
        // No transition passed → getTransition() returns null → transition name != 'relaunch'
        $event = new EnteredEvent($offer, new Marking());
        $listener($event);
    }

    // ---- AttendanceStatusTransactionListener ----

    public function testAttendanceStatusTransactionListenerThrowsForNonAttendanceSubject(): void
    {
        $this->expectException(\RuntimeException::class);

        $listener = new AttendanceStatusTransactionListener(
            $this->createMock(MessageBusInterface::class),
            new RequestStack()
        );
        $event = new AnnounceEvent(new \stdClass(), new Marking());
        $listener($event);
    }

    public function testAttendanceStatusTransactionListenerSkipsWhenStatusIsNull(): void
    {
        $bus = $this->createMock(MessageBusInterface::class);
        $bus->expects(self::never())->method('dispatch');

        $listener = new AttendanceStatusTransactionListener($bus, new RequestStack());

        $offer = new Base();
        $participant = new BaseParticipant();
        $attendance = new Attendance($offer, $participant);
        // Force status to null so the early-return path at line 53 is taken
        new \ReflectionProperty($attendance, 'status')->setValue($attendance, null);

        $event = new AnnounceEvent($attendance, new Marking());
        $listener($event);
    }

    // ---- AttendanceUnfulfilledTransactionListener ----

    public function testAttendanceUnfulfilledTransactionListenerThrowsForNonAttendanceSubject(): void
    {
        $this->expectException(\RuntimeException::class);

        $listener = new AttendanceUnfulfilledTransactionListener($this->createMock(MessageBusInterface::class));
        $event = new EnteredEvent(new \stdClass(), new Marking());
        $listener($event);
    }

    // ---- PaymentPayTransactionListener ----

    public function testPaymentPayTransactionListenerThrowsForNonPaymentSubject(): void
    {
        $this->expectException(\RuntimeException::class);

        $listener = new PaymentPayTransactionListener(
            $this->createMock(ReceiptNumberGenerator::class),
            $this->createMock(MessageBusInterface::class)
        );
        $event = new EnteredEvent(new \stdClass(), new Marking());
        $listener($event);
    }

    // ---- Happy-path tests ----

    // AttendanceStatusTransactionListener

    public function testAttendanceStatusDispatchesAttendanceCreatedWhenInitial(): void
    {
        $bus = $this->createMock(MessageBusInterface::class);
        $bus->expects(self::once())->method('dispatch')
            ->with(self::callback(static fn ($msg) => $msg instanceof AttendanceCreated && true === $msg->shallNotify()))
            ->willReturn(new Envelope(new \stdClass()));

        $requestStack = new RequestStack();
        $request = Request::create('https://example.com/test');
        $requestStack->push($request);

        $listener = new AttendanceStatusTransactionListener($bus, $requestStack);

        $offer = new Base();
        $participant = new BaseParticipant();
        $attendance = new Attendance($offer, $participant);

        $event = new AnnounceEvent($attendance, new Marking(), context: ['initial' => true]);
        $listener($event);
    }

    public function testAttendanceStatusDispatchesStatusChangedWhenNotInitial(): void
    {
        $bus = $this->createMock(MessageBusInterface::class);
        $bus->expects(self::once())->method('dispatch')
            ->with(self::callback(static fn ($msg) => $msg instanceof AttendanceStatusChanged))
            ->willReturn(new Envelope(new \stdClass()));

        $listener = new AttendanceStatusTransactionListener($bus, new RequestStack());

        $offer = new Base();
        $participant = new BaseParticipant();
        $attendance = new Attendance($offer, $participant);

        $transition = new Transition('confirm', 'waiting', 'confirmed');
        $event = new AnnounceEvent($attendance, new Marking(), $transition, context: ['initial' => false]);
        $listener($event);
    }

    public function testAttendanceStatusRespectsNotifyFalse(): void
    {
        $bus = $this->createMock(MessageBusInterface::class);
        $bus->expects(self::once())->method('dispatch')
            ->with(self::callback(static fn ($msg) => $msg instanceof AttendanceCreated && false === $msg->shallNotify()))
            ->willReturn(new Envelope(new \stdClass()));

        $listener = new AttendanceStatusTransactionListener($bus, new RequestStack());

        $offer = new Base();
        $participant = new BaseParticipant();
        $attendance = new Attendance($offer, $participant);

        $event = new AnnounceEvent($attendance, new Marking(), context: ['initial' => true, 'notify' => false]);
        $listener($event);
    }

    // OfferCancelTransactionListener

    public function testOfferCancelDispatchesCancelOffer(): void
    {
        $bus = $this->createMock(MessageBusInterface::class);
        $bus->expects(self::once())->method('dispatch')
            ->with(self::callback(static fn ($envelope) => $envelope instanceof Envelope))
            ->willReturn(new Envelope(new \stdClass()));

        $listener = new OfferCancelTransactionListener($bus);
        $offer = new Base();
        $event = new EnteredEvent($offer, new Marking());
        $listener($event);
    }

    // OfferCompleteTransactionListener

    public function testOfferCompleteDispatchesOfferCompleted(): void
    {
        $bus = $this->createMock(MessageBusInterface::class);
        $bus->expects(self::once())->method('dispatch')
            ->with(self::callback(static fn ($msg) => $msg instanceof OfferCompleted))
            ->willReturn(new Envelope(new \stdClass()));

        $listener = new OfferCompleteTransactionListener($bus);
        $offer = new Base();
        $event = new EnteredEvent($offer, new Marking(), context: ['survey_data' => ['q1' => 'a1']]);
        $listener($event);
    }

    // OfferFinalizeTransactionListener

    public function testOfferFinalizeDispatchesOfferFinalized(): void
    {
        $bus = $this->createMock(MessageBusInterface::class);
        $bus->expects(self::once())->method('dispatch')
            ->with(self::callback(static fn ($msg) => $msg instanceof OfferFinalized))
            ->willReturn(new Envelope(new \stdClass()));

        $listener = new OfferFinalizeTransactionListener($bus);
        $offer = new Base();
        $event = new EnteredEvent($offer, new Marking());
        $listener($event);
    }

    // AttendanceConfirmTransactionListener

    public function testAttendanceConfirmSetsPayableWhenCommitAndFee(): void
    {
        $listener = new AttendanceConfirmTransactionListener();

        $offer = new Base();
        $offer->setFee(500);
        // isOnlinePayable() returns !isDisableOnlinePayment(), default is false → payable is true
        $participant = new BaseParticipant();
        $attendance = new Attendance($offer, $participant);

        $event = new EnteredEvent($attendance, new Marking(), context: ['commit' => true]);
        $listener($event);

        self::assertTrue($attendance->isPayable());
    }

    public function testAttendanceConfirmDoesNotSetPayableWhenNoFee(): void
    {
        $listener = new AttendanceConfirmTransactionListener();

        $offer = new Base();
        // No fee set (null)
        $participant = new BaseParticipant();
        $attendance = new Attendance($offer, $participant);

        $event = new EnteredEvent($attendance, new Marking(), context: ['commit' => true]);
        $listener($event);

        self::assertFalse($attendance->isPayable());
    }

    // PaymentPayTransactionListener

    public function testPaymentPaySetsReceiptNumber(): void
    {
        $generator = $this->createMock(ReceiptNumberGenerator::class);
        $generator->expects(self::once())->method('generate')->willReturn('R-2024-001');

        $bus = $this->createMock(MessageBusInterface::class);
        $bus->method('dispatch')->willReturn(new Envelope(new \stdClass()));

        $listener = new PaymentPayTransactionListener($generator, $bus);

        $payment = new Payment();
        $event = new EnteredEvent($payment, new Marking());
        $listener($event);

        self::assertSame('R-2024-001', $payment->getReceiptNumber());
    }
}
