<?php

/**
 * Test: Nette\Utils\Callback closures tests.
 */

declare(strict_types=1);

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 Closure::fromCallable([$this, 'privateFun']);
	}


	public static function createPrivateStaticClosure(): Closure
	{
		return Closure::fromCallable([self::class, '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(Closure::fromCallable('trim')));
	Assert::same('trim', Callback::toString('trim'));
	Assert::same('{closure trim}', Callback::toString(Closure::fromCallable('trim')));
	Assert::same('trim', getName(Callback::toReflection('trim')));
	Assert::same('trim', getName(Callback::toReflection(Closure::fromCallable('trim'))));
	Assert::same('x', Closure::fromCallable('trim')->__invoke(' 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)->__invoke('*'));
});


test('object methods', function () {
	$test = new Test;
	Assert::same([$test, 'publicFun'], Callback::unwrap(Closure::fromCallable([$test, 'publicFun'])));

	Assert::same('Test::publicFun', Callback::toString([$test, 'publicFun']));
	Assert::same('{closure Test::publicFun}', Callback::toString(Closure::fromCallable([$test, 'publicFun'])));

	Assert::same('Test::publicFun', getName(Callback::toReflection([$test, 'publicFun'])));
	Assert::same('Test::publicFun', getName(Callback::toReflection(Closure::fromCallable([$test, 'publicFun']))));

	Assert::same('Test::publicFun*', Closure::fromCallable([$test, 'publicFun'])->__invoke('*'));


	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()->__invoke('*'));

	Assert::same('Test::ref', Closure::fromCallable([$test, 'ref'])(...[&$res]));
	Assert::same('Test::ref', $res);
});


test('static methods', function () {
	$test = new Test;
	Assert::same(['Test', 'publicStatic'], Callback::unwrap(Closure::fromCallable(['Test', 'publicStatic'])));
	Assert::same(['Test', 'publicStatic'], Callback::unwrap(Closure::fromCallable('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(Closure::fromCallable('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(Closure::fromCallable('Test::publicStatic'))));

	Assert::same('Test::publicStatic*', Closure::fromCallable(['Test', 'publicStatic'])->__invoke('*'));
	Assert::same('Test::publicStatic*', Closure::fromCallable([$test, 'publicStatic'])->__invoke('*'));


	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()->__invoke('*'));
});


test('magic methods', function () {
	$test = new TestDynamic;
	Assert::same([$test, 'magic'], Callback::unwrap(Closure::fromCallable([$test, 'magic'])));
	Assert::same('TestDynamic::magic', Callback::toString([$test, 'magic']));
	Assert::same('{closure TestDynamic::magic}', Callback::toString(Closure::fromCallable([$test, 'magic'])));
	Assert::same('TestDynamic::__call magic *', Closure::fromCallable([$test, 'magic'])->__invoke('*'));

	Assert::same(['TestDynamic', 'magic'], Callback::unwrap(Closure::fromCallable('TestDynamic::magic')));
	Assert::same('TestDynamic::magic', Callback::toString('TestDynamic::magic'));
	Assert::same('{closure TestDynamic::magic}', Callback::toString(Closure::fromCallable('TestDynamic::magic')));
	Assert::same('TestDynamic::__callStatic magic *', Closure::fromCallable('TestDynamic::magic')->__invoke('*'));

	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',
	);
});
