<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

/**
 * Layout integration tests
 *
 * Note that some methods are not covered here, see the \Magento\Framework\View\LayoutDirectivesTest
 *
 * @see \Magento\Framework\View\LayoutDirectivesTest
 */
namespace Magento\Framework\View;

/**
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 */
class LayoutTest extends \PHPUnit\Framework\TestCase
{
    /**
     * @var \Magento\Framework\View\Layout
     */
    protected $_layout;

    /**
     * @var \Magento\Framework\View\LayoutFactory
     */
    protected $layoutFactory;

    protected function setUp()
    {
        $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
        $this->layoutFactory = $objectManager->get(\Magento\Framework\View\LayoutFactory::class);
        $this->_layout = $this->layoutFactory->create();
        $objectManager->get(\Magento\Framework\App\Cache\Type\Layout::class)->clean();
    }

    public function testConstructorStructure()
    {
        $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
        $structure = $objectManager->get(\Magento\Framework\View\Layout\Data\Structure::class);
        $structure->createElement('test.container', []);
        /** @var $layout \Magento\Framework\View\LayoutInterface */
        $layout = $this->layoutFactory->create(['structure' => $structure]);
        $this->assertTrue($layout->hasElement('test.container'));
    }

    /**
     * @magentoAppIsolation enabled
     * @magentoAppArea frontend
     */
    public function testDestructor()
    {
        $this->_layout->addBlock(\Magento\Framework\View\Element\Text::class, 'test');
        $this->assertNotEmpty($this->_layout->getAllBlocks());
        $this->_layout->__destruct();
        $this->assertEmpty($this->_layout->getAllBlocks());
    }

    public function testGetUpdate()
    {
        $this->assertInstanceOf(\Magento\Framework\View\Layout\ProcessorInterface::class, $this->_layout->getUpdate());
    }

    public function testGenerateXml()
    {
        $layoutUtility = new Utility\Layout($this);
        /** @var $layout \Magento\Framework\View\LayoutInterface */
        $layout = $this->getMockBuilder(\Magento\Framework\View\Layout::class)
            ->setMethods(['getUpdate'])
            ->setConstructorArgs($layoutUtility->getLayoutDependencies())
            ->getMock();

        $merge = $this->createPartialMock(\StdClass::class, ['asSimplexml']);
        $merge->expects(
            $this->once()
        )->method(
            'asSimplexml'
        )->will(
            $this->returnValue(
                simplexml_load_string(
                    '<layout><container name="container1"></container></layout>',
                    \Magento\Framework\View\Layout\Element::class
                )
            )
        );
        $layout->expects($this->once())->method('getUpdate')->will($this->returnValue($merge));
        $this->assertEmpty($layout->getXpath('/layout/container[@name="container1"]'));
        $layout->generateXml();
        $this->assertNotEmpty($layout->getXpath('/layout/container[@name="container1"]'));
    }

    /**
     * A smoke test for generating elements
     *
     * See sophisticated tests at \Magento\Framework\View\LayoutDirectivesTest
     * @see \Magento\Framework\View\LayoutDirectivesTest
     * @magentoAppIsolation enabled
     */
    public function testGenerateGetAllBlocks()
    {
        $this->_layout->setXml(
            simplexml_load_string(
                '<layout>
                <block class="Magento\Framework\View\Element\Text" name="block1">
                    <block class="Magento\Framework\View\Element\Text"/>
                </block>
                <block class="Magento\Framework\View\Element\Text" template="test" ttl="360"/>
                <block class="Magento\Framework\View\Element\Text"/>
            </layout>',
                \Magento\Framework\View\Layout\Element::class
            )
        );
        $this->assertEquals([], $this->_layout->getAllBlocks());
        $this->_layout->generateElements();
        $expected = ['block1', 'block1_schedule_block0', 'schedule_block1', 'schedule_block2'];
        $this->assertSame($expected, array_keys($this->_layout->getAllBlocks()));
        $child = $this->_layout->getBlock('block1_schedule_block0');
        $this->assertSame($this->_layout->getBlock('block1'), $child->getParentBlock());
        $this->assertEquals('test', $this->_layout->getBlock('schedule_block1')->getData('template'));
        $this->assertEquals('360', $this->_layout->getBlock('schedule_block1')->getData('ttl'));
        $this->assertFalse($this->_layout->getBlock('nonexisting'));
    }

