<?php

namespace App\Tests\Security;

use App\Entity\User;
use App\Security\KeycloakAuthenticator;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Client\OAuth2ClientInterface;
use League\OAuth2\Client\Token\AccessToken;
use PHPUnit\Framework\TestCase;
use Stevenmaguire\OAuth2\Client\Provider\KeycloakResourceOwner;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBag;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;

class KeycloakAuthenticatorTest extends TestCase
{
    private ClientRegistry $clientRegistry;
    private EntityManagerInterface $em;
    private RouterInterface $router;
    private KeycloakAuthenticator $authenticator;

    protected function setUp(): void
    {
        $this->clientRegistry = $this->createMock(ClientRegistry::class);
        $this->em = $this->createMock(EntityManagerInterface::class);
        $this->router = $this->createMock(RouterInterface::class);

        $this->authenticator = new KeycloakAuthenticator(
            $this->clientRegistry,
            $this->em,
            $this->router,
        );
    }

    public function testSupportsReturnsTrueForCheckRoute(): void
    {
        $request = new Request();
        $request->attributes->set('_route', 'app_oauth_keycloak_check');

        self::assertTrue($this->authenticator->supports($request));
    }

    public function testSupportsReturnsFalseForOtherRoute(): void
    {
        $request = new Request();
        $request->attributes->set('_route', 'app_login');

        self::assertFalse($this->authenticator->supports($request));
    }

    public function testOnAuthenticationSuccessRedirectsToAccount(): void
    {
        $this->router->method('generate')
            ->with('app_account')
            ->willReturn('/mon-compte');

        $request = new Request();
        $token = $this->createMock(TokenInterface::class);

        $response = $this->authenticator->onAuthenticationSuccess($request, $token, 'main');

        self::assertSame(302, $response->getStatusCode());
        self::assertSame('/mon-compte', $response->getTargetUrl());
    }

    public function testOnAuthenticationFailureRedirectsToLoginWithFlash(): void
    {
        $this->router->method('generate')
            ->with('app_login')
            ->willReturn('/connexion');

        $flashBag = new FlashBag();
        $session = $this->createMock(Session::class);
        $session->method('getFlashBag')->willReturn($flashBag);

        $request = new Request();
        $request->setSession($session);

        $exception = new AuthenticationException('Test error');

        $response = $this->authenticator->onAuthenticationFailure($request, $exception);

        self::assertSame(302, $response->getStatusCode());
        self::assertSame('/connexion', $response->getTargetUrl());
        self::assertSame(['Echec de la connexion SSO E-Cosplay.'], $flashBag->get('error'));
    }

    public function testAuthenticateCreatesNewUser(): void
    {
        $keycloakUser = $this->createKeycloakUser('kc-123', 'new@example.com', 'Jean', 'Dupont', []);

        $repo = $this->createMock(EntityRepository::class);
        $repo->method('findOneBy')->willReturn(null);
        $this->em->method('getRepository')->willReturn($repo);
        $this->em->expects(self::once())->method('persist');
        $this->em->expects(self::once())->method('flush');

        $passport = $this->callAuthenticate($keycloakUser);
        $user = $passport->getUser();

        self::assertInstanceOf(User::class, $user);
        self::assertSame('new@example.com', $user->getEmail());
        self::assertSame('Jean', $user->getFirstName());
        self::assertSame('Dupont', $user->getLastName());
        self::assertSame('kc-123', $user->getKeycloakId());
    }

    public function testAuthenticateLinksExistingUserByEmail(): void
    {
        $existingUser = new User();
        $existingUser->setEmail('existing@example.com');
        $existingUser->setFirstName('Existing');
        $existingUser->setLastName('User');
        $existingUser->setPassword('$2y$13$hashed');

        $keycloakUser = $this->createKeycloakUser('kc-456', 'existing@example.com', 'Existing', 'User', []);

        $repo = $this->createMock(EntityRepository::class);
        $repo->method('findOneBy')->willReturnCallback(function (array $criteria) use ($existingUser) {
            if (isset($criteria['keycloakId'])) {
                return null;
            }

            return $existingUser;
        });
        $this->em->method('getRepository')->willReturn($repo);
        $this->em->expects(self::never())->method('persist');
        $this->em->expects(self::once())->method('flush');

        $passport = $this->callAuthenticate($keycloakUser);
        $user = $passport->getUser();

        self::assertSame($existingUser, $user);
        self::assertSame('kc-456', $user->getKeycloakId());
    }

