<?php

/**
 * Tests the session class
 *
 * @group kohana
 * @group kohana.core
 * @group kohana.core.session
 *
 * @package    Kohana
 * @category   Tests
 * @author     Kohana Team
 * @author     Jeremy Bush <contractfrombelow@gmail.com>
 * @copyright  (c) Kohana Team
 * @license    https://koseven.ga/LICENSE.md
 */
class Kohana_SessionTest extends Unittest_TestCase
{

	/**
	 * Gets a mock of the session class
	 *
	 * @return Session
	 */
	// @codingStandardsIgnoreStart
	public function getMockSession(array $config = [])
	// @codingStandardsIgnoreEnd
	{
		return $this->getMockForAbstractClass('Session', [$config]);
	}

	/**
	 * Provides test data for
	 *
	 * test_constructor_uses_name_from_config_and_casts()
	 *
	 * @return array
	 */
	public function provider_constructor_uses_settings_from_config_and_casts()
	{
		return [
			// array(expected, input)
			// data set 0
			[
				[
					'name'      => 'awesomeness',
					'lifetime'  =>  1231456421,
					'encrypted' =>  FALSE
				],
				[
					'name'      => 'awesomeness',
					'lifetime'  => '1231456421',
					'encrypted' =>  FALSE,
				],
			],
			// data set 1
			[
				[
					'name'       => '123',
					'encrypted'  => 'default',
				],
				[
					'name'       =>  123,
					'encrypted'  =>  TRUE,
				],
			],
		];
	}

	/**
	 * The constructor should change its attributes based on config
	 * passed as the first parameter
	 *
	 * @test
	 * @dataProvider provider_constructor_uses_settings_from_config_and_casts
	 * @covers Session::__construct
	 */
	public function test_constructor_uses_settings_from_config_and_casts($expected, $config)
	{
		$session = $this->getMockForAbstractClass('Session', [$config]);

		foreach ($expected as $var => $value)
		{
			$this->assertAttributeSame($value, '_'.$var, $session);
		}
	}

	/**
	 * Check that the constructor will load a session if it's provided
	 * witha session id
	 *
	 * @test
	 * @covers Session::__construct
	 * @covers Session::read
	 */
	public function test_constructor_loads_session_with_session_id()
	{
		$config = [];
		$session_id = 'lolums';

		// Don't auto-call constructor, we need to setup the mock first
		$session = $this->getMockBuilder('Session')
			->disableOriginalConstructor()
			->setMethods(['read'])
			->getMockForAbstractClass();

		$session
			->expects($this->once())
			->method('read')
			->with($session_id);

		$session->__construct($config, $session_id);
	}

	/**
	 * Calling $session->bind() should allow you to bind a variable
	 * to a session variable
	 *
	 * @test
	 * @covers Session::bind
	 * @ticket 3164
	 */
	public function test_bind_actually_binds_variable()
	{
		$session = $this->getMockForAbstractClass('Session');

		$var = 'asd';

		$session->bind('our_var', $var);

		$var = 'foobar';

		$this->assertSame('foobar', $session->get('our_var'));
	}


	/**
	 * When a session is initially created it should have no data
	 *
	 *
	 * @test
	 * @covers Session::__construct
	 * @covers Session::set
	 */
	public function test_initially_session_has_no_data()
	{
		$session = $this->getMockSession();

		$this->assertAttributeSame([], '_data', $session);
	}

	/**
	 * Make sure that the default session name (the one used if the
	 * driver does not set one) is 'session'
	 *
	 * @test
	 * @covers Session::__construct
	 */
	public function test_default_session_name_is_set()
	{
		$session = $this->getMockSession();

		$this->assertAttributeSame('session', '_name', $session);
	}

	/**
	 * By default sessions are unencrypted
	 *
	 * @test
	 * @covers Session::__construct
	 */
	public function test_default_session_is_unencrypted()
	{
		$session = $this->getMockSession();

		$this->assertAttributeSame(FALSE, '_encrypted', $session);
	}

	/**
	 * A new session should not be classed as destroyed
	 *
	 * @test
	 * @covers Session::__construct
	 */
	public function test_default_session_is_not_classed_as_destroyed()
	{
		$session = $this->getMockSession();

		$this->assertAttributeSame(FALSE, '_destroyed', $session);
	}

	/**
	 * Provides test data for test_get_returns_default_if_var_dnx()
	 *
	 * @return array
	 */
	public function provider_get_returns_default_if_var_dnx()
	{
		return [
			['something_crazy', FALSE],
			['a_true', TRUE],
			['an_int', 158163158],
		];
	}

	/**
	 * Make sure that get() is using the default value we provide and
	 * isn't tampering with it
	 *
	 * @test
	 * @dataProvider provider_get_returns_default_if_var_dnx
	 * @covers Session::get
	 */
	public function test_get_returns_default_if_var_dnx($var, $default)
	{
		$session = $this->getMockSession();

		$this->assertSame($default, $session->get($var, $default));
	}

	/**
	 * By default get() should be using null as the var DNX return value
	 *
	 * @test
	 * @covers Session::get
	 */
	public function test_get_uses_null_as_default_return_value()
	{
		$session = $this->getMockSession();

		$this->assertSame(NULL, $session->get('level_of_cool'));
	}

	/**
	 * This test makes sure that session is using array_key_exists
	 * as isset will return FALSE if the value is NULL
	 *
	 * @test
	 * @covers Session::get
	 */
	public function test_get_returns_value_if_it_equals_null()
	{
		$session = $this->getMockSession();

		$session->set('arkward', NULL);

		$this->assertSame(NULL, $session->get('arkward', 'uh oh'));
	}

