<?php

namespace Velis\Test;

use ArrayObject;
use DateTime;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use Velis\App;
use Velis\Arrays;
use Velis\Db\NullValue;
use Velis\Exception;
use Velis\Filter;
use Velis\Model\DataObject;

/**
 * Velis\Arrays test
 * @author Olek Procki <olo@velis.pl>
 */
class ArraysTest extends TestCase
{
    /**
     * @var array
     */
    protected $_input = [
        [
            'id' => '1',
            'name' => 'field1',
            'colors' => 'red',
        ],
        [
            'id' => '2',
            'name' => 'field2',
            'colors' => 'red',
        ],
        [
            'id' => '3',
            'name' => 'field3',
            'colors' => 'red',
        ],
        [
            'id' => '4',
            'name' => 'field4',
            'colors' => ['red', 'green', 'white'],
        ],
    ];

    /**
     * {@inheritdoc}
     */
    protected function setUp(): void
    {
        $_SERVER['ON_DEV'] = true;
    }

    /**
     * @param $source
     * @param $column
     * @param $expectedResult
     *
     * @dataProvider provideDataForHasColumnMethod
     */
    public function testHasColumn($source, $column, $expectedResult)
    {
        $actualResult = Arrays::hasColumn($source, $column);

        $this->assertEquals($expectedResult, $actualResult);
    }

    /**
     * @return array
     */
    public function provideDataForHasColumnMethod()
    {
        return [
            [[], 'key', false],
            [$this->_input, 'name', true],
            [$this->_input, 'desc', false],
            [
                [
                    self::createDataObjectInstance([
                        'id' => 1,
                        'name' => 'apple'
                    ]),
                    self::createDataObjectInstance([
                        'id' => 2,
                        'name' => 'banana',
                    ]),
                ],
                'name',
                true,
            ],
            [
                [
                    self::createDataObjectInstance([
                        'id' => 1,
                        'name' => 'apple'
                    ]),
                    self::createDataObjectInstance([
                        'id' => 2,
                        'name' => 'banana',
                    ]),
                ],
                'desc',
                false,
            ],
        ];
    }

    /**
     * @param $source
     * @param $column
     * @param $assocKey
     * @param $filter
     * @param $expectedResult
     *
     * @dataProvider provideDataForGetColumnMethod
     */
    public function testGetColumn($source, $column, $assocKey, $filter, $expectedResult)
    {
        $actualResult = Arrays::getColumn($source, $column, $assocKey, $filter);

        $this->assertEquals($expectedResult, $actualResult);
    }

