<?php

class DefaultProcesserTest extends \PHPUnit\Framework\TestCase
{
	use \Dalten\TestHelper\DomDocumentTestTrait;

	/**
	 * @var Dalten_Config_Processer_DefaultProcesser
	 */
	private $_processer;

    /**
	 * Obsah configu s číselníky.
	 *
	 * @var array
	 */
	private $_codebookArray = array(
		'Common' => array(
			'yesnomaybe' => array('?', -1 => 'Ne', 1 => 'Ano'),
			'currency' => array(1 => 'CZK', 2 => 'EUR', 3 => 'USD'),
			'yesno' => array('Ne', 'Ano')
		)
	);

	/**
	 * @var \PHPUnit\Framework\MockObject\MockObject&\Dalten_Config_Processer_DefaultFunctions
	 */
	private $_processerFunctions;

	public function setUp(): void
	{
		$parser = new Serenity_Config_Parser_Ini();
		$codebookParser = $this->getMockBuilder('Serenity_Config_Parser_Ini')
			->disableOriginalConstructor()
			->getMock();

		$codebookData = $this->_convertArrayToObject($this->_codebookArray);
		foreach ($codebookData->Common as $name => $value) {
			$codebookData->Common->$name = new ArrayObject($value, ArrayObject::ARRAY_AS_PROPS);
		}

		$codebookParser->expects($this->any())->method('parse')->will($this->returnValue($codebookData));
		$codebooks = new Serenity_Config_Config(__FILE__, $codebookParser);
		$this->_processerFunctions = $this->getMockBuilder('Dalten_Config_Processer_DefaultFunctions')
			->disableOriginalConstructor()
			->getMock();
		$this->_processer = new Dalten_Config_Processer_DefaultProcesser($codebooks, $this->_processerFunctions);
	}

	public function testProcesserCanUnsetItems()
	{
		$values = array('id' => 0);
		$convert = array(
			'id' => array(
				'column' => 'result_id',
				'type' => 'int',
			)
		);

		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($convert));
		$this->assertTrue(array_key_exists('result_id', $result));

