<?php

declare(strict_types=1);

/**
 * This file is part of the Carbon package.
 *
 * (c) Brian Nesbitt <brian@nesbot.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Tests\CarbonImmutable;

use Carbon\CarbonImmutable as Carbon;
use DateTimeImmutable;
use InvalidArgumentException;
use ReflectionClass;
use ReflectionObject;
use ReflectionProperty;
use Tests\AbstractTestCase;
use Throwable;

class SerializationTest extends AbstractTestCase
{
    /**
     * @var string
     */
    protected $serialized;

    protected function setUp(): void
    {
        parent::setUp();

        $this->serialized = \extension_loaded('msgpack')
            ? [
                "O:22:\"Carbon\CarbonImmutable\":4:{s:4:\"date\";s:26:\"2016-02-01 13:20:25.000000\";s:13:\"timezone_type\";i:3;s:8:\"timezone\";s:15:\"America/Toronto\";s:18:\"dumpDateProperties\";a:2:{s:4:\"date\";s:26:\"2016-02-01 13:20:25.000000\";s:8:\"timezone\";s:96:\"O:21:\"Carbon\CarbonTimeZone\":2:{s:13:\"timezone_type\";i:3;s:8:\"timezone\";s:15:\"America/Toronto\";}\";}}",
                "O:22:\"Carbon\CarbonImmutable\":4:{s:4:\"date\";s:26:\"2016-02-01 13:20:25.000000\";s:13:\"timezone_type\";i:3;s:8:\"timezone\";s:15:\"America/Toronto\";s:18:\"dumpDateProperties\";a:2:{s:4:\"date\";s:26:\"2016-02-01 13:20:25.000000\";s:8:\"timezone\";s:23:\"s:15:\"America/Toronto\";\";}}",
            ]
            : ['O:22:"Carbon\CarbonImmutable":3:{s:4:"date";s:26:"2016-02-01 13:20:25.000000";s:13:"timezone_type";i:3;s:8:"timezone";s:15:"America/Toronto";}'];
    }

    protected function cleanSerialization(string $serialization): string
    {
        return preg_replace('/s:\d+:\"[^"]*dumpDateProperties\"/', 's:18:"dumpDateProperties"', $serialization);
    }

    public function testSerialize()
    {
        $dt = Carbon::create(2016, 2, 1, 13, 20, 25);
        $message = "not in:\n".implode("\n", $this->serialized);
        $this->assertContains($this->cleanSerialization($dt->serialize()), $this->serialized, $message);
        $this->assertContains($this->cleanSerialization(serialize($dt)), $this->serialized, $message);
    }

    public function testFromUnserialized()
    {
        $dt = Carbon::fromSerialized($this->serialized[0]);
        $this->assertCarbon($dt, 2016, 2, 1, 13, 20, 25);

        $dt = unserialize($this->serialized[0]);
        $this->assertCarbon($dt, 2016, 2, 1, 13, 20, 25);
    }

    public function testSerialization()
    {
        $this->assertEquals(Carbon::now(), unserialize(serialize(Carbon::now())));
    }

    public static function dataForTestFromUnserializedWithInvalidValue()
    {
        return [
            [null],
            [true],
            [false],
            [123],
            ['foobar'],
        ];
    }

    /**
     * @param mixed $value
     *
     * @dataProvider \Tests\CarbonImmutable\SerializationTest::dataForTestFromUnserializedWithInvalidValue
     */
    public function testFromUnserializedWithInvalidValue($value)
    {
        $this->expectExceptionObject(new InvalidArgumentException(
            "Invalid serialized value: $value"
        ));

        Carbon::fromSerialized($value);
    }

    public function testDateSerializationReflectionCompatibility()
    {
        $tz = $this->firstValidTimezoneAmong(['America/Los_Angeles', 'US/Pacific'])->getName();

        try {
            $reflection = (new ReflectionClass(DateTimeImmutable::class))->newInstanceWithoutConstructor();

            @$reflection->date = '1990-01-17 10:28:07';
            @$reflection->timezone_type = 3;
            @$reflection->timezone = $tz;

            $date = unserialize(serialize($reflection));
        } catch (Throwable $exception) {
            $this->markTestSkipped(
                "It fails on DateTime so Carbon can't support it, error was:\n".$exception->getMessage()
            );
        }

        $this->assertSame('1990-01-17 10:28:07', $date->format('Y-m-d h:i:s'));

        $reflection = (new ReflectionClass(Carbon::class))->newInstanceWithoutConstructor();

        @$reflection->date = '1990-01-17 10:28:07';
        @$reflection->timezone_type = 3;
        @$reflection->timezone = $tz;

        $date = unserialize(serialize($reflection));

        $this->assertSame('1990-01-17 10:28:07', $date->format('Y-m-d h:i:s'));

        $reflection = new ReflectionObject(Carbon::parse('1990-01-17 10:28:07'));
        $target = (new ReflectionClass(Carbon::class))->newInstanceWithoutConstructor();
        /** @var ReflectionProperty[] $properties */
        $properties = [];

        foreach ($reflection->getProperties() as $property) {
            $property->setAccessible(true);
            $properties[$property->getName()] = $property;
        }

        $setValue = function ($key, $value) use (&$properties, &$target) {
            if (isset($properties[$key])) {
                $properties[$key]->setValue($target, $value);

                return;
            }

            @$target->$key = $value;
        };

        $setValue('date', '1990-01-17 10:28:07');
        $setValue('timezone_type', 3);
        $setValue('timezone', $tz);

        $date = unserialize(serialize($target));

        $this->assertSame('1990-01-17 10:28:07', $date->format('Y-m-d h:i:s'));
    }

    public function testMsgPackExtension(): void
    {
        if (!\extension_loaded('msgpack')) {
            $this->markTestSkipped('This test needs msgpack extension to be enabled.');
        }

        $string = '2018-06-01 21:25:13.321654 Europe/Vilnius';
        $date = Carbon::parse('2018-06-01 21:25:13.321654 Europe/Vilnius');
        $message = @msgpack_pack($date);
        $copy = msgpack_unpack($message);

        $this->assertSame($string, $copy->format('Y-m-d H:i:s.u e'));
    }

    public function testSleepRawMethod(): void
    {
        $date = Carbon::parse('2018-06-01 21:25:13.321654 Europe/Vilnius');

        $expected = ['date', 'timezone_type', 'timezone'];

        if (\extension_loaded('msgpack')) {
            $expected[] = 'dumpDateProperties';
        }

        $this->assertSame($expected, $date->__sleep());

        $date->locale('fr_FR');
        $expected[] = 'dumpLocale';

        $this->assertSame($expected, $date->__sleep());
    }

    public function testSerializeRawMethod(): void
    {
        $date = Carbon::parse('2018-06-01 21:25:13.321654 Europe/Vilnius');

        $expected = [
            'date' => '2018-06-01 21:25:13.321654',
            'timezone_type' => 3,
            'timezone' => 'Europe/Vilnius',
        ];

        if (\extension_loaded('msgpack')) {
            $expected['dumpDateProperties'] = [
                'date' => $date->format('Y-m-d H:i:s.u'),
                'timezone' => serialize($date->timezone),
            ];
        }

        $this->assertSame($expected, $date->__serialize());

        $date->locale('lt_LT');
        $expected['dumpLocale'] = 'lt_LT';

        $this->assertSame($expected, $date->__serialize());
    }

    public function testWakeupRawMethod(): void
    {
        $tz = $this->firstValidTimezoneAmong(['America/Los_Angeles', 'US/Pacific'])->getName();

        /** @var Carbon $date */
        $date = (new ReflectionClass(Carbon::class))->newInstanceWithoutConstructor();

        @$date->date = '1990-01-17 10:28:07';
        @$date->timezone_type = 3;
        @$date->timezone = $tz;
        @$date->dumpLocale = 'es';

        $date->__wakeup();

        $this->assertSame('1990-01-17 10:28:07 '.$tz, $date->format('Y-m-d H:i:s e'));
        $this->assertSame('es', $date->locale);
    }

    public function testUnserializeRawMethod(): void
    {
        /** @var Carbon $date */
        $date = (new ReflectionClass(Carbon::class))->newInstanceWithoutConstructor();

        $date->__unserialize([
            'date' => '2018-06-01 21:25:13.321654',
            'timezone_type' => 3,
            'timezone' => 'Europe/Vilnius',
        ]);

        $this->assertSame('2018-06-01 21:25:13.321654 Europe/Vilnius', $date->format('Y-m-d H:i:s.u e'));
        $this->assertSame('en', $date->locale);

        /** @var Carbon $date */
        $date = (new ReflectionClass(Carbon::class))->newInstanceWithoutConstructor();

        $date->__unserialize([
            'date' => '2018-06-01 21:25:13.321654',
            'timezone_type' => 3,
            'timezone' => 'Europe/Vilnius',
            'dumpLocale' => 'lt_LT',
        ]);

        $this->assertSame('2018-06-01 21:25:13.321654 Europe/Vilnius', $date->format('Y-m-d H:i:s.u e'));
        $this->assertSame('lt_LT', $date->locale);
    }
}
