<?php namespace spec\im\Primitive\Container;

use JWT;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class ContainerSpec extends ObjectBehavior
{
    protected $initializer;

    function let()
    {
        $this->initializer = [
            'name' => 'John',
            'surname' => 'Doe',
            'email' => 'johndoe@example.com',
            'wife' => ['name' => 'Jane',
                       'surname' => 'Doe',
                       'email' => 'janedoe@example.com',
                       'hobby' =>'music']
        ];

        $this->beConstructedWith($this->initializer);
    }

    function it_is_initializable()
    {
        $this->shouldHaveType('im\Primitive\Container\Container');
    }

    function it_should_construct_from_array()
    {
        $initializer = ['foo', 'bar', 'baz'];

        $this->fromArray($initializer);

        $this->all()->shouldHaveCount(count($initializer));
    }

    function it_should_construct_from_json()
    {
        $initializer = '{"key": "value"}';

        $this->fromJson($initializer);

        $this->all()->shouldHaveCount(count(json_decode($initializer, true)));

        $this->has('key')->shouldBe(true);

        $this->all()->shouldBeEqualTo(['key' => 'value']);
    }

    function it_should_construct_from_file_with_json()
    {
        $initializer = __DIR__ . DIRECTORY_SEPARATOR . 'data.json';

        $this->fromFile($initializer);

        $this->all()->shouldBeEqualTo($this->initializer);

        $this->has('name')->shouldBe(true);

        $this->lengthCheck();
    }

    function it_should_call_method_with_magic_get_if_it_looks_like_variable()
    {
        $this->pop;

        $this->shift;

        $this->has('name')->shouldBe(false);

        $this->minusLengthCheck(2);
    }

    function it_should_get_value_by_key_with_dot_notation()
    {
        $obj = new \stdClass();
        $obj->value = 'added';

        $this->set('new.value', 'added');

        $this->get('new.value')->shouldBe('added');
    }

    function it_should_set_item_by_key_with_dot_notation()
    {
        $match = $this->initializer;

        $match['wife']['like'] = 'husband';

        $this->set('wife.like', 'husband')->all()->shouldBe($match);

        $obj = new \stdClass();
        $obj->value = 'added';

        $this->set('new.value', 'added');

        $this->get('new.value')->shouldBe('added');
    }

    function it_should_push_item_without_key_to_Container()
    {
        $this->push('newItem');

        $this->hasValue('newItem')->shouldBe(true);

        $this->plusLengthCheck();
    }

    function it_should_pop_item_from_Container()
    {
        $this->pop();

        $this->minusLengthCheck();
    }

    function it_should_unshift_item_to_the_first_index()
    {
        $match = $this->initializer;

        array_unshift($match, 'newItem');

        $this->prepend('newItem')->all()->shouldBe($match);

        $this->plusLengthCheck();
    }

    function it_should_shift_item_from_the_first_index()
    {
        $this->shift();

        $this->minusLengthCheck();
    }

    function it_should_find_index_of_given_key()
    {
        $this->search('John')->shouldBe('name');
        $this->search('Doe')->shouldBe('surname');

        $this->search('fake')->shouldBe(null);
    }

    function it_should_return_true_if_Container_has_key()
    {
        $this->has('name')->shouldBe(true);

        $this->has('wife.hobby')->shouldBe(true);

        $obj = new \stdClass();
        $obj->value = 15;

        $this->initializer['new'] = $obj;

        $this->fromArray($this->initializer)->has('new.value')->shouldBe(true);

        $this->has('noKey')->shouldBe(false);
    }

    function it_should_return_first_key_from_Container()
    {
        $this->firstKey()->shouldBe('name');

        $this->fromArray([]);

        $this->shouldThrow('im\Primitive\Container\Exceptions\EmptyContainerException')->duringFirstKey();
    }

    function it_should_return_last_key_from_Container()
    {
        $this->lastKey()->shouldBe('wife');

        $this->fromArray([]);

        $this->shouldThrow('im\Primitive\Container\Exceptions\EmptyContainerException')->duringLastKey();
    }

    function it_should_return_first_value_from_Container()
    {
        $this->first()->shouldReturn('John');
    }

    function it_should_return_first_value_that_passes_truth_test()
    {
        $this->firstWhere(function($key)
        {
            return $key == 'surname';

        })->shouldBeEqualTo('Doe');

        $this->lengthCheck();
    }

    function it_should_return_last_value_that_passes_truth_test()
    {
        $this->lastWhere(function($key)
        {
            return $key == 'surname';

        })->shouldBeEqualTo('Doe');

        $this->lengthCheck();
    }

    function it_should_return_last_value_from_Container()
    {
        $this->last()->shouldReturn(['name' => 'Jane',
                                     'surname' => 'Doe',
                                     'email' => 'janedoe@example.com',
                                     'hobby' =>'music']);
    }

    function it_should_assign_keys_by_inner_arrays_key_value()
    {
        $this->fromArray([
            [
                'name' => 'John',
                'surname' => 'Doe',
                'email' => 'johndoe@example.com'
            ],
            [
                'name' => 'Jane',
                'surname' => 'Doe',
                'email' => 'janedoe@example.com'
            ]
        ]);

        $keysBy = $this->keysByField('name');

        $keysBy->all()->shouldBeEqualTo([
            'John' => [
                'name' => 'John',
                'surname' => 'Doe',
                'email' => 'johndoe@example.com'
            ],
            'Jane' => [
                'name' => 'Jane',
                'surname' => 'Doe',
                'email' => 'janedoe@example.com'
            ]
        ]);
    }

    function it_should_unique_Container_items()
    {
        $initializer = ['foo', 'bar', 'bar', 'bar'];

        $this->fromArray($initializer);

        $this->unique()->all()->shouldBeEqualTo(['foo', 'bar']);
    }

    function it_should_return_all_keys_from_Container()
    {
        $this->keys()->all()->shouldBe(['name', 'surname', 'email', 'wife']);
    }

    function it_should_return_all_values_from_Container()
    {
        $this->values()->all()->shouldBeEqualTo(
            [
                0 => 'John',
                1 => 'Doe',
                2 => 'johndoe@example.com',
                3 => [
                    'name' => 'Jane',
                    'surname' => 'Doe',
                    'email' => 'janedoe@example.com',
                    'hobby' =>'music'
                ]
            ]);
    }

    function it_should_divide_keys_and_values()
    {
        $this->divide()->all()->shouldBeEqualTo(
            [
                [
                    'name',
                    'surname',
                    'email',
                    'wife'
                ],
                [
                    0 => 'John',
                    1 => 'Doe',
                    2 => 'johndoe@example.com',
                    3 => [
                        'name' => 'Jane',
                        'surname' => 'Doe',
                        'email' => 'janedoe@example.com',
                        'hobby' =>'music'
                    ]
                ]
            ]
        );
    }

    function it_should_return_items_with_only_numeric_keys()
    {
        $init = [
            0 => 'John',
            1 => 'Doe',
            'email' => 'johndoe@example.com',
            3 => [
                'name' => 'Jane',
                'surname' => 'Doe',
                'email' => 'janedoe@example.com',
                'hobby' =>'music'
            ]
        ];

        $match = $init;

        unset($match['email']);

        $this->fromArray($init)->numericKeys()->all()->shouldBe($match);
    }

    function it_should_return_items_with_only_not_numeric_keys()
    {
        $init = [
            0 => 'John',
            1 => 'Doe',
            'email' => 'johndoe@example.com',
            3 => [
                'name' => 'Jane',
                'surname' => 'Doe',
                'email' => 'janedoe@example.com',
                'hobby' =>'music'
            ]
        ];

        $this->fromArray($init)->notNumericKeys()->all()->shouldBe(['email' => 'johndoe@example.com']);
    }

    function it_should_shuffle_items()
    {
        $this->shuffle()->shouldNotBeEqualTo($this->initializer);

        $this->lengthCheck();
    }

    function it_should_implode_items_even_if_its_multi_dimensional()
    {
        $this->join()->get()->shouldBeEqualTo('JohnDoejohndoe@example.comJaneDoejanedoe@example.commusic');
    }

    function it_should_join_values_by_key_from_second_level()
    {
        $this->fromArray(
            [
                [
                    'name' => 'John',
                    'surname' => 'Doe',
                    'email' => 'johndoe@example.com',
                ],
                [
                    'name' => 'Jane',
                    'surname' => 'Doe',
                    'email' => 'janedoe@example.com',
                ]
            ]
        );

        $this->joinByKey('name')->get()->shouldBeEqualTo('JohnJane');
    }

    function it_should_find_all_second_level_values_by_key_and_return_array()
    {
        $this->lists('hobby')->all()->shouldBe(['music']);

        $this->lists('name')->all()->shouldBe(['Jane']);

        $this->lengthCheck();
    }

    function it_should_split_items_into_chunks()
    {
        $chunks = $this->flatten()->chunk(2)->all();

        foreach ($chunks as $chunk)
        {
            $chunk->shouldHaveType('im\Primitive\Container\Container');
        }
    }

    function it_should_combine_given_keys_with_inner_values_without_second_argument()
    {
        $keys = [
            'first', 'second', 'third', 'forth'
        ];

        $this->combine($keys)->all()->shouldBe(
            [
                'first' => 'John',
                'second' => 'Doe',
                'third' => 'johndoe@example.com',
                'forth' => [
                    'name' => 'Jane',
                    'surname' => 'Doe',
                    'email' => 'janedoe@example.com',
                    'hobby' =>'music'
                ]
            ]
        );
    }

    function it_should_combine_given_keys_with_inner_values_with_second_argument_given()
    {
        $keys = [
            'first', 'second', 'third', 'forth'
        ];

        $this->combine($keys, 'keys')->all()->shouldBe(
            [
                'first' => 'John',
                'second' => 'Doe',
                'third' => 'johndoe@example.com',
                'forth' => [
                    'name' => 'Jane',
                    'surname' => 'Doe',
                    'email' => 'janedoe@example.com',
                    'hobby' =>'music'
                ]
            ]
        );
    }

    function it_should_combine_given_values_with_inner_keys()
    {
        $values = [
            'first', 'second', 'third', 'forth'
        ];

        $this->combine($values, 'values')->all()->shouldBe(
            [
                'name' => 'first',
                'surname' => 'second',
                'email' => 'third',
                'wife' => 'forth'
            ]
        );
    }

//    function it_should_throw_exception_if_length_of_give_array_is_not_equal_with_inner_length()
//    {
//        $keys = [
//            'first', 'second', 'third', 'forth', 'fifth'
//        ];
//
//        $this->shouldThrow('im\Primitive\Container\Exceptions\BadLengthException')
//             ->duringCombine($keys, 'keys');
//    }

    function it_should_throw_exception_if_given_wrong_second_param_to_combine()
    {
        $keys = [
            'first', 'second', 'third', 'forth'
        ];

        $this->shouldThrow('\BadMethodCallException')
             ->duringCombine($keys, 'foo');
    }

    function it_should_filter_items_with_callback_in_Container()
    {
        $filtered = $this->filter('is_int');
        $filtered->all()->shouldBe([]);

        $filtered->shouldHaveCount(0);

        $this->lengthCheck();
    }

    function it_should_traverse_through_each_item()
    {
        $this->each(function( & $value)
        {
            if ( ! is_array($value)) $value .= 'ok';

        })->first()->shouldBeEqualTo('John');

        $this->lengthCheck();
    }

    function it_should_map_items_them_and_return_new_Container()
    {
        $map = $this->map(function( & $value)
        {
            if ( ! is_array($value)) return $value .= 'Ok';

            return $value;
        });

        $map->shouldHaveType('im\Primitive\Container\Container');
        $map->first()->shouldBe('JohnOk');

        $this->all()->shouldBeEqualTo($this->initializer);
        $this->lengthCheck();
    }

    function it_should_transform_items_with_callback()
    {
        $this->transform(function( & $value)
        {
            if ( ! is_array($value)) return $value .= 'Ok';

            return $value;
        })->first()->shouldBe('JohnOk');

        $this->lengthCheck();
    }

    function it_should_walk_through_all_first_level_items()
    {
        $this->walk(function( & $value)
        {
            if ( ! is_array($value)) $value .= 'Ok';

        })->first()->shouldBe('JohnOk');

        $this->lengthCheck();
    }

    function it_should_walk_through_all_items_recursive()
    {
        $this->walk(function( & $value)
        {
            $value .= 'Ok';

        }, true)->get('wife.hobby')->shouldBe('musicOk');

        $this->lengthCheck();
    }

    function it_should_walk_through_all_items_recursive_and_take_keys_in_callback()
    {
        $this->walk(function( & $value, $key)
        {
            $value .= "-{$key}";

        }, true)->get('wife.hobby')->shouldBe('music-hobby');

        $this->lengthCheck();
    }

    function it_should_walk_through_all_items_and_receive_userdata_in_callback_passed_as_third_param()
    {
        $this->walk(function( & $value, $key, $userdata)
        {
            $value .= "-{$key}-{$userdata}";

        }, true, 'USERDATA')->get('wife.hobby')->shouldBe('music-hobby-USERDATA');

        $this->lengthCheck();
    }

    function it_should_merge_items_with_given_array_and_leave_length_the_same()
    {
        $this->merge(['name' => 'Mike'])->first()->shouldBe('Mike');

        $this->lengthCheck();
    }

    function it_should_merge_items_with_given_array_and_recalculateLength()
    {
        $this->merge(['name' => 'Mike', 'mother' => 'Linda'])->length->shouldBe(count($this->initializer) + 1);
    }

    function it_should_merge_items_by_key_with_given_array_and_leave_length_the_same()
    {
        $this->mergeWithKey(['name' => 'Mike'], 'wife')->get('wife.name')->shouldBe('Mike');

        $this->lengthCheck();
    }

    function it_should_increase_Container_length_and_add_given_value_to_new_indexes()
    {
        $this->increase('3', 'foo')->get(2)->shouldBe('foo');

        $this->plusLengthCheck(3);
    }

    function it_should_return_one_random_key_from_items_keys()
    {
        $this->randomKey()->shouldBeInRange($this->keys()->all());

        $this->lengthCheck();
    }

    function it_should_return_array_of_exact_number_of_random_keys_from_item_keys_but_not_larger_than_Container_length()
    {
        $this->randomKey(3)->all()->shouldBeArrayAndExactLengthOf(3);

        $this->lengthCheck();
    }

    function it_should_return_random_value_from_Container()
    {
        $this->random()->shouldBeInRange($this->values()->all());

        $this->lengthCheck();
    }

    function it_should_return_array_of_exact_number_of_random_values_but_not_larger_than_Container_length()
    {
        $this->random(3)->shouldHaveCount(3);

        $this->lengthCheck();
    }

    function it_should_cut_all_items_after_given_offset()
    {
        $this->cut(1)->firstKey()->shouldBeEqualTo('surname');

        $this->lengthCheck();
    }

    function it_should_cut_items_between_given_offset_and_length()
    {
        $this->cut(1, 2)->lastKey()->shouldBeEqualTo('email');

        $this->lengthCheck();
    }

    function it_should_return_new_Container_without_rejected_values_by_given_value()
    {
        $reject = $this->reject('John');

        $reject->first()->shouldBe('Doe');
        $reject->length->shouldBe(count($this->initializer) - 1);

        $this->lengthCheck();
    }

    function it_should_return_new_Container_without_rejected_values_by_given_callback()
    {
        $reject = $this->reject(function($value)
        {
            return $value == 'John';
        });

        $reject->first()->shouldBe('Doe');
        $reject->length->shouldBe(count($this->initializer) - 1);

        $this->lengthCheck();
    }

    function it_should_encrypt_items_and_should_be_exact_like_encrypted_initializer()
    {
        $key = 'cypher';
        $expires = time() + 3600;

        $encrypted = $this->encrypt($key, $expires);

        $encrypted->shouldBeString();

        $payload = [
            'exp' => $expires,
            'container' => json_encode($this->initializer, 0)
        ];

        $encrypted->shouldBeEqualTo(JWT::encode($payload, $key));
    }

    function it_should_reduce_items_to_one_value()
    {
        $initializer = [1,2,3,4,5];

        $reduce = $this->fromArray($initializer)->reduce(function($carry, $item)
        {
            return $carry += $item;
        });

        $reduce->shouldBeInteger();

        $reduce->shouldBe(15);
    }

    function it_should_decrypt_items_and_should_be_equal_to_initializer()
    {
        $key = 'cypher';
        $expires = time() + 3600;

        $encrypted = $this->encrypt($key, $expires);

        $this->fromEncrypted($encrypted, $key)->all()->shouldBeEqualTo($this->initializer);
    }

    function it_should_forget_item_by_key()
    {
        $initializer = $this->initializer;

        unset($initializer['wife']);

        $this->forget('wife')->all()->shouldBeEqualTo($initializer);

        $this->minusLengthCheck();
    }

    function it_should_forget_item_by_key_with_dot_notation_syntax()
    {
        $initializer = $this->initializer;

        unset($initializer['wife']['hobby']);

        $this->forget('wife.hobby')->all()->shouldBeEqualTo($initializer);

        $obj = new \stdClass();
        $obj->value = 15;

        $this->initializer['new'] = $obj;

        $match = $this->initializer;

        unset($match['new']->value);

        $this->fromArray($this->initializer)->forget('new.value')->all()->shouldBe($match);

        $this->lengthCheck();
    }

    function it_should_reset_Container_to_empty_array()
    {
        $reset = $this->reset()->all();

        $reset->shouldBeArray();
        $reset->shouldHaveCount(0);
    }

    function it_should_reverse_items()
    {
        $this->reverse()->all()->shouldBeEqualTo(
            array_reverse($this->initializer)
        );

        $this->lengthCheck();
    }

    function it_should_return_all_items()
    {
        $this->all()->shouldBeEqualTo($this->initializer);
    }

    function it_should_return_Container_copy()
    {
        $copy = $this->copy();

        $copy->shouldHaveType('im\Primitive\Container\Container');
        $copy->all()->shouldBeEqualTo($this->initializer);
        $copy->shouldHaveCount(count($this->initializer));

        $this->lengthCheck();
    }

    function it_should_group_items_by_similar_key()
    {
        $initializer = [
            [
                'name' => 'John',
                'surname' => 'Doe',
                'email' => 'johndoe@example.com',
                'order' => 'first'
            ],
            [
                'name' => 'Jane',
                'surname' => 'Doe',
                'email' => 'janedoe@example.com',
                'order' => 'first'
            ],
            [
                'name' => 'John',
                'surname' => 'McDonald',
                'email' => 'johnmc@example.com',
                'order' => 'second'
            ],
            [
                'name' => 'Jane',
                'surname' => 'McDonald',
                'email' => 'janemc@example.com',
                'order' => 'second'
            ]
        ];

        $groupBy = $this->fromArray($initializer)->groupBy('order');

        $groupBy->shouldHaveType('im\Primitive\Container\Container');
        $groupBy->all()->shouldBe(
            [
                'first' => [
                    [
                        'name' => 'John',
                        'surname' => 'Doe',
                        'email' => 'johndoe@example.com',
                        'order' => 'first'
                    ],
                    [
                        'name' => 'Jane',
                        'surname' => 'Doe',
                        'email' => 'janedoe@example.com',
                        'order' => 'first'
                    ]
                ],
                'second' => [
                    [
                        'name' => 'John',
                        'surname' => 'McDonald',
                        'email' => 'johnmc@example.com',
                        'order' => 'second'
                    ],
                    [
                        'name' => 'Jane',
                        'surname' => 'McDonald',
                        'email' => 'janemc@example.com',
                        'order' => 'second'
                    ]
                ]
            ]
        );

        $this->lengthCheck();
    }

    function it_should_group_items_by_key_with_callback()
    {
        $initializer = [
            [
                'name' => 'John',
                'surname' => 'Doe',
                'email' => 'johndoe@example.com',
                'order' => 'first'
            ],
            [
                'name' => 'Jane',
                'surname' => 'Doe',
                'email' => 'janedoe@example.com',
                'order' => 'first'
            ],
            [
                'name' => 'John',
                'surname' => 'McDonald',
                'email' => 'johnmc@example.com',
                'order' => 'second'
            ],
            [
                'name' => 'Jane',
                'surname' => 'McDonald',
                'email' => 'janemc@example.com',
                'order' => 'second'
            ]
        ];

        $groupBy = $this->fromArray($initializer)->groupBy(function($value, $key)
        {
            return $value['order'];
        });

        $groupBy->shouldHaveType('im\Primitive\Container\Container');
        $groupBy->all()->shouldBe(
            [
                'first' => [
                    [
                        'name' => 'John',
                        'surname' => 'Doe',
                        'email' => 'johndoe@example.com',
                        'order' => 'first'
                    ],
                    [
                        'name' => 'Jane',
                        'surname' => 'Doe',
                        'email' => 'janedoe@example.com',
                        'order' => 'first'
                    ]
                ],
                'second' => [
                    [
                        'name' => 'John',
                        'surname' => 'McDonald',
                        'email' => 'johnmc@example.com',
                        'order' => 'second'
                    ],
                    [
                        'name' => 'Jane',
                        'surname' => 'McDonald',
                        'email' => 'janemc@example.com',
                        'order' => 'second'
                    ]
                ]
            ]
        );
    }

    function it_should_return_new_Container_except_given_keys()
    {
        $except = $this->except(['email', 'wife']);

        $initializer = $this->initializer;

        unset($initializer['email'], $initializer['wife']);

        $except->shouldHaveType('im\Primitive\Container\Container');
        $except->all()->shouldBe($initializer);
        $except->shouldHaveCount(count($initializer));

        $this->lengthCheck();
        $this->all()->shouldBe($this->initializer);
    }

    function it_should_return_new_Container_except_given_index()
    {
        $exceptIndex = $this->exceptIndex(0);

        $initializer = $this->initializer;

        unset($initializer['name']);

        $exceptIndex->shouldHaveType('im\Primitive\Container\Container');
        $exceptIndex->all()->shouldBe($initializer);
        $exceptIndex->shouldHaveCount(count($initializer));

        $this->lengthCheck();
        $this->all()->shouldBe($this->initializer);
    }

    function it_should_throw_exception_if_given_equal_or_larger_index()
    {
        $this->shouldThrow('im\Primitive\Support\Exceptions\OffsetNotExistsException')
             ->during('exceptIndex', [count($this->initializer)]);
    }

    function it_should_return_rest_items_after_given_index()
    {
        $initializer = $this->initializer;

        unset($initializer['name'], $initializer['surname']);

        $rest = $this->restAfterIndex(1);

        $rest->all()->shouldBeEqualTo($initializer);
        $rest->shouldHaveType('im\Primitive\Container\Container');
        $rest->shouldHaveCount(count($initializer));

        $this->lengthCheck();
    }

    function it_should_return_rest_items_after_given_key()
    {
        $initializer = $this->initializer;

        unset($initializer['name'], $initializer['surname']);

        $rest = $this->restAfterKey('surname');

        $rest->all()->shouldBeEqualTo($initializer);
        $rest->shouldHaveType('im\Primitive\Container\Container');
        $rest->shouldHaveCount(count($initializer));

        $this->lengthCheck();
    }

    function it_should_flatten_items()
    {
        $match = ['John', 'Doe', 'johndoe@example.com', 'Jane', 'Doe', 'janedoe@example.com', 'music'];

        $this->flatten()->all()->shouldBeEqualTo($match);
    }

    function it_should_take_column_by_key_from_items_arrays()
    {
        $initializer = [
                        ['name' => 'John',
                         'surname' => 'Doe',
                         'email' => 'johndoe@example.com'],
                        ['name' => 'Jane',
                         'surname' => 'Doe',
                         'email' => 'janedoe@example.com',
                         'hobby' =>'music']
                       ];

        $this->fromArray($initializer);

        $column = $this->column('name');

        $column->all()->shouldBeEqualTo(['John', 'Jane']);
        $column->shouldHaveType('im\Primitive\Container\Container');
        $column->shouldHaveCount(2);

        $this->lengthCheck(count($initializer));
    }

    function it_should_leave_only_truly_items()
    {
        $initializer = [
            'name' => 'Jane',
            'surname' => 'Doe',
            'email' => 'janedoe@example.com',
            'hobby' =>'music',
            false,
            0,
            '',
            null
        ];

        $this->fromArray($initializer);

        $truly = $this->truly();
        $truly->all()->shouldBeEqualTo(['name' => 'Jane',
                                        'surname' => 'Doe',
                                        'email' => 'janedoe@example.com',
                                        'hobby' =>'music']);
        $truly->shouldHaveCount(4);

        $this->lengthCheck(count($initializer));
    }

    function it_should_take_all_items_by_key_recursively()
    {
        $take = $this->take('name');

        $take->all()->shouldBe(['John', 'Jane']);
        $take->shouldHaveCount(2);

        $this->lengthCheck();
    }

    function it_should_pull_value_and_remove_it()
    {
        $pulled = $this->pull('email');

        $pulled->shouldBe($this->initializer['email']);

        $initializer = $this->initializer;

        unset($initializer['email']);

        $this->all()->shouldBeEqualTo($initializer);

        $this->minusLengthCheck();
    }

    function it_should_pull_value_with_dot_notation_and_remove_it()
    {
        $pulled = $this->pull('wife.name');

        $pulled->shouldBe($this->initializer['wife']['name']);

        $initializer = $this->initializer;

        unset($initializer['wife']['name']);

        $this->all()->shouldBeEqualTo($initializer);

        $this->lengthCheck();
    }

    function it_should_recursively_remove_all_items_by_key_and_return_new_Container_without_this_key()
    {
        $without = $this->without('name');

        $without->shouldHaveType('im\Primitive\Container\Container');
        $without->has('name')->shouldBe(false);
        $without->has('wife.name')->shouldBe(false);
        $without->shouldHaveCount(count($this->initializer) - 1);

        $this->lengthCheck();
    }

    function it_should_intersect_items_values_with_given_array_values()
    {
        $this->fromArray(['name' => 'John',
                          'surname' => 'Doe',
                          'email' => 'johndoe@example.com']);

        $intersect = $this->intersect(['John', 'johndoe@example.com']);
        $intersect->shouldHaveType('im\Primitive\Container\Container');
        $intersect->all()->shouldBe(['name' => 'John', 'email' => 'johndoe@example.com']);

        $this->lengthCheck(3);
    }

    function it_should_intersect_items_values_with_given_array_values_with_key_check()
    {
        $with = [
            'John',
            'email' => 'johndoe@example.com'
        ];

        $this->fromArray(['name' => 'John',
                          'surname' => 'Doe',
                          'email' => 'johndoe@example.com']);

        $intersect = $this->intersect($with, true);
        $intersect->shouldHaveType('im\Primitive\Container\Container');
        $intersect->all()->shouldBe(['email' => 'johndoe@example.com']);

        $this->lengthCheck(3);
    }

    function it_should_intersect_items_keys_with_given_array_keys()
    {
        $keys = ['name' => 'foo'];

        $this->fromArray(['name' => 'John',
                          'surname' => 'Doe',
                          'email' => 'johndoe@example.com']);

        $intersectKeys = $this->intersectKey($keys);
        $intersectKeys->shouldHaveType('im\Primitive\Container\Container');
        $intersectKeys->all()->shouldBe(['name' => 'John']);

        $this->lengthCheck(3);
    }

    function it_should_sort_items_with_callable_in_Container()
    {
        $initializer = [
            [
                'name' => 'John',
                'surname' => 'Doe',
                'email' => 'johndoe@example.com',
                'order' => 1
            ],
            [
                'name' => 'Jane',
                'surname' => 'Doe',
                'email' => 'janedoe@example.com',
                'order' => 2
            ],
            [
                'name' => 'John',
                'surname' => 'McDonald',
                'email' => 'johnmc@example.com',
                'order' => 0
            ],
            [
                'name' => 'Jane',
                'surname' => 'McDonald',
                'email' => 'janemc@example.com',
                'order' => 4
            ]
        ];

        $this->fromArray($initializer);

        $sorted = $this->sort(function($a, $b)
        {
            $a = $a['order'];
            $b = $b['order'];

            if ($a > $b) return 1;
            if ($a < $b) return -1;
            return 0;
        });

        $sorted->first()->shouldBe([
            'name' => 'John',
            'surname' => 'McDonald',
            'email' => 'johnmc@example.com',
            'order' => 0
        ]);

        $sorted->last()->shouldBe([
            'name' => 'Jane',
            'surname' => 'McDonald',
            'email' => 'janemc@example.com',
            'order' => 4
        ]);

        $this->all()->shouldBe($initializer);
    }

    function it_should_calculate_sum_of_items()
    {
        $sum = [10, 20, 1, [20, [10]]];
        $init = array_merge($this->initializer, $sum);

        $int = $this->fromArray($init)->sum();

        $int->shouldHaveType('im\Primitive\Int\Int');

        $int->value()->shouldBe(61);
    }

    function it_should_reset_keys_in_Container()
    {
        $this->resetKeys()->keys()->all()->shouldBe([0, 1, 2, 3]);
    }

    function it_should_check_if_Container_is_associative()
    {
        $this->isAssoc()->shouldBe(true);
    }

    function it_should_check_if_Container_is_not_associative()
    {
        $this->isNotAssoc()->shouldBe(false);
    }

    function it_should_check_if_Container_is_multi_dimension()
    {
        $this->isMulti()->shouldBe(true);
    }

    function it_should_check_if_Container_is_not_multi_dimension()
    {
        $this->isNotMulti()->shouldBe(false);
    }

    function it_should_check_if_Container_is_empty()
    {
        $this->isEmpty()->shouldBe(false);
    }

    function it_should_check_if_Container_is_not_empty()
    {
        $this->isNotEmpty()->shouldBe(true);
    }

    function it_should_convert_Container_to_array()
    {
        $this->fromArray([
            container(['John']), container(['Jane'])
        ]);

        $array = $this->toArray();
        $array->shouldBe([
            ['John'], ['Jane']
        ]);

        $this->lengthCheck(2);
    }

    function it_should_convert_Container_to_json()
    {
        $initializer = [
            'name' => 'Jane',
            'surname' => 'McDonald'
        ];

        $this->fromArray($initializer);

        $json = $this->toJson();
        $json->shouldBeString();
        $json->shouldBe('{"name":"Jane","surname":"McDonald"}');
    }

    function it_should_write_items_to_file()
    {
        $file = __DIR__.'/container.cdata';

        $this->toFile($file);

        $this->fromFile($file)->all()->shouldBe($this->initializer);
    }

    function it_should_construct_from_encrypted_Container()
    {
        $key = 'cypher';
        $expires = time() + 3600;

        $encrypted = $this->encrypt($key, $expires);

        $this->fromArray([]);

        $this->fromEncrypted($encrypted, $key)->all()->shouldBe($this->initializer);
    }

    function it_should_call_magic_to_string_and_return_json()
    {
        $match = $this->toJson();

        $this->__toString()->shouldBe($match);
    }

//    function it_should_clone_Container_without_references()
//    {
//        $clone = clone $this;
//
//        $clone->push(1);
//
//        $this->all()->shouldBe($this->initializer);
//        $this->lengthCheck();
//    }

    /*
    |--------------------------------------------------------------------------
    | Helpers
    |--------------------------------------------------------------------------
    */
    /**
     * Checks length equal to $this->initializer
     *
     * @param null $length
     */
    protected function lengthCheck($length = null)
    {
        if ( ! is_null($length))
        {
            $this->all()->shouldHaveCount($length);
            $this->length->shouldBe($length);
        }
        else
        {
            $this->all()->shouldHaveCount(count($this->initializer));
            $this->length->shouldBe(count($this->initializer));
        }
    }

    /**
     * Checks length equal to $this->initializer plus $add
     *
     * @param $add
     */
    protected function plusLengthCheck($add = 1)
    {
        $this->all()->shouldHaveCount(count($this->initializer) + $add);
        $this->length->shouldBe(count($this->initializer) + $add);
    }

    /**
     * Checks length equal to $this->initializer minus $add
     *
     * @param $add
     */
    protected function minusLengthCheck($add = 1)
    {
        $this->all()->shouldHaveCount(count($this->initializer) - $add);
        $this->length->shouldBe(count($this->initializer) - $add);
    }

    /**
     * Appends new matchers
     */
    public function getMatchers()
    {
        return array(

            'beInRange' => function($key, $match)
            {
                return in_array($key, $match);
            },

            'beArrayAndExactLengthOf' => function($array, $count)
            {
                return is_array($array) && $count === count($array);
            }

        );
    }
}


