<?php

namespace LifeStyle\SamlAuthBundle\Tests\Security\Firewall;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Bundle\FrameworkBundle\Client;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Psr\Log\LoggerInterface;
use JMS\Serializer\Serializer;
use LifeStyle\SamlAuthBundle\Services\SamlService;
use LifeStyle\SamlAuthBundle\Configuration\SamlConfig;
use LifeStyle\SamlAuthBundle\Security\Authentication\Token\SamlToken;
use LifeStyle\SamlAuthBundle\Model\UserApplication\Application as UserApplication;
use LifeStyle\SamlAuthBundle\Security\Firewall\SamlListener;

/**
 * Testing of SamlListener
 *
 * Class SamlListenerTest
 * @package LifeStyle\SamlAuthBundle\Security\Firewall
 */
class SamlListenerTest extends WebTestCase
{
    const TEST_APP_NAME = 'TestApplication';
    const TEST_SP_NAME = 'testsp';

    /**
     * @var Client
     */
    private $client;

    /**
     * @var ContainerInterface
     */
    private $container;

    /**
     * @var Serializer
     */
    private $serializer;

    /**
     * @var SamlService
     */
    private $samlServiceMock;

    /**
     * @var SamlConfig
     */
    private $config;

    /**
     * @var LoggerInterface
     */
    private $logMock;


    /**
     * Test SamlListener::handle() with valid input data (simulated SAML response)
     */
    public function testHandleWithValidData()
    {
        $this->setUpBaseMocks(true, $this->getSamlAttributesValid());
        $samlServiceMock = $this->samlServiceMock;

        $tokenStoreMock = $this->createMock(TokenStorageInterface::class);

        $authManagerMock = $this->createMock(AuthenticationManagerInterface::class);
        $authManagerMock->expects($this->once())
            ->method('authenticate')
            ->with($this->callback(
                '\LifeStyle\SamlAuthBundle\Tests\Security\Firewall\SamlListenerTest::checkTokenAfterValidData'
            ));

        $eventMock = $this->createMock(GetResponseEvent::class);

        $listener = new SamlListener(
            $tokenStoreMock,
            $authManagerMock,
            $samlServiceMock,
            $this->serializer,
            $this->config
        );
        $listener->handle($eventMock);
    }

    /**
     * Callback function referenced by the actual test for the authenticate() call for checking contents of
     * the token object to match expectations of the test
     *
     * @param $token
     * @return bool
     */
    public static function checkTokenAfterValidData($token)
    {
        return ('testuser1' == $token->userName)
        && ('test1@test.tst' == $token->email)
        && ('Test1firstname' == $token->firstname)
        && ('TestRole1' == $token->appData->getRoles()[0]->getName());
    }


    /**
     * Test SamlListener::handle() with  incomplete input data, some fields missing (simulated SAML response)
     */
    public function testHandleWithIncompleteData()
    {
        $this->setUpBaseMocks(true, $this->getSamlAttributesIncomplete());
        $samlServiceMock = $this->samlServiceMock;

        $tokenStoreMock = $this->createMock(TokenStorageInterface::class);

        $authManagerMock = $this->createMock(AuthenticationManagerInterface::class);
        $authManagerMock->expects($this->once())
            ->method('authenticate')
            ->with($this->callback(
                '\LifeStyle\SamlAuthBundle\Tests\Security\Firewall\SamlListenerTest::checkTokenAfterIncompleteData'
            ));

        $eventMock = $this->createMock(GetResponseEvent::class);

        $listener = new SamlListener(
            $tokenStoreMock,
            $authManagerMock,
            $samlServiceMock,
            $this->serializer,
            $this->config
        );
        $listener->handle($eventMock);
    }

    /**
     * Callback function referenced by the actual test for the authenticate() call for checking contents of
     * the token object to match expectations of the test
     *
     * @param $token
     * @return bool
     */
    public static function checkTokenAfterIncompleteData($token)
    {
        return ('testuser3' == $token->userName)
        && ('' == $token->email)
        && ('' == $token->firstname)
        && ('TestRole1' == $token->appData->getRoles()[0]->getName());
    }


