<?php

use Carbon\Carbon;
use Carbon\CarbonImmutable;
use Laravel\SerializableClosure\SerializableClosure;
use Laravel\SerializableClosure\Serializers\Signed;
use Laravel\SerializableClosure\Support\ReflectionClosure;
use Laravel\SerializableClosure\UnsignedSerializableClosure;
use Tests\Fixtures\Model;

test('closure use return value', function () {
    $a = 100;
    $c = function () use ($a) {
        return $a;
    };

    $u = s($c);

    expect($a)->toEqual($u());
})->with('serializers');

test('closure use transformation with Native', function () {
    $a = 100;

    SerializableClosure::transformUseVariablesUsing(function ($data) {
        foreach ($data as $key => $value) {
            $data[$key] = $value * 2;
        }

        return $data;
    });

    SerializableClosure::resolveUseVariablesUsing(function ($data) {
        foreach ($data as $key => $value) {
            $data[$key] = $value / 4;
        }

        return $data;
    });

    if ($this->serializer == UnsignedSerializableClosure::class) {
        $c = unserialize(serialize(SerializableClosure::unsigned(function () use ($a) {
            return $a;
        })));
    } else {
        $c = unserialize(serialize(new SerializableClosure(function () use ($a) {
            return $a;
        })));
    }

    expect($c())->toEqual(50);
})->with('serializers')->skip((float) phpversion() < '7.4');

test('closure use transformation with Signed', function () {
    $a = 100;

    SerializableClosure::setSecretKey('secret');
    SerializableClosure::transformUseVariablesUsing(function ($data) {
        foreach ($data as $key => $value) {
            $data[$key] = $value * 2;
        }

        return $data;
    });

    SerializableClosure::resolveUseVariablesUsing(function ($data) {
        foreach ($data as $key => $value) {
            $data[$key] = $value / 4;
        }

        return $data;
    });

    if ($this->serializer == UnsignedSerializableClosure::class) {
        $c = unserialize(serialize(SerializableClosure::unsigned(function () use ($a) {
            return $a;
        })));
    } else {
        $c = unserialize(serialize(new SerializableClosure(function () use ($a) {
            return $a;
        })));
    }

    expect($c())->toEqual(50);
})->with('serializers')->skip((float) phpversion() < '7.4');

test('closure use return closure', function () {
    $a = function ($p) {
        return $p + 1;
    };
    $b = function ($p) use ($a) {
        return $a($p);
    };

    $v = 1;
    $u = s($b);

    expect($u(1))->toEqual($v + 1);
})->with('serializers');

test('closure use return closure by ref', function () {
    $a = function ($p) {
        return $p + 1;
    };
    $b = function ($p) use (&$a) {
        return $a($p);
    };

    $v = 1;
    $u = s($b);

    expect($u(1))->toEqual($v + 1);
})->with('serializers');

test('closure use self', function () {
    $a = function () use (&$a) {
        return $a;
    };
    $u = s($a);

    expect($u())->toEqual($u);
})->with('serializers');

test('closure use self in array', function () {
    $a = [];

    $b = function () use (&$a) {
        return $a[0];
    };

    $a[] = $b;

    $u = s($b);

    expect($u())->toEqual($u);
})->with('serializers');

test('closure use self in object', function () {
    $a = new stdClass();

    $b = function () use (&$a) {
        return $a->me;
    };

    $a->me = $b;

    $u = s($b);

    expect($u())->toEqual($u);
})->with('serializers');

test('closure use self in multi array', function () {
    $a = [];
    $x = null;

    $b = function () use (&$x) {
        return $x;
    };

    $c = function ($i) use (&$a) {
        $f = $a[$i];

        return $f();
    };

    $a[] = $b;
    $a[] = $c;
    $x = $c;

    $u = s($c);

    expect($u(0))->toEqual($u);
})->with('serializers');

test('closure use self in instance', function () {
    $i = new ObjSelf();
    $c = function ($c) use ($i) {
        return $c === $i->o;
    };
    $i->o = $c;
    $u = s($c);
    expect($u($u))->toBeTrue();
})->with('serializers');

