<?php

use PHPUnit\Framework\TestCase;

if (session_status() === PHP_SESSION_NONE) {
    session_start();
}

/**
 * This test has a setup similar to the LoginHandlerTest one, however SessionData and
 * UserHandler are not mocked, so interaction between session and login handler can be
 * tested and complete login and join flows can be simulated step by step.
 */
final class LoginFlowTest extends TestCase {

    private $locatorStub;
    private $userDaoStub;
    private $oAuth2RequestHandler;
    private $auditLogger;
    private $gmsClientStub;
    private $sessionData;
    private $userHandler;
    private $loginHandler;

    public function setUp(): void {
        $this->locatorStub = $this->createMock(\RAP\Locator::class);
        $this->locatorStub->method('getBasePath')->willReturn('http://rap-ia2');

        $this->userDaoStub = $this->createMock(\RAP\UserDAO::class);
        $this->locatorStub->method('getUserDAO')->willReturn($this->userDaoStub);

        $this->oAuth2RequestHandler = $this->createMock(\RAP\OAuth2RequestHandler::class);
        $this->locatorStub->method('getOAuth2RequestHandler')->willReturn($this->oAuth2RequestHandler);

        $this->auditLogger = $this->createMock(\Monolog\Logger::class);
        $this->locatorStub->method('getAuditLogger')->willReturn($this->auditLogger);

        $this->gmsClientStub = $this->createMock(\RAP\GmsClient::class);
        $this->locatorStub->method('getGmsClient')->willReturn($this->gmsClientStub);

        $this->sessionData = new \RAP\SessionData();
        $this->locatorStub->method('getSession')->willReturn($this->sessionData);

        $this->userHandler = new \RAP\UserHandler($this->locatorStub);
        $this->locatorStub->method('getUserHandler')->willReturn($this->userHandler);

        $this->loginHandler = new \RAP\LoginHandler($this->locatorStub);
    }

    public function testNewIdentityLogin(): void {

        $this->sessionData->setAction('account');

        $identity = $this->getFakeIdentity1();

        // verify DAO is called for checking user existence
        $this->userDaoStub->expects($this->once())
                ->method('findUserByIdentity')->with($identity->type, $identity->typedId);

        $redirect1 = $this->loginHandler->onIdentityDataReceived($identity);

        // verify ids are not set
        $this->assertNull($this->sessionData->getUser()->id);
        $this->assertNull($this->sessionData->getUser()->identities[0]->id);

        $this->assertEquals("http://rap-ia2/tou-check", $redirect1);

        $this->userDaoStub->method('createUser')->willReturn('2');
        $this->userDaoStub->method('insertIdentity')->willReturn('3');

        $redirect2 = $this->loginHandler->register();

        $this->assertEquals('2', $this->sessionData->getUser()->id);
        $this->assertEquals('3', $this->sessionData->getUser()->identities[0]->id);

        $this->assertEquals("http://rap-ia2/account", $redirect2);
    }

    public function testExistingUserLogin(): void {

        $this->sessionData->setAction('account');

        $user = $this->getFakeUser1();
        $this->assertEquals('test@example.com', $user->identities[0]->email);

        $this->userDaoStub->method('findUserByIdentity')->willReturn($user);

        $this->userDaoStub->expects($this->once())
                ->method('updateIdentity')->with($this->anything());

        $redirect = $this->loginHandler->onIdentityDataReceived($this->getFakeIdentity1NewEmail());

        // verify email update
        $this->assertEquals('test2@example.com', $user->identities[0]->email);

        $this->assertEquals('http://rap-ia2/account', $redirect);
    }

