<?php

declare(strict_types=1);

namespace Ferienpass\AdminBundle\Tests\Menu;

use Ferienpass\AdminBundle\Menu\MenuBuilder;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Payments\PaymentSystem;
use Ferienpass\CoreBundle\Repository\EditionRepository;
use Ferienpass\CoreBundle\Repository\HostRepository;
use Ferienpass\CoreBundle\Repository\OfferRepositoryInterface;
use Knp\Menu\MenuFactory;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator;

final class MenuBuilderTest extends TestCase
{
    private function createBuilder(
        ?AuthorizationCheckerInterface $authChecker = null,
        ?RequestStack $requestStack = null,
        ?EditionRepository $editions = null,
        ?Security $security = null,
        ?HostRepository $hosts = null,
        ?OfferRepositoryInterface $offers = null,
        ?PaymentSystem $paymentSystem = null,
    ): MenuBuilder {
        return new MenuBuilder(
            new MenuFactory(),
            $this->createStub(LogoutUrlGenerator::class),
            $authChecker ?? $this->createStub(AuthorizationCheckerInterface::class),
            $requestStack ?? new RequestStack(),
            $editions ?? $this->createStub(EditionRepository::class),
            $security ?? $this->createStub(Security::class),
            $hosts ?? $this->createStub(HostRepository::class),
            $offers ?? $this->createStub(OfferRepositoryInterface::class),
            $paymentSystem ?? $this->paymentSystem(true),
        );
    }

    private function paymentSystem(bool $enabled): PaymentSystem
    {
        $paymentSystem = $this->createStub(PaymentSystem::class);
        $paymentSystem->method('isEnabled')->willReturn($enabled);

        return $paymentSystem;
    }

    private function grantedChecker(array $grantedRoles): AuthorizationCheckerInterface
    {
        $checker = $this->createMock(AuthorizationCheckerInterface::class);
        $checker->method('isGranted')->willReturnCallback(
            static fn (string $role) => \in_array($role, $grantedRoles, true)
        );

        return $checker;
    }

    private function requestStackWithRoute(string $route, array $attributes = []): RequestStack
    {
        $request = new Request();
        $request->attributes->set('_route', $route);
        foreach ($attributes as $key => $value) {
            $request->attributes->set($key, $value);
        }

        $stack = new RequestStack();
        $stack->push($request);

        return $stack;
    }

    // ---- primaryNavigation ----

    public function testPrimaryNavigationAlwaysContainsOffersAndFiles(): void
    {
        $menu = $this->createBuilder(
            authChecker: $this->grantedChecker([]),
            requestStack: $this->requestStackWithRoute('admin_home'),
        )->primaryNavigation();

        self::assertNotNull($menu->getChild('offers.title'));
        self::assertNotNull($menu->getChild('files.title'));
        self::assertNull($menu->getChild('accounts.title'));
        self::assertNull($menu->getChild('tools.title'));
    }

    public function testPrimaryNavigationShowsProfileForNonAdmin(): void
    {
        $menu = $this->createBuilder(
            authChecker: $this->grantedChecker([]),
            requestStack: $this->requestStackWithRoute('admin_home'),
        )->primaryNavigation();

        self::assertNotNull($menu->getChild('profile.title'));
    }

    public function testPrimaryNavigationHidesProfileForAdmin(): void
    {
        $menu = $this->createBuilder(
            authChecker: $this->grantedChecker(['ROLE_ADMIN']),
            requestStack: $this->requestStackWithRoute('admin_home'),
        )->primaryNavigation();

        self::assertNull($menu->getChild('profile.title'));
    }

    public function testPrimaryNavigationShowsParticipantsForParticipantsAdmin(): void
    {
        $menu = $this->createBuilder(
            authChecker: $this->grantedChecker(['ROLE_PARTICIPANTS_ADMIN']),
            requestStack: $this->requestStackWithRoute('admin_home'),
        )->primaryNavigation();

        self::assertNotNull($menu->getChild('participants.title'));
    }

    public function testPrimaryNavigationShowsHostsForAdmin(): void
    {
        $menu = $this->createBuilder(
            authChecker: $this->grantedChecker(['ROLE_ADMIN']),
            requestStack: $this->requestStackWithRoute('admin_home'),
        )->primaryNavigation();

        self::assertNotNull($menu->getChild('hosts.title'));
    }

    public function testPrimaryNavigationShowsPaymentsForPaymentsAdmin(): void
    {
        $menu = $this->createBuilder(
            authChecker: $this->grantedChecker(['ROLE_PAYMENTS_ADMIN']),
            requestStack: $this->requestStackWithRoute('admin_home'),
        )->primaryNavigation();

        self::assertNotNull($menu->getChild('payments.title'));
    }

    public function testPrimaryNavigationHidesPaymentsWhenPaymentsDisabled(): void
    {
        $menu = $this->createBuilder(
            authChecker: $this->grantedChecker(['ROLE_PAYMENTS_ADMIN']),
            requestStack: $this->requestStackWithRoute('admin_home'),
            paymentSystem: $this->paymentSystem(false),
        )->primaryNavigation();

        self::assertNull($menu->getChild('payments.title'));
    }

    public function testPrimaryNavigationShowsAccountsAndToolsForAdmin(): void
    {
        $menu = $this->createBuilder(
            authChecker: $this->grantedChecker(['ROLE_ADMIN']),
            requestStack: $this->requestStackWithRoute('admin_home'),
        )->primaryNavigation();

        self::assertNotNull($menu->getChild('accounts.title'));
        self::assertNotNull($menu->getChild('tools.title'));
    }