    public function testGetElementProperty()
    {
        $name = 'test';
        $this->_layout->addContainer($name, 'Test', ['option1' => 1, 'option2' => 2]);
        $this->assertEquals(
            'Test',
            $this->_layout->getElementProperty($name, \Magento\Framework\View\Layout\Element::CONTAINER_OPT_LABEL)
        );
        $this->assertEquals(
            \Magento\Framework\View\Layout\Element::TYPE_CONTAINER,
            $this->_layout->getElementProperty($name, 'type')
        );
        $this->assertSame(2, $this->_layout->getElementProperty($name, 'option2'));

        $this->_layout->addBlock(\Magento\Framework\View\Element\Text::class, 'text', $name);
        $this->assertEquals(
            \Magento\Framework\View\Layout\Element::TYPE_BLOCK,
            $this->_layout->getElementProperty('text', 'type')
        );
        $this->assertSame(
            ['text' => 'text'],
            $this->_layout->getElementProperty($name, \Magento\Framework\Data\Structure::CHILDREN)
        );
    }

    /**
     * @magentoAppIsolation enabled
     */
    public function testIsBlock()
    {
        $this->assertFalse($this->_layout->isBlock('container'));
        $this->assertFalse($this->_layout->isBlock('block'));
        $this->_layout->addContainer('container', 'Container');
        $this->_layout->addBlock(\Magento\Framework\View\Element\Text::class, 'block');
        $this->assertFalse($this->_layout->isBlock('container'));
        $this->assertTrue($this->_layout->isBlock('block'));
    }

    public function testSetUnsetBlock()
    {
        $expectedBlockName = 'block_' . __METHOD__;
        $expectedBlock = $this->_layout->createBlock(\Magento\Framework\View\Element\Text::class);

        $this->_layout->setBlock($expectedBlockName, $expectedBlock);
        $this->assertSame($expectedBlock, $this->_layout->getBlock($expectedBlockName));

        $this->_layout->unsetElement($expectedBlockName);
        $this->assertFalse($this->_layout->getBlock($expectedBlockName));
        $this->assertFalse($this->_layout->hasElement($expectedBlockName));
    }

    /**
     * @dataProvider createBlockDataProvider
     */
    public function testCreateBlock($blockType, $blockName, array $blockData, $expectedName)
    {
        $expectedData = $blockData + ['type' => $blockType];

        $block = $this->_layout->createBlock($blockType, $blockName, ['data' => $blockData]);

        $this->assertRegExp($expectedName, $block->getNameInLayout());
        $this->assertEquals($expectedData, $block->getData());
    }

    public function createBlockDataProvider()
    {
        return [
            'named block' => [\Magento\Framework\View\Element\Template::class,
                'some_block_name_full_class',
                ['type' => \Magento\Framework\View\Element\Template::class, 'is_anonymous' => false],
                '/^some_block_name_full_class$/',
            ],
            'no name block' => [\Magento\Framework\View\Element\Text\ListText::class,
                '',
                ['type' => \Magento\Framework\View\Element\Text\ListText::class, 'key1' => 'value1'],
                '/text\\\\list/',
            ]
        ];
    }

    /**
     * @dataProvider blockNotExistsDataProvider
     * @expectedException \Magento\Framework\Exception\LocalizedException
     */
    public function testCreateBlockNotExists($name)
    {
        $this->_layout->createBlock($name);
    }

    public function blockNotExistsDataProvider()
    {
        return [[''], ['block_not_exists']];
    }

    public function testAddBlock()
    {
        $this->assertInstanceOf(
            \Magento\Framework\View\Element\Text::class,
            $this->_layout->addBlock(\Magento\Framework\View\Element\Text::class, 'block1')
        );
        $block2 = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(
            \Magento\Framework\View\Element\Text::class
        );
        $block2->setNameInLayout('block2');
        $this->_layout->addBlock($block2, '', 'block1');

        $this->assertTrue($this->_layout->hasElement('block1'));
        $this->assertTrue($this->_layout->hasElement('block2'));
        $this->assertEquals('block1', $this->_layout->getParentName('block2'));
    }

    /**
     * @magentoAppIsolation enabled
     * @dataProvider addContainerDataProvider()
     */
    public function testAddContainer($htmlTag)
    {
        $this->assertFalse($this->_layout->hasElement('container'));
        $this->_layout->addContainer('container', 'Container', ['htmlTag' => $htmlTag]);
        $this->assertTrue($this->_layout->hasElement('container'));
        $this->assertTrue($this->_layout->isContainer('container'));
        $this->assertEquals($htmlTag, $this->_layout->getElementProperty('container', 'htmlTag'));

        $this->_layout->addContainer('container1', 'Container 1', [], 'container', 'c1');
        $this->assertEquals('container1', $this->_layout->getChildName('container', 'c1'));
    }

    public function addContainerDataProvider()
    {
        return [
            ['dd'],
            ['div'],
            ['dl'],
            ['fieldset'],
            ['header'],
            ['ol'],
            ['p'],
            ['section'],
            ['table'],
            ['tfoot'],
            ['ul']
        ];
    }

