<?php
/**
 * Nextcloud - Gallery
 *
 *
 * This file is licensed under the Affero General Public License version 3 or
 * later. See the COPYING file.
 *
 * @author Olivier Paroz <galleryapps@oparoz.com>
 *
 * @copyright Olivier Paroz 2017
 */

namespace OCA\Gallery\Tests\Middleware;

use OC\AppFramework\Utility\ControllerMethodReflector;

use OCP\Constants;
use OCP\IRequest;
use OCP\Security\IHasher;
use OCP\ISession;
use OCP\ILogger;
use OCP\IURLGenerator;
use OCP\Share;
use OCP\Share\IManager;

use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Utility\IControllerMethodReflector;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;

use OCA\Gallery\Environment\EnvironmentException;
use OCA\Gallery\Middleware\SharingCheckMiddleware;
use OCA\Gallery\Middleware\EnvCheckMiddleware;
use OCA\Gallery\Middleware\CheckException;

/**
 * Class EnvCheckMiddlewareTest
 *
 * @package OCA\Gallery\Tests\Middleware
 */
class EnvCheckMiddlewareTest extends \OCA\Gallery\Tests\GalleryUnitTest {

	/** @var IRequest */
	private $request;
	/** @var IHasher */
	private $hasher;
	/** @var ISession */
	private $session;
	/** @var IControllerMethodReflector */
	protected $reflector;
	/** @var IURLGenerator */
	private $urlGenerator;
	/** @var ILogger */
	protected $logger;
	/** @var Controller */
	private $controller;
	/** @var SharingCheckMiddleware */
	private $middleware;
	/** @var IManager */
	private $shareManager;

	/**
	 * Test set up
	 */
	protected function setUp() {
		parent::setUp();

		$this->request = $this->getMockBuilder('\OCP\IRequest')
							  ->disableOriginalConstructor()
							  ->getMock();
		$this->hasher = $this->getMockBuilder('\OCP\Security\IHasher')
							 ->disableOriginalConstructor()
							 ->getMock();
		$this->session = $this->getMockBuilder('\OCP\ISession')
							  ->disableOriginalConstructor()
							  ->getMock();
		$this->environment = $this->getMockBuilder('\OCA\Gallery\Environment\Environment')
								  ->disableOriginalConstructor()
								  ->getMock();
		// We need to use a real reflector to be able to test our custom notation
		$this->reflector = new ControllerMethodReflector();
		$this->urlGenerator = $this->getMockBuilder('\OCP\IURLGenerator')
								   ->disableOriginalConstructor()
								   ->getMock();
		$this->logger = $this->getMockBuilder('\OCP\ILogger')
							 ->disableOriginalConstructor()
							 ->getMock();
		$this->controller = $this->getMockBuilder('OCP\AppFramework\Controller')
								 ->disableOriginalConstructor()
								 ->getMock();
		$this->shareManager = $this->getMockBuilder('\OCP\Share\IManager')
								   ->disableOriginalConstructor()
								   ->getMock();

		$this->middleware = new EnvCheckMiddleware(
			$this->appName,
			$this->request,
			$this->hasher,
			$this->session,
			$this->environment,
			$this->reflector,
			$this->urlGenerator,
			$this->shareManager,
			$this->logger
		);
	}

	/**
	 * @todo Mock an environment response
	 */
	public function testBeforeControllerWithoutNotation() {
		$this->reflector->reflect(__CLASS__, __FUNCTION__);
		$this->middleware->beforeController(__CLASS__, __FUNCTION__);
	}

	/**
	 * @PublicPage
	 *
	 * @expectedException \OCA\Gallery\Middleware\CheckException
	 */
	public function testBeforeControllerWithPublicNotationAndInvalidToken() {
		$this->reflector->reflect(__CLASS__, __FUNCTION__);

		$token = 'aaaabbbbccccdddd';
		$this->mockGetTokenParam($token);

		$this->mockShareManagerGetShareByTokenThrowsException($token);

		$this->middleware->beforeController(__CLASS__, __FUNCTION__);
	}

