<?php

/**
 * Tests the Validation lib that's shipped with Kohana
 *
 * @group kohana
 * @group kohana.core
 * @group kohana.core.validation
 *
 * @package    Kohana
 * @category   Tests
 * @author     Kohana Team
 * @author     BRMatt <matthew@sigswitch.com>
 * @copyright  (c) 2008-2012 Kohana Team
 * @license    https://kohana.top/license
 */
class Kohana_ValidationTest extends Unittest_TestCase
{
    /**
     * Tests Validation::factory()
     *
     * Makes sure that the factory method returns an instance of Validation lib
     * and that it uses the variables passed
     *
     * @test
     */
    public function test_factory_method_returns_instance_with_values()
    {
        $values = [
            'this' => 'something else',
            'writing tests' => 'sucks',
            'why the hell' => 'amIDoingThis',
        ];

        $instance = Validation::factory($values);

        $this->assertTrue($instance instanceof Validation);

        $this->assertSame(
            $values, $instance->data()
        );
    }

    /**
     * When we copy() a validation object, we should have a new validation object
     * with the exact same attributes, apart from the data, which should be the
     * same as the array we pass to copy()
     *
     * @test
     * @covers Validation::copy
     */
    public function test_copy_copies_all_attributes_except_data()
    {
        $validation = new Validation([
            'foo' => 'bar',
            'fud' => 'fear, uncertainty, doubt',
            'num' => 9
        ]);

        $validation->rule('num', 'is_int')->rule('foo', 'is_string');

        $copy_data = ['foo' => 'no', 'fud' => 'maybe', 'num' => 42];

        $copy = $validation->copy($copy_data);

        $this->assertNotSame($validation, $copy);

        foreach (['_rules', '_bound', '_labels', '_empty_rules', '_errors'] as $attribute) {
            // This is just an easy way to check that the attributes are identical
            // Without hardcoding the expected values
            $this->assertAttributeSame(
                self::readAttribute($validation, $attribute), $attribute, $copy
            );
        }

        $this->assertSame($copy_data, $copy->data());
    }

    /**
     * When the validation object is initially created there should be no labels
     * specified
     *
     * @test
     */
    public function test_initially_there_are_no_labels()
    {
        $validation = new Validation([]);

        $this->assertAttributeSame([], '_labels', $validation);
    }

    /**
     * Adding a label to a field should set it in the labels array
     * If the label already exists it should overwrite it
     *
     * In both cases thefunction should return a reference to $this
     *
     * @test
     * @covers Validation::label
     */
    public function test_label_adds_and_overwrites_label_and_returns_this()
    {
        $validation = new Validation([]);

        $this->assertSame($validation, $validation->label('email', 'Email Address'));

        $this->assertAttributeSame([
            'email' => 'Email Address'
            ], '_labels', $validation);

        $this->assertSame($validation, $validation->label('email', 'Your Email'));

        $validation->label('name', 'Your Name');

        $this->assertAttributeSame([
            'email' => 'Your Email',
            'name' => 'Your Name'
            ], '_labels', $validation);
    }

    /**
     * Using labels() we should be able to add / overwrite multiple labels
     *
     * The function should also return $this for chaining purposes
     *
     * @test
     * @covers Validation::labels
     */
    public function test_labels_adds_and_overwrites_multiple_labels_and_returns_this()
    {
        $validation = new Validation([]);
        $initial_data = ['kung fu' => 'fighting', 'fast' => 'cheetah'];

        $this->assertSame($validation, $validation->labels($initial_data));

        $this->assertAttributeSame($initial_data, '_labels', $validation);

        $this->assertSame($validation, $validation->labels([
                'fast' => 'lightning'
        ]));

        $this->assertAttributeSame([
            'fast' => 'lightning',
            'kung fu' => 'fighting'
            ], '_labels', $validation);
    }

    /**
     * Using bind() we should be able to add / overwrite multiple bound variables
     *
     * The function should also return $this for chaining purposes
     *
     * @test
     * @covers Validation::bind
     */
    public function test_bind_adds_and_overwrites_multiple_variables_and_returns_this()
    {
        $validation = new Validation([]);
        $data = ['kung fu' => 'fighting', 'fast' => 'cheetah'];
        $bound = [':foo' => 'some value'];

        // Test binding an array of values
        $this->assertSame($validation, $validation->bind($bound));
        $this->assertAttributeSame($bound, '_bound', $validation);

        // Test binding one value
        $this->assertSame($validation, $validation->bind(':foo', 'some other value'));
        $this->assertAttributeSame([
            ':foo' => 'some other value'
            ], '_bound', $validation);
    }