    public function testAuthenticateUpdatesExistingKeycloakUser(): void
    {
        $existingUser = new User();
        $existingUser->setEmail('old@example.com');
        $existingUser->setFirstName('Old');
        $existingUser->setLastName('Name');
        $existingUser->setPassword('$2y$13$hashed');
        $existingUser->setKeycloakId('kc-789');

        $keycloakUser = $this->createKeycloakUser('kc-789', 'updated@example.com', 'New', 'Name', []);

        $repo = $this->createMock(EntityRepository::class);
        $repo->method('findOneBy')->willReturnCallback(function (array $criteria) use ($existingUser) {
            if (isset($criteria['keycloakId']) && 'kc-789' === $criteria['keycloakId']) {
                return $existingUser;
            }

            return null;
        });
        $this->em->method('getRepository')->willReturn($repo);
        $this->em->expects(self::once())->method('flush');

        $passport = $this->callAuthenticate($keycloakUser);
        $user = $passport->getUser();

        self::assertSame($existingUser, $user);
        self::assertSame('updated@example.com', $user->getEmail());
        self::assertSame('New', $user->getFirstName());
        self::assertSame('Name', $user->getLastName());
    }

    public function testAuthenticateMapsSuperadminGroupToRoleRoot(): void
    {
        $keycloakUser = $this->createKeycloakUser('kc-admin', 'admin@example.com', 'Admin', 'User', ['/superadmin']);

        $repo = $this->createMock(EntityRepository::class);
        $repo->method('findOneBy')->willReturn(null);
        $this->em->method('getRepository')->willReturn($repo);
        $this->em->expects(self::once())->method('persist');

        $passport = $this->callAuthenticate($keycloakUser);
        $user = $passport->getUser();

        self::assertContains('ROLE_ROOT', $user->getRoles());
    }

    public function testAuthenticateIgnoresUnknownGroups(): void
    {
        $keycloakUser = $this->createKeycloakUser('kc-normal', 'normal@example.com', 'Normal', 'User', ['/editors', '/viewers']);

        $repo = $this->createMock(EntityRepository::class);
        $repo->method('findOneBy')->willReturn(null);
        $this->em->method('getRepository')->willReturn($repo);
        $this->em->expects(self::once())->method('persist');

        $passport = $this->callAuthenticate($keycloakUser);
        $user = $passport->getUser();

        self::assertSame(['ROLE_USER'], $user->getRoles());
    }

    /**
     * @param list<string> $groups
     */
    private function createKeycloakUser(string $id, string $email, string $firstName, string $lastName, array $groups): KeycloakResourceOwner
    {
        return new KeycloakResourceOwner([
            'sub' => $id,
            'email' => $email,
            'given_name' => $firstName,
            'family_name' => $lastName,
            'groups' => $groups,
        ]);
    }

    private function callAuthenticate(KeycloakResourceOwner $keycloakUser): SelfValidatingPassport
    {
        $accessToken = $this->createMock(AccessToken::class);
        $accessToken->method('getToken')->willReturn('fake-token');

        $oauthClient = $this->createMock(OAuth2ClientInterface::class);
        $oauthClient->method('fetchUserFromToken')->with($accessToken)->willReturn($keycloakUser);

        $this->clientRegistry->method('getClient')->with('keycloak')->willReturn($oauthClient);

        // Use reflection to mock fetchAccessToken from parent OAuth2Authenticator
        $authenticator = $this->getMockBuilder(KeycloakAuthenticator::class)
            ->setConstructorArgs([$this->clientRegistry, $this->em, $this->router])
            ->onlyMethods(['fetchAccessToken'])
            ->getMock();

        $authenticator->method('fetchAccessToken')->willReturn($accessToken);

        $request = new Request();
        $request->attributes->set('_route', 'app_oauth_keycloak_check');

        $passport = $authenticator->authenticate($request);

        // Resolve the UserBadge callback
        $passport->getUser();

        return $passport;
    }
}