<?php

namespace Sonic\Routes\Feed;

/**
 * AtomRoute
 *
 * This route is executed when an Atom feed is requested
 *
 * @package    Sonic
 * @subpackage Route
 * @author     Hassan Khan <contact@hassankhan.me>
 * @link       https://github.com/hassankhan/Sonic
 * @license    MIT
 * @since      0.7
 */
class AtomRoute extends \Sonic\Routes\ListRoute
{

    /**
     * Initializes the route by creating a regex pattern from the provided URL,
     * and assigns the callable function for this route
     *
     * @param string $url
     * @param string $title
     * @param string $body
     * @codeCoverageIgnore
     */
    public function __construct($url = '')
    {
        parent::__construct($url, array($this, 'build_route'));
    }

    /**
     * Method required by abstract class. Returns an array of posts
     * that match whatever requirements for this route.
     *
     * @return array
     */
    public function posts()
    {
        // Get dates from all content
        $dates = $this->dates('content');

        // Get filenames of content and create array to hold posts
        $posts = $this->excerpts(array_keys($dates));

        foreach ($posts as $path => $post) {

            // Try and get post date, otherwise use Unix Epoch date
            $post_date = isset($post['meta']['date']) === TRUE
                ? new \DateTime($post['meta']['date'])
                : new \DateTime('01-01-1970');

            $timestamp = $this->sonic->app['filesystem']->getTimestamp(
                $this->sonic->app['settings']['sonic.content_dir'] .'/' . $path
            );

            // Create additional fields for Atom stuff
            $posts[$path]['id']             = $this->make_id($this->sonic->app['helper']->url_for($path));
            $posts[$path]['published_date'] = $post_date->format(\DateTime::ATOM);
            $posts[$path]['modified_date']  = date('Y-m-d\TH:i:sP', $timestamp);
        }

        return $posts;
    }