	/**
	 * @PublicPage
	 */
	public function testBeforeControllerWithPublicNotationAndToken() {
		$this->reflector->reflect(__CLASS__, __FUNCTION__);

		$token = 'aaaabbbbccccdddd';
		$this->mockGetTokenAndPasswordParams($token, null);

		$share = $this->mockShare(
			'folder', 'tester', 'shared1', Share::SHARE_TYPE_LINK, null,
			Constants::PERMISSION_READ, null
		);

		$this->mockShareManagerGetShareByToken($token, $share);

		$this->middleware->beforeController(__CLASS__, __FUNCTION__);
	}

	/**
	 * @PublicPage
	 */
	public function testBeforeControllerWithPublicNotationAndTokenAndValidPassword() {
		$this->reflector->reflect(__CLASS__, __FUNCTION__);

		$token = 'aaaabbbbccccdddd';
		$password = 'validpassword';
		$this->mockGetTokenAndPasswordParams($token, $password);

		$share = $this->mockShare(
			'folder', 'tester', 'shared1', Share::SHARE_TYPE_LINK, null,
			Constants::PERMISSION_READ, 'validpassword'
		);

		$this->mockShareManagerGetShareByToken($token, $share);
		$this->mockShareManagerCheckPassword($share, $password, true);

		$this->middleware->beforeController(__CLASS__, __FUNCTION__);
	}

	/**
	 * @PublicPage
	 *
	 * @expectedException \OCA\Gallery\Middleware\CheckException
	 */
	public function testBeforeControllerWithPublicNotationAndTokenAndInvalidPassword() {
		$this->reflector->reflect(__CLASS__, __FUNCTION__);

		$token = 'aaaabbbbccccdddd';
		$password = 'NOTvalidpassword';
		$this->mockGetTokenAndPasswordParams($token, $password);

		$share = $this->mockShare(
			'folder', 'tester', 'shared1', Share::SHARE_TYPE_LINK, null,
			Constants::PERMISSION_READ, 'validpassword'
		);

		$this->mockShareManagerGetShareByToken($token, $share);
		$this->mockShareManagerCheckPassword($share, 'NOTvalidpassword', false);

		$this->middleware->beforeController(__CLASS__, __FUNCTION__);
	}

	/**
	 * @PublicPage
	 *
	 * @expectedException \OCA\Gallery\Middleware\CheckException
	 */
	public function testBeforeControllerWithPublicNotationAndNoToken() {
		$this->reflector->reflect(__CLASS__, __FUNCTION__);

		$token = null;
		$this->mockGetTokenParam($token);
		$this->middleware->beforeController(__CLASS__, __FUNCTION__);
	}

	/**
	 * @@Guest
	 */
	public function testBeforeControllerWithGuestNotation() {
		$this->reflector->reflect(__CLASS__, __FUNCTION__);

		$this->middleware->beforeController(__CLASS__, __FUNCTION__);
	}

	public function testCheckSessionAfterPasswordEntry() {
		$share = $this->mockShare('file', 'tester', 'image.png');

		$this->mockSessionExists((string)$share->getId());
		$this->mockSessionWithShareId((string)$share->getId());

		parent::invokePrivate($this->middleware, 'checkSession', [$share]);
	}

	/**
	 * @expectedException \OCA\Gallery\Middleware\CheckException
	 */
	public function testCheckSessionBeforePasswordEntry() {
		$share = $this->mockShare('file', 'tester', 'image.png');

		$this->mockSessionExists(false);

		parent::invokePrivate($this->middleware, 'checkSession', [$share]);
	}

	/**
	 * Ids of shares do not match
	 *
	 * @expectedException \OCA\Gallery\Middleware\CheckException
	 */
	public function testCheckSessionWithWrongSession() {
		$share = $this->mockShare('file', 'tester', 'image.png');

		$this->mockSessionExists(true);
		$this->mockSessionWithShareId(99999);

		parent::invokePrivate($this->middleware, 'checkSession', [$share]);
	}

