<?php declare(strict_types=1);

/**
 * Test: Nette\Utils\Callback closures tests.
 */

use Nette\Utils\Callback;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';


class Test
{
	public function __invoke($a)
	{
		return __METHOD__ . $a;
	}


	public function publicFun($a)
	{
		return __METHOD__ . $a;
	}


	private function privateFun($a)
	{
		return __METHOD__ . $a;
	}


	public static function publicStatic($a)
	{
		return __METHOD__ . $a;
	}


	private static function privateStatic($a)
	{
		return __METHOD__ . $a;
	}


	public function createPrivateClosure(): Closure
	{
		return $this->privateFun(...);
	}


	public static function createPrivateStaticClosure(): Closure
	{
		return self::privateStatic(...);
	}


	public function ref(&$a)
	{
		$a = __METHOD__;
		return $a;
	}
}


class TestDynamic
{
	public function __call($nm, $args)
	{
		return __METHOD__ . " $nm $args[0]";
	}


	public static function __callStatic($nm, $args)
	{
		return __METHOD__ . " $nm $args[0]";
	}
}

class TestChild extends Test
{
}


function getName($ref)
{
	if ($ref instanceof ReflectionFunction) {
		return $ref->getShortName();
	} elseif ($ref instanceof ReflectionMethod) {
		return $ref->getDeclaringClass()->getName() . '::' . $ref->getName();
	}
}


test('global function', function () {
	Assert::same('trim', Callback::unwrap(trim(...)));
	Assert::same('trim', Callback::toString('trim'));
	Assert::same('{closure trim}', Callback::toString(trim(...)));
	Assert::same('trim', getName(Callback::toReflection('trim')));
	Assert::same('trim', getName(Callback::toReflection(trim(...))));
	Assert::same('x', trim(...)(' x '));


	Assert::same('undefined', Callback::toString('undefined'));

	Assert::exception(
		fn() => Callback::toReflection('undefined'),
		ReflectionException::class,
		'Function undefined() does not exist',
	);
});


test('closure', function () {
	$closure = function (&$a) {
		$a = __FUNCTION__;
		return $a;
	};
	Assert::same($closure, Closure::fromCallable($closure));
	Assert::same($closure, Callback::unwrap($closure));
	Assert::same('{closure}', Callback::toString($closure));
	Assert::match('{closure%a?%}', getName(Callback::toReflection($closure)));
	Assert::match('{closure%a?%}', Closure::fromCallable($closure)(...[&$res]));
	Assert::match('{closure%a?%}', $res);
});


test('invokable object', function () {
	$test = new Test;
	Assert::same([$test, '__invoke'], Callback::unwrap(Closure::fromCallable($test)));
	Assert::same('Test::__invoke', Callback::toString($test));
	Assert::same('{closure Test::__invoke}', Callback::toString(Closure::fromCallable($test)));
	Assert::same('Test::__invoke', getName(Callback::toReflection($test)));
	Assert::same('Test::__invoke', getName(Callback::toReflection(Closure::fromCallable($test))));
	Assert::same('Test::__invoke*', Closure::fromCallable($test)('*'));
});


test('object methods', function () {
	$test = new Test;
	Assert::same([$test, 'publicFun'], Callback::unwrap($test->publicFun(...)));

	Assert::same('Test::publicFun', Callback::toString([$test, 'publicFun']));
	Assert::same('{closure Test::publicFun}', Callback::toString($test->publicFun(...)));

	Assert::same('Test::publicFun', getName(Callback::toReflection([$test, 'publicFun'])));
	Assert::same('Test::publicFun', getName(Callback::toReflection($test->publicFun(...))));

	Assert::same('Test::publicFun*', $test->publicFun(...)('*'));


	Assert::same([$test, 'privateFun'], Callback::unwrap($test->createPrivateClosure()));

	Assert::same('Test::privateFun', Callback::toString([$test, 'privateFun']));
	Assert::same('{closure Test::privateFun}', Callback::toString($test->createPrivateClosure()));

	Assert::same('Test::privateFun', getName(Callback::toReflection([$test, 'privateFun'])));
	Assert::same('Test::privateFun', getName(Callback::toReflection($test->createPrivateClosure())));

	Assert::same(['Test', 'privateFun'], Callback::unwrap((new TestChild)->createPrivateClosure()));
	Assert::same('Test::privateFun', getName(Callback::toReflection((new TestChild)->createPrivateClosure())));

	Assert::same('Test::privateFun*', $test->createPrivateClosure()('*'));

	Assert::same('Test::ref', $test->ref(...)(...[&$res]));
	Assert::same('Test::ref', $res);
});


