<?php

namespace Velis\Test;

use DateTime;
use PHPUnit\Framework\TestCase;
use Velis\Db\NullValue;
use Velis\Filter;
use Velis\Lang;

/**
 * Velis\Filter test
 * @author Olek Procki <olo@velis.pl>
 */
class FilterTest extends TestCase
{
    private array $filterXssInput = [
        "script" => "<script>alert('xss')</script>",
        "bold" => "<b>bold",
        "nested" => [
            "header" => "<h1>header</h1>",
        ],
    ];

    /**
     * Int filtering test
     */
    public function testInt()
    {
        $filter = new Filter(['var' => '4a']);

        // positive validation tests
        $this->assertEquals(
            4,
            $filter->getInt('var'),
            'Value should be <4>'
        );

        $this->assertTrue(
            Filter::validateInts(array(1, 2, 3)),
            'Array [1,2,3] should be validated'
        );

        // negative validation tests
        $this->assertFalse(
            $filter->isInt('var'),
            'Value 4a should NOT be validated'
        );

        $this->assertFalse(
            Filter::validateInts(array(1, 2, 'a')),
            "Array [1,2,'a'] should NOT be validated"
        );

        $this->assertEquals(
            [0 => 123, 3 => 321],
            Filter::filterInts([123, 'abc', 'a222', '321', '333b']),
            'Value should be [123, 321]'
        );

    }


    /**
     * Email validator test
     */
    public function testEmail()
    {
        // positive validation tests
        $this->assertTrue(
            Filter::validateEmail('singu.bpm@gmail.com'),
            'singu.bpm@gmail.com should be validated'
        );

        // negative validation tests
        $this->assertFalse(
            Filter::validateEmail('singu.bpm@gmail'),
            'singu.bpm@gmail should NOT be validated'
        );
        $this->assertFalse(
            Filter::validateEmail('singu.bpm(at)gmail.com'),
            'singu.bpm(at)gmail.com should NOT be validated'
        );
    }


    /**
     * URL validation test
     */
    public function testUrl()
    {
        // positive validation tests
        $this->assertTrue(
            Filter::validateUrl('http://www.velis.pl'),
            'http://www.velis.pl should be validated'
        );
        $this->assertTrue(
            Filter::validateUrl('https://velis.pl'),
            'https://velis.pl should be validated'
        );
        $this->assertTrue(
            Filter::validateUrl('www.velis.pl'),
            'www.velis.pl should be validated'
        );
        $this->assertTrue(
            Filter::validateUrl('velis.pl'),
            'velis.pl should be validated'
        );

        // negative validation tests
        $this->assertFalse(
            Filter::validateUrl('http://velis'),
            'http://velis should NOT be validated'
        );
        $this->assertFalse(
            Filter::validateUrl('velis'),
            'velis should NOT be validated'
        );
    }


    /**
     * Date validation test
     */
    public function testDate()
    {
        // positive validation tests
        $this->assertTrue(
            Filter::validateDate('2012-12-12'),
            '2012-12-12 should validate'
        );
        $this->assertTrue(
            Filter::validateDate('2012-12-12 12:10', Filter::DATE_ALL),
            '2012-12-12 12:10 should validate'
        );
        $this->assertTrue(
            Filter::validateDate('2012-12-12 12:10:03'),
            '2012-12-12 12:10:03 should validate'
        );
        $this->assertTrue(
            Filter::validateDate('2012-12-12T12:10:03'),
            '2012-12-12T12:10:03 should validate'
        );


        // negative validation tests
        $this->assertFalse(
            Filter::validateDate('2012-12-12 12:10:03', true),
            '2012-12-12 12:10:03 should NOT validate in strict mode'
        );
        $this->assertFalse(
            Filter::validateDate('2012-12-12/12:10:03'),
            '2012-12-12/12:10:03 should NOT validate'
        );
        $this->assertFalse(
            Filter::validateDate('2012-12-12 12:10'),
            '2012-12-12 12:10 should NOT validate'
        );
        $this->assertFalse(
            Filter::validateDate('2012-12-12 12:10:0'),
            '2012-12-12 12:10:0 should NOT validate'
        );
        $this->assertFalse(
            Filter::validateDate('12/12/2013'),
            '12/12/2013 should NOT validate'
        );
    }

    public function testValidateAlpha()
    {
        // positive validation tests
        $this->assertTrue(
            Filter::validateAlpha('abc'),
            'abc should validate'
        );
        $this->assertTrue(
            Filter::validateAlpha('abcDEF'),
            'abcDEF should validate'
        );

        // negative validation tests
        $this->assertFalse(
            Filter::validateAlpha('abc123'),
            'abc123 should NOT validate'
        );
        $this->assertFalse(
            Filter::validateAlpha('abc-DEF'),
            'abc-DEF should NOT validate'
        );
    }

