<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Tests\Definition;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\Definition\ArrayNode;
use Symfony\Component\Config\Definition\PrototypedArrayNode;
use Symfony\Component\Config\Definition\ScalarNode;
use Symfony\Component\Config\Definition\VariableNode;

class PrototypedArrayNodeTest extends TestCase
{
    public function testGetDefaultValueReturnsAnEmptyArrayForPrototypes()
    {
        $node = new PrototypedArrayNode('root');
        $prototype = new ArrayNode(null, $node);
        $node->setPrototype($prototype);
        $this->assertEmpty($node->getDefaultValue());
    }

    public function testGetDefaultValueReturnsDefaultValueForPrototypes()
    {
        $node = new PrototypedArrayNode('root');
        $prototype = new ArrayNode(null, $node);
        $node->setPrototype($prototype);
        $node->setDefaultValue(['test']);
        $this->assertEquals(['test'], $node->getDefaultValue());
    }

    // a remapped key (e.g. "mapping" -> "mappings") should be unset after being used
    public function testRemappedKeysAreUnset()
    {
        $node = new ArrayNode('root');
        $mappingsNode = new PrototypedArrayNode('mappings');
        $node->addChild($mappingsNode);

        // each item under mappings is just a scalar
        $prototype = new ScalarNode(null, $mappingsNode);
        $mappingsNode->setPrototype($prototype);

        $remappings = [];
        $remappings[] = ['mapping', 'mappings'];
        $node->setXmlRemappings($remappings);

        $normalized = $node->normalize(['mapping' => ['foo', 'bar']]);
        $this->assertEquals(['mappings' => ['foo', 'bar']], $normalized);
    }

    /**
     * Tests that when a key attribute is mapped, that key is removed from the array.
     *
     *     <things>
     *         <option id="option1" value="foo">
     *         <option id="option2" value="bar">
     *     </things>
     *
     * The above should finally be mapped to an array that looks like this
     * (because "id" is the key attribute).
     *
     *     [
     *         'things' => [
     *             'option1' => 'foo',
     *             'option2' => 'bar',
     *         ]
     *     ]
     */
    public function testMappedAttributeKeyIsRemoved()
    {
        $node = new PrototypedArrayNode('root');
        $node->setKeyAttribute('id', true);

        // each item under the root is an array, with one scalar item
        $prototype = new ArrayNode(null, $node);
        $prototype->addChild(new ScalarNode('foo'));
        $node->setPrototype($prototype);

        $children = [];
        $children[] = ['id' => 'item_name', 'foo' => 'bar'];
        $normalized = $node->normalize($children);

        $expected = [];
        $expected['item_name'] = ['foo' => 'bar'];
        $this->assertEquals($expected, $normalized);
    }

    /**
     * Tests the opposite of the testMappedAttributeKeyIsRemoved because
     * the removal can be toggled with an option.
     */
    public function testMappedAttributeKeyNotRemoved()
    {
        $node = new PrototypedArrayNode('root');
        $node->setKeyAttribute('id', false);

        // each item under the root is an array, with two scalar items
        $prototype = new ArrayNode(null, $node);
        $prototype->addChild(new ScalarNode('foo'));
        $prototype->addChild(new ScalarNode('id')); // the key attribute will remain
        $node->setPrototype($prototype);

        $children = [];
        $children[] = ['id' => 'item_name', 'foo' => 'bar'];
        $normalized = $node->normalize($children);

        $expected = [];
        $expected['item_name'] = ['id' => 'item_name', 'foo' => 'bar'];
        $this->assertEquals($expected, $normalized);
    }