    /**
     * Builds and returns the rendered HTML
     *
     * @return string
     */
    public function build_route($param = '')
    {
        // Get dates from all content
        $dates = $this->dates('content');
        // Get filenames of content and create array to hold posts
        $posts = $this->posts();
        // Get today's date as a DateTime object
        $todays_date = new \DateTime();

        // Set response content-type
        $this->sonic->app['router']->response()->headers->set('Content-type', 'application/xml');

        // Render template with Twig
        return $this->sonic->app['twig']->render(
            'feed.twig',
            array(
                'date' => $todays_date->format(\DateTime::ATOM),
                'entries' => $posts
            )
        );
    }

    /**
     * Creates a ``tag:`` URI as per @diveintomark's article
     * @link   http://web.archive.org/web/20110514113830/http://diveintomark.org/archives/2004/05/28/howto-atom-id#tag
     * @param  string $url
     * @return string
     */
    public function make_id($url)
    {
        $tag             = str_replace(array('http://', 'https://'), '', $url);
        $tag             = str_replace('#', '/', $tag);
        $exploded_tag    = explode('/', $tag);
        $exploded_tag[0] = $exploded_tag[0] . ',' . date('Y-m-d') . ':';
        return implode('/', $exploded_tag);
    }

}


<?php

namespace Sonic\Routing;