    /**
     * We should be able to used bound variables in callbacks
     *
     * @test
     * @covers Validation::check
     */
    public function test_bound_callback()
    {
        $data = [
            'kung fu' => 'fighting',
            'fast' => 'cheetah',
        ];
        $validation = new Validation($data);
        $validation->bind(':class', 'Valid')
            // Use the bound value in a callback
            ->rule('fast', [':class', 'max_length'], [':value', 2]);

        // The rule should have run and check() should fail
        $this->assertSame($validation->check(), false);
    }

    /**
     * Provides test data for test_check
     *
     * @return array
     */
    public function provider_check()
    {
        // $data_array, $rules, $labels, $first_expected, $expected_error
        return [
            [
                ['foo' => 'bar'],
                ['foo' => [['not_empty', null]]],
                [],
                true,
                []
            ],
            [
                ['unit' => 'test'],
                [
                    'foo' => [['not_empty', null]],
                    'unit' => [['min_length', [':value', 6]]]
                ],
                [],
                false,
                [
                    'foo' => 'foo must not be empty',
                    'unit' => 'unit must be at least 6 characters long'
                ]
            ],
            [
                ['foo' => 'bar'],
                [
                    // Tests wildcard rules
                    true => [['min_length', [':value', 4]]],
                    'foo' => [
                        ['not_empty', null],
                        // Tests the array syntax for callbacks
                        [['Valid', 'exact_length'], [':value', 3]],
                        // Tests the Class::method syntax for callbacks
                        ['Valid::exact_length', [':value', 3]],
                        // Tests the lambda function syntax for callbacks
                        // Commented out for PHP 5.2 support
                        // [function($value){return true;}, [':value']],
                        // Tests using a function as a rule
                        ['is_string', [':value']]
                    ],
                    // Tests that rules do not run on empty fields unless they are in _empty_rules
                    'unit' => [['exact_length', [':value', 4]]]
                ],
                [],
                false,
                ['foo' => 'foo must be at least 4 characters long']
            ],
            // Switch things around and make :value an array
            [
                ['foo' => ['test', 'data']],
                ['foo' => [['in_array', ['kohana', ':value']]]],
                [],
                false,
                ['foo' => 'foo must be one of the available options']
            ],
            // Test wildcard rules with no other rules
            [
                ['foo' => ['test']],
                [true => [['is_string', [':value']]]],
                ['foo' => 'foo'],
                false,
                ['foo' => '1.foo.is_string']
            ],
            // Test array rules use method as error name
            [
                ['foo' => 'test'],
                ['foo' => [[['Valid', 'min_length'], [':value', 10]]]],
                [],
                false,
                ['foo' => 'foo must be at least 10 characters long']
            ],
        ];
    }

    /**
     * Tests Validation::check()
     *
     * @test
     * @covers Validation::check
     * @covers Validation::rule
     * @covers Validation::rules
     * @covers Validation::errors
     * @covers Validation::error
     * @dataProvider provider_check
     * @param array   $array            The array of data
     * @param array   $rules            The array of rules
     * @param array   $labels           The array of labels
     * @param boolean $expected         Is it valid?
     * @param boolean $expected_errors  Array of expected errors
     */
    public function test_check($array, $rules, $labels, $expected, $expected_errors)
    {
        $validation = new Validation($array);

        foreach ($labels as $field => $label) {
            $validation->label($field, $label);
        }

        foreach ($rules as $field => $field_rules) {
            foreach ($field_rules as $rule)
                $validation->rule($field, $rule[0], $rule[1]);
        }

        $status = $validation->check();
        $errors = $validation->errors(true);
        $this->assertSame($expected, $status);
        $this->assertSame($expected_errors, $errors);

        $validation = new validation($array);
        foreach ($rules as $field => $rules) {
            $validation->rules($field, $rules);
        }
        $validation->labels($labels);

        $this->assertSame($expected, $validation->check());
    }