    /**
     * @return array
     */
    public function provideDataForGetColumnMethod()
    {
        return [
            'source is null' => [null, 'id', null, true, []],
            'source is not an array' => ['array', 'id', null, true, []],
            'source is an empty array' => [[], 'id', null, true, []],
            'array of columns without assocKey and with filtering' => [
                [
                    [
                        'id' => 1,
                        'name' => 'apple'
                    ],
                    [
                        'id' => 2,
                        'name' => 'banana',
                    ],
                    [
                        'id' => 3,
                        'name' => null,
                    ],
                ],
                ['id', 'name'],
                null,
                true,
                [1, 2, 3, 'apple', 'banana'],
            ],
            'array of columns without assocKey and without filtering' => [
                [
                    [
                        'id' => 1,
                        'name' => 'apple'
                    ],
                    [
                        'id' => 2,
                        'name' => 'banana',
                    ],
                    [
                        'id' => 3,
                        'name' => null,
                    ],
                ],
                ['id', 'name'],
                null,
                false,
                [1, 2, 3, 'apple', 'banana', null],
            ],
            'array of columns with assocKey and filtering' => [
                [
                    [
                        'id' => 'id1',
                        'name' => 'first',
                        'colors' => 'blue',
                    ],
                    [
                        'id' => 'id2',
                        'name' => null,
                        'colors' => 'yellow',
                    ],
                    [
                        'id' => 'id3',
                        'name' => 'last',
                        'colors' => null,
                    ],
                    [
                        'id' => 'id4',
                        'name' => null,
                        'colors' => null,
                    ],
                ],
                ['name', 'colors'],
                'id',
                true,
                [
                    'id1' => 'blue',
                    'id2' => 'yellow',
                    'id3' => 'last',
                ],
            ],
            'array of columns with assocKey and without filtering' => [
                [
                    [
                        'id' => 'id1',
                        'name' => 'first',
                        'colors' => 'blue',
                    ],
                    [
                        'id' => 'id2',
                        'name' => null,
                        'colors' => 'yellow',
                    ],
                    [
                        'id' => 'id3',
                        'name' => 'last',
                        'colors' => null,
                    ],
                    [
                        'id' => 'id4',
                        'name' => null,
                        'colors' => null,
                    ],
                ],
                ['name', 'colors'],
                'id',
                false,
                [
                    'id1' => 'blue',
                    'id2' => 'yellow',
                    'id3' => null,
                    'id4' => null,
                ],
            ],
            'single column without assocKey and with filtering' => [
                [
                    [
                        'id' => 1,
                        'name' => 'apple'
                    ],
                    [
                        'id' => 2,
                        'name' => 'banana',
                    ],
                    [
                        'id' => 3,
                        'name' => null,
                    ],
                ],
                'name',
                null,
                true,
                [
                    'apple',
                    'banana',
                ],
            ],
            'single column without assocKey and without filtering' => [
                [
                    [
                        'id' => 1,
                        'name' => 'apple'
                    ],
                    [
                        'id' => 2,
                        'name' => 'banana',
                    ],
                    [
                        'id' => 3,
                        'name' => null,
                    ],
                ],
                'name',
                null,
                false,
                [
                    'apple',
                    'banana',
                    null,
                ],
            ],
            'single column with assocKey and filtering' => [
                [
                    [
                        'id' => 1,
                        'name' => 'apple'
                    ],
                    [
                        'id' => 2,
                        'name' => 'banana',
                    ],
                    [
                        'id' => 3,
                        'name' => null,
                    ],
                ],
                'name',
                'id',
                true,
                [
                    1 => 'apple',
                    2 => 'banana',
                ],
            ],
            'single column with assocKey and without filtering' => [
                [
                    [
                        'id' => 1,
                        'name' => 'apple'
                    ],
                    [
                        'id' => 2,
                        'name' => 'banana',
                    ],
                    [
                        'id' => 3,
                        'name' => null,
                    ],
                ],
                'name',
                'id',
                false,
                [
                    1 => 'apple',
                    2 => 'banana',
                    3 => null,
                ],
            ],
            'ArrayObject' => [
                new ArrayObject([
                    [
                        'id' => 1,
                        'name' => 'apple'
                    ],
                    [
                        'id' => 2,
                        'name' => 'banana',
                    ],
                    [
                        'id' => 3,
                        'name' => null,
                    ],
                ]),
                'name',
                null,
                true,
                [
                    'apple',
                    'banana',
                ],
            ],
            'array containing instances of DataObject' => [
                [
                    self::createDataObjectInstance([
                        'id' => 1,
                        'name' => 'apple'
                    ]),
                    self::createDataObjectInstance([
                        'id' => 2,
                        'name' => 'banana',
                    ]),
                    self::createDataObjectInstance([
                        'id' => 3,
                        'name' => null,
                    ]),
                ],
                'name',
                null,
                true,
                [
                    'apple',
                    'banana',
                ],
            ],
        ];
    }

    /**
     * @param $source
     * @param $column
     * @param $assocKey
     *
     * @dataProvider provideInvalidDataForGetColumnMethod
     */
    public function testGetColumnWhenDataAreInvalid($source, $column, $assocKey)
    {
        $this->expectException(Exception::class);

        Arrays::getColumn($source, $column, $assocKey);
    }

    /**
     * @return array
     */
    public function provideInvalidDataForGetColumnMethod()
    {
        return [
            'invalid column name' => [$this->_input, 'wrong', null],
            'invalid assocKey name' => [$this->_input, 'name', 'wrong'],
        ];
    }

    /**
     * @param array $paramArr
     * @param mixed $expectedResult
     *
     * @dataProvider provideDataForGetColumnsMethod
     */
    public function testGetColumns(array $paramArr, $expectedResult)
    {
        $actualResult = call_user_func_array([Arrays::class, 'getColumns'], $paramArr);

        $this->assertEquals($expectedResult, $actualResult);
    }

    /**
     * @return array
     */
    public function provideDataForGetColumnsMethod()
    {
        return [
            'no column' => [
                [$this->_input],
                [],
            ],
            'one column' => [
                [$this->_input, 'id'],
                ['1', '2', '3', '4']
            ],
            'two columns' => [
                [$this->_input, 'id', 'name'],
                ['1', '2', '3', '4', 'field1', 'field2', 'field3', 'field4']
            ],
        ];
    }

    /**
     * @param $source
     * @param $field
     * @param $value
     * @param $expectedResult
     *
     * @dataProvider provideDataForByValueMethod
     */
    public function testByValue($source, $field, $value, $expectedResult)
    {
        $actualResult = Arrays::byValue($source, $field, $value);

        $this->assertEquals($expectedResult, $actualResult);
    }

