<?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\Export\ParticipantList;

use Doctrine\Common\Collections\ArrayCollection;
use Ferienpass\CoreBundle\ConsentManager\ConsentManager;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\Participant\ParticipantInterface;
use Ferienpass\CoreBundle\Export\ParticipantList\XlsxExport;
use Ferienpass\CoreBundle\Repository\ConsentFormRepository;
use Ferienpass\CoreBundle\Settings\SystemSettingsManager;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Translation\Loader\ArrayLoader;
use Symfony\Component\Translation\Translator;
use Symfony\Contracts\Translation\TranslatorInterface;

final class XlsxExportTest extends TestCase
{
    public function testCreatesSpreadsheetWithCorrectColumns(): void
    {
        $offer = $this->createStub(OfferInterface::class);
        $filesystem = $this->createStub(Filesystem::class);
        $security = $this->createStub(Security::class);
        $consentManager = $this->createStub(ConsentManager::class);
        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $translator = $this->createStub(TranslatorInterface::class);

        $offer->method('getName')->willReturn('My Offer');
        $offer->method('getDates')->willReturn(new ArrayCollection([]));
        $offer->method('getApplicationExtra')->willReturn([]);

        $participant = $this->createStub(ParticipantInterface::class);
        $participant->method('getLastname')->willReturn('Lastname');
        $participant->method('getFirstname')->willReturn('Firstname');
        $participant->method('getEmail')->willReturn('email@example.com');
        $participant->method('getMobile')->willReturn('0123456789');
        $participant->method('getPhone')->willReturn('0987654321');

        $attendance = $this->createStub(Attendance::class);
        $attendance->method('getParticipant')->willReturn($participant);
        $attendance->method('getStatus')->willReturn('confirmed');

        $offer->method('getAttendancesConfirmedOrWaitlisted')
            ->willReturn(new ArrayCollection([$attendance]));

        $consentFormRepository->method('findForOffer')->willReturn([]);

        // Role ADMIN sees DOB
        $security->method('isGranted')->willReturn(true);

        $export = new XlsxExport($filesystem, $security, $consentManager, $consentFormRepository, $translator, $this->sortSetting());
        $path = $export->generate($offer);

        self::assertStringEndsWith('.xlsx', $path);
        self::assertFileExists($path);

        // We could use a library to read the xlsx and verify columns, but simple check is enough for now
        unlink($path);
    }

    public function testRoleBasedVisibility(): void
    {
        $offer = $this->createStub(OfferInterface::class);
        $filesystem = $this->createStub(Filesystem::class);
        $security = $this->createStub(Security::class);
        $consentManager = $this->createStub(ConsentManager::class);
        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $translator = $this->createStub(TranslatorInterface::class);

        $offer->method('getDates')->willReturn(new ArrayCollection([]));
        $offer->method('getApplicationExtra')->willReturn([]);
        $offer->method('getAttendancesConfirmedOrWaitlisted')->willReturn(new ArrayCollection([]));
        $consentFormRepository->method('findForOffer')->willReturn([]);

        // Role ADMIN sees DOB
        $security->method('isGranted')->willReturn(false);

        $export = new XlsxExport($filesystem, $security, $consentManager, $consentFormRepository, $translator, $this->sortSetting());
        $path = $export->generate($offer);

        self::assertStringEndsWith('.xlsx', $path);
        unlink($path);
    }