test('static methods', function () {
	$test = new Test;
	Assert::same(['Test', 'publicStatic'], Callback::unwrap(Test::publicStatic(...)));
	Assert::same(['Test', 'publicStatic'], Callback::unwrap(Test::publicStatic(...)));

	Assert::same('Test::publicStatic', Callback::toString(['Test', 'publicStatic']));
	Assert::same('Test::publicStatic', Callback::toString([$test, 'publicStatic']));
	Assert::same('Test::publicStatic', Callback::toString('Test::publicStatic'));
	Assert::same('{closure Test::publicStatic}', Callback::toString(Test::publicStatic(...)));

	Assert::same('Test::publicStatic', getName(Callback::toReflection(['Test', 'publicStatic'])));
	Assert::same('Test::publicStatic', getName(Callback::toReflection([$test, 'publicStatic'])));
	Assert::same('Test::publicStatic', getName(Callback::toReflection('Test::publicStatic')));
	Assert::same('Test::publicStatic', getName(Callback::toReflection(Test::publicStatic(...))));

	Assert::same('Test::publicStatic*', Test::publicStatic(...)('*'));
	Assert::same('Test::publicStatic*', $test->publicStatic(...)('*'));


	Assert::same(['Test', 'privateStatic'], Callback::unwrap(Test::createPrivateStaticClosure()));
	Assert::same('Test::privateStatic', Callback::toString('Test::privateStatic'));
	Assert::same('{closure Test::privateStatic}', Callback::toString(Test::createPrivateStaticClosure()));
	Assert::same('Test::privateStatic', getName(Callback::toReflection('Test::privateStatic')));
	Assert::same('Test::privateStatic', getName(Callback::toReflection(Test::createPrivateStaticClosure())));
	Assert::same('Test::privateStatic', getName(Callback::toReflection(TestChild::createPrivateStaticClosure())));

	Assert::same('Test::privateStatic*', Test::createPrivateStaticClosure()('*'));
});


test('magic methods', function () {
	$test = new TestDynamic;
	Assert::same([$test, 'magic'], Callback::unwrap($test->magic(...)));
	Assert::same('TestDynamic::magic', Callback::toString([$test, 'magic']));
	Assert::same('{closure TestDynamic::magic}', Callback::toString($test->magic(...)));
	Assert::same('TestDynamic::__call magic *', $test->magic(...)('*'));

	Assert::same(['TestDynamic', 'magic'], Callback::unwrap(TestDynamic::magic(...)));
	Assert::same('TestDynamic::magic', Callback::toString('TestDynamic::magic'));
	Assert::same('{closure TestDynamic::magic}', Callback::toString(TestDynamic::magic(...)));
	Assert::same('TestDynamic::__callStatic magic *', TestDynamic::magic(...)('*'));

	Assert::exception(
		fn() => Callback::toReflection([new TestDynamic, 'magic']),
		ReflectionException::class,
		'Method TestDynamic::magic() does not exist',
	);

	Assert::exception(
		fn() => Callback::toReflection(Closure::fromCallable([new TestDynamic, 'magic'])),
		ReflectionException::class,
		'Method TestDynamic::magic() does not exist',
	);
});


test('PHP bugs - is_callable($object, true) fails', function () {
	Assert::same('stdClass::__invoke', Callback::toString(new stdClass));

	Assert::exception(
		fn() => Callback::toReflection(new stdClass),
		ReflectionException::class,
		'Method stdClass::__invoke() does not exist',
	);
});