test('closure use self in instance2', function () {
    $i = new ObjSelf();
    $c = function () use (&$c, $i) {
        return $c == $i->o;
    };
    $i->o = &$c;
    $u = s($c);
    expect($u())->toBeTrue();
})->with('serializers');

test('closure serialization twice', function () {
    $a = function ($p) {
        return $p;
    };

    $b = function ($p) use ($a) {
        return $a($p);
    };

    $u = s(s($b));

    expect($u('ok'))->toEqual('ok');
})->with('serializers');

test('closure real serialization', function () {
    $f = function ($a, $b) {
        return $a + $b;
    };

    $u = s(s($f));
    expect($u(2, 3))->toEqual(5);
})->with('serializers');

test('closure nested', function () {
    $o = function ($a) {
        // this should never happen
        if ($a === false) {
            return false;
        }

        $n = function ($b) {
            return ! $b;
        };

        $ns = s($n);

        return $ns(false);
    };

    $os = s($o);

    expect($os(true))->toEqual(true);
})->with('serializers');

test('closure curly syntax', function () {
    $f = function () {
        $x = (object) ['a' => 1, 'b' => 3];
        $b = 'b';

        return $x->{'a'} + $x->{$b};
    };
    $f = s($f);
    expect($f())->toEqual(4);
})->with('serializers');

test('closure bind to object', function () {
    $a = new A();

    $b = function () {
        return $this->aPublic();
    };

    $b = $b->bindTo($a, __NAMESPACE__.'\\A');

    $u = s($b);

    expect($u())->toEqual('public called');
})->with('serializers');

test('closure bind to object scope', function () {
    $a = new A();

    $b = function () {
        return $this->aProtected();
    };

    $b = $b->bindTo($a, __NAMESPACE__.'\\A');

    $u = s($b);

    expect($u())->toEqual('protected called');
})->with('serializers');

test('closure bind to object static scope', function () {
    $a = new A();

    $b = function () {
        return static::aStaticProtected();
    };

    $b = $b->bindTo(null, __NAMESPACE__.'\\A');

    $u = s($b);

    expect($u())->toEqual('static protected called');
})->with('serializers');

test('closure static', function () {
    $f = static function () {
    };
    $rc = new ReflectionClosure($f);
    expect($rc->isStatic())->toBeTrue();
})->with('serializers');

test('closure static fail', function () {
    $f = static // This will not work
    function () {
    };
    $rc = new ReflectionClosure($f);
    expect($rc->isStatic())->toBeFalse();
})->with('serializers');

test('closure scope remains the same', function () {
    $f = function () {
        static $i = 0;
    };
    $o = s($f);

    $rf = new ReflectionClosure($f);
    $ro = new ReflectionClosure($o);

    test()->assertNotNull($rf->getClosureScopeClass());
    test()->assertNotNull($ro->getClosureScopeClass());
    expect($ro->getClosureScopeClass()->name)->toEqual($rf->getClosureScopeClass()->name);
    expect($ro->getClosureScopeClass()->name)->toEqual($rf->getClosureScopeClass()->name);

    $f = $f->bindTo(null, null);
    $o = s($f);
    $rf = new ReflectionClosure($f);
    $ro = new ReflectionClosure($o);

    expect($rf->getClosureScopeClass())->toBeNull();
    expect($ro->getClosureScopeClass())->toBeNull();
})->with('serializers');

test('mixed encodings', function () {
    $a = iconv('utf-8', 'utf-16', 'Düsseldorf');
    $b = mb_convert_encoding('Düsseldorf', 'ISO-8859-1', 'UTF-8');

    $closure = function () use ($a, $b) {
        return [$a, $b];
    };

    $u = s($closure);
    $r = $u();

    expect($r[0])->toEqual($a);
    expect($r[1])->toEqual($b);
})->with('serializers');

