<?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 - 2023  Timothy J. Warren
 * @license     http://www.opensource.org/licenses/mit-license.html  MIT License
 * @version     4.1.1
 * @link        https://git.timshomepage.net/timw4mail/banker
 */
namespace Aviat\Banker\Tests;

use Exception;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ReflectionProperty;
use InvalidArgumentException;
use BadMethodCallException;

/**
 * Friend class for testing
 */
class Friend {

	/**
	 * Object to create a friend of
	 */
	private mixed $_friend_;

	/**
	 * Reflection class of the object
	 */
	private mixed $_reflect_;

	/**
	 * Create a friend object
	 *
	 * @param object $obj
	 * @throws InvalidArgumentException|ReflectionException
	 */
	public function __construct(mixed $obj)
	{
		if ( ! is_object($obj))
		{
			throw new InvalidArgumentException("Friend must be an object");
		}

		$this->_friend_ = $obj;
		$this->_reflect_ = new ReflectionClass($obj);
	}

	/**
	 * Retrieve a friend's property
	 *
	 * @param  string $key
	 * @return mixed
	 */
	public function __get(string $key): mixed
	{
		if ($this->_reflect_->hasProperty($key))
		{
			$property = $this->_get_property($key);
			return $property->getValue($this->_friend_);
		}

		return NULL;
	}

	/**
	 * Set a friend's property
	 *
	 * @param string $key
	 * @param mixed  $value
	 * @return void
	 */
	public function __set(string $key, mixed $value): void
	{
		if ($this->_reflect_->hasProperty($key))
		{
			$property = $this->_get_property($key);
			$property->setValue($this->_friend_, $value);
		}
	}

	/**
	 * Calls a protected or private method on the friend
	 *
	 * @param string $method
	 * @param array $args
	 * @return mixed
	 * @throws BadMethodCallException
	 * @throws ReflectionException
	 */
	public function __call(string $method, array $args): mixed
	{
		if ( ! $this->_reflect_->hasMethod($method))
		{
			throw new BadMethodCallException("Method '{$method}' does not exist");
		}

		$friendMethod = new ReflectionMethod($this->_friend_, $method);
		$friendMethod->setAccessible(TRUE);
		return $friendMethod->invokeArgs($this->_friend_, $args);
	}

	/**
	 * Iterates over parent classes to get a ReflectionProperty
	 *
	 * @codeCoverageIgnore
	 * @param  string $name
	 * @return ReflectionProperty|null
	 */
	private function _get_property(string $name): ?ReflectionProperty
	{
		try
		{
			$property = $this->_reflect_->getProperty($name);
			$property->setAccessible(TRUE);
			return $property;
		}
		// Return NULL on any exception, so no further logic needed
		// in the catch block
		catch (Exception)
		{
			return NULL;
		}
	}
}