    /**
     * Test SamlListener::handle() with completely invalid/missing input data (simulated SAML response)
     */
    public function testHandleWithInvalidData()
    {
        $this->setUpBaseMocks(true, $this->getSamlAttributesInvalid());
        $samlServiceMock = $this->samlServiceMock;

        $tokenStoreMock = $this->createMock(TokenStorageInterface::class);

        $authManagerMock = $this->createMock(AuthenticationManagerInterface::class);
        $authManagerMock->expects($this->once())
            ->method('authenticate')
            ->with($this->callback(
                '\LifeStyle\SamlAuthBundle\Tests\Security\Firewall\SamlListenerTest::checkTokenAfterInvalidData'
            ));

        $eventMock = $this->createMock(GetResponseEvent::class);

        $listener = new SamlListener(
            $tokenStoreMock,
            $authManagerMock,
            $samlServiceMock,
            $this->serializer,
            $this->config
        );
        $listener->handle($eventMock);
    }

    /**
     * Callback function referenced by the actual test for the authenticate() call for checking contents of
     * the token object to match expectations of the test
     *
     * @param $token
     * @return bool
     */
    public static function checkTokenAfterInvalidData($token)
    {
        return ('' == $token->userName)
        && ('' == $token->email)
        && ('' == $token->firstname)
        && (null == $token->appData);
    }


    /**
     * Test SamlListener::handle() with internal authentication failing
     */
    public function testHandleWithAuthFail()
    {
        $this->setUpBaseMocks(true, $this->getSamlAttributesInvalid());
        $samlServiceMock = $this->samlServiceMock;

        $tokenStoreMock = $this->getMockBuilder(TokenStorageInterface::class)
            ->setMethods(['setToken', 'getToken'])
            ->getMock();
        $tokenStoreMock->expects($this->once())
            ->method('getToken')
            ->will($this->returnValue(new SamlToken()));

        $authManagerMock = $this->getMockBuilder(AuthenticationManagerInterface::class)
            ->setMethods(['authenticate'])
            ->getMock();
        $authManagerMock->expects($this->once())
            ->method('authenticate')
            ->will($this->throwException(new AuthenticationException()));

        $eventMock = $this->getMockBuilder(GetResponseEvent::class)
            ->setMethods(['setResponse'])
            ->disableOriginalConstructor()
            ->getMock();
        $eventMock->expects($this->once())
            ->method('setResponse');

        $listener = new SamlListener(
            $tokenStoreMock,
            $authManagerMock,
            $samlServiceMock,
            $this->serializer,
            $this->config
        );
        $listener->handle($eventMock);
    }


    /**
     * Test SamlListener::handle() with user not being logged in to the SSO IdP and requiring redirect to
     * the login page
     */
    public function testHandleSamlLoginCall()
    {
        $this->setUpBaseMocks(false, $this->getSamlAttributesInvalid());
        $samlServiceMock = $this->samlServiceMock;
        $samlServiceMock->expects($this->once())->method('login');

        $tokenStoreMock = $this->createMock(TokenStorageInterface::class);

        $authManagerMock = $this->createMock(AuthenticationManagerInterface::class);

        $eventMock = $this->createMock(GetResponseEvent::class);

        $listener = new SamlListener(
            $tokenStoreMock,
            $authManagerMock,
            $samlServiceMock,
            $this->serializer,
            $this->config
        );
        $listener->handle($eventMock);
    }