/**
 * This is the Sonic URL Router, the layer of a web application between the URL
 * and the route executed to perform a request.
 *
 * The router controls the boring, base-framework-y stuff. This includes
 * handling the request and response objects, adding routes, and figuring
 * out which route to execute for a given request.
 *
 * It supports adding routes to different HTTP methods (GET, POST and friends)
 * ```php
 * $router = new Sonic\Router;
 *
 * // Adding a basic HTTP GET route
 * $router->get( '/get', 'get_page' );
 *
 * // Adding a basic HTTP POST route
 * $router->post( '/post', 'post_page' );
 *
 * // Adding a route with a named alphanumeric capture, using the <:var_name> syntax
 * $router->get( '/user/view/<:username>', 'view_username' );
 *
 * // Adding a route with a named numeric capture, using the <#var_name> syntax
 * $router->get( '/user/view/<#user_id>', array( 'UserClass', 'view_user' ) );
 *
 * // Adding a route with a wildcard capture (Including directory separtors), using
 * // the <*var_name> syntax
 * $router->get( '/browse/<*categories>', 'category_browse' );
 *
 * // Adding a wildcard capture (Excludes directory separators), using the
 * // <!var_name> syntax
 * $router->get( '/browse/<!category>', 'browse_category' );
 *
 * // Adding a custom regex capture using the <:var_name|regex> syntax
 * $router->get( '/lookup/zipcode/<:zipcode|[0-9]{5}>', 'zipcode_func' );
 *
 * // Specifying priorities
 * $router->get( '/users/all', 'view_users', 1 ); // Executes first
 * $router->get( '/users/<:status>', 'view_users_by_status', 100 ); // Executes after
 *
 * // Specifying a callback function if no other route is matched
 * $router->not_found( 'page_404' );
 * $router->not_found(array('SomeClass', 'page_404_method'));
 *
 * // Specifying a callback function if any errors occur
 * $router->error( 'page_500' );
 * $router->not_found(array('SomeClass', 'page_500_method'));
 *
 * // Run the router
 * $router->run();
 * ```
 * @package    Sonic
 * @author     Brandon Wamboldt <brandon.wamboldt@gmail.com>
 * @author     Hassan Khan <contact@hassankhan.me>
 * @link       https://github.com/hassankhan/Sonic
 * @license    MIT
 * @since      0.2
 */
