<?php declare(strict_types=1);

namespace Newland\ToubizWidgetRendering\Tests\Unit\Renderer;

use Neos\Flow\Tests\FunctionalTestCase;
use Newland\ToubizWidgetRendering\Renderer\FallbackStack;
use Newland\ToubizWidgetRendering\Tests\Unit\Renderer\Mock\OutputMock;
use Newland\ToubizWidgetRendering\Tests\Unit\Renderer\Mock\RendererMock;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;

class FallbackStackTest extends FunctionalTestCase
{

    /** @var FallbackStack */
    protected $subject;

    public function setUp(): void
    {
        parent::setUp();
        $this->subject = $this->objectManager->get(FallbackStack::class);
    }

    public function tearDown(): void
    {
        RendererMock::$onConfiguration = null;
    }

    public function testPassesConfigurationToRenderers(): void
    {
        $configurations = [];
        RendererMock::$onConfiguration = function($configuration) use (&$configurations) {
            $configurations[] = $configuration;
        };

        $this->subject->setConfiguration([
            'renderers' => [
                'foo' => [
                    'priority' => 300,
                    'renderer' => RendererMock::class,
                    'rendererConfiguration' => [ 'someConfigOption' => 'foo' ],
                ],
                'bar' => [
                    'priority' => 200,
                    'renderer' => RendererMock::class,
                    'rendererConfiguration' => [ 'someConfigOption' => 'bar' ],
                ],
                'baz' => [
                    'priority' => 100,
                    'renderer' => RendererMock::class,
                    'rendererConfiguration' => [ 'someConfigOption' => 'baz' ],
                ],
            ]
        ]);

        $options = array_map(function($config) {
            return $config['someConfigOption'];
        }, $configurations);

        $this->assertContains('foo', $options);
        $this->assertContains('bar', $options);
        $this->assertContains('baz', $options);
    }

    public function testUsesRendererWithHighestPriorityFirst(): void
    {
        $renderersCalled = [];

        $highestPriority = new RendererMock();
        $highestPriority->render = function() use (&$renderersCalled) {
            $renderersCalled[] = 'highestPriority';
            return '';
        };

        $secondPriority = new RendererMock();
        $secondPriority->render = function() use (&$renderersCalled) {
            $renderersCalled[] = 'secondPriority';
            return '';
        };

        $lowestPriority = new RendererMock();
        $lowestPriority->render = function() use (&$renderersCalled) {
            $renderersCalled[] = 'lowestPriority';
            return '';
        };

        $this->subject->setConfiguration([
            'renderers' => [
                'foo' => [
                    'priority' => 100,
                    'renderer' => $lowestPriority,
                    'rendererConfiguration' => [ ],
                ],
                'bar' => [
                    'priority' => 300,
                    'renderer' => $highestPriority,
                    'rendererConfiguration' => [ ],
                ],
                'baz' => [
                    'priority' => 200,
                    'renderer' => $secondPriority,
                    'rendererConfiguration' => [ ],
                ],
            ]
        ]);

        $this->subject->render('foo', [ ]);

        $this->assertContains('highestPriority', $renderersCalled);
        $this->assertNotContains('secondPriority', $renderersCalled);
        $this->assertNotContains('lowestPriority', $renderersCalled);
    }

    public function testFallsBackDependingOnPriorityIfRenderingFails(): void
    {
        $renderersCalled = [];

        $highestPriority = new RendererMock();
        $highestPriority->render = function() use (&$renderersCalled) {
            $renderersCalled[] = 'highestPriority';
            throw new \Exception('rendering failed here');
        };

        $secondPriority = new RendererMock();
        $secondPriority->render = function() use (&$renderersCalled) {
            $renderersCalled[] = 'secondPriority';
            throw new \Exception('oh no, not again');
        };

        $lowestPriority = new RendererMock();
        $lowestPriority->render = function() use (&$renderersCalled) {
            $renderersCalled[] = 'lowestPriority';
            return 'Lowest Priority renderer saves us all.';
        };

        $this->subject->setConfiguration([
            'renderers' => [
                'foo' => [
                    'priority' => 100,
                    'renderer' => $lowestPriority,
                    'rendererConfiguration' => [ ],
                ],
                'bar' => [
                    'priority' => 300,
                    'renderer' => $highestPriority,
                    'rendererConfiguration' => [ ],
                ],
                'baz' => [
                    'priority' => 200,
                    'renderer' => $secondPriority,
                    'rendererConfiguration' => [ ],
                ],
            ]
        ]);

        $result = $this->subject->render('foo', [ ]);

        $this->assertEquals('highestPriority', $renderersCalled[0]);
        $this->assertEquals('secondPriority', $renderersCalled[1]);
        $this->assertEquals('lowestPriority', $renderersCalled[2]);
        $this->assertEquals('Lowest Priority renderer saves us all.', $result);
    }

