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

use Ferienpass\CoreBundle\DependencyInjection\Configuration;
use Ferienpass\CoreBundle\DependencyInjection\FerienpassCoreExtension;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
 * The guard that keeps a multi-tenant build from booting with configuration
 * that would give every municipality the same reminder period, receipt prefix
 * or reply-to address.
 *
 * Most of these poke the guard directly: a `load()` that gets *past* it goes on
 * to register the bundle's whole service graph, which needs far more of the
 * container than a unit test has. One test covers the wiring into `load()`.
 */
class FerienpassCoreExtensionTest extends TestCase
{
    public function testTheGuardRunsDuringLoad(): void
    {
        $this->expectException(InvalidConfigurationException::class);
        $this->expectExceptionMessageMatches('/ferienpass\.reply_address/');

        new FerienpassCoreExtension()->load(
            [['multi_tenancy' => ['enabled' => true], 'reply_address' => 'info@example.com']],
            new ContainerBuilder(),
        );
    }

    #[DataProvider('perTenantOptions')]
    public function testRejectsAPerMunicipalityOption(string $key, mixed $value): void
    {
        $this->expectException(InvalidConfigurationException::class);
        $this->expectExceptionMessageMatches('/ferienpass\.'.preg_quote($key, '/').'/');

        $this->guard([$key => $value]);
    }

    public static function perTenantOptions(): iterable
    {
        yield 'reply address' => ['reply_address', 'info@example.com'];
        yield 'receipt prefix' => ['receipt_number_prefix', 'FP-'];
        yield 'friends booking' => ['friends_booking', true];
        yield 'require postal address' => ['require_postal_address', true];
        yield 'erase period' => ['erase_data_after', '50 WEEK'];
        yield 'reminder period' => ['remind_attendance_before', '+36 hours'];
        yield 'payment selectable' => ['payment_allow_select', true];
        yield 'age tolerance' => ['age_tolerance', '90 days'];
        yield 'withdraw grace period' => ['withdraw_grace_period', '+3 days'];
        yield 'no withdraw after grace' => ['no_withdraw_after_grace_period', true];
        yield 'no withdraw after deadline' => ['no_withdraw_after_application_deadline', true];
        yield 'postal codes' => ['preferred_postal_codes', ['48529']];

        yield 'payments' => ['payments_enabled', true];
        yield 'participant list sort' => ['participant_list_sort', 'firstname'];

        // A deliberate `false` decides something about one municipality just as
        // much as a `true` does, so it has to be caught too.
        yield 'reminders switched off' => ['remind_attendance', false];
        yield 'payments switched off' => ['payments_enabled', false];
    }

    public function testNamesEveryOffendingOptionAtOnce(): void
    {
        try {
            $this->guard(['reply_address' => 'info@example.com', 'age_tolerance' => '90 days']);

            $this->fail('Expected the guard to reject the configuration.');
        } catch (InvalidConfigurationException $e) {
            $this->assertStringContainsString('ferienpass.reply_address', $e->getMessage());
            $this->assertStringContainsString('ferienpass.age_tolerance', $e->getMessage());
        }
    }

    public function testAcceptsAConfigurationThatSaysNothingAboutAMunicipality(): void
    {
        $this->expectNotToPerformAssertions();

        // Everything left at its default, plus options that are genuinely
        // installation-wide.
        $this->guard([
            'reply_address' => null,
            'preferred_postal_codes' => [],
            'inbound_domain' => 'reply.ferienpass.online',
            'logos_dir' => '/srv/files/logos',
        ]);
    }

    /**
     * A platform console without multi-tenancy would be configuration that
     * silently does nothing: no tenant to switch to, and one municipality that
     * is already served on its own host.
     */
    public function testRejectsAPlatformHostOnASingleTenantBuild(): void
    {
        $this->expectException(InvalidConfigurationException::class);
        $this->expectExceptionMessageMatches('/platform_host/');

        $this->process(['multi_tenancy' => ['platform_host' => 'platform.my-ferienpass.de']]);
    }

    public function testAcceptsAPlatformHostOnAMultiTenantBuild(): void
    {
        $config = $this->process(['multi_tenancy' => ['enabled' => true, 'platform_host' => 'platform.my-ferienpass.de']]);

        self::assertSame('platform.my-ferienpass.de', $config['multi_tenancy']['platform_host']);
    }

    private function process(array $config): array
    {
        return new Processor()->processConfiguration(new Configuration(), [$config]);
    }

    private function guard(array $config): void
    {
        $method = new \ReflectionMethod(FerienpassCoreExtension::class, 'assertNothingFlattensTheTenants');
        $method->invoke(new FerienpassCoreExtension(), $config);
    }
}