class Router
{

    /**
     * Supported HTTP Methods (might not be strictly accurate yet - I've only tested GET and POST')
     */
    const METHOD_HEAD    = 'HEAD';
    const METHOD_GET     = 'GET';
    const METHOD_POST    = 'POST';
    const METHOD_PUT     = 'PUT';
    const METHOD_PATCH   = 'PATCH';
    const METHOD_DELETE  = 'DELETE';
    const METHOD_OPTIONS = 'OPTIONS';

    /**
     * Request object
     *
     * @var \Symfony\Component\HttpFoundation\Request
     */
    protected $request;

    /**
     * Response object
     * @var \Symfony\Component\HttpFoundation\Response
     */
    protected $response;

    /**
     * Currently matched route, if one has been set
     *
     * @var Route
     */
    protected $current_route;

    /**
     * Current route's status code. Is either 200, 404 or 500
     *
     * @var integer
     */
    protected $current_http_status;

    /**
     * An array containing the list of routing rules and their callback
     * functions, as well as their request method and any additional paramters.
     *
     * @var Route[]
     */
    protected $routes;

    /**
     * Callback function to execute when no matching URL is found
     *
     * @var \Closure
     */
    protected $not_found_handler;

    /**
     * Callback function to execute on any other error
     *
     * @var \Closure
     */
    protected $error_handler;

    /**
     * Initializes the router, and handles the request and response objects
     *
     * @param \Symfony\Component\HttpFoundation\Request  $request
     * @link  http://api.symfony.com/2.4/Symfony/Component/HttpFoundation/Request.html Documentation for Request object
     * @param \Symfony\Component\HttpFoundation\Response $response
     * @link  http://api.symfony.com/2.4/Symfony/Component/HttpFoundation/Response.html Documentation for Response object
     */
    public function __construct(
        \Symfony\Component\HttpFoundation\Request  $request,
        \Symfony\Component\HttpFoundation\Response $response
    ) {
        $this->request  = $request;
        $this->response = $response;
    }

    /**
     * ROUTING
     */

    /**
     * Add HTTP GET route
     *
     * @see    Router::route()
     * @param  string                $route
     * @param  array|\Closure|string $callback
     */
    public function get($route, $callback)
    {
        $this->route(new Route($route, $callback));
    }

    /**
     * Add HTTP POST route
     *
     * @see    Router::route()
     * @param  string                $route
     * @param  array|\Closure|string $callback
     */
    public function post($route, $callback)
    {
        $this->route(new Route($route, $callback), self::METHOD_POST);
    }

    /**
     * Adds a new URL routing rule to the routing table, after converting any of
     * our special tokens into proper regular expressions.
     *
     * @param  Route  $route
     * @param  string $http_method
     * @throws \InvalidArgumentException If an invalid HTTP method is specified
     * @throws \LogicException           If the route already exists in the routing table
     */
    public function route(Route $route, $http_method = self::METHOD_GET)
    {
        // Is this a valid HTTP method?
        if (!in_array($http_method, array(
            self::METHOD_HEAD,
            self::METHOD_GET,
            self::METHOD_POST,
            self::METHOD_PUT,
            self::METHOD_PATCH,
            self::METHOD_DELETE
        ))) {
            throw new \InvalidArgumentException("The method {$http_method} is invalid");
        }

        // Does this URL already exist in the routing table?
        if (isset($this->routes[$http_method][$route->pattern()])) {
            // Trigger a new error and exception if errors are on
            throw new \LogicException("The URI {$route->url()} already exists in the routing table");
        }

        // Add the route to the routing table
        $this->routes[$http_method][$route->pattern()] = $route;
    }

    /**
     * Returns a Route object if matching URL is found
     *
     * @param  string $url
     * @param  string $http_method
     * @return Route|null
     */
    public function match($url, $http_method = self::METHOD_GET)
    {
        // Make sure there is a trailing slash
        $url = rtrim($url, '/') . '/';

        foreach ($this->routes[$http_method] as $route) {
            if (preg_match($route->pattern(), $url)) {
                return $route;
            }
        }
        return null;
    }

    /**
     * ROUTE EXECUTION
     */

    /**
     * Runs the router matching engine and then calls the matching route's
     * callback. otherwise execute the not found handler
     *
     * @return bool
     * @throws \RuntimeException If no routes exist in the routing table
     */
    public function run()
    {
        // If no routes have been added, then throw an exception
        try {
            if (empty($this->routes)) {
                throw new \RuntimeException('No routes exist in the routing table. Add some');
            }
        }
        catch (\Exception $e) {
            $this->error($e);
            return FALSE;
        }

        // Try and get a matching route for the current URL
        $route = $this->match(
            $this->request->getPathInfo(),
            $this->request->getMethod()
        );

        // Call not found handler if no match was found
        if ($route === null) {
            $this->not_found();
            return FALSE;
        }

        // Set current route
        $this->current_route = $route;

        // Get parameters from request
        $params = $this->parse_parameters($route);
        // Try to execute callback for route, if it fails, catch the exception and generate a HTTP 500 error
        try {
            $this->current_http_status = \Symfony\Component\HttpFoundation\Response::HTTP_OK;

            // Set response content
            $this->response->setContent($route->execute($params));

            // Send response
            $this->response->send();
            return TRUE;
        }
        catch (\Exception $e) {
            $this->error($e);
            return FALSE;
        }
    }

    /**
     * Redirects to another URL
     *
     * @param  string $url
     * @return bool
     */
    public function redirect($url)
    {
        // If no URL is given, throw exception
        try {
            if (empty($url)) {
                throw new \InvalidArgumentException('No URL given');
            }
        }
        catch (\Exception $e) {
            $this->error($e);
            return FALSE;
        }

        // Set redirect status codes and location
        $this->current_http_status = \Symfony\Component\HttpFoundation\Response::HTTP_FOUND;
        $this->response->setStatusCode($this->current_http_status);
        $this->response->headers->set('Location', $url);
        return TRUE;
    }

    /**
     * ACCESSORS
     */

    /**
     * Returns request object
     *
     * @return \Symfony\Component\HttpFoundation\Request
     */
    public function request()
    {
        return $this->request;
    }

    /**
     * Returns response object
     *
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function response()
    {
        return $this->response;
    }

    /**
     * Returns all routes mapped on the routing table.
     *
     * @return Route[]
     */
    public function routes()
    {
        return $this->routes;
    }

    /**
     * Returns the currently matched route.
     *
     * @return Route
     */
    public function current_route()
    {
        return $this->current_route;
    }

    /**
     * Returns the HTTP status code of the currently matched route
     *
     * @return integer
     */
    public function current_http_status()
    {
        return $this->current_http_status;
    }

    /**
     * ERROR HANDLING
     */