	/**
	 * as_array() should return the session data by reference.
	 *
	 * i.e. if we modify the returned data, the session data also changes
	 *
	 * @test
	 * @covers Session::as_array
	 */
	public function test_as_array_returns_data_by_ref_or_copy()
	{
		$session = $this->getMockSession();

		$data_ref =& $session->as_array();

		$data_ref['something'] = 'pie';

		$this->assertAttributeSame($data_ref, '_data', $session);

		$data_copy = $session->as_array();

		$data_copy['pie'] = 'awesome';

		$this->assertAttributeNotSame($data_copy, '_data', $session);
	}

	/**
	 * set() should add new session data and modify existing ones
	 *
	 * Also makes sure that set() returns $this
	 *
	 * @test
	 * @covers Session::set
	 */
	public function test_set_adds_and_modifies_to_session_data()
	{
		$session = $this->getMockSession();

		$this->assertSame($session, $session->set('pork', 'pie'));

		$this->assertAttributeSame(
			['pork' => 'pie'],
			'_data',
			$session
		);

		$session->set('pork', 'delicious');

		$this->assertAttributeSame(
			['pork' => 'delicious'],
			'_data',
			$session
		);
	}

	/**
	 * This tests that delete() removes specified session data
	 *
	 * @test
	 * @covers Session::delete
	 */
	public function test_delete_removes_select_session_data()
	{
		$session = $this->getMockSession();

		// Bit of a hack for mass-loading session data
		$data =& $session->as_array();

		$data += [
			'a' => 'A',
			'b' => 'B',
			'c' => 'C',
			'easy' => '123'
		];

		// Make a copy of $data for testing purposes
		$copy = $data;

		// First we make sure we can delete one item
		// Also, check that delete returns $this
		$this->assertSame($session, $session->delete('a'));

		unset($copy['a']);

		// We could test against $data but then we'd be testing
		// that as_array() is returning by ref
		$this->assertAttributeSame($copy, '_data', $session);

		// Now we make sure we can delete multiple items
		// We're checking $this is returned just in case
		$this->assertSame($session, $session->delete('b', 'c'));
		unset($copy['b'], $copy['c']);

		$this->assertAttributeSame($copy, '_data', $session);
	}

	/**
	 * Provides test data for test_read_loads_session_data()
	 *
	 * @return array
	 */
	public function provider_read_loads_session_data()
	{
		return [
			// If driver returns array then just load it up
			[
				[],
				'wacka_wacka',
				[]
			],
			[
				['the it' => 'crowd'],
				'the_it_crowd',
				['the it' => 'crowd'],
			],
			// If it's a string an encrpytion is disabled (by default) base64decode and unserialize
			[
				['dead' => 'arrival'],
				'lolums',
				'YToxOntzOjQ6ImRlYWQiO3M6NzoiYXJyaXZhbCI7fQ=='
			],
		];
	}

	/**
	 * This is one of the "big" tests for the session lib
	 *
	 * The test makes sure that
	 *
	 * 1. Session asks the driver for the data relating to $session_id
	 * 2. That it will load the returned data into the session
	 *
	 * @test
	 * @dataProvider provider_read_loads_session_data
	 * @covers Session::read
	 */
	public function test_read_loads_session_data($expected_data, $session_id, $driver_data, array $config = [])
	{
		$session = $this->getMockSession($config);

		$session->expects($this->once())
				->method('_read')
				->with($session_id)
				->will($this->returnValue($driver_data));

		$session->read($session_id);
		$this->assertAttributeSame($expected_data, '_data', $session);
	}

	/**
	 * regenerate() should tell the driver to regenerate its id
	 *
	 * @test
	 * @covers Session::regenerate
	 */
	public function test_regenerate_tells_driver_to_regenerate()
	{
		$session = $this->getMockSession();

		$new_session_id = 'asdnoawdnoainf';

		$session->expects($this->once())
				->method('_regenerate')
				->with()
				->will($this->returnValue($new_session_id));

		$this->assertSame($new_session_id, $session->regenerate());
	}

	/**
	 * If the driver destroys the session then all session data should be
	 * removed
	 *
	 * @test
	 * @covers Session::destroy
	 */
	public function test_destroy_deletes_data_if_driver_destroys_session()
	{
		$session = $this->getMockSession();

		$session
			->set('asd', 'dsa')
			->set('dog', 'god');

		$session
			->expects($this->once())
			->method('_destroy')
			->with()
			->will($this->returnValue(TRUE));

		$this->assertTrue($session->destroy());

		$this->assertAttributeSame([], '_data', $session);
	}

	/**
	 * The session data should only be deleted if the driver reports
	 * that the session was destroyed ok
	 *
	 * @test
	 * @covers Session::destroy
	 */
	public function test_destroy_only_deletes_data_if_driver_destroys_session()
	{
		$session = $this->getMockSession();

		$session
			->set('asd', 'dsa')
			->set('dog', 'god');

		$session
			->expects($this->once())
			->method('_destroy')
			->with()
			->will($this->returnValue(FALSE));

		$this->assertFalse($session->destroy());
		$this->assertAttributeSame(
			['asd' => 'dsa', 'dog' => 'god'],
			'_data',
			$session
		);
	}

	/**
	 * If a session variable exists then get_once should get it then remove it.
	 * If the variable does not exist then it should return the default
	 *
	 * @test
	 * @covers Session::get_once
	 */
	public function test_get_once_gets_once_or_returns_default()
	{
		$session = $this->getMockSession();

		$session->set('foo', 'bar');

		// Test that a default is returned
		$this->assertSame('mud', $session->get_once('fud', 'mud'));

		// Now test that it actually removes the value
		$this->assertSame('bar', $session->get_once('foo'));

		$this->assertAttributeSame([], '_data', $session);

		$this->assertSame('maybe', $session->get_once('foo', 'maybe'));
	}
}
