<?php

namespace SilverStripe\Security\Tests;

use SilverStripe\Control\Director;
use SilverStripe\Control\Tests\HttpRequestMockBuilder;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Core\Validation\ValidationException;
use SilverStripe\Security\PasswordExpirationMiddleware;
use SilverStripe\Security\Member;
use SilverStripe\Security\Security;

class PasswordExpirationMiddlewareTest extends SapphireTest
{
    use HttpRequestMockBuilder;

    protected function setUp(): void
    {
        parent::setUp();

        Director::config()->set('alternate_base_url', 'http://localhost/custom-base/');
        PasswordExpirationMiddleware::config()->set('default_redirect', null);
        PasswordExpirationMiddleware::config()->set('whitelisted_url_startswith', []);
    }

    /**
     * Returns Member mock object
     *
     * @param bool $isPasswordExpired result of the function {@see SilverStripe\Security\Member::isPasswordExpired}
     *
     * @return Member
     */
    private function getMemberMock($isPasswordExpired) : Member
    {
        $mock = $this->createMock(Member::class);
        $mock->method('isPasswordExpired')->willReturn($isPasswordExpired);

        return $mock;
    }

    public function test200()
    {
        $member = $this->getMemberMock(false);

        $a = new PasswordExpirationMiddleware();

        $request = $this->buildRequestMock('/');
        $executed = false;
        Security::setCurrentUser($member);
        $response = $a->process($request, static function () use (&$executed) {
            $executed = true;
            return "delegated";
        });

        $this->assertEquals($member, Security::getCurrentUser());
        $this->assertTrue($executed);
        $this->assertEquals("delegated", $response);
    }

    /**
     * Check a member with an expired password is allowed to process the request in
     * deauthorised mode (Security::getCurrentUser() === null) if there are no
     * change password redirects registered
     */
    public function testDeauthorised()
    {
        $member = $this->getMemberMock(true);
        $this->assertTrue($member->isPasswordExpired());

        $a = new PasswordExpirationMiddleware();

        $request = $this->buildRequestMock('/');
        $executed = false;
        $activeMember = null;
        Security::setCurrentUser($member);
        $response = $a->process($request, static function () use (&$executed, &$activeMember) {
            $executed = true;
            $activeMember = Security::getCurrentUser();
        });

        $this->assertTrue($executed);
        $this->assertNull($activeMember);
    }

    /**
     * Check a member with an expired password is redirected to a change password form
     * instead of processing its original request
     */
    public function testRedirected()
    {
        $member = $this->getMemberMock(true);
        $this->assertTrue($member->isPasswordExpired());

        $a = new PasswordExpirationMiddleware();

        $request = $this->buildRequestMock('/');
        $request->method('getAcceptMimetypes')->willReturn(['*/*']);
        $session = $request->getSession();

        $a->setRedirect($session, '/redirect-address-custom');

        $executed = false;
        $activeMember = null;
        Security::setCurrentUser($member);
        $response = $a->process($request, static function () use (&$executed, &$activeMember) {
            $executed = true;
            $activeMember = Security::getCurrentUser();
        });

        $this->assertFalse($executed);
        $this->assertNull($activeMember);

        $this->assertEquals(302, $response->getStatusCode());
        $this->assertEquals(Director::absoluteURL('redirect-address-custom'), $response->getHeader('Location'));
    }

    /**
     * Check we handle network locations correctly (the relative urls starting with //)
     */
    public function testNetworkLocationRedirect()
    {
        $member = $this->getMemberMock(true);
        $this->assertTrue($member->isPasswordExpired());

        $a = new PasswordExpirationMiddleware();

        $request = $this->buildRequestMock('/');
        $request->method('getAcceptMimetypes')->willReturn(['*/*']);
        $session = $request->getSession();

        $a->setRedirect($session, '//localhost/custom-base/redirect-address-custom');

        $executed = false;
        $activeMember = null;
        Security::setCurrentUser($member);
        $response = $a->process($request, static function () use (&$executed, &$activeMember) {
            $executed = true;
            $activeMember = Security::getCurrentUser();
        });

        $this->assertFalse($executed);
        $this->assertNull($activeMember);

        $this->assertEquals(302, $response->getStatusCode());
        $this->assertEquals(Director::absoluteURL('redirect-address-custom'), $response->getHeader('Location'));
    }