    public function testApplicationExtrasArePopulatedAndAlignedWithSharedNotes(): void
    {
        $extras = [
            ['uuid' => 'extra-1', 'type' => 'txt', 'question' => 'Allergien?'],
            ['uuid' => 'extra-2', 'type' => 'yn', 'question' => 'Schwimmer?'],
        ];

        $offer = $this->createStub(OfferInterface::class);
        $offer->method('getName')->willReturn('My Offer');
        $offer->method('getDates')->willReturn(new ArrayCollection([]));
        $offer->method('isCollectDateOfBirth')->willReturn(false);
        $offer->method('getApplicationExtra')->willReturn($extras);

        $participant = $this->createMock(ParticipantWithSharedNotesStub::class);
        $participant->method('getLastname')->willReturn('Doe');
        $participant->method('getFirstname')->willReturn('Jane');
        $participant->method('getEmail')->willReturn('jane@example.com');
        $participant->method('getMobile')->willReturn('');
        $participant->method('getPhone')->willReturn('');
        $participant->method('getSharedNotes')->willReturn('Wichtige Info Text');

        $attendance = $this->createMock(Attendance::class);
        $attendance->method('getParticipant')->willReturn($participant);
        $attendance->method('getStatus')->willReturn('confirmed');
        $attendance->method('getEmail')->willReturn('');
        $attendance->method('getPhone')->willReturn('');
        $attendance->method('getExtra')->willReturnMap([
            ['extra-1', 'Erdnüsse'],
            ['extra-2', 'y'],
        ]);

        $offer->method('getAttendancesConfirmedOrWaitlisted')
            ->willReturn(new ArrayCollection([$attendance]));

        $security = $this->createStub(Security::class);
        $security->method('isGranted')->willReturn(false);

        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentFormRepository->method('findForOffer')->willReturn([]);

        $export = new XlsxExport(
            $this->createStub(Filesystem::class),
            $security,
            $this->createStub(ConsentManager::class),
            $consentFormRepository,
            $this->createStub(TranslatorInterface::class),
            $this->sortSetting()
        );

        $path = $export->generate($offer);
        $sheet = IOFactory::load($path)->getActiveSheet();

        // Header row 3: Nr, Name, Vorname, Alter, E-Mail, Mobil, Telefon, Status, Allergien?, Schwimmer?, Wichtige Info
        $headers = [];
        for ($i = 1; $i <= 12; ++$i) {
            $value = $sheet->getCell([$i, 3])->getValue();
            if (null !== $value && '' !== $value) {
                $headers[] = $value;
            }
        }

        self::assertSame(
            ['Nr.', 'Name', 'Vorname', 'Alter', 'E-Mail', 'Mobil', 'Telefon', 'Status', 'Allergien?', 'Schwimmer?', 'Wichtige Info'],
            $headers
        );

        // Data row 4: matching positions for the extras and shared notes
        self::assertSame('Erdnüsse', $sheet->getCell([9, 4])->getValue());
        self::assertSame('Ja', $sheet->getCell([10, 4])->getValue());
        self::assertSame('Wichtige Info Text', $sheet->getCell([11, 4])->getValue());

        unlink($path);
    }

    public function testStatusIsTranslated(): void
    {
        $translator = new Translator('de');
        $translator->addLoader('array', new ArrayLoader());
        $translator->addResource('array', ['status.confirmed' => 'Bestätigt'], 'de');

        $offer = $this->createStub(OfferInterface::class);
        $offer->method('getName')->willReturn('My Offer');
        $offer->method('getDates')->willReturn(new ArrayCollection([]));
        $offer->method('isCollectDateOfBirth')->willReturn(false);
        $offer->method('getApplicationExtra')->willReturn([]);

        $participant = $this->createStub(ParticipantInterface::class);
        $participant->method('getLastname')->willReturn('Doe');
        $participant->method('getFirstname')->willReturn('Jane');
        $participant->method('getEmail')->willReturn('jane@example.com');
        $participant->method('getMobile')->willReturn('');
        $participant->method('getPhone')->willReturn('');

        $attendance = $this->createStub(Attendance::class);
        $attendance->method('getParticipant')->willReturn($participant);
        $attendance->method('getStatus')->willReturn('confirmed');
        $attendance->method('getEmail')->willReturn('');
        $attendance->method('getPhone')->willReturn('');

        $offer->method('getAttendancesConfirmedOrWaitlisted')
            ->willReturn(new ArrayCollection([$attendance]));

        $security = $this->createStub(Security::class);
        $security->method('isGranted')->willReturn(false);

        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentFormRepository->method('findForOffer')->willReturn([]);

        $export = new XlsxExport(
            $this->createStub(Filesystem::class),
            $security,
            $this->createStub(ConsentManager::class),
            $consentFormRepository,
            $translator,
            $this->sortSetting()
        );

        $path = $export->generate($offer);
        $sheet = IOFactory::load($path)->getActiveSheet();

        // Status is the 8th column (Nr, Name, Vorname, Alter, E-Mail, Mobil, Telefon, Status)
        self::assertSame('Bestätigt', $sheet->getCell([8, 4])->getValue());

        unlink($path);
    }

