<?php declare(strict_types=1);
/**
 * Banker
 *
 * A Caching library implementing psr/cache (PSR 6) and psr/simple-cache (PSR 16)
 *
 * PHP version 8
 *
 * @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     4.0.0
 * @link        https://git.timshomepage.net/timw4mail/banker
 */
namespace Aviat\Banker\Tests;

use Aviat\Banker\{Item, ItemCollection, Pool};
use Aviat\Banker\Exception\InvalidArgumentException;
use Monolog\Logger;
use Monolog\Handler\SyslogHandler;
use PHPUnit\Framework\TestCase;
use Psr\Log\{LoggerInterface, NullLogger};
use TypeError;

class PoolTest extends TestCase {

	protected $pool;

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

	public function testGetDefaultLogger(): void
	{
		$friend = new Friend($this->pool);
		$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));

		$pool = new Pool([
			'driver' => 'null',
			'connection' => [],
		], $logger);

		$friend = new Friend($pool);
		$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->pool->setLogger($logger);

		$friend = new Friend($this->pool);
		$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 testGetItem(): void
	{
		$item = $this->pool->getItem('foo');
		$this->assertInstanceOf(Item::class, $item);
	}

	public function testItemBadKey(): void
	{
		$this->expectException(TypeError::class);

		$this->pool->getItem([]);
	}

	public function testGetItemsBadKey(): void
	{
		$this->expectException(InvalidArgumentException::class);
		$this->pool->getItems([1,3,2]);
	}

	public function testGetItems(): void
	{
		$collection = $this->pool->getItems(['foo', 'bar', 'baz']);
		$this->assertInstanceOf(ItemCollection::class, $collection);

		foreach($collection as $item)
		{
			$this->assertInstanceOf(Item::class, $item);
		}
	}

	public function testGetItemsDeferredItems(): void
	{
		$this->pool->clear();

		$deferredValues = ['x' => 1, 'y' => 2, 'z' => 3];
		$keys = ['x', 'y', 'z'];

		foreach ($deferredValues as $key => $value)
		{
			$item = $this->pool->getItem($key)->set($value);
			$this->pool->saveDeferred($item);
		}

		$collection = $this->pool->getItems($keys);

		foreach($collection as $key => $item)
		{
			$this->assertSame($deferredValues[$key], $item->get());
		}

		$this->assertCount(3, $collection);
	}

	public function testEmptyGetItems(): void
	{
		$this->pool->clear();

		$collection = $this->pool->getItems();

		$this->assertInstanceOf(ItemCollection::class, $collection);
		$this->assertCount(0, $collection);
	}

	public function testHasItem(): void
	{
		$this->pool->clear();

		// The key doesn't exist yet
		$this->assertFalse($this->pool->hasItem('foobar'));

		// Create the item
		$item = $this->pool->getItem('foobar')
			->set('baz')
			->save();

		// The item exists now
		$this->assertTrue($this->pool->hasItem('foobar'));
	}

	public function testHasItemBadKey(): void
	{
		$this->pool->clear();

		$this->expectException(TypeError::class);

		$this->pool->hasItem(34);
	}

	public function testClear(): void
	{
		// Call clear to make sure we are working from a clean slate to start
		$this->pool->clear();

		$data = [
			'foo' => 'bar',
			'bar' => 'baz',
			'foobar' => 'foobarbaz'
		];

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

		foreach($data as $key => $val)
		{
			$this->assertTrue($this->pool->hasItem($key));
			$item = $this->pool->getItem($key);
			$this->assertEquals($val, $item->get());
		}

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

		foreach($data as $key => $val)
		{
			$this->assertFalse($this->pool->hasItem($key));
			$item = $this->pool->getItem($key);
			$this->assertNull($item->get());
		}
	}

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

		$this->pool->deleteItem(34);
	}

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

		$this->pool->deleteItems([34]);
	}

	public function testDeleteItemThatDoesNotExist(): void
	{
		$this->pool->clear();
		$this->assertFalse($this->pool->deleteItem('foo'));
	}

	public function testDeleteItem(): void
	{
		// Start with a clean slate
		$this->pool->clear();

		$data = [
			'foo' => 'bar',
			'bar' => 'baz',
			'foobar' => 'foobarbaz'
		];

		$this->setupDataInCache($data);

		$this->pool->deleteItem('foo');

		// The item no longer exists
		$this->assertFalse($this->pool->hasItem('foo'));
		$item = $this->pool->getItem('foo');
		$this->assertNull($item->get());

		// The other items still exist
		foreach(['bar', 'foobar'] as $key)
		{
			$this->assertTrue($this->pool->hasItem($key));
			$item = $this->pool->getItem($key);
			$this->assertFalse(is_null($item->get()));
		}
	}

	public function testDeleteItems(): void
	{
		$this->pool->clear();

		$data = [
			'foo' => 'bar',
			'bar' => 'baz',
			'foobar' => 'foobarbaz'
		];

		$this->setupDataInCache($data);

		$this->pool->deleteItems(['foo', 'bar']);

		foreach(['foo', 'bar'] as $key)
		{
			$this->assertFalse($this->pool->hasItem($key));
			$item = $this->pool->getItem($key);
			$this->assertNull($item->get());
		}
	}

	public function testSaveDeferred(): void
	{
		$this->pool->clear();

		$data = [
			'foo' => 'bar',
			'bar' => 'baz',
			'foobar' => 'foobarbaz'
		];

		$this->setupDeferredData($data);

		// See that the data is returned by the pool
		foreach($data as $key => $val)
		{
			$this->assertTrue($this->pool->hasItem($key));
			$item = $this->pool->getItem($key);

			// Since the value has been deferred,
			// the pool will return the updated value,
			// even though the cache hasn't been updated yet
			$this->assertEquals($data[$key], $item->get());
		}
	}

	public function testCommit(): void
	{
		$this->pool->clear();

		// If there are no deferred items, this will return true
		$this->assertTrue($this->pool->commit());

		$data = [
			'foo' => 'bar',
			'bar' => 'baz',
			'foobar' => 'foobarbaz'
		];

		$this->setupDeferredData($data);

		// See that the data is returned by the pool
		foreach($this->pool->getItems(array_keys($data)) as $key => $item)
		{
			$this->assertTrue($this->pool->hasItem($key));
			$this->assertEquals($data[$key], $item->get());
		}

		$this->pool->commit();

		// See that the data is saved in the cache backend
		foreach($this->pool->getItems(array_keys($data)) as $key => $item)
		{
			$this->assertTrue($this->pool->hasItem($key));
			$this->assertEquals($data[$key], $item->get());
		}
	}

	protected function setupDeferredData($data): void
	{
		foreach($data as $key => $val)
		{
			$item = $this->pool->getItem($key)
				->set($val);

			$this->assertTrue($this->pool->saveDeferred($item));
		}
	}

	protected function setupDataInCache($data): void
	{
		foreach($data as $key => $val)
		{
			$item = $this->pool->getItem($key)
				->set($val);

			$this->pool->save($item);
		}
	}
}