<?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\Controller;

use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\ILogger;
use OCP\Files\File;

use OCP\AppFramework\IAppContainer;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;

use OCA\Gallery\AppInfo\Application;
use OCA\Gallery\Controller\PreviewController;
use OCA\Gallery\Http\ImageResponse;
use OCA\Gallery\Service\ConfigService;
use OCA\Gallery\Service\ThumbnailService;
use OCA\Gallery\Service\PreviewService;
use OCA\Gallery\Service\DownloadService;
use OCA\Gallery\Utility\EventSource;
use OCA\Gallery\Service\NotFoundServiceException;
use OCA\Gallery\Service\InternalServerErrorServiceException;

/**
 * Class PreviewControllerTest
 *
 * @package OCA\Gallery\Controller
 */
class PreviewControllerTest extends \OCA\Gallery\Tests\GalleryUnitTest {

	/** @var IAppContainer */
	protected $container;
	/** @var string */
	protected $appName = 'gallery';
	/** @var IRequest */
	protected $request;
	/** @var PreviewController */
	protected $controller;
	/** @var IURLGenerator */
	protected $urlGenerator;
	/** @var ConfigService */
	protected $configService;
	/** @var ThumbnailService */
	protected $thumbnailService;
	/** @var PreviewService */
	protected $previewService;
	/** @var DownloadService */
	protected $downloadService;
	/** @var EventSource */
	protected $eventSource;
	/** @var ILogger */
	protected $logger;

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