	public function testCheckPasswordAfterValidPasswordEntry() {
		$password = 'Je suis une pipe';
		$share = $this->mockShare(
			'file', 'tester', 'image.png', Share::SHARE_TYPE_USER, 'externaluser',
			Constants::PERMISSION_READ, $password
		);

		$this->mockShareManagerCheckPassword($share, $password, true);

		parent::invokePrivate($this->middleware, 'checkPassword', [$share, $password]);
	}

	/**
	 * Given password and token password don't match
	 *
	 * @expectedException \OCA\Gallery\Middleware\CheckException
	 */
	public function testCheckPasswordAfterInvalidPasswordEntry() {
		$password = 'Je suis une pipe';
		$wrongPassword = 'Empyrion Galactic Survival';
		$share = $this->mockShare(
			'file', 'tester', 'image.png', Share::SHARE_TYPE_USER, 'externaluser',
			Constants::PERMISSION_READ,
			$wrongPassword
		);

		$this->mockShareManagerCheckPassword($share, $password, false);

		parent::invokePrivate($this->middleware, 'checkPassword', [$share, $password]);
	}

	public function testAuthenticateAfterValidPasswordEntry() {
		$password = 'Je suis une pipe';
		$share = $this->mockShare(
			'file', 'tester', 'image.png', Share::SHARE_TYPE_LINK, null, Constants::PERMISSION_READ,
			$password
		);

		$this->mockShareManagerCheckPassword($share, $password, true);

		$this->assertTrue(
			parent::invokePrivate($this->middleware, 'authenticate', [$share, $password])
		);
	}

	/**
	 * Given password and token password don't match
	 *
	 * @expectedException \OCA\Gallery\Middleware\CheckException
	 */
	public function testAuthenticateAfterInvalidPasswordEntry() {
		$password = 'Je suis une pipe';
		$wrongPassword = 'Empyrion Galactic Survival';
		$share = $this->mockShare(
			'file', 'tester', 'image.png', Share::SHARE_TYPE_LINK, null, Constants::PERMISSION_READ,
			$wrongPassword
		);

		$this->mockShareManagerCheckPassword($share, $password, false);

		parent::invokePrivate($this->middleware, 'authenticate', [$share, $password]);
	}

	/**
	 * @expectedException \OCA\Gallery\Middleware\CheckException
	 */
	public function testAuthenticateWithWrongLinkType() {
		$password = 'Je suis une pipe';
		$share = $this->mockShare(
			'file', 'tester', 'image.png', Share::SHARE_TYPE_LINK, 'tester',
			Constants::PERMISSION_READ, $password
		);

		$this->mockShareManagerCheckPassword($share, $password, false);

		parent::invokePrivate($this->middleware, 'authenticate', [$share, $password]);
	}

	public function testCheckAuthorisationAfterValidPasswordEntry() {
		$password = 'Je suis une pipe';
		$share = $this->mockShare(
			'file', 'tester', 'image.png', Share::SHARE_TYPE_LINK, 'tester',
			Constants::PERMISSION_READ, $password
		);

		$this->mockShareManagerCheckPassword($share, $password, true);

		parent::invokePrivate($this->middleware, 'checkAuthorisation', [$share, $password]);
	}

	/**
	 * Given password and token password don't match
	 *
	 * @expectedException \OCA\Gallery\Middleware\CheckException
	 */
	public function testCheckAuthorisationAfterInvalidPasswordEntry() {
		$password = 'Je suis une pipe';
		$wrongPassword = 'Empyrion Galactic Survival';
		$share = $this->mockShare(
			'file', 'tester', 'image.png', Share::SHARE_TYPE_LINK, 'tester',
			Constants::PERMISSION_READ, $wrongPassword
		);

		$this->mockShareManagerCheckPassword($share, $password, false);

		parent::invokePrivate($this->middleware, 'checkAuthorisation', [$share, $password]);
	}

	/**
	 * It will use the session, wich is a valid one in this case
	 * Other cases are in the checkSession tests
	 */
	public function testCheckAuthorisationWithNoPassword() {
		$password = null;
		$wrongPassword = 'Empyrion Galactic Survival';
		$share = $this->mockShare(
			'file', 'tester', 'image.png', Share::SHARE_TYPE_LINK, 'tester',
			Constants::PERMISSION_READ, $wrongPassword
		);

		$this->mockSessionExists((string)$share->getId());
		$this->mockSessionWithShareId((string)$share->getId());
		parent::invokePrivate($this->middleware, 'checkAuthorisation', [$share, $password]);
	}