    /**
     * Tests Validation::check()
     *
     * @test
     * @covers Validation::check
     */
    public function test_check_stops_when_error_added_by_callback()
    {
        $validation = new Validation(['foo' => 'foo']);

        $validation
            ->rule('foo', [$this, '_validation_callback'], [':validation'])
            // This rule should never run
            ->rule('foo', 'min_length', [':value', 20]);

        $validation->check();
        $errors = $validation->errors();

        $expected = [
            'foo' => [0 => '_validation_callback', 1 => null],
        ];

        $this->assertSame($errors, $expected);
    }

    public function _validation_callback(Validation $object)
    {
        // Simply add the error
        $object->error('foo', '_validation_callback');
    }

    /**
     * Provides test data for test_errors()
     *
     * @return array
     */
    public function provider_errors()
    {
        // [data, rules, expected], ...
        return [
            // No Error
            [
                ['username' => 'frank'],
                ['username' => [['not_empty', null]]],
                []
            ],
            // Error from message file
            [
                ['username' => ''],
                ['username' => [['not_empty', null]]],
                ['username' => 'username must not be empty']
            ],
            // No error message exists, display the path expected
            [
                ['username' => 'John'],
                ['username' => [['strpos', [':value', 'Kohana']]]],
                ['username' => 'Validation.username.strpos']
            ],
        ];
    }

    /**
     * Tests Validation::errors()
     *
     * @test
     * @covers Validation::errors
     * @dataProvider provider_errors
     * @param array $array     The array of data
     * @param array $rules     The array of rules
     * @param array $expected  Array of expected errors
     */
    public function test_errors($array, $rules, $expected)
    {
        $validation = Validation::factory($array);

        foreach ($rules as $field => $field_rules) {
            $validation->rules($field, $field_rules);
        }

        $validation->check();

        $this->assertSame($expected, $validation->errors('Validation', false));
        // Should be able to get raw errors array
        $this->assertAttributeSame($validation->errors(null), '_errors', $validation);
    }

    /**
     * Provides test data for test_translated_errors()
     *
     * @return array
     */
    public function provider_translated_errors()
    {
        // [data, rules, expected], ...
        return [
            [
                ['Spanish' => ''],
                ['Spanish' => [['not_empty', null]]],
                // Errors are not translated yet so only the label will translate
                ['Spanish' => 'Español must not be empty'],
                ['Spanish' => 'Spanish must not be empty']
            ],
        ];
    }

    /**
     * Tests Validation::errors()
     *
     * @test
     * @covers Validation::errors
     * @dataProvider provider_translated_errors
     * @param array   $data                   The array of data to test
     * @param array   $rules                  The array of rules to add
     * @param array   $translated_expected    The array of expected errors when translated
     * @param array   $untranslated_expected  The array of expected errors when not translated
     */
    public function test_translated_errors($data, $rules, $translated_expected, $untranslated_expected)
    {
        $validation = Validation::factory($data);

        $current = i18n::lang();
        i18n::lang('es');

        foreach ($rules as $field => $field_rules) {
            $validation->rules($field, $field_rules);
        }

        $validation->check();

        $result_1 = $validation->errors('Validation', true);
        $result_2 = $validation->errors('Validation', 'en');
        $result_3 = $validation->errors('Validation', false);

        // Restore the current language
        i18n::lang($current);

        $this->assertSame($translated_expected, $result_1);
        $this->assertSame($translated_expected, $result_2);
        $this->assertSame($untranslated_expected, $result_3);
    }

    /**
     * Tests Validation::errors()
     *
     * @test
     * @covers Validation::errors
     */
    public function test_parameter_labels()
    {
        $validation = Validation::factory(['foo' => 'bar'])
            ->rule('foo', 'equals', [':value', 'something'])
            ->label('something', 'Spanish');

        $current = i18n::lang();
        i18n::lang('es');

        $validation->check();

        $translated_expected = ['foo' => 'foo must equal Español'];
        $untranslated_expected = ['foo' => 'foo must equal Spanish'];

        $result_1 = $validation->errors('Validation', true);
        $result_2 = $validation->errors('Validation', 'en');
        $result_3 = $validation->errors('Validation', false);

        // Restore the current language
        i18n::lang($current);

        $this->assertSame($translated_expected, $result_1);
        $this->assertSame($translated_expected, $result_2);
        $this->assertSame($untranslated_expected, $result_3);
    }