    /**
     * @return array
     */
    public function provideDataForByValueMethod()
    {
        return [
            'find by single value which is a scalar value in searched array' => [
                $this->_input,
                'name',
                'field2',
                [
                    1 => [
                        'id' => '2',
                        'name' => 'field2',
                        'colors' => 'red',
                    ],
                ],
            ],
            'find by single value which is contained within an array in searched array' => [
                $this->_input,
                'colors',
                'green',
                [
                    3 => [
                        'id' => '4',
                        'name' => 'field4',
                        'colors' => ['red', 'green', 'white'],
                    ],
                ],
            ],
            'find by array of values' => [
                [
                    [
                        'id' => '1',
                        'name' => 'field1',
                        'colors' => ['green', 'blue'],
                    ],
                    [
                        'id' => '2',
                        'name' => 'field2',
                        'colors' => ['red', 'green', 'white'],
                    ],
                ],
                ['colors' => ['pink', 'red']],
                null,
                [
                    1 => [
                        'id' => '2',
                        'name' => 'field2',
                        'colors' => ['red', 'green', 'white'],
                    ]
                ],
            ],
            'find by single value that cannot be found' => [
                $this->_input,
                ['colors' => 'turquoise'],
                null,
                [],
            ],
            'find by array of values of single field' => [
                [
                    [
                        'id' => '1',
                        'name' => 'field1',
                        'colors' => 'blue',
                    ],
                    [
                        'id' => '2',
                        'name' => 'field2',
                        'colors' => 'green',
                    ],
                    [
                        'id' => '3',
                        'name' => 'field3',
                        'colors' => 'red',
                    ],
                ],
                'colors',
                ['red', 'green'],
                [
                    1 => [
                        'id' => '2',
                        'name' => 'field2',
                        'colors' => 'green',
                    ],
                    2 => [
                        'id' => '3',
                        'name' => 'field3',
                        'colors' => 'red',
                    ],
                ],
            ],
            'find by null value in an array' => [
                [
                    123 => [
                        'id' => 123,
                        'value' => null,
                    ],
                ],
                'value',
                null,
                [
                    123 => [
                        'id' => 123,
                        'value' => null,
                    ],
                ],
            ],
            'find by null value in an ArrayObject' => [
                [
                    123 => self::createDataObjectInstance([
                        'id' => 123,
                        'value' => null,
                    ]),
                ],
                'value',
                null,
                [
                    123 => self::createDataObjectInstance([
                        'id' => 123,
                        'value' => null,
                    ]),
                ],
            ],
        ];
    }

    /**
     * @param $source
     * @param $values
     * @param $expectedResult
     *
     * @dataProvider provideDataForByAnyValueMethod
     */
    public function testByAnyValue($source, $values, $expectedResult)
    {
        $actualResult = Arrays::byAnyValue($source, $values);

        $this->assertEquals($expectedResult, $actualResult);
    }

    /**
     * @return array
     */
    public function provideDataForByAnyValueMethod()
    {
        return [
            [
                $this->_input,
                [
                    'name' => 'field1',
                    'id' => '2',
                ],
                [
                    [
                        'id' => '1',
                        'name' => 'field1',
                        'colors' => 'red',
                    ],
                    [
                        'id' => '2',
                        'name' => 'field2',
                        'colors' => 'red',
                    ],
                ]
            ],
        ];
    }

    /**
     * @param $source
     * @param $field
     * @param $value
     * @param $expectedResult
     *
     * @dataProvider provideDataForFindMethod
     */
    public function testFind($source, $field, $value, $expectedResult)
    {
        $actualResult = Arrays::find($source, $field, $value);

        $this->assertEquals($expectedResult, $actualResult);
    }

    /**
     * @return array
     */
    public function provideDataForFindMethod()
    {
        return [
            [
                $this->_input,
                'name',
                'field2',
                [
                    'id' => '2',
                    'name' => 'field2',
                    'colors' => 'red',
                ],
            ],
        ];
    }

    /**
     * @param $source
     * @param $fields
     * @param $notNull
     * @param $expectedResult
     *
     * @dataProvider provideDataForExtractFieldsMethod
     */
    function testExtractFields($source, $fields, $notNull, $expectedResult)
    {
        $actualResult = Arrays::extractFields($source, $fields, $notNull);

        $this->assertEquals($expectedResult, $actualResult);
    }

    /**
     * @return array
     */
    public function provideDataForExtractFieldsMethod()
    {
        return [
            'source is array' => [
                [
                    'id' => 1,
                    'name' => 'field1',
                ],
                ['id'],
                false,
                [
                    'id' => 1,
                ],
            ],
            'source is ArrayObject' => [
                new ArrayObject([
                    'id' => 1,
                    'name' => 'field1',
                    'value' => new NullValue(),
                ]),
                ['id', 'value'],
                false,
                [
                    'id' => 1,
                    'value' => new NullValue(),
                ],
            ],
            'source is an empty array' => [
                [],
                ['id'],
                false,
                [],
            ],
            'notNull is true and the field is null' => [
                [
                    'id' => 1,
                    'name' => null,
                ],
                ['name'],
                true,
                [],
            ],
            'notNull is true and the field is not null' => [
                [
                    'id' => 1,
                    'name' => 'field1',
                ],
                ['name'],
                true,
                [
                    'name' => 'field1',
                ],
            ],
        ];
    }