    /**
     * Test SamlListener::handle() with broken JSON input data (simulated SAML response).
     * This test makes sure the code does not crash uncontrolled and runns stable even with bad or corrupted
     * JSON data.
     */
    public function testHandleWithBrokenJson()
    {
        $this->setUpBaseMocks(true, $this->getSamlAttributesBrokenJson());
        $samlServiceMock = $this->samlServiceMock;

        $tokenStoreMock = $this->createMock(TokenStorageInterface::class);

        $authManagerMock = $this->createMock(AuthenticationManagerInterface::class);

        $eventMock = $this->createMock(GetResponseEvent::class);

        // Assert: authentication will success - a token has to be set
        $tokenStoreMock->expects($this->once())->method('setToken');

        $listener = new SamlListener(
            $tokenStoreMock,
            $authManagerMock,
            $samlServiceMock,
            $this->serializer,
            $this->config
        );
        $listener->handle($eventMock);
    }


    /**
     * initialize a few things that are used by several tests
     */
    public function setUp()
    {
        $this->client = static::createClient();
        $this->container = $this->client->getContainer();
        $this->serializer = $this->container->get('jms_serializer');
    }

    /**
     * Set up the base mock objects needed by almost all tests.
     * (This is mainly here in this method to reduce duplicated code and some other Sonar warnings.)
     *
     * @param boolean $authResponse
     * @param array $samlResponse
     */
    private function setUpBaseMocks($authResponse, $samlResponse)
    {
        $this->config = new SamlConfig();
        $this->config->setIdpAppName(static::TEST_APP_NAME)->setSpName(static::TEST_SP_NAME);

        $this->logMock = $this->createMock(LoggerInterface::class);

        $this->samlServiceMock = $this->getMockBuilder(SamlService::class)
            ->setMethods(['getSimpleSaml', 'isAuthenticated', 'getAttributes', 'login'])
            ->setConstructorArgs([$this->config])
            ->getMock();
        $this->samlServiceMock->method('getSimpleSaml')->will($this->returnSelf());
        $this->samlServiceMock->method('isAuthenticated')->will($this->returnValue($authResponse));
        $this->samlServiceMock->method('getAttributes')->will($this->returnValue($samlResponse));
    }

    /**
     * Generate simulated, valid SAML response data
     *
     * @return array
     */
    private function getSamlAttributesValid()
    {
        return [
            'guid' => ['123456789ABCDEF0123456789ABCDEF0'],
            'username' => ['testuser1'],
            'email' => ['test1@test.tst'],
            'firstname' => ['Test1firstname'],
            'lastname' => ['Test1lastname'],
            'application::'.static::TEST_APP_NAME
            => [
                '{"name":"TestApplication","attributes":[{"name":"TestAttribute1","value":"TestAttribute1Value"}],'.
                '"roles":[{"name":"TestRole1"}]}',
            ],
        ];
    }

    /**
     * Generate simulated SAML response data with broken/invalid user application JSON
     *
     * @return array
     */
    private function getSamlAttributesBrokenJson()
    {
        return [
            'guid' => ['123456789ABCDEF0123456789ABCDEF1'],
            'username' => ['testuser2'],
            'email' => ['test2@test.tst'],
            'firstname' => ['Test2firstname'],
            'lastname' => ['Test2lastname'],
            'application::'.static::TEST_APP_NAME
            => [
                '{"name":"TestApplication","attributes": {"name":"TestAttribute1","value":"TestAttribute1Value"}],'.
                '"roles":[{"name":"TestRole1"}]}',
            ],
        ];
    }

    /**
     * Generate simulated, incomplete SAML response data
     *
     * @return array
     */
    private function getSamlAttributesIncomplete()
    {
        return [
            'guid' => ['123456789ABCDEF0123456789ABCDEF2'],
            'username' => ['testuser3'],
            'application::'.static::TEST_APP_NAME
            => [
                '{"name":"TestApplication","attributes":[{"name":"TestAttribute1","value":"TestAttribute1Value"}],'.
                '"roles":[{"name":"TestRole1"}]}',
            ],
        ];
    }

    /**
     * Generate simulated, completely invalid SAML response data
     *
     * @return array
     */
    private function getSamlAttributesInvalid()
    {
        return [];
    }
}