    public function testNewIdentityAutojoin(): void {

        $this->oAuth2RequestHandler->method('getRedirectResponseUrl')->willReturn('http://redirect-url');
        $this->sessionData->setOAuth2RequestData(new \RAP\OAuth2RequestData());

        $this->userDaoStub->method('findJoinableUsersByEmail')->willReturn(['1', '2']);

        $this->userDaoStub->method('findUserById')->will($this->returnValueMap([
                    ['1', $this->getFakeUser1()],
                    ['2', $this->getFakeUser2()]
        ]));

        // Login: two joinable users detected
        $redirect1 = $this->loginHandler->onIdentityDataReceived($this->getFakeIdentity3());

        $this->assertTrue($this->sessionData->isAutojoin());
        $this->assertEquals('http://rap-ia2/confirm-join', $redirect1);
        $this->assertNotNull($this->sessionData->getUserToJoin());
        $this->assertNull($this->sessionData->getUser()->id);

        $this->userDaoStub->method('findJoinableUsersByUserId')->will($this->onConsecutiveCalls(['2'], []));

        // First confirm join
        $redirect2 = $this->loginHandler->confirmJoin();

        $this->assertEquals('1', $this->sessionData->getUser()->id);
        $this->assertEquals('http://rap-ia2/confirm-join', $redirect2);

        $this->gmsClientStub->method('joinGroups')->willReturn('2');

        $this->gmsClientStub->expects($this->once())
                ->method('joinGroups')->with($this->anything());

        // Second confirm join, then redirect to caller application
        $redirect3 = $this->loginHandler->confirmJoin();

        $this->assertEquals('2', $this->sessionData->getUser()->id);
        $this->assertEquals('http://redirect-url', $redirect3);
    }

    public function testJoinAlreadyJoinedUser(): void { // go to account page without joining
        $user = $this->getFakeUser1();

        $this->sessionData->setAction('join');
        $this->sessionData->setUser($user);

        $this->userDaoStub->method('findUserByIdentity')->willReturn($user);

        $redirect = $this->loginHandler->onIdentityDataReceived($this->getFakeIdentity1());

        $this->assertEquals('http://rap-ia2/account', $redirect);
    }

    public function testRejectJoinExistingUser(): void {

        $this->sessionData->setAction('account');

        $this->userDaoStub->method('findUserByIdentity')->willReturn($this->getFakeUser1());
        $this->userDaoStub->method('findUserById')->willReturn($this->getFakeUser2());
        $this->userDaoStub->method('findJoinableUsersByUserId')->will($this->onConsecutiveCalls(['2'], []));

        $redirect1 = $this->loginHandler->onIdentityDataReceived($this->getFakeIdentity1());

        $this->assertTrue($this->sessionData->isAutojoin());
        $this->assertEquals('http://rap-ia2/confirm-join', $redirect1);

        $this->userDaoStub->expects($this->once())->method('insertRejectedJoin');

        // User rejects join, redirect to account page
        $redirect2 = $this->loginHandler->rejectJoin();

        $this->assertEquals('http://rap-ia2/account', $redirect2);
    }

    public function testRejectJoinNewUser(): void {

        $this->sessionData->setAction('account');

        $this->userDaoStub->method('findJoinableUsersByEmail')->willReturn(['1']);
        $this->userDaoStub->method('findUserById')->willReturn($this->getFakeUser1());

        // Login: one joinable user detected
        $redirect1 = $this->loginHandler->onIdentityDataReceived($this->getFakeIdentity3());

        $this->assertTrue($this->sessionData->isAutojoin());
        $this->assertEquals('http://rap-ia2/confirm-join', $redirect1);

        // User rejects join, redirect to TOU check
        $redirect2 = $this->loginHandler->rejectJoin();

        $this->assertTrue($this->sessionData->isJoinRejected());
        $this->assertEquals('http://rap-ia2/tou-check', $redirect2);

        $this->userDaoStub->method('createUser')->willReturn('5');
        $this->userDaoStub->expects($this->once())->method('insertRejectedJoin');

        // User accepts TOU
        $redirect3 = $this->loginHandler->register();
        $this->assertEquals('http://rap-ia2/account', $redirect3);
    }

    public function testExplicitJoin(): void {

        // First login result
        $this->sessionData->setUser($this->getFakeUser2());

        $this->sessionData->setAction('join');

        $this->userDaoStub->method('findUserById')->willReturn($this->getFakeUser1());

        // Second login
        $redirect1 = $this->loginHandler->onIdentityDataReceived($this->getFakeIdentity1());

        $this->assertFalse($this->sessionData->isAutojoin());
        $this->assertEquals('http://rap-ia2/confirm-join', $redirect1);

        // User confirms the join
        $redirect2 = $this->loginHandler->confirmJoin();
        $this->assertEquals('http://rap-ia2/account', $redirect2);
    }