    /**
     * This method can either set the callback to execute on a 'Server error' (50x)
     * error, or it invokes the callback
     * To set the callback, provide a function to the method
     * To invoke the callback, call the method with a string detailing the error
     * as a parameter
     *
     * @param  \Closure|\Exception $arg
     * @return
     */
    public function error($arg = null)
    {
        if (is_callable($arg)) {
            // Set provided callback function as error handler
            $this->error_handler = $arg;
        }
        else {
            // Set HTTP status on router
            $this->current_http_status = \Symfony\Component\HttpFoundation\Response::HTTP_INTERNAL_SERVER_ERROR;

            // Execute error handler and set result as response content
            $result = is_callable($this->error_handler) === TRUE
                ? call_user_func($this->error_handler, $arg)
                : $this->default_error_handler($arg)->execute();

            $this->response->setContent($result);

            // Set response's status code
            $this->response->setStatusCode($this->current_http_status);

            // Send response
            $this->response->send();
        }
    }

    /**
     * This method can either set the callback to execute on a 'Page not found' (404)
     * error, or it invokes the callback
     * To set the callback, provide a function to the method
     * To invoke the callback, call the method without any parameters
     *
     * @param  \Closure $callback
     * @return
     */
    public function not_found($callback = null)
    {
        if (is_callable($callback)) {
            // Set provided callback function as not found handler
            $this->not_found_handler = $callback;
        }
        else {
            // Set HTTP status on router
            $this->current_http_status = \Symfony\Component\HttpFoundation\Response::HTTP_NOT_FOUND;

            // Execute not found handler and set result as response content
            $result = is_callable($this->not_found_handler) === TRUE
                ? call_user_func($this->not_found_handler)
                : $this->default_not_found_handler()->execute();

            $this->response->setContent($result);

            // Set response's status code
            $this->response->setStatusCode($this->current_http_status);

            // Send response
            $this->response->send();
        }
    }

    /**
     * HELPERS
     */

    /**
     * Default callback for any 404 errors
     *
     * @return string
     * @codeCoverageIgnore
     */
    protected function default_not_found_handler()
    {
        return new Route\ErrorRoute('/404', 'Page Not Found', "Couldn't find your, like, page, dude");
    }

    /**
     * Default callback for any 500 errors. If $error is provided
     * as a parameter, then the message is added to the HTML output
     *
     * @param  \Exception $error
     * @return string
     * @codeCoverageIgnore
     */
    protected function default_error_handler(\Exception $error)
    {
        return new Route\ErrorRoute('/500', 'Server Error', $error->getMessage());
    }

    /**
     * Parses parameters from URI as per the given route's pattern
     *
     * @param  Route  $route
     * @return array
     */
    protected function parse_parameters(Route $route)
    {
        // Get all parameter matches from URL for this route
        $request_url = rtrim($this->request->getPathInfo(), '/') . '/';
        preg_match($route->pattern(), "{$request_url}", $matches);

        $params = array();

        // Retrieve any matches
        foreach ($matches as $key => $value) {
            if (is_string($key)) {
                $params[] = $value;
            }
        }

        return $params;
    }

}


<?php

namespace spec\im\Primitive\Int;

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class IntSpec extends ObjectBehavior
{
    protected $init;

    function let()
    {
        $this->init = 10;

        $this->beConstructedWith($this->init);
    }

    function it_is_initializable()
    {
        $this->shouldHaveType('im\Primitive\Int\Int');
    }

    /**
     * Construction and Conversion
     */

    function it_should_construct_from_int()
    {
        $init = 1;

        $this->beConstructedWith($init);

        $this->value()->shouldBeEqualTo($init);
    }

    function it_should_construct_from_float()
    {
        $init = 1.1;

        $this->beConstructedWith($init);

        $this->value()->shouldBeEqualTo((int) $init);
    }

    function it_should_construct_from_string()
    {
        $init = 'false';

        $this->beConstructedWith($init);

        $this->value()->shouldBeEqualTo((int) $init);
    }

    function it_should_construct_from_bool()
    {
        $init = true;

        $this->beConstructedWith($init);

        $this->value()->shouldBeEqualTo((int) $init);
    }

    function it_should_construct_from_String_Type()
    {
        $init = 'false';

        $this->beConstructedWith(string($init));

        $this->value()->shouldBeEqualTo((int) $init);
    }

    function it_should_construct_from_Int_Type()
    {
        $init = 13;

        $this->beConstructedWith(int($init));

        $this->value()->shouldBeEqualTo($init);
    }

    function it_should_construct_from_Float_Type()
    {
        $init = 12.3;

        $this->beConstructedWith(float($init));

        $this->value()->shouldBeEqualTo((int) $init);
    }

    function it_should_construct_from_Bool_Type()
    {
        $init = false;

        $this->beConstructedWith(bool($init));

        $this->value()->shouldBeEqualTo((int) $init);
    }

    function it_should_call_method_if_it_looks_looks_variable()
    {
        $this->value->shouldBe($this->init);
    }

    function it_should_convert_to_Bool_Type()
    {
        $this->toBool()->shouldHaveType('im\Primitive\Bool\Bool');
    }

    function it_should_convert_to_Float_Type()
    {
        $this->toFloat()->shouldHaveType('im\Primitive\Float\Float');
    }

    function it_should_convert_to_String_Type()
    {
        $this->toString()->shouldHaveType('im\Primitive\String\String');
    }

    function it_should_convert_to_Container_Type()
    {
        $this->toContainer()->shouldHaveType('im\Primitive\Container\Container');
    }

    function it_should_return_value_with_get()
    {
        $this->get()->shouldBe($this->init);
    }

    /**
     * Math
     */

    function it_should_plus_values()
    {
        $plus = 10;

        $this->plus($plus)->value()->shouldBe($this->init + $plus);
    }

    function it_should_minus_values()
    {
        $minus = 5;

        $this->minus($minus)->value()->shouldBe($this->init - $minus);
    }

    function it_should_multiply_values()
    {
        $multiply = 10;

        $this->multiply($multiply)->value()->shouldBe($this->init * $multiply);
    }

    function it_should_divide_values()
    {
        $divide = 10;

        $this->divide($divide)->value()->shouldBe($this->init / $divide);
    }

    function it_should_throw_if_divide_by_zero()
    {
        $divide = 0;

        $this->shouldThrow('\UnexpectedValueException')->duringDivide($divide);
    }

    function it_should_modulo_values()
    {
        $modulo = 2;

        $this->modulo($modulo)->value()->shouldBe($this->init % $modulo);
    }

    function it_should_power_by_value()
    {
        $power = 2;

        $this->power($power)->value()->shouldBe(pow($this->init, $power));
    }

    function it_should_sqrt_by_value()
    {
        $sqrt = 2;

        $this->sqrt($sqrt)->value()->shouldBe(sqrt($this->init));
    }

    function it_should_abs_value()
    {
        $init = -20;

        $this->set($init)->abs()->value()->shouldBe(abs($init));
    }

    function it_should_return_sin_of_value()
    {
        $this->sin()->value()->shouldBe(sin($this->init));
    }

    function it_should_return_cos_of_value()
    {
        $this->cos()->value()->shouldBe(cos($this->init));
    }

    function it_should_return_tan_of_value()
    {
        $this->tan()->value()->shouldBe(tan($this->init));
    }

    function it_should_return_Float_Type_with_PI_value()
    {
        $this->pi()->value->shouldBe(pi());
    }

    function it_should_return_factorial_of_value()
    {
        $this->factorial()->shouldBe(factorial($this->init));

        $this->set(120)->factorial()->shouldBe(factorial(120));
    }

    function it_should_format_int_to_correct_float_string()
    {
        $formatted = $this->format();

        $formatted->shouldHaveType('im\Primitive\String\String');

        $formatted->value->shouldBe(number_format($this->init, 2, '.', ' '));
    }

    function it_should_return_length_of_value()
    {
        $this->length()->shouldBe(mb_strlen((string) $this->init));
    }

    function it_should_return_true_ifEqual()
    {
        $this->isEqual(int($this->init));
        $this->isEqual($this->init);
    }

    function it_should_check_ifNegative()
    {
        $this->isNegative()->shouldBe(false);

        $this->set(-10)->isNegative()->shouldBe(true);
    }

    function it_should_check_ifNotNegative()
    {
        $this->isNotNegative()->shouldBe(true);

        $this->set(-10)->isNotNegative()->shouldBe(false);
    }

    function it_should_check_if_isLowerThan()
    {
        $this->isLowerThan(int(20))->shouldBe(true);
        $this->isLowerThan(5)->shouldBe(false);
    }

    function it_should_check_if_isLowerThanOrEqual()
    {
        $this->isLowerThanOrEqual(int(10))->shouldBe(true);
        $this->isLowerThanOrEqual(5)->shouldBe(false);
    }

    function it_should_check_if_isGreaterThan()
    {
        $this->isGreaterThan(int(0))->shouldBe(true);
        $this->isGreaterThan(15)->shouldBe(false);
    }

    function it_should_check_if_isGreaterThanOrEqual()
    {
        $this->isGreaterThanOrEqual(int(10))->shouldBe(true);
        $this->isGreaterThanOrEqual(5)->shouldBe(true);
    }

    /**
     * Helpers
     */

    function it_should_return_true_if_isTrue_called()
    {
        $result = (bool) $this->init ?: false;

        $this->isTrue()->shouldBe($result);
    }

    function it_should_return_false_if_isFalse_called()
    {
        $result = (bool) $this->init ? false: true;

        $this->isFalse()->shouldBe($result);
    }

    /**
     * Abstract Type methods
     */

    function it_should_return_value_if_call_instance_like_function()
    {
        $this()->shouldBe($this->init);
    }

    function it_should_serialize()
    {
        $match = serialize($this->init);

        $this->serialize()->shouldBe($match);
    }

    function it_should_unserialize()
    {
        $serialized = serialize($this->init);

        $this->unserialize($serialized);
        $this->value()->shouldBe($this->init);
    }

    function it_should_make_new_instance()
    {
        $match = $this->init;

        $this->make($match)->value()->shouldBe($match);
    }

    function it_should_create_new_instance_statically()
    {
        $match = $this->init;

        static::create($match)->value()->shouldBe($match);
    }

    function it_should_have_helper_function_and_create_new_instance()
    {
        $match = $this->init;

        $this->set(int($match))->value()->shouldBe($match);
    }
}