    public function testExtractFieldsWhenSourceIsInvalid()
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('Parameter $source in Velis\Arrays::extractFields must be array or ArrayObject');

        $date = new DateTime();

        Arrays::extractFields($date, ['id']);
    }

    /**
     * @param $source
     * @param $fields
     * @param $expectedModifiedSource
     *
     * @dataProvider provideDataForExtractFieldsMethodWhenUnsettingSource
     */
    public function testExtractFieldsUnsetsFieldsInSource($source, $fields, $expectedModifiedSource)
    {
        Arrays::extractFields($source, ['id'], false, true);

        $this->assertEquals($expectedModifiedSource, $source);
    }

    /**
     * @return array
     */
    public function provideDataForExtractFieldsMethodWhenUnsettingSource()
    {
        return [
            [
                [
                    'id' => 1,
                    'name' => 'foo',
                ],
                ['id'],
                [
                    'name' => 'foo',
                ],
            ],
        ];
    }

    /**
     * @param $source
     * @param $expectedResult
     *
     * @dataProvider provideDataForFilterKeepZerosMethod
     */
    public function testFilterKeepZeros($source, $expectedResult)
    {
        $actualResult = Arrays::filterKeepZeros($source);

        $this->assertEquals($expectedResult, $actualResult);
    }

    /**
     * @return array
     */
    public function provideDataForFilterKeepZerosMethod()
    {
        return [
            'source is an array' => [
                [
                    'a' => 'a',
                    'b' => 1,
                    'c' => 0,
                    'd' => null,
                    'e' => [],
                ],
                [
                    'a' => 'a',
                    'b' => 1,
                    'c' => 0,
                ],
            ],
            'source is not an array' => [
                'notAnArray',
                'notAnArray',
            ],
        ];
    }

    /**
     * @param array $inputArray
     * @param mixed $expectedResult
     *
     * @dataProvider provideTestDataForGetFirstMethod
     */
    public function testGetFirst($inputArray, $expectedResult)
    {
        $actualResult = Arrays::getFirst($inputArray);

        $this->assertEquals($expectedResult, $actualResult);
    }

    public function provideTestDataForGetFirstMethod()
    {
        $firstValue = uniqid();

        return [
            [
                [
                    'first' => $firstValue,
                    'second' => uniqid(),
                ],
                $firstValue,
            ],
            [
                [
                    $firstValue,
                    uniqid(),
                ],
                $firstValue,
            ],
            [
                [
                    10 => $firstValue,
                    20 => uniqid(),
                ],
                $firstValue,
            ],
        ];
    }


    /**
     * @param mixed $input
     * @param array $expectedOutput
     *
     * @dataProvider provideTestDataForToArrayMethod
     */
    public function testToArray($input, array $expectedOutput)
    {
        $actualOutput = Arrays::toArray($input);

        $this->assertIsArray($actualOutput);
        $this->assertEquals($expectedOutput, $actualOutput);
    }


    public function provideTestDataForToArrayMethod()
    {
        $intValue = 123;
        $stringValue = 'foobar';
        $assocArray = [
            'key1' => 'value1',
            'key2' => 'value2',
        ];

        return [
            [$intValue, [$intValue]],
            [$stringValue, [$stringValue]],
            [$assocArray, $assocArray],
            [new App\Config($assocArray), $assocArray],
            [new Filter($assocArray), $assocArray],
            [null, []],
        ];
    }

    /**
     * @param array $array1
     * @param array $array2
     * @param bool $expectedResult
     *
     * @dataProvider provideDataForAreDifferentMethod
     */
    public function testAreDifferent(array $array1, array $array2, bool $expectedResult)
    {
        $actualResult = Arrays::areDifferent($array1, $array2);

        $this->assertEquals($expectedResult, $actualResult);
    }

    public function provideDataForAreDifferentMethod()
    {
        return [
            [
                [1, 2],
                [1, 2],
                false,
            ],
            [
                [1, 2],
                [2, 1],
                false,
            ],
            [
                [1, 1],
                [1],
                false,
            ],
            [
                [1],
                [1, 1],
                false,
            ],
            [
                [1, 2, 3],
                [1, 2],
                true,
            ],
            [
                [1, 2],
                [1, 2, 3],
                true,
            ],
            [
                [1, 2],
                [1, 3],
                true,
            ],
            [
                [1],
                [],
                true,
            ],
            [
                [],
                [1],
                true,
            ],
        ];
    }

    private static function createDataObjectInstance(array $params = []): DataObject
    {
        return new class($params) extends DataObject {
            protected function _getTableName() {
                return 'test.mock_tab';
            }
            public function parseCompositeFields() {}
        };
    }
}