    /**
     * @magentoAppIsolation enabled
     */
    public function testAddContainerInvalidHtmlTag()
    {
        $msg = 'Html tag "span" is forbidden for usage in containers. ' .
            'Consider to use one of the allowed: aside, dd, div, dl, fieldset, main, nav, ' .
            'header, footer, ol, p, section, table, tfoot, ul.';
        $this->expectException(\Magento\Framework\Exception\LocalizedException::class);
        $this->expectExceptionMessage($msg);
        $this->_layout->addContainer('container', 'Container', ['htmlTag' => 'span']);
    }

    /**
     * @magentoAppIsolation enabled
     */
    public function testGetChildBlock()
    {
        $this->_layout->addContainer('parent', 'Parent');
        $block = $this->_layout->addBlock(
            \Magento\Framework\View\Element\Text::class,
            'block',
            'parent',
            'block_alias'
        );
        $this->_layout->addContainer('container', 'Container', [], 'parent', 'container_alias');
        $this->assertSame($block, $this->_layout->getChildBlock('parent', 'block_alias'));
        $this->assertFalse($this->_layout->getChildBlock('parent', 'container_alias'));
    }

    /**
     * @return \Magento\Framework\View\Layout
     */
    public function testSetChild()
    {
        $this->_layout->addContainer('one', 'One');
        $this->_layout->addContainer('two', 'Two');
        $this->_layout->addContainer('three', 'Three');
        $this->assertSame($this->_layout, $this->_layout->setChild('one', 'two', ''));
        $this->_layout->setChild('one', 'three', 'three_alias');
        $this->assertSame(['two', 'three'], $this->_layout->getChildNames('one'));
        return $this->_layout;
    }

    /**
     * @param \Magento\Framework\View\LayoutInterface $layout
     * @depends testSetChild
     */
    public function testReorderChild(\Magento\Framework\View\LayoutInterface $layout)
    {
        $layout->addContainer('four', 'Four', [], 'one');

        // offset +1
        $layout->reorderChild('one', 'four', 1);
        $this->assertSame(['two', 'four', 'three'], $layout->getChildNames('one'));

        // offset -2
        $layout->reorderChild('one', 'three', 2, false);
        $this->assertSame(['two', 'three', 'four'], $layout->getChildNames('one'));

        // after sibling
        $layout->reorderChild('one', 'two', 'three');
        $this->assertSame(['three', 'two', 'four'], $layout->getChildNames('one'));

        // after everyone
        $layout->reorderChild('one', 'three', '-');
        $this->assertSame(['two', 'four', 'three'], $layout->getChildNames('one'));

        // before sibling
        $layout->reorderChild('one', 'four', 'two', false);
        $this->assertSame(['four', 'two', 'three'], $layout->getChildNames('one'));

        // before everyone
        $layout->reorderChild('one', 'two', '-', false);
        $this->assertSame(['two', 'four', 'three'], $layout->getChildNames('one'));

        //reorder by sibling alias
        $layout->reorderChild('one', 'two', 'three_alias', true);
        $this->assertSame(['four', 'three', 'two'], $layout->getChildNames('one'));
    }

    /**
     * @magentoAppIsolation enabled
     */
    public function testGetChildBlocks()
    {
        $this->_layout->addContainer('parent', 'Parent');
        $block1 = $this->_layout->addBlock(\Magento\Framework\View\Element\Text::class, 'block1', 'parent');
        $this->_layout->addContainer('container', 'Container', [], 'parent');
        $block2 = $this->_layout->addBlock(\Magento\Framework\View\Element\Template::class, 'block2', 'parent');
        $this->assertSame(['block1' => $block1, 'block2' => $block2], $this->_layout->getChildBlocks('parent'));
    }

    /**
     * @expectedException \Magento\Framework\Exception\LocalizedException
     */
    public function testAddBlockInvalidType()
    {
        $this->_layout->addBlock('invalid_name', 'child');
    }

    /**
     * @magentoAppIsolation enabled
     */
    public function testIsContainer()
    {
        $block = 'block';
        $container = 'container';
        $this->_layout->addBlock(\Magento\Framework\View\Element\Text::class, $block);
        $this->_layout->addContainer($container, 'Container');
        $this->assertFalse($this->_layout->isContainer($block));
        $this->assertTrue($this->_layout->isContainer($container));
        $this->assertFalse($this->_layout->isContainer('invalid_name'));
    }

    public function testIsManipulationAllowed()
    {
        $this->_layout->addBlock(\Magento\Framework\View\Element\Text::class, 'block1');
        $this->_layout->addBlock(\Magento\Framework\View\Element\Text::class, 'block2', 'block1');
        $this->assertFalse($this->_layout->isManipulationAllowed('block1'));
        $this->assertFalse($this->_layout->isManipulationAllowed('block2'));

        $this->_layout->addContainer('container1', 'Container 1');
        $this->_layout->addBlock(\Magento\Framework\View\Element\Text::class, 'block3', 'container1');
        $this->_layout->addContainer('container2', 'Container 2', [], 'container1');
        $this->assertFalse($this->_layout->isManipulationAllowed('container1'));
        $this->assertTrue($this->_layout->isManipulationAllowed('block3'));
        $this->assertTrue($this->_layout->isManipulationAllowed('container2'));
    }