	public function testCheckItemTypeWithItemTypeSet() {
		$share = $this->mockShare('folder', 'tester', 'folder1');

		parent::invokePrivate($this->middleware, 'checkItemType', [$share]);
	}

	/**
	 * @expectedException \OCA\Gallery\Middleware\CheckException
	 */
	public function testCheckItemTypeWithItemTypeNotSet() {
		$share = $this->mockShare(null, 'tester', 'folder1');

		parent::invokePrivate($this->middleware, 'checkItemType', [$share]);
	}

	public function testCheckShareIsValidWithValidShare() {
		$share = $this->mockShare('file', 'tester', 'image.png');

		$token = 'aaaabbbbccccdddd';

		parent::invokePrivate($this->middleware, 'checkShareIsValid', [$share, $token]);
	}

	/**
	 * @expectedException \OCA\Gallery\Middleware\CheckException
	 */
	public function testCheckShareIsValidWithMissingOwner() {
		$share = $this->mockShare('file', null, 'image.png');

		$token = 'aaaabbbbccccdddd';

		parent::invokePrivate($this->middleware, 'checkShareIsValid', [$share, $token]);
	}

	/**
	 * @expectedException \OCA\Gallery\Middleware\CheckException
	 */
	public function testCheckShareIsValidWithMissingSource() {
		$share = $this->mockShare('file', 'tester', null);

		$token = 'aaaabbbbccccdddd';

		parent::invokePrivate($this->middleware, 'checkShareIsValid', [$share, $token]);
	}

	public function testAfterExceptionWithCheckExceptionAndHtmlAcceptAnd401Code() {
		$message = 'fail';
		$code = Http::STATUS_UNAUTHORIZED;
		$exception = new CheckException($message, $code);

		$template = $this->mockHtml401Response();

		$response =
			$this->middleware->afterException($this->controller, 'checkSession', $exception);

		$this->assertEquals($template, $response);
	}

	public function testAfterExceptionWithCheckExceptionAndHtmlAcceptAnd404Code() {
		$message = 'fail';
		$code = Http::STATUS_NOT_FOUND;
		$exception = new CheckException($message, $code);

		$redirectUrl = '/app/error/route';
		$this->mockHtml404Response($redirectUrl, $code);

		$response =
			$this->middleware->afterException($this->controller, 'authenticate', $exception);

		$this->assertEquals($redirectUrl, $response->getRedirectURL());
		$this->assertEquals(Http::STATUS_SEE_OTHER, $response->getStatus());
		$this->assertEquals($message, $response->getCookies()['galleryErrorMessage']['value']);
	}

	public function testAfterExceptionWithCheckExceptionAndJsonAccept() {
		$message = 'fail';
		$code = Http::STATUS_NOT_FOUND;
		$exception = new CheckException($message, $code);

		$template = $this->mockJsonResponse($message, $code);

		$response =
			$this->middleware->afterException(
				$this->controller, 'checkShareIsValid', $exception
			);

		$this->assertEquals($template, $response);
	}

	/**
	 * @expectedException \OCA\Gallery\Environment\EnvironmentException
	 */
	public function testAfterExceptionWithNonCheckException() {
		$message = 'fail';
		$code = Http::STATUS_NOT_FOUND;
		$exception = new EnvironmentException($message, $code);

		$this->middleware->afterException($this->controller, 'checkShareIsValid', $exception);
	}

	/**
	 * Mocks ISession->exists('public_link_authenticated')
	 *
	 * @param int $shareId
	 */
	private function mockSessionExists($shareId) {
		$this->session->expects($this->once())
					  ->method('exists')
					  ->with('public_link_authenticated')
					  ->willReturn($shareId);
	}