    public function testValidateAlnum()
    {
        // positive validation tests
        $this->assertTrue(
            Filter::validateAlnum('abc123'),
            'abc123 should validate'
        );
        $this->assertTrue(
            Filter::validateAlnum('abcDEF123'),
            'abcDEF123 should validate'
        );

        // negative validation tests
        $this->assertFalse(
            Filter::validateAlnum('abc-DEF'),
            'abc-DEF should NOT validate'
        );

        $this->assertFalse(
            Filter::validateAlnum('12.3+181'),
            '12.3+181 should NOT validate'
        );
    }

    /**
     * @dataProvider provideDataForGetBoolTest
     */
    public function testGetBool(Filter $filter, $expected)
    {
        $this->assertSame($expected, $filter->getBool('bool'));
    }

    public function provideDataForGetBoolTest()
    {
        return [
            'string true' => [new Filter(['bool' => 'true']), true],
            'string false' => [new Filter(['bool' => 'false']), false],
            'bool true' => [new Filter(['bool' => true]), true],
            'bool false' => [new Filter(['bool' => false]), false],
            'int 1' => [new Filter(['bool' => 1]), true],
            'int 0' => [new Filter(['bool' => 0]), false],
            'random string' => [new Filter(['bool' => 'random']), null],
            'empty string' => [new Filter(['bool' => '']), null],
            'empty filter' => [new Filter([]), null],
        ];
    }

