<?php declare(strict_types=1);
/**
 * Banker
 *
 * A Caching library implementing psr/cache (PSR 6) and psr/simple-cache (PSR 16)
 *
 * PHP version 7.4+
 *
 * @package     Banker
 * @author      Timothy J. Warren <tim@timshomepage.net>
 * @copyright   2016 - 2021  Timothy J. Warren
 * @license     http://www.opensource.org/licenses/mit-license.html  MIT License
 * @version     3.2.0
 * @link        https://git.timshomepage.net/timw4mail/banker
 */
namespace Aviat\Banker\Tests;

use Aviat\Banker\Pool;
use Aviat\Banker\Teller;
use Aviat\Banker\Exception\InvalidArgumentException;
use Monolog\Handler\SyslogHandler;
use Monolog\Logger;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;

class TellerTest extends TestCase {

	protected Teller $teller;

	private array $testValues = [
		'foo' => 24,
		'bar' => '87',
		'baz' => [1, 2, 3],
		'a' => TRUE,
		'b' => 1,
		'c' => FALSE,
		'd' => 0,
		'e' => NULL,
		'f' => [
			'a' => [
				'b' => 'c',
				'd' => [1, 2, 3]
			]
		]
	];

	public function setUp(): void
	{
		$this->teller = new Teller([
			'driver' => 'null',
			'connection' => []
		]);

		// Call clear to make sure we are working from a clean slate to start
		$this->teller->clear();
	}

	public function testGetDefaultLogger(): void
	{
		$friend = new Friend($this->teller);
		$driverFriend = new Friend($friend->driver);

		// Check that a valid logger is set
		$this->assertInstanceOf(LoggerInterface::class, $friend->getLogger(), "Logger exists after being set");
		$this->assertInstanceOf(LoggerInterface::class, $driverFriend->getLogger(), "Logger exists on driver after being set");

		// Make sure we get the default Null logger
		$this->assertTrue(is_a($friend->getLogger(), NullLogger::class));
		$this->assertTrue(is_a($driverFriend->getLogger(), NullLogger::class));
	}

	public function testSetLoggerInConstructor(): void
	{
		$logger = new Logger('test');
		$logger->pushHandler(new SyslogHandler('warning', LOG_USER, Logger::WARNING));

		$teller = new Teller([
			'driver' => 'null',
			'connection' => [],
		], $logger);

		$friend = new Friend($teller);
		$driverFriend = new Friend($friend->driver);

		// Check that a valid logger is set
		$this->assertInstanceOf(LoggerInterface::class, $friend->getLogger(), "Logger exists after being set");
		$this->assertInstanceOf(LoggerInterface::class, $driverFriend->getLogger(), "Logger exists on driver after being set");

		// Make sure we aren't just getting the default Null logger
		$this->assertFalse(is_a($friend->getLogger(), NullLogger::class));
		$this->assertFalse(is_a($driverFriend->getLogger(), NullLogger::class));
	}

	public function testGetSetLogger(): void
	{
		$logger = new Logger('test');
		$logger->pushHandler(new SyslogHandler('warning2',LOG_USER, Logger::WARNING));

		$this->teller->setLogger($logger);

		$friend = new Friend($this->teller);
		$driverFriend = new Friend($friend->driver);

		// Check that a valid logger is set
		$this->assertInstanceOf(LoggerInterface::class, $friend->getLogger(), "Logger exists after being set");
		$this->assertInstanceOf(LoggerInterface::class, $driverFriend->getLogger(), "Logger exists on driver after being set");

		// Make sure we aren't just getting the default Null logger
		$this->assertFalse(is_a($friend->getLogger(), NullLogger::class));
		$this->assertFalse(is_a($driverFriend->getLogger(), NullLogger::class));
	}

	public function testGetSet(): void
	{
		foreach ($this->testValues as $key => $value)
		{
			$this->assertTrue($this->teller->set($key, $value, 0), "Failed to set value for key: {$key}");

			$received = $this->teller->get($key);

			$this->assertEquals($value, $received, "Invalid value returned for key: {$key}");
		}
	}

	public function testGetSetMultiple(): void
	{
		$this->assertTrue($this->teller->setMultiple($this->testValues));

		$received = $this->teller->getMultiple(array_keys($this->testValues));
		$this->assertEquals($this->testValues, $received);
	}

	public function testClear(): void
	{
		$data = [
			'foo' => 'bar',
			'bar' => 'baz',
			'foobar' => 'foobarbaz'
		];

		// Set up some data
		$this->teller->setMultiple($data);

		foreach($data as $key => $val)
		{
			$this->assertTrue($this->teller->has($key));
			$this->assertEquals($val, $this->teller->get($key));
		}

		// Now we clear it all!
		$this->teller->clear();

		foreach($data as $key => $val)
		{
			$this->assertFalse($this->teller->has($key));
			$this->assertNull($this->teller->get($key));
		}
	}

	public function testDelete(): void
	{
		$this->teller->setMultiple($this->testValues);

		$this->assertTrue($this->teller->delete('foo'));
		$this->assertFalse($this->teller->has('foo'));

		// Make sure we get the default value for the key
		$this->assertEquals('Q', $this->teller->get('foo', 'Q'));
	}

	public function testDeleteMultiple(): void
	{
		$this->teller->setMultiple($this->testValues);

		$deleteKeys = ['foo', 'bar', 'baz'];
		$hasKeys = ['a', 'b', 'c', 'd', 'e', 'f'];

		$this->assertTrue($this->teller->deleteMultiple($deleteKeys));

		array_walk($deleteKeys, fn ($key) => $this->assertFalse($this->teller->has($key)));
		array_walk($hasKeys, fn ($key) => $this->assertTrue($this->teller->has($key)));
	}

	public function testBadKeyType(): void
	{
		$this->expectException(InvalidArgumentException::class);
		$this->expectExceptionMessage('Cache key must be a string.');

		$this->teller->get(546567);
	}

	public function testBadKeysType (): void
	{
		$this->expectException(InvalidArgumentException::class);
		$this->expectExceptionMessage('Keys must be an array or a traversable object');

		$keys = (object)[];
		$this->teller->getMultiple($keys);
	}

	/**
	 * @dataProvider keyValidationTests
	 * @param string $key
	 * @throws \Psr\SimpleCache\InvalidArgumentException
	 */
	public function testKeyValidation(string $key): void
	{
		$this->expectException(InvalidArgumentException::class);
		$this->expectExceptionMessage('Invalid characters in cache key');

		$this->teller->get($key);
	}

	public function keyValidationTests(): array
	{
		// {}()/@:\\\
		return [
			['key' => '{}()/@:\\'],
			['key' => 'a: b'],
			['key' => 'a/b'],
			['key' => '{'],
			['key' => 'a@b'],
		];
	}
}