    /**
     * Check we can allow the current request handling even with an expired password
     */
    public function testAllowRequest()
    {
        $member = $this->getMemberMock(true);
        $this->assertTrue($member->isPasswordExpired());

        $a = new PasswordExpirationMiddleware();

        $request = $this->buildRequestMock('/');
        $session = $request->getSession();
        PasswordExpirationMiddleware::allowCurrentRequest($session);

        $executed = false;
        $activeMember = null;
        Security::setCurrentUser($member);
        $response = $a->process($request, static function () use (&$executed, &$activeMember) {
            $executed = true;
            $activeMember = Security::getCurrentUser();
        });

        $this->assertTrue($executed);
        $this->assertEquals($member, $activeMember);
    }

    /**
     * Check a member with an expired password is redirected to a default change password form
     * if a custom not set
     */
    public function testDefaultRedirect()
    {
        $member = $this->getMemberMock(true);
        $this->assertTrue($member->isPasswordExpired());

        PasswordExpirationMiddleware::config()->set('default_redirect', 'redirect-address-default');

        $a = new PasswordExpirationMiddleware();

        $request = $this->buildRequestMock('/');
        $request->method('getAcceptMimetypes')->willReturn(['*/*']);
        $session = $request->getSession();

        $executed = false;
        $activeMember = null;
        Security::setCurrentUser($member);
        $response = $a->process($request, static function () use (&$executed, &$activeMember) {
            $executed = true;
            $activeMember = Security::getCurrentUser();
        });

        $this->assertFalse($executed);
        $this->assertNull($activeMember);

        $this->assertEquals(302, $response->getStatusCode());
        $this->assertEquals(Director::absoluteURL('redirect-address-default'), $response->getHeader('Location'));
    }

    /**
     * Check a member with an expired password is redirected to a default change password form
     * if a custom not set
     */
    public function testCustomRedirect()
    {
        $member = $this->getMemberMock(true);
        $this->assertTrue($member->isPasswordExpired());

        PasswordExpirationMiddleware::config()->set('default_redirect', '/redirect-address-default');

        $a = new PasswordExpirationMiddleware();

        $request = $this->buildRequestMock('/');
        $request->method('getAcceptMimetypes')->willReturn(['*/*']);

        $executed = false;
        $activeMember = null;
        Security::setCurrentUser($member);
        $response = $a->process($request, static function () use (&$executed, &$activeMember) {
            $executed = true;
            $activeMember = Security::getCurrentUser();
        });

        $this->assertFalse($executed);
        $this->assertNull($activeMember);

        $this->assertEquals(302, $response->getStatusCode());
        $this->assertEquals(Director::absoluteURL('redirect-address-default'), $response->getHeader('Location'));
    }

    /**
     * Check a custom redirect URL overrides the default one
     */
    public function testCustomOverDefaultRedirect()
    {
        $member = $this->getMemberMock(true);
        $this->assertTrue($member->isPasswordExpired());

        PasswordExpirationMiddleware::config()->set('default_redirect', '/redirect-address-default');

        $a = new PasswordExpirationMiddleware();

        $request = $this->buildRequestMock('/');
        $request->method('getAcceptMimetypes')->willReturn(['*/*']);
        $session = $request->getSession();
        $a->setRedirect($session, '/redirect-address-custom');

        $executed = false;
        $activeMember = null;
        Security::setCurrentUser($member);
        $response = $a->process($request, static function () use (&$executed, &$activeMember) {
            $executed = true;
            $activeMember = Security::getCurrentUser();
        });

        $this->assertFalse($executed);
        $this->assertNull($activeMember);

        $this->assertEquals(302, $response->getStatusCode());
        $this->assertEquals(Director::absoluteURL('redirect-address-custom'), $response->getHeader('Location'));
    }

    /**
     * Test we can allow URLs to be visited without redirections through config
     */
    public function testAllowedUrlStartswithNegative()
    {
        $member = $this->getMemberMock(true);
        $this->assertTrue($member->isPasswordExpired());

        PasswordExpirationMiddleware::config()->set('whitelisted_url_startswith', [
            '/allowed-address-configured/'
        ]);

        $a = new PasswordExpirationMiddleware();

        $request = $this->buildRequestMock('/not-allowed');
        $request->method('getAcceptMimetypes')->willReturn(['*/*']);
        $session = $request->getSession();

        $a->setRedirect($session, '/redirect-address-custom');

        $executed = false;
        $activeMember = null;
        Security::setCurrentUser($member);
        $response = $a->process($request, static function () use (&$executed, &$activeMember) {
            $executed = true;
            $activeMember = Security::getCurrentUser();
        });

        $this->assertFalse($executed);
        $this->assertNull($activeMember);

        $this->assertEquals(302, $response->getStatusCode());
        $this->assertEquals(Director::absoluteURL('redirect-address-custom'), $response->getHeader('Location'));
    }