<?php

namespace Sonic\Routes;

/**
 * ListRoute
 *
 * This route displays a list of posts
 *
 * @package    Sonic
 * @subpackage Route
 * @author     Hassan Khan <contact@hassankhan.me>
 * @link       https://github.com/hassankhan/Sonic
 * @license    MIT
 * @since      0.7
 */
class ListRoute extends ListRouteAbstract
{

    /**
     * Initializes the route by creating a regex pattern from the provided URL,
     * and assigns the callable function for this route
     *
     * @param string $url
     * @codeCoverageIgnore
     */
    public function __construct($url = '')
    {
        parent::__construct($url, array($this, 'build_route'));
    }

    /**
     * Method required by abstract class. Returns an array of posts
     * that match whatever requirements for this route.
     *
     * @return array
     */
    public function posts()
    {
        // Get dates from all content
        $dates = $this->dates('content');

        return $this->excerpts(array_keys($dates));
    }

    /**
     * Plugin handler
     *
     * @param  string $path
     * @return array
     */
    public function dates($path = '')
    {
        // Get all files in path
        $contents = $this->sonic->app['filesystem']->listContents($path, TRUE);
        // Create array
        $date_list = array();

        // Iterate through files and get dates
        foreach ($contents as $file) {
            if (
                isset($file['extension']) === TRUE
                && $file['extension'] === 'md'
            ) {
                $date_list[$file['path']] = $this->date($file);
            }
        }

        // Sort the files by date
        arsort($date_list);

        return $date_list;
    }

    /**
     * Gets a date from a file
     *
     * @param  string $file
     * @return \DateTime|NULL
     */
    protected function date($file)
    {
        $file_contents = $this->sonic->app['filesystem']->parse($file['path']);
        if (isset($file_contents['meta']['date']) === FALSE) {
            return NULL;
        }
        return new \DateTime($file_contents['meta']['date']);
    }

}


<?php
/**
 * HttpRequest class file
 */

namespace Bonnier\WP\Cxense\Http;

use Bonnier\WP\Cxense\Services\DocumentSearch;
use Bonnier\WP\Cxense\Settings\SettingsPage;
use Bonnier\WP\Cxense\WpCxense;

/**
 * HttpRequest class
 */
class HttpRequest
{
    const DEFAULT_OPTIONS = [
        'timeout' => 15,
        'redirection' => 15,
    ];

    /**
     * Cxense main url
     *
     * @var string $strBaseUrl
     */
    private $strBaseUri = 'https://api.cxense.com';

    /**
     * Instance object
     *
     * @var DocumentSearch $objInstance
     */
    private static $objInstance;

    /**
     * Headers array
     *
     * @var array $arrHeaders
     */
    private $arrHeaders = [
        'Content-Type' => 'application/json'
    ];

    /**
     * Singleton implementation
     *
     * @return HttpRequest
     */
    public static function get_instance()
    {
        if (!isset(self::$objInstance)) {
            $obj = __CLASS__;
            self:: $objInstance = new $obj();
        }
        return self::$objInstance;
    }

    /**
     * Get http request
     *
     * @param string $strPath
     * @param array $arrOptions
     * @return HttpResponse
     */
    public function get($strPath, array $arrOptions = [])
    {
        $request = wp_remote_get($this->build_uri($strPath), array_merge(
            self::DEFAULT_OPTIONS,
            $arrOptions, [
                'headers' => $this->arrHeaders
            ])
        );

        return new HttpResponse($request);
    }

    /**
     * Post http request
     *
     * @param string $strPath
     * @param array $arrOptions
     * @return HttpResponse
     */
    public function post($strPath, array $arrOptions = [])
    {
        $request = wp_remote_post($this->build_uri($strPath), array_merge(
            self::DEFAULT_OPTIONS,
            $arrOptions, [
                'headers' => $this->arrHeaders
            ])
        );

        return new HttpResponse($request);
    }

    /**
     * Build url
     *
     * @param string $strPath
     */
    private function build_uri($strPath)
    {
        return rtrim($this->strBaseUri, '/') . '/' . ltrim($strPath, '/');
    }

    /**
     * Set auth header
     *
     * @return null
     */
    public function set_auth()
    {
        $date = date("Y-m-d\TH:i:s.000O");
        $this->arrHeaders['X-cXense-Authentication'] = sprintf(
            'username=%s date=%s hmac-sha256-hex=%s',
            WpCxense::instance()->settings->getApiUser(),
            $date,
            hash_hmac("sha256", $date, WpCxense::instance()->settings->getApiKey())
        );

        return $this;
    }
}


<?php

namespace Sonic;

/**
 * Helper class to hold all helper-y functions
 *
 *
 * @package    Sonic
 * @author     Hassan Khan <contact@hassankhan.me>
 * @link       https://github.com/hassankhan/Sonic
 * @license    MIT
 * @since      0.6
 */
class Helper
{
    /**
     * Instance of Sonic's container
     *
     * @var \Pimple
     */
    protected $app;

    /**
     * Constructor
     *
     * @param \Pimple $app
     * @codeCoverageIgnore
     */
    public function __construct(\Pimple $app)
    {
        // Get app container
        $this->app = $app;

        // Convert errors to exceptions
        set_error_handler(array($this, 'handle_errors'));
    }

    /**
     * Returns Sonic configuration option
     *
     * @return array
     */
    public function config($option)
    {
        return isset($this->app['settings'][$option]) === TRUE
            ? $this->app['settings'][$option]
            : NULL;
    }

    /**
     * Returns the site root
     *
     * @return string
     */
    public function site_url()
    {
        return $this->app['settings']['site.site_root'];
    }

    /**
     * Returns the site title
     *
     * @return string
     */
    public function site_title()
    {
        return $this->app['settings']['site.site_title'];
    }

    /**
     * Returns a fully-qualified URL for a given filename in the 'content' directory
     *
     * @param  string $file_name
     * @return string|null
     */
    public function url_for($file_name)
    {
        // Check if file exists
        try {

            // Try to read file, if none exists then return null
            $this->app['filesystem']->read($this->app['settings']['sonic.content_dir'] . '/' . $file_name);

            // Create URL and return
            $clean_file_name = str_replace(
                array_merge(array('index'), $this->dot_extensions()),
                '',
                $file_name
            );

            return trim($this->app['settings']['site.site_root'] . $clean_file_name, '/') . '/';
        }
        catch (\Exception $e) {
            $this->app['router']->error($e);
        }
        return null;
    }

    /**
     * Returns a HTML <a> for a given filename in the 'content' directory
     *
     * @param  string      $file_name
     * @return string|null
     */
    public function link_for($file_name)
    {
        try {
            // Check if file exists
            $content = $this->app['filesystem']->parse($this->app['settings']['sonic.content_dir'] . '/' . $file_name);

            // Get file title and URL and return
            $title   = $content['meta']['title'];
            $url     = $this->url_for($file_name);
            return sprintf('<a href="%s"> ' . $title . ' </a>', $url);
        }
        catch (\Exception $e) {
            $this->app['router']->error($e);
        }
        return null;
    }

    /**
     * Returns a standard configuration for Sonic
     *
     * @return array
     * @static
     */
    public static function default_config()
    {
        return array(
            'sonic.environment'           => 'dev',
            'sonic.content_dir'           => 'content',
            'sonic.plugins_dir'           => 'plugins',
            'sonic.templates_dir'         => 'templates',
            'sonic.default_template'      => 'base.twig',
            'sonic.default_list_template' => 'list.twig',
            'sonic.content_ext'           => array('md', 'markdown'),
            'sonic.plugins_enabled'       => false,
            'site.site_root'              => 'http://localhost:8888/zepto/',
            'site.site_title'             => 'Sonic',
            'site.author'                 => '',
            'site.author_email'           => '',
            'site.date_format'            => 'jS M Y',
            'site.excerpt_newline_limit'  => '5',
            'twig'                        => array(
                'charset'           => 'utf-8',
                'cache'             => 'cache',
                'strict_variables'  => false,
                'autoescape'        => false,
                'auto_reload'       => true
            )
        );
    }