		$app = new Application;
		$this->container = $app->getContainer();
		$this->container['UserFolder'] = $this->getMockBuilder('OCP\Files\Folder')
											  ->disableOriginalConstructor()
											  ->getMock();
		$this->request = $this->getMockBuilder('\OCP\IRequest')
							  ->disableOriginalConstructor()
							  ->getMock();
		$this->urlGenerator = $this->getMockBuilder('\OCP\IURLGenerator')
								   ->disableOriginalConstructor()
								   ->getMock();
		$this->configService = $this->getMockBuilder('\OCA\Gallery\Service\ConfigService')
									->disableOriginalConstructor()
									->getMock();
		$this->thumbnailService = $this->getMockBuilder('\OCA\Gallery\Service\ThumbnailService')
									   ->disableOriginalConstructor()
									   ->getMock();
		$this->previewService = $this->getMockBuilder('\OCA\Gallery\Service\PreviewService')
									 ->disableOriginalConstructor()
									 ->getMock();
		$this->downloadService = $this->getMockBuilder('\OCA\Gallery\Service\DownloadService')
									  ->disableOriginalConstructor()
									  ->getMock();
		$this->eventSource = $this->getMockBuilder('\OCA\Gallery\Utility\EventSource')
								  ->disableOriginalConstructor()
								  ->getMock();
		$this->logger = $this->getMockBuilder('\OCP\ILogger')
							 ->disableOriginalConstructor()
							 ->getMock();
		$this->controller = new PreviewController(
			$this->appName,
			$this->request,
			$this->urlGenerator,
			$this->configService,
			$this->thumbnailService,
			$this->previewService,
			$this->downloadService,
			$this->eventSource,
			$this->logger
		);
	}

	/**
	 * @expectedException \Exception
	 */
	public function testGetThumbnailsWillDie() {
		$square = true;
		$scale = 2.5;
		$thumbnailId = 1234;

		$file = $this->mockJpgFile($thumbnailId);
		$this->mockGetFile($this->previewService, $thumbnailId, $file);

		$this->controller->getThumbnails($thumbnailId, $square, $scale);
	}

	public function testEventSourceSend() {
		$thumbnailId = 1234;
		$file = $this->mockFile($thumbnailId);
		$preview = $this->mockBase64PreviewData($file);

		$event = 'preview';
		$message = 'event: ' . $event . PHP_EOL .
				   'data: ' . json_encode($preview) . PHP_EOL .
				   PHP_EOL;
		$this->mockEventSourceSend($event, $preview, $message);

		$this->assertSame($message, $this->eventSource->send('preview', $preview));
	}

	/**
	 * @todo Really base64 encode the preview instead of just passing around the binary content
	 */
	public function testGetThumbnail() {
		$square = true;
		$scale = 2.5;
		$width = 400;
		$height = 400;
		$aspect = !$square;
		$animatedPreview = false;
		$base64Encode = true;
		$thumbnailId = 1234;
		$thumbnailSpecs = [
			$width,
			$height,
			$aspect,
			$animatedPreview,
			$base64Encode
		];
		$this->mockGetThumbnailSpecs($square, $scale, $thumbnailSpecs);
		/** @type File $file */
		$file = $this->mockJpgFile($thumbnailId);
		$mockedPreview =
			$this->mockGetData(
				$thumbnailId, $file, $width, $height, $aspect, $animatedPreview, $base64Encode
			);

		list($preview, $status) = self::invokePrivate(
			$this->controller, 'getThumbnail', [$thumbnailId, $square, $scale]
		);

		$this->assertEquals(Http::STATUS_OK, $status);
		$this->assertEquals($mockedPreview, $preview);
	}

	public function testGetBrokenThumbnail() {
		$square = true;
		$scale = 2.5;
		$width = 400;
		$height = 400;
		$aspect = !$square;
		$animatedPreview = false;
		$base64Encode = true;
		$thumbnailId = 1234;
		$thumbnailSpecs = [
			$width,
			$height,
			$aspect,
			$animatedPreview,
			$base64Encode
		];
		$this->mockGetThumbnailSpecs($square, $scale, $thumbnailSpecs);

		/** @type File $file */
		$file = $this->mockJpgFile($thumbnailId);
		$this->mockGetDataWithBrokenPreview(
			$thumbnailId, $file, $width, $height, $aspect, $animatedPreview, $base64Encode
		);

		list($preview, $status) = self::invokePrivate(
			$this->controller, 'getThumbnail', [$thumbnailId, $square, $scale]
		);

		$this->assertEquals(Http::STATUS_INTERNAL_SERVER_ERROR, $status);
		$this->assertNull($preview['preview']);
		$this->assertEquals($file->getMimeType(), $preview['mimetype']);
	}

	public function testGetThumbnailWithBrokenSetup() {
		$square = true;
		$scale = 2.5;
		$thumbnailId = 1234;
		$animatedPreview = false;

		/** @type File $file */
		$file = $this->mockGetDataWithBrokenSetup($thumbnailId, $animatedPreview);
		$this->mockIsPreviewRequiredThrowsException($file, $animatedPreview);

		list($preview, $status) = self::invokePrivate(
			$this->controller, 'getThumbnail', [$thumbnailId, $square, $scale]
		);

		$this->assertEquals(Http::STATUS_INTERNAL_SERVER_ERROR, $status);
		$this->assertNull($preview['preview']);
		$this->assertEquals($file->getMimeType(), $preview['mimetype']);
	}

	public function testGetPreview() {
		$fileId = 1234;
		$width = 1024;
		$height = 768;

		/** @type File $file */
		$file = $this->mockJpgFile($fileId);
		$preview = $this->mockGetData($fileId, $file, $width, $height);
		$preview['name'] = $file->getName();

		/** @type ImageResponse $response */
		$response = $this->controller->getPreview($fileId, $width, $height);

		$this->assertEquals(Http::STATUS_OK, $response->getStatus());

		// Media type of a JPG preview is always PNG
		$this->assertEquals(
			'image/png; charset=utf-8', $response->getHeaders()['Content-type']
		);
	}

	public function testGetPreviewWithWrongId() {
		$fileId = 99999;
		$width = 1024;
		$height = 768;

		$exception = new NotFoundServiceException('Not found');
		$this->mockGetFileWithBadFile($this->previewService, $fileId, $exception);

		$errorResponse = $this->jsonErrorMessage(Http::STATUS_NOT_FOUND);

		$response = $this->controller->getPreview($fileId, $width, $height);

		$this->assertEquals($errorResponse->getStatus(), $response->getStatus());
		$this->assertEquals($errorResponse->getData()['success'], $response->getData()['success']);
	}

	public function testGetPreviewWithBrokenGif() {
		$fileId = 1234;
		$width = 1024;
		$height = 768;

		/** @type File $file */
		$file = $this->mockAnimatedGifFile($fileId);
		$this->mockGetDataWithEmptyPreview($fileId, $file, $width, $height);

		$errorResponse = $this->jsonErrorMessage(Http::STATUS_INTERNAL_SERVER_ERROR);

		$response = $this->controller->getPreview($fileId, $width, $height);

		$this->assertEquals($errorResponse->getStatus(), $response->getStatus());
		$this->assertEquals($errorResponse->getData()['success'], $response->getData()['success']);
	}

	/**
	 * Mocks Preview->getData
	 *
	 * @param int $fileId the ID of the file of which we need a large preview of
	 * @param File $file
	 * @param int $width
	 * @param int $height
	 * @param bool $keepAspect
	 * @param bool $animatedPreview
	 * @param bool $base64Encode
	 * @param bool $previewRequired
	 *
	 * @return array
	 */
	protected function mockGetData(
		$fileId, $file, $width, $height, $keepAspect = true, $animatedPreview = true,
		$base64Encode = false, $previewRequired = true
	) {
		$this->mockGetFile($this->previewService, $fileId, $file);

		$this->mockIsPreviewRequired($file, $animatedPreview, $previewRequired);
		$previewData = $this->mockPreviewData($file, $previewRequired);

		if ($previewRequired) {
			$this->mockCreatePreview(
				$file, $width, $height, $keepAspect, $base64Encode, $previewData
			);
		} else {
			$this->mockDownloadFile($file, $base64Encode, $previewData);
		}

		return $previewData;
	}

	/**
	 * Mocks Preview->getData
	 *
	 * @param int $fileId the ID of the file of which we need a large preview of
	 * @param File $file
	 * @param int $width
	 * @param int $height
	 * @param bool $keepAspect
	 * @param bool $animatedPreview
	 * @param bool $base64Encode
	 *
	 * @return array
	 */
	private function mockGetDataWithBrokenPreview(
		$fileId, $file, $width, $height, $keepAspect = true, $animatedPreview = true,
		$base64Encode = false
	) {
		$this->mockGetFile($this->previewService, $fileId, $file);

		$this->mockIsPreviewRequired($file, $animatedPreview, true);

		$this->mockCreatePreviewThrowsException($file, $width, $height, $keepAspect, $base64Encode);
	}

	/**
	 * Mocks Preview->getData
	 *
	 * @param int $fileId the ID of the file of which we need a large preview of
	 * @param File $file
	 * @param int $width
	 * @param int $height
	 * @param bool $keepAspect
	 * @param bool $animatedPreview
	 * @param bool $base64Encode
	 *
	 * @return array
	 */
	private function mockGetDataWithEmptyPreview(
		$fileId, $file, $width, $height, $keepAspect = true, $animatedPreview = true,
		$base64Encode = false
	) {
		$this->mockGetFile($this->previewService, $fileId, $file);

		$this->mockIsPreviewRequired($file, $animatedPreview, true);

		$this->mockCreatePreview($file, $width, $height, $keepAspect, $base64Encode, null);
	}

	/**
	 * @param int $fileId
	 * @param bool $animatedPreview
	 *
	 * @return object|\PHPUnit_Framework_MockObject_MockObject
	 */
	private function mockGetDataWithBrokenSetup($fileId, $animatedPreview) {
		$file = $this->mockJpgFile($fileId);
		$this->mockGetFile($this->previewService, $fileId, $file);

		$this->mockIsPreviewRequiredThrowsException($file, $animatedPreview);

		return $file;
	}


	/**
	 * @param object|\PHPUnit_Framework_MockObject_MockObject $file
	 * @param bool $animatedPreview
	 * @param bool $response
	 */
	private function mockIsPreviewRequired($file, $animatedPreview, $response) {
		$this->previewService->expects($this->once())
							 ->method('isPreviewRequired')
							 ->with(
								 $this->equalTo($file),
								 $this->equalTo($animatedPreview)
							 )
							 ->willReturn($response);
	}

	/**
	 * @param object|\PHPUnit_Framework_MockObject_MockObject $file
	 * @param bool $animatedPreview
	 */
	private function mockIsPreviewRequiredThrowsException($file, $animatedPreview) {
		$exception = new InternalServerErrorServiceException('Broken');
		$this->previewService->expects($this->once())
							 ->method('isPreviewRequired')
							 ->with(
								 $this->equalTo($file),
								 $this->equalTo($animatedPreview)
							 )
							 ->willThrowException($exception);
	}

	/**
	 * @param File $file
	 * @param bool $previewRequired
	 *
	 * @return array <string,mixed>
	 */
	private function mockPreviewData($file, $previewRequired) {
		$mimeType = $previewRequired ? 'image/png' : $file->getMimeType();

		$preview = [
			'preview'  => $file->getContent(), // Not a real preview, but it's not important
			'mimetype' => $mimeType,
		];

		return $preview;
	}

	/**
	 * @param object|\PHPUnit_Framework_MockObject_MockObject $file
	 *
	 * @return array<string,mixed>
	 */
	private function mockBase64PreviewData($file) {
		$preview = [
			'preview'  => base64_encode($file->getContent()),
			// Not a real preview, but it's not important
			'mimetype' => 'image/png', //Most previews are PNGs
		];

		return $preview;
	}

	/**
	 * @param File $file
	 * @param int $width
	 * @param int $height
	 * @param bool $keepAspect
	 * @param bool $base64Encode
	 * @param array $preview
	 */
	private function mockCreatePreview($file, $width, $height, $keepAspect, $base64Encode, $preview
	) {
		$this->previewService->expects($this->once())
							 ->method('createPreview')
							 ->with(
								 $this->equalTo($file),
								 $this->equalTo($width),
								 $this->equalTo($height),
								 $this->equalTo($keepAspect),
								 $this->equalTo($base64Encode)
							 )
							 ->willReturn($preview);
	}

	/**
	 * @param File $file
	 * @param int $width
	 * @param int $height
	 * @param bool $keepAspect
	 * @param bool $base64Encode
	 */
	private function mockCreatePreviewThrowsException(
		$file, $width, $height, $keepAspect, $base64Encode
	) {
		$exception = new InternalServerErrorServiceException('Encryption ate your file');
		$this->previewService->expects($this->once())
							 ->method('createPreview')
							 ->with(
								 $this->equalTo($file),
								 $this->equalTo($width),
								 $this->equalTo($height),
								 $this->equalTo($keepAspect),
								 $this->equalTo($base64Encode)
							 )
							 ->willthrowException($exception);
	}

	/**
	 * @param $file
	 * @param $base64Encode
	 * @param $preview
	 */
	private function mockDownloadFile($file, $base64Encode, $preview) {
		$this->downloadService->expects($this->once())
							  ->method('downloadFile')
							  ->with(
								  $this->equalTo($file),
								  $this->equalTo($base64Encode)
							  )
							  ->willReturn($preview);
	}

	/**
	 * @param $event
	 * @param $data
	 * @param $message
	 */
	private function mockEventSourceSend($event, $data, $message) {
		$this->eventSource->expects($this->once())
						  ->method('send')
						  ->with(
							  $event,
							  $data
						  )
						  ->willReturn($message);

	}

	/**
	 * @param $square
	 * @param $scale
	 * @param $array
	 */
	private function mockGetThumbnailSpecs($square, $scale, $array) {
		$this->thumbnailService->expects($this->once())
							   ->method('getThumbnailSpecs')
							   ->with(
								   $square,
								   $scale
							   )
							   ->willReturn($array);

	}

	private function jsonErrorMessage($code) {
		return new JSONResponse(
			[
				'message' => "I'm truly sorry, but we were unable to generate a preview for this file",
				'success' => false
			], $code
		);
	}

}
