<?php

namespace App\Tests\Unit\Service\Export;

use App\ApiResources\Export\ExportRequest;
use App\Entity\Access\Access;
use App\Entity\Core\File;
use App\Entity\Organization\Organization;
use App\Enum\Core\FileTypeEnum;
use App\Repository\Access\AccessRepository;
use App\Repository\Core\FileRepository;
use App\Service\Export\BaseExporter;
use App\Service\Export\Encoder\EncoderInterface;
use App\Service\Export\Model\ExportModelInterface;
use App\Service\File\Storage\LocalStorage;
use App\Service\ServiceIterator\EncoderIterator;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Twig\Environment;
use Twig\Error\SyntaxError;

class TestableBaseExporter extends BaseExporter {
    public function buildModel(ExportRequest $exportRequest): ExportModelInterface { return parent::buildModel($exportRequest); }
    public function getBasename(): string { return parent::getBasename(); }
    public function getTemplatePath(): string { return parent::getTemplatePath(); }
    public function render(ExportModelInterface $model): string { return parent::render($model); }
    public function encode(string $html, string $format): string { return parent::encode($html, $format); }
    public function getFileBasename(ExportRequest $exportRequest): string { return parent::getFileBasename($exportRequest); }
    public function getFileType(): FileTypeEnum { return parent::getFileType(); }
}


class BaseExporterTest extends TestCase
{
    private AccessRepository $accessRepository;
    private FileRepository $fileRepository;
    private Environment $twig;
    private EncoderIterator $encoderIterator;
    private EntityManagerInterface $entityManager;
    private LocalStorage $localStorage;
    private LoggerInterface $logger;

    public function setUp(): void {
        $this->accessRepository = $this->getMockBuilder(AccessRepository::class)->disableOriginalConstructor()->getMock();
        $this->fileRepository = $this->getMockBuilder(FileRepository::class)->disableOriginalConstructor()->getMock();
        $this->twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock();
        $this->encoderIterator = $this->getMockBuilder(EncoderIterator::class)->disableOriginalConstructor()->getMock();
        $this->entityManager = $this->getMockBuilder(EntityManagerInterface::class)->disableOriginalConstructor()->getMock();
        $this->localStorage = $this->getMockBuilder(LocalStorage::class)->disableOriginalConstructor()->getMock();
        $this->logger = $this->getMockBuilder(LoggerInterface::class)->disableOriginalConstructor()->getMock();
    }

    private function getBaseExporterMockFor(string $method): MockObject | TestableBaseExporter
    {
        $exporter = $this->getMockBuilder(TestableBaseExporter::class)
            ->setMethodsExcept([$method, 'setAccessRepository', 'setFileRepository', 'setTwig', 'setEncoderIterator',
                'setEntityManager', 'setLocalStorage', 'setLogger'])
            ->getMock();

        $exporter->setAccessRepository($this->accessRepository);
        $exporter->setFileRepository($this->fileRepository);
        $exporter->setTwig($this->twig);
        $exporter->setEncoderIterator($this->encoderIterator);
        $exporter->setEntityManager($this->entityManager);
        $exporter->setLocalStorage($this->localStorage);
        $exporter->setLogger($this->logger);

        return $exporter;
    }

    /**
     * @see BaseExporter::support()
     */
    public function testSupport(): void {
        $exporter = $this->getBaseExporterMockFor('support');

        $exportRequest = $this->getMockBuilder(ExportRequest::class)->disableOriginalConstructor()->getMock();

        $this->assertFalse($exporter->support($exportRequest));
    }

    /**
     * @see BaseExporter::export()
     */
    public function testExport(): void {
        $exporter = $this->getBaseExporterMockFor('export');

        $exportRequest = $this->getMockBuilder(ExportRequest::class)->disableOriginalConstructor()->getMock();
        $exportRequest->method('getRequesterId')->willReturn(123);
        $exportRequest->method('getFileId')->willReturn(456);

        $model = $this->getMockBuilder(ExportModelInterface::class)->disableOriginalConstructor()->getMock();
        $exporter->expects(self::once())->method('buildModel')->with($exportRequest)->willReturn($model);

        $html = "<div>foo</div>";
        $exporter->expects(self::once())->method('render')->with($model)->willReturn($html);

        $data = 'azerty';
        $exporter->expects(self::once())->method('encode')->with($html)->willReturn($data);

        $access = $this->getMockBuilder(Access::class)->disableOriginalConstructor()->getMock();
        $this->accessRepository->method('find')->with(123, null, null)->willReturn($access);

        $file = $this->getMockBuilder(File::class)->disableOriginalConstructor()->getMock();
        $this->fileRepository->method('find')->with(456, null, null)->willReturn($file);

        $this->localStorage->expects(self::once())->method('writeFile')->with($file, $data, $access);

        $exporter->export($exportRequest);
    }