    /**
     * Tests Validation::errors()
     *
     * @test
     * @covers Validation::errors
     */
    public function test_arrays_in_parameters()
    {
        $validation = Validation::factory(['foo' => 'bar'])
            ->rule('foo', 'equals', [':value', ['one', 'two']]);

        $validation->check();

        $expected = ['foo' => 'foo must equal one, two'];

        $this->assertSame($expected, $validation->errors('Validation', false));
    }

    /**
     * Tests Validation::check()
     *
     * @test
     * @covers Validation::check
     */
    public function test_data_stays_unaltered()
    {
        $validation = Validation::factory(['foo' => 'bar'])
            ->rule('something', 'not_empty');

        $before = $validation->data();
        $validation->check();
        $after = $validation->data();

        $expected = ['foo' => 'bar'];

        $this->assertSame($expected, $before);
        $this->assertSame($expected, $after);
    }

    /**
     * Tests Validation::errors()
     *
     * @test
     * @covers Validation::errors
     */
    public function test_object_parameters_not_in_messages()
    {
        $validation = Validation::factory(['foo' => 'foo'])
            ->rule('bar', 'matches', [':validation', ':field', 'foo']);

        $validation->check();
        $errors = $validation->errors('validation');
        $expected = ['bar' => 'bar must be the same as foo'];

        $this->assertSame($expected, $errors);
    }

    /**
     * Tests Validation::data()
     *
     * @test
     * @covers Validation::data
     */
    public function test_data_returns_original_array()
    {
        $data = ['one' => 'hello', 'two' => 'world', 'ten' => ''];

        $validation = Validation::factory($data);

        $this->assertSame($data, $validation->data());
    }

    // @codingStandardsIgnoreStart
    public function test_offsetExists()
    // @codingStandardsIgnoreEnd
    {
        $array = ['one' => 'Hello', 'two' => 'World', 'ten' => null];

        $validation = Validation::factory($array);

        $this->assertTrue(isset($validation['one']));
        $this->assertFalse(isset($validation['ten']));
        $this->assertFalse(isset($validation['five']));
    }

    // @codingStandardsIgnoreStart
    public function test_offsetSet_throws_exception()
    // @codingStandardsIgnoreEnd
    {
        $this->setExpectedException('Kohana_Exception');

        $validation = Validation::factory([]);

        // Validation is read-only
        $validation['field'] = 'something';
    }

    // @codingStandardsIgnoreStart
    public function test_offsetGet()
    // @codingStandardsIgnoreEnd
    {
        $array = ['one' => 'Hello', 'two' => 'World', 'ten' => null];

        $validation = Validation::factory($array);

        $this->assertSame($array['one'], $validation['one']);
        $this->assertSame($array['two'], $validation['two']);
        $this->assertSame($array['ten'], $validation['ten']);
    }

    // @codingStandardsIgnoreStart
    public function test_offsetUnset()
    // @codingStandardsIgnoreEnd
    {
        $this->setExpectedException('Kohana_Exception');

        $validation = Validation::factory(['one' => 'Hello, World!']);

        // Validation is read-only
        unset($validation['one']);
    }

    /**
     * http://dev.kohanaframework.org/issues/4365
     *
     * @test
     * @covers Validation::errors
     */
    public function test_error_type_check()
    {
        $array = ['email' => 'not an email address'];

        $validation = Validation::factory($array)
            ->rule('email', 'not_empty')
            ->rule('email', 'email')
        ;

        $validation->check();

        $errors = $validation->errors('tests/validation/error_type_check');

        $this->assertSame($errors, $validation->errors('validation'));
    }

    /**
     * Provides test data for test_rule_label_regex
     *
     * @return array
     */
    public function provider_rule_label_regex()
    {
        // $data, $field, $rules, $expected
        return [
            [
                ['email1' => ''],
                'email1',
                [['not_empty']],
                ['email1' => 'email1 must not be empty']
            ],
        ];
    }

    /**
     * http://dev.kohanaframework.org/issues/4201
     *
     * @test
     * @ticket 4201
     * @covers Validation::rule
     * @dataProvider provider_rule_label_regex
     */
    public function test_rule_label_regex($data, $field, $rules, $expected)
    {
        $validation = Validation::factory($data)->rules($field, $rules);

        $validation->check();

        $errors = $validation->errors('');

        $this->assertSame($errors, $expected);
    }

}