    public function testPrimaryNavigationCurrentRouteDetection(): void
    {
        $menu = $this->createBuilder(
            authChecker: $this->grantedChecker(['ROLE_ADMIN', 'ROLE_PARTICIPANTS_ADMIN', 'ROLE_PAYMENTS_ADMIN']),
            requestStack: $this->requestStackWithRoute('admin_offers_edit'),
        )->primaryNavigation();

        self::assertTrue($menu->getChild('offers.title')->isCurrent());
        self::assertFalse($menu->getChild('hosts.title')->isCurrent());
        self::assertFalse($menu->getChild('payments.title')->isCurrent());
    }

    public function testPrimaryNavigationToolsCurrentDetectsMultipleRoutes(): void
    {
        foreach (['admin_tools_mailing', 'admin_accessCodes_index', 'admin_notifications', 'admin_outbox'] as $route) {
            $menu = $this->createBuilder(
                authChecker: $this->grantedChecker(['ROLE_ADMIN']),
                requestStack: $this->requestStackWithRoute($route),
            )->primaryNavigation();

            self::assertTrue($menu->getChild('tools.title')->isCurrent(), "Route $route should set tools.title to current");
        }
    }

    // ---- secondaryNavigation ----

    public function testSecondaryNavigationReturnsEmptyMenuWhenNoRequest(): void
    {
        $menu = $this->createBuilder()->secondaryNavigation();
        self::assertCount(0, $menu->getChildren());
    }

    public function testSecondaryNavigationAccountsRouteIsEmpty(): void
    {
        // The accounts secondary strip has been replaced by the in-headline
        // AccountTypeSwitcher; the secondary nav should emit nothing for these
        // routes.
        $menu = $this->createBuilder(
            authChecker: $this->grantedChecker(['ROLE_SUPER_ADMIN']),
            requestStack: $this->requestStackWithRoute('admin_accounts_index'),
        )->secondaryNavigation();

        self::assertCount(0, $menu->getChildren());
    }

    public function testSecondaryNavigationFilesRouteIsEmpty(): void
    {
        $menu = $this->createBuilder(
            requestStack: $this->requestStackWithRoute('admin_files', ['storage' => 'media']),
        )->secondaryNavigation();

        self::assertCount(0, $menu->getChildren());
    }

    public function testSecondaryNavigationParticipantsRouteShowsTabs(): void
    {
        $menu = $this->createBuilder(
            requestStack: $this->requestStackWithRoute('admin_participants_index'),
        )->secondaryNavigation();

        self::assertNotNull($menu->getChild('participants.all'));
        self::assertNotNull($menu->getChild('participants.duplicates'));
        self::assertTrue($menu->getChild('participants.all')->isCurrent());
    }

    public function testSecondaryNavigationPaymentsRouteShowsFourTabs(): void
    {
        $menu = $this->createBuilder(
            requestStack: $this->requestStackWithRoute('admin_payments_debtors'),
        )->secondaryNavigation();

        self::assertNotNull($menu->getChild('payments.debtors'));
        self::assertNotNull($menu->getChild('payments.title'));
        self::assertNotNull($menu->getChild('payments.unpaid'));
        self::assertNotNull($menu->getChild('payments.overpaid'));
        self::assertTrue($menu->getChild('payments.debtors')->isCurrent());
    }

    public function testSecondaryNavigationIsEmptyOnToolRoutes(): void
    {
        // Tool pages no longer get a secondary nav strip — discovery is handled
        // by the "Werkzeuge & Einstellungen" mega-menu in the primary nav, and
        // system pages use an in-page VerticalNav.
        foreach (['admin_tools_mailing', 'admin_notifications', 'admin_outbox', 'admin_accessCodes_index', 'admin_tools_settings_categories'] as $route) {
            $menu = $this->createBuilder(
                requestStack: $this->requestStackWithRoute($route),
            )->secondaryNavigation();

            self::assertFalse($menu->hasChildren(), "Route $route should produce no secondary nav children");
        }
    }

    // ---- userNavigation ----

    public function testUserNavigationContainsUserPasswordLogout(): void
    {
        $security = $this->createStub(Security::class);
        $security->method('getToken')->willReturn(null);

        $menu = $this->createBuilder(
            security: $security,
            requestStack: $this->requestStackWithRoute('admin_home'),
        )->userNavigation();

        self::assertNotNull($menu->getChild('user.title'));
        self::assertNotNull($menu->getChild('user.password.title'));
        self::assertNotNull($menu->getChild('user.logout'));
        self::assertNull($menu->getChild('user.impersonation_exit'));
    }

    public function testUserNavigationShowsImpersonationExitForSwitchUserToken(): void
    {
        $impersonator = $this->createStub(User::class);
        $impersonator->method('getUserIdentifier')->willReturn('admin@example.com');

        $originalToken = new UsernamePasswordToken($impersonator, 'ferienpass_admin', ['ROLE_ADMIN']);
        $switchToken = new SwitchUserToken($this->createStub(User::class), 'ferienpass_admin', ['ROLE_USER'], $originalToken);

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

        $menu = $this->createBuilder(
            security: $security,
            requestStack: $this->requestStackWithRoute('admin_home'),
        )->userNavigation();

        self::assertNotNull($menu->getChild('user.impersonation_exit'));
    }

    // ---- participantListActions ----

    public function testParticipantListActionsThrowsWithoutOffer(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->createBuilder()->participantListActions([]);
    }

    public function testParticipantListActionsReturnsMenuWithPdfItem(): void
    {
        $offer = $this->createStub(OfferInterface::class);
        $offer->method('getUuid')->willReturn(\Symfony\Component\Uid\Uuid::v7());

        $menu = $this->createBuilder()->participantListActions(['offer' => $offer]);
        self::assertNotNull($menu->getChild('pdf'));
    }
}