    /**
     * @see BaseExporter::export()
     */
    public function testExportNewFile(): void {
        $exporter = $this->getBaseExporterMockFor('export');

        $exportRequest = $this->getMockBuilder(ExportRequest::class)->disableOriginalConstructor()->getMock();
        $exportRequest->method('getRequesterId')->willReturn(123);
        $exportRequest->method('getFileId')->willReturn(null);

        $model = $this->getMockBuilder(ExportModelInterface::class)->disableOriginalConstructor()->getMock();
        $exporter->expects(self::once())->method('buildModel')->with($exportRequest)->willReturn($model);

        $html = "<div>foo</div>";
        $exporter->expects(self::once())->method('render')->with($model)->willReturn($html);

        $data = 'azerty';
        $exporter->expects(self::once())->method('encode')->with($html)->willReturn($data);

        $organization = $this->getMockBuilder(Organization::class)->disableOriginalConstructor()->getMock();
        $access = $this->getMockBuilder(Access::class)->disableOriginalConstructor()->getMock();
        $access->method('getOrganization')->willReturn($organization);
        $this->accessRepository->method('find')->with(123, null, null)->willReturn($access);

        $file = $this->getMockBuilder(File::class)->disableOriginalConstructor()->getMock();
        $exporter->expects(self::once())->method('prepareFile')->willReturn($file);

        $this->localStorage->expects(self::once())->method('writeFile')->with($file, $data, $access);

        $exporter->export($exportRequest);
    }

    /**
     * @see BaseExporter::export()
     */
    public function testExportUndefinedRequester(): void {
        $exporter = $this->getBaseExporterMockFor('export');

        $exportRequest = $this->getMockBuilder(ExportRequest::class)->disableOriginalConstructor()->getMock();
        $exportRequest->method('getRequesterId')->willReturn(123);
        $exportRequest->method('getFileId')->willReturn(456);

        $this->accessRepository->method('find')->with(123, null, null)->willReturn(null);

        $this->localStorage->expects(self::never())->method('writeFile');

        $this->expectException(\RuntimeException::class);
        $this->expectExceptionMessage('Unable to determine the user; abort.');

        $exporter->export($exportRequest);
    }

    /**
     * @see BaseExporter::export()
     */
    public function testExportUndefinedOrganization(): void {
        $exporter = $this->getBaseExporterMockFor('export');

        $exportRequest = $this->getMockBuilder(ExportRequest::class)->disableOriginalConstructor()->getMock();
        $exportRequest->method('getRequesterId')->willReturn(123);
        $exportRequest->method('getFileId')->willReturn(null);

        $model = $this->getMockBuilder(ExportModelInterface::class)->disableOriginalConstructor()->getMock();
        $exporter->expects(self::once())->method('buildModel')->with($exportRequest)->willReturn($model);

        $html = "<div>foo</div>";
        $exporter->expects(self::once())->method('render')->with($model)->willReturn($html);

        $data = 'azerty';
        $exporter->expects(self::once())->method('encode')->with($html)->willReturn($data);

        $access = $this->getMockBuilder(Access::class)->disableOriginalConstructor()->getMock();
        $access->method('getOrganization')->willReturn(null);
        $this->accessRepository->method('find')->with(123, null, null)->willReturn($access);

        $this->localStorage->expects(self::never())->method('writeFile');

        $this->expectException(\RuntimeException::class);
        $this->expectExceptionMessage('Unable to determine the organization of the curent user; abort.');

        $exporter->export($exportRequest);
    }

    /**
     * @see BaseExporter::prepareFile()
     */
    public function testPrepareFile(): void {
        $exporter = $this->getBaseExporterMockFor('prepareFile');

        $exportRequest = $this->getMockBuilder(ExportRequest::class)->disableOriginalConstructor()->getMock();
        $exportRequest->method('getRequesterId')->willReturn(123);
        $exportRequest->method('getFormat')->willReturn('pdf');

        $access = $this->getMockBuilder(Access::class)->disableOriginalConstructor()->getMock();
        $this->accessRepository->method('find')->with(123, null, null)->willReturn($access);

        $exporter->method('getFileBasename')->with($exportRequest)->willReturn('Foo');
        $exporter->method('getFileType')->willReturn(FileTypeEnum::UNKNOWN());

        $file = $this->getMockBuilder(File::class)->disableOriginalConstructor()->getMock();

        $this->localStorage->expects(self::once())
            ->method('prepareFile')
            ->with(
                $access, 'Foo.pdf', FileTypeEnum::UNKNOWN(), $access, true, 'NOBODY', 'application/pdf', false
            )->willReturn($file);

        $result = $exporter->prepareFile($exportRequest, false);

        $this->assertSame($file, $result);
    }