    /**
     * @param array $arrayInput
     * @param string $offset
     * @param mixed $expectedOutput
     *
     * @dataProvider provideTestDataForOffsetGetMethod
     */
    public function testOffsetGet(array $arrayInput, string $offset, $expectedOutput)
    {
        $filter = new Filter($arrayInput);
        $actualOutput = $filter[$offset];

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

    public function provideTestDataForOffsetGetMethod()
    {
        $arrayWithIntegerKeys = [
            'items' => [
                '<script>alert(0);</script>',
                '<script>alert(1);</script>',
            ],
        ];

        $arrayWithIntegerKeysFiltered = [
            '﹤script﹥alert(0);﹤/script﹥',
            '﹤script﹥alert(1);﹤/script﹥',
        ];

        $visit = [
            'visit' => [
                'ticket_id' => 123456,
                'work_description' => '<script>console.log(\'xss\');</script>',
                'actions' => [
                    'action_id' => [
                        -1,
                    ],
                    'name' => [
                        '<script>console.log(\'xss\');</script>',
                    ],
                    'qty' => [
                        1,
                    ],
                    'price' => [
                        0,
                    ],
                ],
            ],
        ];

        $visitFiltered = [
            'ticket_id' => '123456',
            'work_description' => '﹤script﹥console.log(’xss’);﹤/script﹥',
            'actions' => [
                'action_id' => [
                    '-1',
                ],
                'name' => [
                    '﹤script﹥console.log(’xss’);﹤/script﹥',
                ],
                'qty' => [
                    '1',
                ],
                'price' => [
                    '0',
                ],
            ],
        ];

        $arrayWithNullValue = [
            'value' => new NullValue(),
        ];

        return [
            [$this->filterXssInput, 'script', '﹤script﹥alert(’xss’)﹤/script﹥'],
            [$this->filterXssInput, 'bold', '﹤b﹥bold'],
            [$arrayWithIntegerKeys, 'items', $arrayWithIntegerKeysFiltered],
            [$visit, 'visit', $visitFiltered],
            [$arrayWithNullValue, 'value', new NullValue()],
        ];
    }

    /**
     * @param array $arrayInput
     * @param string $offset1
     * @param string $offset2
     * @param $expectedOutput
     *
     * @dataProvider provideTestDataForNestedOffsetGetMethod
     */
    public function testOffsetGetNested(array $arrayInput, string $offset1, string $offset2, $expectedOutput)
    {
        $filter = new Filter($arrayInput);
        $actualOutput = $filter[$offset1][$offset2];

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

    public function provideTestDataForNestedOffsetGetMethod()
    {
        return [
            [$this->filterXssInput, 'nested', 'header', '﹤h1﹥header﹤/h1﹥'],
        ];
    }

    /**
     * @param array $arrayInput
     * @param string $key
     * @param $expectedOutput
     *
     * @dataProvider provideTestDataForGetRawMethod
     */
    public function testGetRaw(array $arrayInput, string $key, $expectedOutput)
    {
        $filter = new Filter($arrayInput);
        $actualOutput = $filter->getRaw($key);

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

    public function provideTestDataForGetRawMethod()
    {
        return [
            [$this->filterXssInput, 'script', "<script>alert('xss')</script>"],
        ];
    }

    /**
     * @param array $arrayInput
     * @param $unfiltered
     * @param $expectedOutput
     *
     * @dataProvider provideTestDataForGetArrayCopyMethod
     */
    public function testGetArrayCopy(array $arrayInput, $unfiltered, $expectedOutput)
    {
        $filter = new Filter($arrayInput);
        $actualOutput = $filter->getArrayCopy($unfiltered);

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

    public function provideTestDataForGetArrayCopyMethod()
    {
        $completelyFiltered = [
            "script" => "﹤script﹥alert(’xss’)﹤/script﹥",
            "bold" => "﹤b﹥bold",
            "nested" => [
                "header" => "﹤h1﹥header﹤/h1﹥",
            ]
        ];

        $headerUnfiltered = [
            "script" => "﹤script﹥alert(’xss’)﹤/script﹥",
            "bold" => "﹤b﹥bold",
            "nested" => [
                "header" => "<h1>header</h1>",
            ],
        ];

        $boldAndHeaderUnfiltered = [
            "script" => "﹤script﹥alert(’xss’)﹤/script﹥",
            "bold" => "<b>bold",
            "nested" => [
                "header" => "<h1>header</h1>",
            ],
        ];

        return [
            [$this->filterXssInput, null, $completelyFiltered],
            [$this->filterXssInput, 'header', $headerUnfiltered],
            [$this->filterXssInput, ['bold', 'header'], $boldAndHeaderUnfiltered],
        ];
    }

    /**
     * @param array $arrayInput
     * @param $key
     * @param $unfiltered
     * @param $expectedOutput
     *
     * @dataProvider provideTestDataForGetMethod
     */
    public function testGet(array $arrayInput, $key, $unfiltered, $expectedOutput)
    {
        $filter = new Filter($arrayInput);
        $actualOutput = $filter->get($key, $unfiltered);

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

    public function provideTestDataForGetMethod()
    {
        $headerUnfiltered = [
            'header' => '<h1>header</h1>',
        ];

        return [
            [$this->filterXssInput, 'nested', 'header', $headerUnfiltered],
        ];
    }

    /**
     * @param array $input
     *
     * @dataProvider provideTestDataForGetRawCopyMethod
     */
    public function testGetRawCopy(array $input)
    {
        $filter = new Filter($input);
        $output = $filter->getRawCopy();

        $this->assertEquals($input, $output);
    }

    public function provideTestDataForGetRawCopyMethod()
    {
        return [
            [$this->filterXssInput],
        ];
    }


    /**
     * @param string $input
     * @param DateTime $expectedOutput
     *
     * @dataProvider provideTestDataForFilterDateTimeMethod
     */
    public function testFilterDateTime($input, $expectedOutput)
    {
        $actualOutput = Filter::filterDateTime($input);

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


    public function provideTestDataForFilterDateTimeMethod()
    {
        return [
            ['2021-04-08 14:35:20', new DateTime('2021-04-08 14:35:20')],
            ['2021-04-08T15:05:45+02:00', new DateTime('2021-04-08 15:05:45')],
        ];
    }

    /**
     * @param string $value
     * @param string $expected
     * @dataProvider provideTestDataForFilterFilenameMethod
     */
    public function testFilterFilename($value, $expected)
    {
        $result = Filter::filterFilename($value);
        $this->assertEquals($expected, $result);
    }

    public function provideTestDataForFilterFilenameMethod()
    {
        return [
            ['test\n\r.html', 'testnr.html'],
            ['te\nst.\rhtml', 'tenst.rhtml'],
            ['\r\n\r\nTest', 'rnrnTest'],
            ['test1.test2.pdf', 'test1.test2.pdf'],
            ['猫耳', '猫耳'],
            ['osochozi?', 'osochozi'],
            ['AC/DC', 'DC'],
            ['<lol>', 'lol'],
        ];
    }

    /**
     * @param string $haystack
     * @param string $needle
     * @param bool $expectedResult
     * @return void
     *
     * @dataProvider provideTestDataForStartsWithMethod
     */
    public function testStartsWith(string $haystack, string $needle, bool $expectedResult)
    {
        $actualResult = Filter::startsWith($haystack, $needle);

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

    public function provideTestDataForStartsWithMethod()
    {
        return [
            ['grzyb', 'g', true],
            ['muchomor', 'g', false],
            ['%pattern%', '%', true],
            ['40%', '%', false],
        ];
    }

    /**
     * @param string $haystack
     * @param string $needle
     * @param bool $expectedResult
     * @return void
     *
     * @dataProvider provideTestDataForEndWithMethod
     */
    public function testEndsWith(string $haystack, string $needle, bool $expectedResult)
    {
        $actualResult = Filter::endsWith($haystack, $needle);

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

    public function provideTestDataForEndWithMethod()
    {
        return [
            ['grzyb', 'b', true],
            ['muchomor', 'b', false],
            ['%pattern%', '%', true],
            ['%pattern', '%', false],
        ];
    }

    /**
     * @param Filter $filter
     * @param string $key
     * @param bool $expectedResult
     * @return void
     *
     * @dataProvider provideTestDataForHasValueMethod
     */
    public function testHasValue(Filter $filter, string $key, bool $expectedResult): void
    {
        $actualResult = $filter->hasValue($key);

        self::assertEquals($expectedResult, $actualResult);
    }

    /**
     * @return array
     */
    public function provideTestDataForHasValueMethod(): array
    {
        return [
            [new Filter(), 'key', false],
            [new Filter(['key' => null]), 'key', false],
            [new Filter(['key' => '']), 'key', false],
            [new Filter(['key' => 1]), 'key', true],
        ];
    }

    /**
     * @param mixed $input
     * @param mixed $expectedResult
     * @return void
     *
     * @dataProvider provideTestDataForFilterXssMethod
     */
    public function testFilterXss($input, $expectedResult): void
    {
        $actualResult = Filter::filterXss($input);

        self::assertEquals($expectedResult, $actualResult);
    }

    /**
     * @return array
     */
    public function provideTestDataForFilterXssMethod(): array
    {
        $multilineInput = <<<'EOD'
Hic dolorum et nostrum omnis ut adipisci autem.
Sunt est blanditiis qui.
Voluptate aspernatur consectetur sunt culpa est quas consequatur voluptatibus.
Id qui tempora in ad.
Nisi ducimus nisi enim consequatur placeat enim.
EOD;

        return [
            [true, true],
            [false, false],
            ['<br>', '﹤br﹥'],
            ['\'quote\'', '’quote’'],
            ['"double quote"', '”double quote”'],
            [$multilineInput, $multilineInput],
        ];
    }

    /**
     * @param string $language
     * @param string $input
     * @param string $expectedResult
     * @return void
     *
     * @dataProvider provideTestDataForFilterToDashMethod
     */
    public function testFilterToDash(string $language, string $input, string $expectedResult): void
    {
        Lang::switchLanguage($language);

        $actualResult = Filter::filterToDash($input);

        self::assertEquals($expectedResult, $actualResult);
    }

    /**
     * @return array
     */
    public function provideTestDataForFilterToDashMethod(): array
    {
        return [
            ['pl', 'BeforeGuestbookEntry', 'before-guestbook-entry'],
            ['pl', 'BeforeTicketChanged', 'before-ticket-changed'],
            ['pl', 'AfterGuestbookEntry', 'after-guestbook-entry'],
            ['en', 'BeforeGuestbookEntry', 'before-guestbook-entry'],
            ['en', 'BeforeTicketChanged', 'before-ticket-changed'],
            ['en', 'AfterGuestbookEntry', 'after-guestbook-entry'],
            ['az', 'BeforeGuestbookEntry', 'before-guestbook-entry'],
            ['az', 'BeforeTicketChanged', 'before-ticket-changed'],
            ['az', 'AfterGuestbookEntry', 'after-guestbook-entry'],
        ];
    }

    /**
     * @param string $input
     * @param string $expectedResult
     * @return void
     *
     * @dataProvider provideTestDataForFilterToUnderscoreMethod
     */
    public function testParseArrayParameters(array $data, array $expectedResult): void
    {
        $filter = new Filter($data);
        $result = $filter->parseArrayParameters();

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


    public function provideTestDataForFilterToUnderscoreMethod(): array
    {
        return [
            [
                [
                    'user_id' => 1,
                    'user_name' => 'John',
                    'roles' => 'Admin,User',
                    'customer_id' => [
                        '[1,2,3]',
                        4,
                        5,
                    ],
                    'partner_id' => [
                        1,
                    ],
                    'privs' => [
                        '["Admin","Partner"]',
                        'Test1, Test2',
                    ],
                ],
                [
                    'user_id' => 1,
                    'user_name' => 'John',
                    'roles' => 'Admin,User',
                    'customer_id' => [1, 2, 3, 4, 5],
                    'partner_id' => [1],
                    'privs' => ['Admin', 'Partner', 'Test1, Test2'],
                ],
            ]
        ];
    }
}