    public function testAddDefaultChildren()
    {
        $node = $this->getPrototypeNodeWithDefaultChildren();
        $node->setAddChildrenIfNoneSet();
        $this->assertTrue($node->hasDefaultValue());
        $this->assertEquals([['foo' => 'bar']], $node->getDefaultValue());

        $node = $this->getPrototypeNodeWithDefaultChildren();
        $node->setKeyAttribute('foobar');
        $node->setAddChildrenIfNoneSet();
        $this->assertTrue($node->hasDefaultValue());
        $this->assertEquals(['defaults' => ['foo' => 'bar']], $node->getDefaultValue());

        $node = $this->getPrototypeNodeWithDefaultChildren();
        $node->setKeyAttribute('foobar');
        $node->setAddChildrenIfNoneSet('defaultkey');
        $this->assertTrue($node->hasDefaultValue());
        $this->assertEquals(['defaultkey' => ['foo' => 'bar']], $node->getDefaultValue());

        $node = $this->getPrototypeNodeWithDefaultChildren();
        $node->setKeyAttribute('foobar');
        $node->setAddChildrenIfNoneSet(['defaultkey']);
        $this->assertTrue($node->hasDefaultValue());
        $this->assertEquals(['defaultkey' => ['foo' => 'bar']], $node->getDefaultValue());

        $node = $this->getPrototypeNodeWithDefaultChildren();
        $node->setKeyAttribute('foobar');
        $node->setAddChildrenIfNoneSet(['dk1', 'dk2']);
        $this->assertTrue($node->hasDefaultValue());
        $this->assertEquals(['dk1' => ['foo' => 'bar'], 'dk2' => ['foo' => 'bar']], $node->getDefaultValue());

        $node = $this->getPrototypeNodeWithDefaultChildren();
        $node->setAddChildrenIfNoneSet([5, 6]);
        $this->assertTrue($node->hasDefaultValue());
        $this->assertEquals([0 => ['foo' => 'bar'], 1 => ['foo' => 'bar']], $node->getDefaultValue());

        $node = $this->getPrototypeNodeWithDefaultChildren();
        $node->setAddChildrenIfNoneSet(2);
        $this->assertTrue($node->hasDefaultValue());
        $this->assertEquals([['foo' => 'bar'], ['foo' => 'bar']], $node->getDefaultValue());
    }

    public function testDefaultChildrenWinsOverDefaultValue()
    {
        $node = $this->getPrototypeNodeWithDefaultChildren();
        $node->setAddChildrenIfNoneSet();
        $node->setDefaultValue(['bar' => 'foo']);
        $this->assertTrue($node->hasDefaultValue());
        $this->assertEquals([['foo' => 'bar']], $node->getDefaultValue());
    }

    protected function getPrototypeNodeWithDefaultChildren()
    {
        $node = new PrototypedArrayNode('root');
        $prototype = new ArrayNode(null, $node);
        $child = new ScalarNode('foo');
        $child->setDefaultValue('bar');
        $prototype->addChild($child);
        $prototype->setAddIfNotSet(true);
        $node->setPrototype($prototype);

        return $node;
    }

    /**
     * Tests that when a key attribute is mapped, that key is removed from the array.
     * And if only 'value' element is left in the array, it will replace its wrapper array.
     *
     *     <things>
     *         <option id="option1" value="value1">
     *     </things>
     *
     * The above should finally be mapped to an array that looks like this
     * (because "id" is the key attribute).
     *
     *     [
     *         'things' => [
     *             'option1' => 'value1'
     *         ]
     *     ]
     *
     * It's also possible to mix 'value-only' and 'non-value-only' elements in the array.
     *
     * <things>
     *     <option id="option1" value="value1">
     *     <option id="option2" value="value2" foo="foo2">
     * </things>
     *
     * The above should finally be mapped to an array as follows
     *
     * [
     *     'things' => [
     *         'option1' => 'value1',
     *         'option2' => [
     *             'value' => 'value2',
     *             'foo' => 'foo2'
     *         ]
     *     ]
     * ]
     *
     * The 'value' element can also be ArrayNode:
     *
     * <things>
     *     <option id="option1">
     *         <value>
     *            <foo>foo1</foo>
     *            <bar>bar1</bar>
     *         </value>
     *     </option>
     * </things>
     *
     * The above should be finally be mapped to an array as follows
     *
     * [
     *     'things' => [
     *         'option1' => [
     *             'foo' => 'foo1',
     *             'bar' => 'bar1'
     *         ]
     *     ]
     * ]
     *
     * If using VariableNode for value node, it's also possible to mix different types of value nodes:
     *
     * <things>
     *     <option id="option1">
     *         <value>
     *            <foo>foo1</foo>
     *            <bar>bar1</bar>
     *         </value>
     *     </option>
     *     <option id="option2" value="value2">
     * </things>
     *
     * The above should be finally mapped to an array as follows
     *
     * [
     *     'things' => [
     *         'option1' => [
     *             'foo' => 'foo1',
     *             'bar' => 'bar1'
     *         ],
     *         'option2' => 'value2'
     *     ]
     * ]
     *
     * @dataProvider getDataForKeyRemovedLeftValueOnly
     */
    public function testMappedAttributeKeyIsRemovedLeftValueOnly($value, array $children, array $expected)
    {
        $node = new PrototypedArrayNode('root');
        $node->setKeyAttribute('id', true);

        // each item under the root is an array, with one scalar item
        $prototype = new ArrayNode(null, $node);
        $prototype->addChild(new ScalarNode('id'));
        $prototype->addChild(new ScalarNode('foo'));
        $prototype->addChild($value);
        $node->setPrototype($prototype);

        $normalized = $node->normalize($children);
        $this->assertEquals($expected, $normalized);
    }