    /**
     * @see BaseExporter::prepareFile()
     */
    public function testPrepareFileUndefinedRequester(): void {
        $exporter = $this->getBaseExporterMockFor('prepareFile');

        $exportRequest = $this->getMockBuilder(ExportRequest::class)->disableOriginalConstructor()->getMock();
        $exportRequest->method('getRequesterId')->willReturn(123);
        $this->accessRepository->method('find')->with(123, null, null)->willReturn(null);

        $this->expectException(\RuntimeException::class);
        $this->expectExceptionMessage('Unable to determine the current user; abort.');

        $exporter->prepareFile($exportRequest, false);
    }

    /**
     * @see BaseExporter::buildModel()
     */
    public function testBuildModel(): void {
        $exporter = $this->getBaseExporterMockFor('buildModel');

        $exportRequest = $this->getMockBuilder(ExportRequest::class)->disableOriginalConstructor()->getMock();

        $this->expectException(\RuntimeException::class);
        $this->expectExceptionMessage('not implemented error');

        $exporter->buildModel($exportRequest);
    }

    /**
     * @see BaseExporter::getBasename()
     */
    public function testGetBaseName(): void {
        $exporter = new TestableBaseExporter();

        $this->assertEquals('testable_base', $exporter->getBasename());
    }

    /**
     * @see BaseExporter::getTemplatePath()
     */
    public function testGetTemplatePath(): void {
        $exporter = $this->getBaseExporterMockFor('getTemplatePath');
        $exporter->method('getBaseName')->willReturn('base_exporter');

        $this->assertEquals('@templates/export/base_exporter.html.twig', $exporter->getTemplatePath());
    }

    /**
     * @see BaseExporter::render()
     */
    public function testRender(): void {
        $exporter = $this->getBaseExporterMockFor('render');

        $exporter->method('getTemplatePath')->willReturn('/path/to/template.html.twig');

        $model = $this->getMockBuilder(ExportModelInterface::class)->disableOriginalConstructor()->getMock();

        $content = "azerty";

        $this->twig->expects(self::once())
            ->method('render')
            ->with('/path/to/template.html.twig', ['model' => $model])
            ->willReturn($content);

        $result = $exporter->render($model);

        $this->assertEquals($content, $result);
    }

    /**
     * @see BaseExporter::render()
     */
    public function testRenderError(): void {
        $exporter = $this->getBaseExporterMockFor('render');

        $exporter->method('getTemplatePath')->willReturn('/path/to/template.html.twig');

        $model = $this->getMockBuilder(ExportModelInterface::class)->disableOriginalConstructor()->getMock();

        $this->twig
            ->expects(self::once())
            ->method('render')
            ->with('/path/to/template.html.twig', ['model' => $model])
            ->willThrowException(new SyntaxError('error'));

        $this->expectException(\RuntimeException::class);

        $exporter->render($model);
    }

    /**
     * @see BaseExporter::encode()
     */
    public function testEncode(): void {
        $exporter = $this->getBaseExporterMockFor('encode');

        $encoder = $this->getMockBuilder(EncoderInterface::class)->getMock();

        $this->encoderIterator->expects(self::once())->method('getEncoderFor')->with('pdf')->willReturn($encoder);

        $html = '<span>foo</span>';
        $content = 'foo';
        $encoder->expects(self::once())->method('encode')->with($html)->willReturn($content);

        $result = $exporter->encode($html, 'pdf');

        $this->assertEquals($content, $result);
    }

    /**
     * @see BaseExporter::getFileBasename()
     */
    public function testGetFileBasename(): void {
        $exporter = $this->getBaseExporterMockFor('getFileBasename');

        $exportRequest = $this->getMockBuilder(ExportRequest::class)->disableOriginalConstructor()->getMock();

        $exporter->method('getBasename')->willReturn('base');

        $this->assertEquals('base', $exporter->getFileBasename($exportRequest));
    }

    /**
     * @see BaseExporter::getFileType()
     */
    public function testGetFileType(): void {
        $exporter = $this->getBaseExporterMockFor('getFileType');

        $this->assertEquals(FileTypeEnum::UNKNOWN(), $exporter->getFileType());
    }
}