    /**
     * The sheet gets printed and taken to the event: waiting-list entries must be
     * announced by a banner row and start counting at 1 again, so they can never
     * read as the continuation of the participant list.
     */
    public function testWaitlistedAttendancesGetTheirOwnNumberedSection(): void
    {
        $translator = new Translator('de');
        $translator->addLoader('array', new ArrayLoader());
        $translator->addResource('array', ['status.confirmed' => 'Bestätigt', 'status.waitlisted' => 'Warteliste'], 'de');

        $offer = $this->createStub(OfferInterface::class);
        $offer->method('getName')->willReturn('Schaf mit Spaß');
        $offer->method('getDates')->willReturn(new ArrayCollection([]));
        $offer->method('isCollectDateOfBirth')->willReturn(false);
        $offer->method('getApplicationExtra')->willReturn([]);

        // Deliberately interleaved by name, and handed over waitlisted-first, so
        // the grouping cannot come out of the input order by accident.
        $attendances = [
            $this->attendanceFor('Auer', 'Wilma', 'waitlisted'),
            $this->attendanceFor('Berg', 'Ada', 'confirmed'),
            $this->attendanceFor('Zander', 'Mia', 'confirmed'),
        ];

        $offer->method('getAttendancesConfirmedOrWaitlisted')->willReturn(new ArrayCollection($attendances));

        $security = $this->createStub(Security::class);
        $security->method('isGranted')->willReturn(false);

        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentFormRepository->method('findForOffer')->willReturn([]);

        $export = new XlsxExport(
            $this->createStub(Filesystem::class),
            $security,
            $this->createStub(ConsentManager::class),
            $consentFormRepository,
            $translator,
            $this->sortSetting()
        );

        $path = $export->generate($offer);
        $sheet = IOFactory::load($path)->getActiveSheet();

        // Rows 4-5: the confirmed participants, numbered 1..2.
        self::assertSame(1, $sheet->getCell([1, 4])->getValue());
        self::assertSame('Berg', $sheet->getCell([2, 4])->getValue());
        self::assertSame(2, $sheet->getCell([1, 5])->getValue());
        self::assertSame('Zander', $sheet->getCell([2, 5])->getValue());

        // Row 6: the section banner, then the waiting list restarting at 1.
        self::assertSame('WARTELISTE', $sheet->getCell([1, 6])->getValue());
        self::assertSame(1, $sheet->getCell([1, 7])->getValue());
        self::assertSame('Auer', $sheet->getCell([2, 7])->getValue());

        unlink($path);
    }

    private function attendanceFor(string $lastname, string $firstname, string $status): Attendance
    {
        $participant = $this->createStub(ParticipantInterface::class);
        $participant->method('getLastname')->willReturn($lastname);
        $participant->method('getFirstname')->willReturn($firstname);
        $participant->method('getEmail')->willReturn('');
        $participant->method('getMobile')->willReturn('');
        $participant->method('getPhone')->willReturn('');

        $attendance = $this->createStub(Attendance::class);
        $attendance->method('getParticipant')->willReturn($participant);
        $attendance->method('getStatus')->willReturn($status);
        $attendance->method('getEmail')->willReturn('');
        $attendance->method('getPhone')->willReturn('');

        return $attendance;
    }

    private function sortSetting(string $sort = 'name'): SystemSettingsManager
    {
        $manager = $this->createStub(SystemSettingsManager::class);
        $manager->method('getParticipantListSort')->willReturn($sort);

        return $manager;
    }
}

abstract class ParticipantWithSharedNotesStub implements ParticipantInterface
{
    abstract public function getSharedNotes(): ?string;
}