    /**
     * Validates a configuration array
     *
     * @param  array   $config
     * @return boolean
     * @throws InvalidArgumentException If a setting is invalid
     * @static
     */
    public function validate_config($config)
    {
        $message = '';

        while ($message === '') {
            if (!is_dir($this->app['ROOT_DIR'] . $config['sonic.content_dir'])) {
                $message = 'Content directory does not exist';
                break;
            }

            if (!is_dir($this->app['ROOT_DIR'] . $config['sonic.plugins_dir'])) {
                $message = 'Plugins directory does not exist';
                break;
            }

            if (!is_dir($this->app['ROOT_DIR'] . $config['sonic.templates_dir'])) {
                $message = 'Templates directory does not exist';
                break;
            }

            if (
                !is_file("{$config['sonic.templates_dir']}/{$config['sonic.default_template']}")
            ) {
                $message = 'No default template exists';
            break;
            }

            if ($config['sonic.environment'] !== 'dev') {
                preg_match('#^(https?://)?([\da-z\.-]+)\.([a-z\.]{2,6})([/\w \.-]*)*/+$#', $config['site.site_root']) === 0
                    ? $message = 'Site root is invalid. Should be like http://www.example.com/'
                    : $message = '';
            }
            break;
        }

        if ($message === '') {
            return TRUE;
        }
        else {
            throw new \InvalidArgumentException($message);
        }

    }

    /**
     * Returns an excerpt of a string by breaking at new lines
     *
     * @param  array  $text
     * @param  int    $br_limit
     * @return string
     */
    public static function get_excerpt($text, $br_limit = 3)
    {
        if (substr_count($text, PHP_EOL) > $br_limit) {
            $excerpt = explode(PHP_EOL, $text, ++$br_limit);
            array_pop($excerpt);
            return implode(' ', $excerpt);
        }
        return $text;
    }

    /**
     * Convert errors into ErrorException objects
     *
     * @param  int            $err_no
     * @param  string         $err_str
     * @param  string         $err_file
     * @param  int            $err_line
     * @return bool
     * @throws \ErrorException
     */
    public static function handle_errors($err_no, $err_str = '', $err_file = '', $err_line = '')
    {
        if (!($err_no & error_reporting())) {
            return;
        }
        throw new \ErrorException($err_str, $err_no, 0, $err_file, $err_line);
    }

    /**
     * Adds a '.' to each extension given
     *
     * @return array
     */
    private function dot_extensions()
    {
        $extensions = $this->app['settings']['sonic.content_ext'];

        foreach ($extensions as $extension) {
            $dotted_extensions[] = '.' . $extension;
        }

        return $dotted_extensions;
    }

}


<?php

namespace Bonnier\WP\Cxense\Services;

use Bonnier\WP\Cxense\Exceptions\WidgetMissingId;
use Bonnier\WP\Cxense\Http\HttpRequest;
use Bonnier\WP\Cxense\Exceptions\HttpException;
use Bonnier\WP\Cxense\Exceptions\WidgetException;
use Bonnier\WP\Cxense\Parsers\Document;
use Bonnier\WP\Cxense\WpCxense;

class WidgetDocumentQuery
{
    private $cxenseUserId;
    private $matchingMode;
    private $arrPayload = [];

    const POPULAR = 'trend';
    const RELATED = 'contextual'; // Articles similar to the current article.
    const SIMILAR_READS = 'collabctx'; // People who have read the current article have also read these articles.
    const RECENTLY_VIEWED = 'recent';

    /**
     * WidgetDocumentQuery constructor.
     */
    public function __construct()
    {
        $this->validateWidgetId(WpCxense::instance()->settings->getSortbyWidgetId());
        $this->setSiteId(WpCxense::instance()->settings->getSiteId());
    }

    /**
     * Create instance
     * @return static
     */
    public static function make()
    {
        return new static();
    }

    /**
     * @param string $key
     * @param $value
     * @return $this
     */
    public function addContext(string $key, $value)
    {
        if (!isset($this->arrPayload['context'])) {
            $this->arrPayload['context'] = [];
        }

        $this->arrPayload['context'][$key] = $value;
        return $this;
    }

    /**
     * @param string $siteId
     */
    public function setSiteId(string $siteId)
    {
        $this->addParameter('siteId', $siteId);
    }

    /**
     * Check for widget id presence
     * @param string $widgetId
     * @return null
     * @throws WidgetMissingId
     */
    private function validateWidgetId(?string $widgetId)
    {
        if (!isset($widgetId) && current_user_can('administrator')) {
            throw new WidgetMissingId('Missing request "widgetId" key!');
        }
        $this->arrPayload['widgetId'] = $widgetId;
    }

    /**
     * @param $key
     * @param $value
     * @return $this
     */
    public function addParameter($key, $value)
    {
        if (!isset($this->arrPayload['context']['parameters'])) {
            $this->arrPayload['context']['parameters'] = [];
        }

        array_push($this->arrPayload['context']['parameters'], [
            'key' => $key,
            'value' => $value
        ]);

        return $this;
    }

    /**
     * @return $this
     */
    public function bySimilarReads()
    {
        $this->setMatchingMode(self::SIMILAR_READS);
        return $this;
    }

    /**
     * @return $this
     */
    public function byRecentlyViewed()
    {
        $this->setMatchingMode(self::RECENTLY_VIEWED);
        //Recently viewed by User requires cxense user ID.
        $this->setCxenseUserId();
        return $this;
    }

    /**
     * @return $this
     */
    public function byShuffle()
    {
        $this->arrPayload['widgetId'] = WpCxense::instance()->settings->getShuffleWidgetId();
        return $this;
    }

    /**
     * @return $this
     */
    public function byPopular()
    {
        $this->setMatchingMode(self::POPULAR);
        return $this;
    }

    /**
     * @return $this
     */
    public function byRelated()
    {
        $this->setMatchingMode(self::RELATED);
        return $this;
    }

    /**
     * @param mixed $context
     * @return WidgetDocumentQuery
     */
    public function setMatchingMode($context)
    {
        $this->matchingMode = $context;
        $this->arrPayload['context']['categories'] = [ 'taxonomy' => $context];
        return $this;
    }

    /**
     * @param $categories
     * @return WidgetDocumentQuery
     */
    public function setCategories(array $categories = null)
    {
        $this->addParameter('category', $this->getWpTerms($categories));
        return $this;
    }

    /**
     * @param array|null $tags
     * @return $this
     */
    public function setTags(array $tags = null)
    {
        $this->addParameter('tag', $this->getWpTerms($tags));
        return $this;
    }

    /**
     * Set cxenseUserId from Cookie. Might not work on WILLOW FRONTEND
     */
    public function setCxenseUserId()
    {
        if (!isset($_COOKIE['cX_P'])) {
            $this->cxenseUserId = '';
            return;
        }

        $this->cxenseUserId = $_COOKIE['cX_P'];
        $this->arrPayload['user']= ['ids' => ['usi' => $this->cxenseUserId]];
    }

    /**
     * @return array
     */
    public function getArrayPayLoad()
    {
        return $this->arrPayload;
    }

    /**
     * Return An array with total documents and matches
     * @return array
     */
    public function get()
    {
        $result = $this->getDocuments();
        $objDocuments = isset($result->items) ? $result->items : [];
        return [
            'totalCount' => count($objDocuments),
            'matches' => $this->parseDocuments($objDocuments)
        ];
    }

    /**
     * Get documents
     * @return mixed|null
     * @throws WidgetException
     */
    private function getDocuments()
    {
        try {
            $cacheKey = md5(json_encode($this->arrPayload));
            $expiresIn = 10 * HOUR_IN_SECONDS;

            if ($cachedResults = wp_cache_get($cacheKey, 'cxense_plugin')) {
                return $cachedResults;
            }

            $objResponse = HttpRequest::get_instance()->post('public/widget/data', [
                'body' => json_encode($this->arrPayload)
            ]);
            wp_cache_add($cacheKey, json_decode($objResponse->getBody()), 'cxense_plugin', $expiresIn);
        } catch (HttpException $exception) {
            if (is_admin()) {
                throw new WidgetException('Failed to load widget:' . $exception->getMessage());
            }
            return null;
        } catch (\Exception $exception) {
            return null;
        }

        return json_decode($objResponse->getBody());
    }

    /**
     * @param $termsArray
     * @return string
     */
    public function getWpTerms($termsArray)
    {
        //The second check to make sure we don't have null
        if (!is_array($termsArray) || !isset($termsArray[0])) {
            return "*";
        }

        return implode(' ', array_column($termsArray, 'name'));
    }

    /**
     * Parse documents to cxense object
     *
     * @param array $arrDocuments
     * @return array
     */
    public function parseDocuments(array $arrDocuments)
    {
        $arrCollection = [];

        foreach ($arrDocuments as $objDocument) {
            $arrCollection[] = new Document($objDocument);
        }

        return $arrCollection;
    }
}