    /**
     * Test we can allow URLs to be visited without redirections through config
     */
    public function testAllowedUrlStartswithPositivePattern()
    {
        $member = $this->getMemberMock(true);
        $this->assertTrue($member->isPasswordExpired());

        PasswordExpirationMiddleware::config()->set('whitelisted_url_startswith', [
            '/allowed-address-configured/'
        ]);

        $a = new PasswordExpirationMiddleware();

        $request = $this->buildRequestMock('/allowed-address-configured/subsection1/subsection2/');
        $request->method('getAcceptMimetypes')->willReturn(['*/*']);
        $session = $request->getSession();

        $a->setRedirect($session, '/redirect-address-custom');

        $executed = false;
        $activeMember = null;
        Security::setCurrentUser($member);
        $response = $a->process($request, static function () use (&$executed, &$activeMember) {
            $executed = true;
            $activeMember = Security::getCurrentUser();
        });

        $this->assertTrue($executed);
        $this->assertEquals($member, $activeMember);
    }

    public function testAllowedUrlStartswithPositiveTrailingSlash()
    {
        $member = $this->getMemberMock(true);
        $this->assertTrue($member->isPasswordExpired());

        PasswordExpirationMiddleware::config()->set('whitelisted_url_startswith', [
            '/allowed-address-configured/'
        ]);

        $a = new PasswordExpirationMiddleware();

        $request = $this->buildRequestMock('/allowed-address-configured?foo=bar');
        $request->method('getAcceptMimetypes')->willReturn(['*/*']);
        $session = $request->getSession();

        $a->setRedirect($session, '/redirect-address-custom');

        $executed = false;
        $activeMember = null;
        Security::setCurrentUser($member);
        $response = $a->process($request, static function () use (&$executed, &$activeMember) {
            $executed = true;
            $activeMember = Security::getCurrentUser();
        });

        $this->assertTrue($executed);
        $this->assertEquals($member, $activeMember);
    }

    public function testAllowedUrlStartswithPositiveRelativeUrl()
    {
        $member = $this->getMemberMock(true);
        $this->assertTrue($member->isPasswordExpired());

        PasswordExpirationMiddleware::config()->set('whitelisted_url_startswith', [
            'allowed-address-configured/'
        ]);

        $a = new PasswordExpirationMiddleware();

        $request = $this->buildRequestMock('/allowed-address-configured?foo=bar');
        $request->method('getAcceptMimetypes')->willReturn(['*/*']);
        $session = $request->getSession();

        $a->setRedirect($session, 'redirect-address-custom');

        $executed = false;
        $activeMember = null;
        Security::setCurrentUser($member);
        $response = $a->process($request, static function () use (&$executed, &$activeMember) {
            $executed = true;
            $activeMember = Security::getCurrentUser();
        });

        $this->assertTrue($executed);
        $this->assertEquals($member, $activeMember);
    }

    /**
     * Test we can allow URLs to be visited without redirections through config
     */
    public function testAllowedUrlStartswithPositiveExactUrl()
    {
        $member = $this->getMemberMock(true);
        $this->assertTrue($member->isPasswordExpired());

        PasswordExpirationMiddleware::config()->set('whitelisted_url_startswith', [
            '/allowed-address-configured/'
        ]);

        $a = new PasswordExpirationMiddleware();

        $request = $this->buildRequestMock('/allowed-address-configured/');
        $request->method('getAcceptMimetypes')->willReturn(['*/*']);
        $session = $request->getSession();

        $a->setRedirect($session, '/redirect-address-custom');

        $executed = false;
        $activeMember = null;
        Security::setCurrentUser($member);
        $response = $a->process($request, static function () use (&$executed, &$activeMember) {
            $executed = true;
            $activeMember = Security::getCurrentUser();
        });

        $this->assertTrue($executed);
        $this->assertEquals($member, $activeMember);
    }
}