    public function testExplicitJoinAndAutojoin(): void {
        // First login result
        $this->sessionData->setUser($this->getFakeUser2());

        $this->sessionData->setAction('join');

        $this->userDaoStub->method('findUserById')->willReturn($this->getFakeUser1());
        $this->userDaoStub->method('findJoinableUsersByUserId')->will($this->onConsecutiveCalls(['2'], []));

        // Second login
        $redirect1 = $this->loginHandler->onIdentityDataReceived($this->getFakeIdentity1());

        $this->assertFalse($this->sessionData->isAutojoin());
        $this->assertEquals('http://rap-ia2/confirm-join', $redirect1);

        // User confirms the join, another join is detected
        $redirect2 = $this->loginHandler->confirmJoin();

        $this->assertTrue($this->sessionData->isAutojoin());
        $this->assertEquals('http://rap-ia2/confirm-join', $redirect2);

        // User confirm the second join too
        $redirect3 = $this->loginHandler->confirmJoin();
        $this->assertEquals('http://rap-ia2/account', $redirect3);
    }

    public function testExplicitJoinAndRejectedAutojoin(): void {

        // First login result
        $this->sessionData->setUser($this->getFakeUser2());

        $this->sessionData->setAction('join');

        $this->userDaoStub->method('findUserById')->willReturn($this->getFakeUser1());
        $this->userDaoStub->method('findJoinableUsersByUserId')->will($this->onConsecutiveCalls(['2'], []));

        // Second login
        $redirect1 = $this->loginHandler->onIdentityDataReceived($this->getFakeIdentity1());

        $this->assertFalse($this->sessionData->isAutojoin());
        $this->assertEquals('http://rap-ia2/confirm-join', $redirect1);

        // User confirms the join, another join is detected
        $redirect2 = $this->loginHandler->confirmJoin();

        $this->assertTrue($this->sessionData->isAutojoin());
        $this->assertEquals('http://rap-ia2/confirm-join', $redirect2);

        $this->userDaoStub->expects($this->once())->method('insertRejectedJoin');

        // User reject the second join
        $redirect3 = $this->loginHandler->rejectJoin();
        $this->assertEquals('http://rap-ia2/account', $redirect3);
    }

    private function getFakeUser1(): \RAP\User {

        $user = new \RAP\User();
        $user->id = '1';
        $identity = new \RAP\Identity('eduGAIN');
        $identity->email = 'test@example.com';
        $identity->id = '4';
        $identity->typedId = '123';
        $user->addIdentity($identity);
        return $user;
    }

    private function getFakeUser2(): \RAP\User {

        $user = new \RAP\User();
        $user->id = '2';
        $identity = new \RAP\Identity('eduGAIN');
        $identity->email = 'test@example.com';
        $identity->id = '6';
        $identity->typedId = '999';
        $user->addIdentity($identity);
        return $user;
    }

    private function getFakeIdentity1(): \RAP\Identity {

        $identity = new \RAP\Identity(\RAP\Identity::EDU_GAIN);
        $identity->typedId = '123';
        $identity->email = 'test@example.com';
        return $identity;
    }

    private function getFakeIdentity1NewEmail(): \RAP\Identity {

        $identity = new \RAP\Identity(\RAP\Identity::EDU_GAIN);
        $identity->typedId = '123';
        $identity->email = 'test2@example.com';
        return $identity;
    }

    private function getFakeIdentity2(): \RAP\Identity {

        $identity = new \RAP\Identity(\RAP\Identity::EDU_GAIN);
        $identity->typedId = '456';
        $identity->email = 'test3@example.com';
        return $identity;
    }

    private function getFakeIdentity3(): \RAP\Identity {

        $identity = new \RAP\Identity(\RAP\Identity::GOOGLE);
        $identity->typedId = '789';
        $identity->email = 'test@example.com';
        return $identity;
    }

}