		$convert['id']['unset'] = array(0, null);

		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($convert));
		$this->assertFalse(array_key_exists('result_id', $result));

		$values = array('id' => null);

		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($convert));
		$this->assertFalse(array_key_exists('result_id', $result));
	}

	public function testProcesserUnsetsItemsOnlyIfUnsetHasItemsValue()
	{
		$values = array('id' => 10);
		$convert = array(
			'id' => array(
				'column' => 'result_id',
				'type' => 'int',
				'unset' => array(0, null)
			)
		);

		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($convert));

		$this->assertTrue(array_key_exists('result_id', $result));
	}

	public function testProcesserUnsetsItemsAfterColumnsValueHasBeenSet()
	{
		$values = array('id' => 20);
		$convert = array(
			'id' => array(
				'column' => 'result_id',
				'type' => 'int',
				'unset' => array(0, 20),
				'value' => 10
			)
		);

		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($convert));

		$this->assertTrue(array_key_exists('result_id', $result));
	}

	public function testProcesserInsertsVariablesPrefixedWithAmpersand()
	{
		$values = array(
			'id' => 20
		);
		$convert = array(
			'id' => array(
				'column' => 'result_id',
				'type' => 'int'
			),
			'variable' => array(
				'column' => 'cloned_variable',
				'type' => 'int',
				'value' => '@id'
			)
		);
		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($convert));

		$this->assertEquals(20, $result['result_id']);
		$this->assertEquals(20, $result['cloned_variable']);
	}

	public function testAtNullInValuesMakesTheValueEmpty()
	{
		$values = array(
			'val' => '_irrelevant_'
		);

		$convert = array(
			'id' => array(
				'column' => 'result_id',
				'value' => '@null'
			)
		);

		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($convert));

		$this->assertEmpty($result['result_id']);
	}

	public function testAtNullInValuesMakesTheValueEmptyWithCorrectTypeIsSet()
	{
		$values = array(
			'val' => '_irrelevant_'
		);

		$convert = array(
			'id' => array(
				'column' => 'result_id',
				'type' => 'int',
				'value' => '@null'
			)
		);

		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($convert));

		$this->assertSame(0, $result['result_id']);
	}

	public function testAtThisMakesValueSameAsColumnsValue()
	{
		$values = array(
			'id' => 'some_value'
		);

		$convert = array(
			'id' => array(
				'column' => 'result_id',
				'value' => '@this'
			)
		);

		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($convert));

		$this->assertEquals('some_value', $result['result_id']);
	}

	public function testValueCanContainAtSignAndOtherDangerousCharacters()
	{
		$values = array(
			'id' => 'b@gr"\':;&#<@, ,>-|'
		);

		$convert = array(
			'id' => array(
				'column' => 'result_id'
			)
		);

		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($convert));

		$this->assertEquals('b@gr"\':;&#<@, ,>-|', $result['result_id']);
	}

	public function testConditionEvaluation()
	{
		$values = array(
			'price' => 10000
		);
		$config = array(
			'price' => array(
				'column' => 'converted_price',
				'value' => '@this ? @this : -1'
			)
		);

		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($config));

		$this->assertEquals(10000, $result['converted_price']);

		$values = array(
			'price' => 0
		);
		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($config));

		$this->assertEquals('-1', $result['converted_price']);
	}

	public function donttestProcessCanInsertCodebookValues()
	{
		$this->markTestSkipped('Parsování codebooků je prozatím vypnuto.');
		$config = array(
			'currency' => array(
				'column' => 'result_currency',
				'type' => 'string',
				'value' => '#Common.currency'
			)
		);

		$values = array(
			'currency' => 1
		);
		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($config));

		$this->assertEquals('CZK', $result['result_currency']);

		$values = array(
			'currency' => 2
		);
		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($config));

		$this->assertEquals('EUR', $result['result_currency']);

		$values = array(
			'currency' => 3
		);
		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($config));

		$this->assertEquals('USD', $result['result_currency']);
	}

	public function donttestProcessInsertsEmptyStringIfCodebookValueIsNotFound()
	{
		$this->markTestSkipped('Parsování codebooků je prozatím vypnuto.');
		$config = array(
			'currency' => array(
				'column' => 'result_currency',
				'type' => 'string',
				'value' => '#Common.currency'
			)
		);

		$values = array(
			'currency' => 4
		);

		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($config));

		$this->assertEquals('', $result['result_currency']);
	}

	public function testProcesserCanCallFunctions()
	{
		$config = array(
			'text' => array(
				'column' => 'result_text',
				'type' => 'string',
				'value' => 'substr<@this, 1, 3>'
			)
		);
		$values = array('text' => 'bagrbagr');
		$this->_processerFunctions->expects($this->once())->method('substr')->with('bagrbagr', 1, 3)
			->will($this->returnValue('agr'));

		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($config));
		$this->assertEquals('agr', $result['result_text']);
	}

	public function testProcesserCanCallNestedFunctions()
	{
		$config = array(
			'text' => array(
				'column' => 'result_text',
				'type' => 'string',
				'value' => 'substr<substr<@this, 1, 3>, 0, 2>'
			)
		);
		$values = array('text' => "bagr'bagr");

		$this->_processerFunctions->expects($this->at(0))->method('substr')->with("bagr'bagr", 1, 3)
			->will($this->returnValue('agr'));
		$this->_processerFunctions->expects($this->at(1))->method('substr')->with('agr', 0, 2)
			->will($this->returnValue('ag'));
		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($config));
		$this->assertEquals('ag', $result['result_text']);
	}

	public function testProcesserCanSetVariableTypes()
	{
		$values = array(
			'int' => '10',
			'float' => '2.5',
			'string' => 1,
			'array' => array('bagr'),
			'bool' => 99
		);
		$config = array(
			'int' => array('column' => 'result_int', 'type' => 'int'),
			'float' => array('column' => 'result_float', 'type' => 'float'),
			'string' => array('column' => 'result_string', 'type' => 'string'),
			'array' => array('column' => 'result_array', 'type' => 'array'),
			'bool' => array('column' => 'result_bool', 'type' => 'bool')
		);

		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($config));

		$expected = array(
			'result_int' => 10,
			'result_float' => 2.5,
			'result_string' => '1',
			'result_array' => array('bagr'),
			'result_bool' => true
		);

		foreach ($expected as $propertyName => $value) {
			$this->assertSame($value, $result[$propertyName]);
		}
	}

	public function testProcesserCanSetTypesOfValuesOfAnArray()
	{
		$values = array(
			'list' => array('2.5', '3.14', '5')
		);
		$config = array(
			'list' => array('column' => 'result_list', 'type' => 'array of float'),
		);

		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($config));

		$expected = array('result_list' => array(2.5, 3.14, (float) 5));
		$this->assertEquals($expected, $result);
	}

	public function testProcesserCanCallFunctionsInTernaryExpressions()
	{
		$values = array(
			'date' => 5
		);

		$config = array(
			'date' => array(
				'column' => 'result_date',
				'type' => 'string',
				'value' => 'date<m-d-Y, @this> == 01-01-1970 ? substr<ahoj, 1> : 0'
			)
		);

		$this->_processerFunctions->expects($this->at(0))->method('date')->with('m-d-Y', 5)
			->will($this->returnValue('01-01-1970'));
		$this->_processerFunctions->expects($this->at(1))->method('substr')->with('ahoj', 1)
			->will($this->returnValue('a'));

		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($config));

		$this->assertEquals('a', $result['result_date']);
	}

	public function testExpressionsInUnsetAreParsed()
	{
		$values = array(
			'test' => 10
		);

		$config = array(
			'test' => array(
				'column' => 'result_test',
				'type' => 'string',
				'value' => 5,
				'unset' => '@this == 5 ? @this : @null'
			)
		);

		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($config));

		$this->assertArrayNotHasKey('test', $result);
	}

	public function testMultipleSameColumnsCanBeDefinedAndOnlyTheNotunsetValueWillBeUsed()
	{
		$values = array(
			'test' => 10,
			'test2' => 99
		);

		$config = array(
			'test' => array(
				'column' => 'result_test',
				'type' => 'string',
				'value' => 15,
				'unset' => '@this == 5 ? @this : @null'
			),
			'test2' => array(
				'column' => 'result_test',
				'type' => 'string',
				'value' => 5,
				'unset' => '@this == 5 ? @this : @null'
			)
		);

		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($config));

		$this->assertSame('5', $result['result_test']);
	}


	public function testProcessorDoesNotRecognizeTernaryExpressionInPropertyValues()
	{
		$config = array(
			'test' => array(
				'column' => 'result_test',
				'type' => 'string',
				'value' => '@this ? @this : 3'
			)
		);

		$result = $this->_processer->processValues(
			array('test' => 'Ahoj. Máš se? Dobře:Špatně.'), $this->_convertArrayToObject($config)
		);
		$this->assertSame('Ahoj. Máš se? Dobře:Špatně.', $result['result_test']);
	}

	/**
	 * @ticket 3023
	 */
	public function testReplaceVariables()
	{
		/*
			foo.column='foo'
			foo.type='string'
			foo.value='"@this" ? ANO : NE'
		 */

		$config = array(
			'test' => array(
				'column' => 'result_test',
				'type' => 'string',
				'value' => '"@this" ? Ano : Ne'
			)
		);

		$result = $this->_processer->processValues(
			array('test' => 'Bagr'), $this->_convertArrayToObject($config)
		);
		$this->assertSame('Ano', $result['result_test']);

		// Dříve bylo na vše "Ne"

		$result = $this->_processer->processValues(
			array('test' => '0'), $this->_convertArrayToObject($config)
		);
		$this->assertSame('Ne', $result['result_test']);
	}


	public function testProcessorAllowExpressionsInsideConditionPartOfTernaryStatement()
	{
		$config = array(
			'nemovitost_balkon_plocha' => array(
				'column' => 'result_test',
				'type' => 'int',
				'unset' => '@nemovitost_balkon > 0 ? @null : @this'
			)
		);

		$values = array('nemovitost_balkon' => 0, 'nemovitost_balkon_plocha' => 15);
		$result = $this->_processer->processValues(
			$values, $this->_convertArrayToObject($config)
		);

		$this->assertArrayNotHasKey('result_test', $result);
	}

	/**
	 * Fix špatného handlování. Nenastavená value (= null) se vkládala jako null, čímž vznikla pro eval podmínka
	 * ' > 0', což je parse error. Správně by mělo být 'null > 0'.
	 */
	public function testProcessorCanHandleExpressionsWithUnsetKeys()
	{
		$config = array(
			'nemovitost_balkon_plocha' => array(
				'column' => 'result_test',
				'type' => 'int',
				'unset' => '@nemovitost_balkon > 0 ? @null : @this'
			)
		);

		$values = array('nemovitost_balkon' => null, 'nemovitost_balkon_plocha' => 15);
		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($config));

		$this->assertArrayNotHasKey('result_test', $result);
	}

	/**
	 * Fix špatného handlování. Nenastavená value (= null) se vkládala jako null, čímž vznikla pro eval podmínka
	 * ' > 0', což je parse error. Správně by mělo být 'null > 0'.
	 */
	public function testProcessorCanHandleExpressionsWithCurrentValueEqualToNull()
	{
		$config = array(
			'nemovitost_balkon' => array(
				'column' => 'result_test',
				'type' => 'int',
				'value' => '@this > 0 ? 1 :2'
			)
		);

		$values = array('nemovitost_balkon' => null);
		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($config));

		$this->assertArrayHasKey('result_test', $result);
		$this->assertEquals(2, $result['result_test']);
	}

	public function testProcessorAllowsBoolAsShortcutForBooleanType()
	{
		$config = array(
			'nemovitost_balkon_plocha' => array(
				'column' => 'result_test_bool',
				'type' => 'bool',
			),
			'nemovitost_balkon' => array(
				'column' => 'result_test_boolean',
				'type' => 'bool',
			)
		);

		$values = array('nemovitost_balkon_plocha' => '15', 'nemovitost_balkon' => '');
		$result = $this->_processer->processValues(
			$values, $this->_convertArrayToObject($config)
		);

		$this->assertArrayHasKey('result_test_bool', $result);
		$this->assertIsBool($result['result_test_bool']);
		$this->assertArrayHasKey('result_test_boolean', $result);
		$this->assertIsBool($result['result_test_boolean']);
	}

	public function testFunctionsCanBeUsedInUnset()
	{
		$values = array(
			'test' => 15
		);

		$config = array(
			'test' => array(
				'column' => 'result_test',
				'type' => 'string',
				'value' => '5',
				'unset' => 'inList<@this, 2, 15> ? 5 : @null'
			)
		);

		$this->_processerFunctions->expects($this->once())->method('inList')->with('15', 2, 15)
			->will($this->returnValue(1));

		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($config));

		$this->assertArrayNotHasKey('test', $result);
	}

	public function testProcesserCanHandleValuesWithCommasInFunctionArgumentList()
	{
		$values = array(
			'test' => 'Ahoj, vole'
		);

		$config = array(
			'test' => array(
				'column' => 'result_test',
				'type' => 'string',
				'value' => 'substr<@this, 0, 4> ? 5 : @null'
			)
		);

		$this->_processerFunctions->expects($this->once())->method('substr')->with('Ahoj, vole', 0, 4)
			->will($this->returnValue('Ahoj'));

		$this->_processer->processValues($values, $this->_convertArrayToObject($config));
	}

	public function testProcesserCanHandleStringValuesWithNumberComparison()
	{
		$values = array('test' => '');

		$config = array(
			'test' => array(
				'column' => 'result_test',
				'type' => 'string',
				'value' => '@this<1 ? @this : @null'
			)
		);

		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($config));
		$this->assertSame(['result_test' => ''], $result);
	}

	public function testAtUnderscoreConvertedValueIsAddedToTheListOfValuesInUnset()
	{
		$values = array(
			'test' => 10
		);

		$config = array(
			'test' => array(
				'column' => 'result_test',
				'type' => 'string',
				'value' => '5',
				'unset' => '@_convertedValue == 5 ? @_convertedValue : @null'
			)
		);

		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($config));

		$this->assertArrayNotHasKey('test', $result);
	}

	public function testAtThisIsAlwaysTheOriginalValueFromTheValuesArray()
	{
		$values = array(
			'test' => 10
		);

		$config = array(
			'test' => array(
				'column' => 'result_test',
				'type' => 'string',
				'value' => '5',
				'unset' => '@this == 10 ? @_convertedValue : @null'
			)
		);

		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($config));

		$this->assertArrayNotHasKey('result_test', $result);
	}

	public function testUnsetCanHandleValuesWithDoubleColonsAndSpaces()
	{
		$values = array(
			'test' => 10
		);

		$config = array(
			'test' => array(
				'column' => 'result_test',
				'type' => 'string',
				'value' => '1970-01-01 15:50:00',
				'unset' => '1970-01-01 15:50:00'
			)
		);

		$result = $this->_processer->processValues($values, $this->_convertArrayToObject($config));

		$this->assertArrayNotHasKey('result_test', $result);
	}

	public function testProcesserDoesNotAlterConfig()
	{
		$values = array(
			'test' => 10
		);

		$config = array(
			'test' => array(
				'column' => 'result_test',
				'type' => 'string',
				'value' => '5',
				'unset' => '@this == 10 ? @_convertedValue : @null'
			)
		);

		$originalConfig = $this->_convertArrayToObject($config);

		$this->_processer->processValues($values, $originalConfig);

		$this->assertEquals($this->_convertArrayToObject($config), $originalConfig);
	}

	/**
	 * Převede rekurzivně pole na objekt.
	 *
	 * @param array $array Pole na převedení.
	 *
	 * @return stdClass Převedený objekt.
	 */
	private function _convertArrayToObject(array $array)
	{
		$return = array();
		foreach ($array as $key => $value) {
			if (is_array($value)) {
				$return[$key] = $this->_convertArrayToObject($value);
			} else {
				$return[$key] = $value;
			}
		}

		return (object) $return;
	}
}