    public static function getDataForKeyRemovedLeftValueOnly(): array
    {
        $scalarValue = new ScalarNode('value');

        $arrayValue = new ArrayNode('value');
        $arrayValue->addChild(new ScalarNode('foo'));
        $arrayValue->addChild(new ScalarNode('bar'));

        $variableValue = new VariableNode('value');

        return [
           [
               $scalarValue,
               [
                   ['id' => 'option1', 'value' => 'value1'],
               ],
               ['option1' => 'value1'],
           ],

           [
               $scalarValue,
               [
                   ['id' => 'option1', 'value' => 'value1'],
                   ['id' => 'option2', 'value' => 'value2', 'foo' => 'foo2'],
               ],
               [
                   'option1' => 'value1',
                   'option2' => ['value' => 'value2', 'foo' => 'foo2'],
               ],
           ],

           [
               $arrayValue,
               [
                   [
                       'id' => 'option1',
                       'value' => ['foo' => 'foo1', 'bar' => 'bar1'],
                   ],
               ],
               [
                   'option1' => ['foo' => 'foo1', 'bar' => 'bar1'],
               ],
           ],

           [$variableValue,
               [
                   [
                       'id' => 'option1', 'value' => ['foo' => 'foo1', 'bar' => 'bar1'],
                   ],
                   ['id' => 'option2', 'value' => 'value2'],
               ],
               [
                   'option1' => ['foo' => 'foo1', 'bar' => 'bar1'],
                   'option2' => 'value2',
               ],
           ],
        ];
    }

    /**
     * @dataProvider getPrototypedArrayNodeDataToMerge
     */
    public function testPrototypedArrayNodeMerge(array $left, array $right, array $expected)
    {
        $node = new PrototypedArrayNode('options');
        $node->setNormalizeKeys(false);
        $node->setPrototype(new VariableNode('value'));
        $node->setDefaultValue([]);

        $result = $node->merge($left, $right);

        self::assertSame($result, $expected);
    }

    public static function getPrototypedArrayNodeDataToMerge(): array
    {
        return [
            // data to merged is a plain array
            [
                ['foo', 'bar'],
                ['foo', 'baz', 'qux'],
                ['foo', 'bar', 'foo', 'baz', 'qux'],
            ],
            // data to be merged is an associative array
            [
                ['option1' => true, 'option2' => 'foo'],
                [
                    'option2' => 'bar',
                    'option3' => 42,
                    'option4' => [
                        'option41' => 'baz',
                        'option42' => [
                            'option423' => 'qux',
                        ],
                    ],
                ],
                [
                    'option1' => true,
                    'option2' => 'bar',
                    'option3' => 42,
                    'option4' => [
                        'option41' => 'baz',
                        'option42' => [
                            'option423' => 'qux',
                        ],
                    ],
                ],
            ],
        ];
    }
}