	/**
	 * Mocks ISession->get('public_link_authenticated')
	 *
	 * @param int $shareId
	 */
	private function mockSessionWithShareId($shareId) {
		$this->session->expects($this->once())
					  ->method('get')
					  ->with('public_link_authenticated')
					  ->willReturn($shareId);
	}

	private function mockHtml401Response() {
		$this->mockAcceptHeader('html');
		$this->mockGetParams();

		return new TemplateResponse($this->appName, 'authenticate', [], 'guest');
	}

	private function mockHtml404Response($redirectUrl, $code) {
		$this->mockAcceptHeader('html');
		$this->mockUrlToErrorPage($code, $redirectUrl);
	}

	private function mockJsonResponse($message, $code) {
		$this->mockAcceptHeader('json');
		$jsonData = [
			'message' => $message,
			'success' => false
		];

		return new JSONResponse($jsonData, $code);
	}

	/**
	 * Mocks IRequest->getHeader('Accept')
	 *
	 * @param string $type
	 */
	private function mockAcceptHeader($type) {
		$this->request->expects($this->once())
					  ->method('getHeader')
					  ->with('Accept')
					  ->willReturn($type);
	}

	/**
	 * Mocks IRequest->getParams()
	 */
	private function mockGetParams() {
		$this->request->expects($this->once())
					  ->method('getParams')
					  ->willReturn([]);
	}

	/**
	 * Mocks IURLGenerator->linkToRoute()
	 *
	 * @param int $code
	 * @param string $url
	 */
	private function mockUrlToErrorPage($code, $url) {
		$this->urlGenerator->expects($this->once())
						   ->method('linkToRoute')
						   ->with($this->appName . '.page.error_page', ['code' => $code])
						   ->willReturn($url);
	}

	/**
	 * Mocks IRequest->getParam()
	 *
	 * @param string $token
	 * @param string $password
	 */
	private function mockGetTokenAndPasswordParams($token, $password = null) {
		$this->request->expects($this->at(0))
					  ->method('getParam')
					  ->with('token')
					  ->willReturn($token);
		$this->request->expects($this->at(1))
					  ->method('getParam')
					  ->with('password')
					  ->willReturn($password);
	}

	/**
	 * Mocks IRequest->getParam('token')
	 *
	 * @param string $token
	 */
	private function mockGetTokenParam($token) {
		$this->request->expects($this->once())
					  ->method('getParam')
					  ->with('token')
					  ->willReturn($token);
	}

	private function mockShare(
		$nodeType,
		$shareOwner,
		$target,
		$shareType = Share::SHARE_TYPE_USER,
		$sharedWith = 'externaluser',
		$permission = Constants::PERMISSION_READ,
		$password = 'securePassword'
	) {
		$share = $this->getMockBuilder('OCP\Share\IShare')
					  ->disableOriginalConstructor()
					  ->getMock();

		$share->method('getId')
			  ->willReturn(12345);
		$share->method('getNodeType')
			  ->willReturn($nodeType);
		$share->method('getShareOwner')
			  ->willReturn($shareOwner);
		$share->method('getTarget')
			  ->willReturn($target);
		$share->method('getShareType')
			  ->willReturn($shareType);
		$share->method('getSharedWith')
			  ->willReturn($sharedWith);
		$share->method('getPermissions')
			  ->willReturn($permission);
		$share->method('getPassword')
			  ->willReturn($password);

		return $share;
	}

	private function mockShareManagerGetShareByToken($token, $share) {
		$this->shareManager->expects($this->once())
						   ->method('getShareByToken')
						   ->with($token)
						   ->willReturn($share);
	}

	private function mockShareManagerGetShareByTokenThrowsException($token) {
		$this->shareManager->expects($this->once())
						   ->method('getShareByToken')
						   ->with($token)
						   ->willThrowException(
							   new \OCP\Share\Exceptions\ShareNotFound(
								   "Can't find a share using that token"
							   )
						   );
	}

	private function mockShareManagerCheckPassword($share, $password, $result) {
		$this->shareManager->expects($this->once())
						   ->method('checkPassword')
						   ->with($share, $password)
						   ->willReturn($result);
	}

}