    public function testPrecompilationIsDoneOnEveryRenderer(): void
    {
        $precompilerCalled = [];

        $highestPriority = new RendererMock();
        $highestPriority->precompile = function() use (&$precompilerCalled) {
            $precompilerCalled[] = 'highestPriority';
        };

        $secondPriority = new RendererMock();
        $secondPriority->precompile = function() use (&$precompilerCalled) {
            $precompilerCalled[] = 'secondPriority';
        };

        $lowestPriority = new RendererMock();
        $lowestPriority->precompile = function() use (&$precompilerCalled) {
            $precompilerCalled[] = 'lowestPriority';
        };

        $this->subject->setConfiguration([
            'renderers' => [
                'foo' => [
                    'priority' => 100,
                    'renderer' => $lowestPriority,
                    'rendererConfiguration' => [ ],
                ],
                'bar' => [
                    'priority' => 300,
                    'renderer' => $highestPriority,
                    'rendererConfiguration' => [ ],
                ],
                'baz' => [
                    'priority' => 200,
                    'renderer' => $secondPriority,
                    'rendererConfiguration' => [ ],
                ],
            ]
        ]);

        $this->subject->precompile(new NullOutput());

        $this->assertContains('highestPriority', $precompilerCalled);
        $this->assertContains('secondPriority', $precompilerCalled);
        $this->assertContains('lowestPriority', $precompilerCalled);
    }

    public function testPrecompilationOutputIsPrefixed(): void
    {
        $highestPriority = new RendererMock();
        $highestPriority->precompile = function(OutputInterface $output) {
            $output->write('highestPriority');
        };

        $secondPriority = new RendererMock();
        $secondPriority->precompile = function(OutputInterface $output) {
            $output->write('secondPriority');
        };

        $lowestPriority = new RendererMock();
        $lowestPriority->precompile = function(OutputInterface $output) {
            $output->write('lowestPriority');
        };

        $this->subject->setConfiguration([
            'renderers' => [
                'foo' => [
                    'priority' => 100,
                    'renderer' => $lowestPriority,
                    'rendererConfiguration' => [ ],
                ],
                'bar' => [
                    'priority' => 300,
                    'renderer' => $highestPriority,
                    'rendererConfiguration' => [ ],
                ],
                'baz' => [
                    'priority' => 200,
                    'renderer' => $secondPriority,
                    'rendererConfiguration' => [ ],
                ],
            ]
        ]);

        $output = new OutputMock();
        $this->subject->precompile($output);

        $this->assertContains('[bar] highestPriority', $output->written);
        $this->assertContains('[baz] secondPriority', $output->written);
        $this->assertContains('[foo] lowestPriority', $output->written);
    }

    public function testAllowsDisablingRendererBySettingItToNull(): void
    {
        $fallbackCalled = false;
        $fallback = new RendererMock();
        $fallback->precompile = function() use (&$fallbackCalled) {
            $fallbackCalled = true;
        };

        $this->subject->setConfiguration([
            'renderers' => [
                'foo' => [
                    'priority' => 100,
                    'renderer' => $fallback,
                    'rendererConfiguration' => [ ],
                ],
                'bar' => null,
                'baz' => [
                    'priority' => 200,
                    'renderer' => null,
                ],
            ]
        ]);

        $this->subject->precompile(new NullOutput());
        $this->assertTrue($fallbackCalled);
    }
}