test('serializable closure serialization string content dont change', function () {
    $a = 100;

    SerializableClosure::setSecretKey('foo');

    $c = new SerializableClosure(function () use ($a) {
        return $a;
    });

    $actual = explode('s:32:', serialize($c))[0];

    expect($actual)->toBe(<<<OEF
O:47:"Laravel\SerializableClosure\SerializableClosure":1:{s:12:"serializable";O:46:"Laravel\SerializableClosure\Serializers\Signed":2:{s:12:"serializable";s:264:"O:46:"Laravel\SerializableClosure\Serializers\Native":5:{s:3:"use";a:1:{s:1:"a";i:100;}s:8:"function";s:47:"function () use (\$a) {
        return \$a;
    }";s:5:"scope";s:22:"P\Tests\SerializerTest";s:4:"this";N;s:4:"self";
OEF
    );
});

test('unsigned serializable closure serialization string content dont change', function () {
    $a = 100;

    SerializableClosure::setSecretKey('foo');

    $c = SerializableClosure::unsigned(function () use ($a) {
        return $a;
    });

    $actual = explode('s:32:', serialize($c))[0];

    expect($actual)->toBe(<<<OEF
O:55:"Laravel\SerializableClosure\UnsignedSerializableClosure":1:{s:12:"serializable";O:46:"Laravel\SerializableClosure\Serializers\Native":5:{s:3:"use";a:1:{s:1:"a";i:100;}s:8:"function";s:47:"function () use (\$a) {
        return \$a;
    }";s:5:"scope";s:22:"P\Tests\SerializerTest";s:4:"this";N;s:4:"self";
OEF
    );
});

test('use objects with serializable closures properties', function () {
    $a = new stdClass();

    if ($this->serializer == Signed::class) {
        SerializableClosure::setSecretKey('secret');
    }

    if ($this->serializer == UnsignedSerializableClosure::class) {
        $a->b = SerializableClosure::unsigned(function () {
            return 'Hi';
        });
    } else {
        $a->b = new SerializableClosure(function () {
            return 'Hi';
        });
    }

    $closure = function () use ($a) {
        return ($a->b)();
    };

    $u = s($closure);
    $r = $u();

    expect($r)->toEqual('Hi');
})->with('serializers');

test('rebound closure', function () {
    $closure = Closure::bind(
        function () {
            return $this->hello();
        },
        new A3(function () {
            return 'Hi';
        }),
        A3::class
    );

    $u = s($closure);
    $r = $u();

    expect($r)->toEqual('Hi');
})->with('serializers');

test('from callable namespaces', function () {
    $f = Closure::fromCallable([new Model, 'make']);

    $f = s($f);

    expect($f(new Model))->toBeInstanceOf(Model::class);
})->with('serializers');

test('from static callable namespaces', function () {
    $f = Closure::fromCallable([new Model, 'staticMake']);

    $f = s($f);

    expect($f(new Model))->toBeInstanceOf(Model::class);
})->with('serializers');

test('serializes used dates', function ($_, $date) {
    $closure = function () use ($date) {
        return $date;
    };

    $u = s($closure);
    $r = $u();

    expect($r)->toEqual($date);
})->with('serializers')->with([
    new DateTime,
    new DateTimeImmutable,
    new Carbon,
    new CarbonImmutable,
]);

test('serializes with used object date properties', function ($_, $date) {
    $obj = new ObjSelf;
    $obj->o = $date;

    $closure = function () use ($obj) {
        return $obj;
    };

    $u = s($closure);
    $r = $u();

    expect($r->o)->toEqual($date);
})->with('serializers')->with([
    new DateTime,
    new DateTimeImmutable,
    new Carbon,
    new CarbonImmutable,
]);

class A
{
    protected static function aStaticProtected()
    {
        return 'static protected called';
    }

    protected function aProtected()
    {
        return 'protected called';
    }

    public function aPublic()
    {
        return 'public called';
    }
}

class A2
{
    private $phrase = 'Hello, World!';

    private $closure1;

    private $closure2;

    private $closure3;

    public function __construct()
    {
        $this->closure1 = function () {
            return $this->phrase;
        };
        $this->closure2 = function () {
            return $this;
        };
        $this->closure3 = function () {
            $c = $this->closure2;

            return $this === $c();
        };
    }

    public function getPhrase()
    {
        $c = $this->closure1;

        return $c();
    }

    public function getEquality()
    {
        $c = $this->closure3;

        return $c();
    }
}

class A3
{
    private $closure;

    public function __construct($closure)
    {
        $this->closure = $closure;
    }

    public function hello()
    {
        return ($this->closure)();
    }
}

class ObjSelf
{
    public $o;
}