    public function testRenameElement()
    {
        $blockName = 'block';
        $expBlockName = 'block_renamed';
        $containerName = 'container';
        $expContainerName = 'container_renamed';
        $block = $this->_layout->createBlock(\Magento\Framework\View\Element\Text::class, $blockName);
        $this->_layout->addContainer($containerName, 'Container');

        $this->assertEquals($block, $this->_layout->getBlock($blockName));
        $this->_layout->renameElement($blockName, $expBlockName);
        $this->assertEquals($block, $this->_layout->getBlock($expBlockName));

        $this->_layout->hasElement($containerName);
        $this->_layout->renameElement($containerName, $expContainerName);
        $this->_layout->hasElement($expContainerName);
    }

    public function testGetBlock()
    {
        $this->assertFalse($this->_layout->getBlock('test'));
        $block = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(
            \Magento\Framework\View\Layout::class
        )->createBlock(
            \Magento\Framework\View\Element\Text::class
        );
        $this->_layout->setBlock('test', $block);
        $this->assertSame($block, $this->_layout->getBlock('test'));
    }

    /**
     * @magentoAppIsolation enabled
     */
    public function testGetParentName()
    {
        $this->_layout->addContainer('one', 'One');
        $this->_layout->addContainer('two', 'Two', [], 'one');
        $this->assertFalse($this->_layout->getParentName('one'));
        $this->assertEquals('one', $this->_layout->getParentName('two'));
    }

    public function testGetElementAlias()
    {
        $this->_layout->addContainer('one', 'One');
        $this->_layout->addContainer('two', 'One', [], 'one', '1');
        $this->assertFalse($this->_layout->getElementAlias('one'));
        $this->assertEquals('1', $this->_layout->getElementAlias('two'));
    }

    /**
     * @covers \Magento\Framework\View\Layout::addOutputElement
     * @covers \Magento\Framework\View\Layout::getOutput
     * @covers \Magento\Framework\View\Layout::removeOutputElement
     */
    public function testGetOutput()
    {
        $blockName = 'block_' . __METHOD__;
        $expectedText = "some_text_for_{$blockName}";

        $block = $this->_layout->addBlock(\Magento\Framework\View\Element\Text::class, $blockName);
        $block->setText($expectedText);

        $this->_layout->addOutputElement($blockName);
        // add the same element twice should not produce output duplicate
        $this->_layout->addOutputElement($blockName);
        $this->assertEquals($expectedText, $this->_layout->getOutput());

        $this->_layout->removeOutputElement($blockName);
        $this->assertEmpty($this->_layout->getOutput());
    }

    public function testGetMessagesBlock()
    {
        $this->assertInstanceOf(\Magento\Framework\View\Element\Messages::class, $this->_layout->getMessagesBlock());
    }

    public function testGetBlockSingleton()
    {
        $block = $this->_layout->getBlockSingleton(\Magento\Framework\View\Element\Text::class);
        $this->assertInstanceOf(\Magento\Framework\View\Element\Text::class, $block);
        $this->assertSame($block, $this->_layout->getBlockSingleton(\Magento\Framework\View\Element\Text::class));
    }

    public function testUpdateContainerAttributes()
    {
        $this->_layout->setXml(
            simplexml_load_file(
                __DIR__ . '/_files/layout/container_attributes.xml',
                \Magento\Framework\View\Layout\Element::class
            )
        );
        $this->_layout->generateElements();
        $result = $this->_layout->renderElement('container1', false);
        $this->assertEquals('<div id="container1-2" class="class12">Test11Test12</div>', $result);
        $result = $this->_layout->renderElement('container2', false);
        $this->assertEquals('<div id="container2-2" class="class22">Test21Test22</div>', $result);
    }

    public function testIsCacheable()
    {
        $this->_layout->setXml(
            simplexml_load_file(
                __DIR__ . '/_files/layout/cacheable.xml',
                \Magento\Framework\View\Layout\Element::class
            )
        );
        $this->_layout->generateElements();
        $this->assertTrue($this->_layout->isCacheable());
    }

    public function testIsNonCacheable()
    {
        $this->_layout->setXml(
            simplexml_load_file(
                __DIR__ . '/_files/layout/non_cacheable.xml',
                \Magento\Framework\View\Layout\Element::class
            )
        );
        $this->_layout->generateElements();
        $this->assertFalse($this->_layout->isCacheable());
    }
}