<?php

namespace Test\Functional\Krizalys\Onedrive;

use Krizalys\Onedrive\Constant\SpecialFolderName;
use Krizalys\Onedrive\DriveItem;
use Krizalys\Onedrive\File;
use Krizalys\Onedrive\Folder;
use Krizalys\Onedrive\Proxy\DriveItemProxy;
use PHPUnit\Framework\TestCase;
use Test\Functional\Krizalys\Onedrive\Traits\AssertionsTrait;
use Test\Functional\Krizalys\Onedrive\Traits\ClientFactoryTrait;
use Test\Functional\Krizalys\Onedrive\Traits\ConfigurationTrait;

class KrizalysOnedriveTest extends TestCase
{
    use AssertionsTrait;
    use ClientFactoryTrait;
    use ConfigurationTrait;

    const DATETIME_REGEX = '/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[-+]\d{4}/';

    const PHP_LOGO_PNG_BASE64 = <<<'EOF'
 
EOF;

    private static $client;

    public static function setUpBeforeClass()
    {
        parent::setUpBeforeClass();

        $clientId = self::getConfig('CLIENT_ID');
        $username = self::getConfig('USERNAME');
        $password = self::getConfig('PASSWORD');
        $secret   = self::getConfig('SECRET');

        self::$client = self::createClient(
            $clientId,
            $username,
            $password,
            $secret
        );
    }

    // Legacy support //////////////////////////////////////////////////////////
    public function testClientRenewAccessTokenLegacy()
    {
        $secret = self::getConfig('SECRET');
        $before = clone self::$client->getState()->token;
        sleep(1);
        self::$client->renewAccessToken($secret);
        $after = clone self::$client->getState()->token;
        $this->assertNotEquals($before->obtained, $after->obtained);
        $this->assertNotEquals($before->data->access_token, $after->data->access_token);
        $this->assertNotEquals($before->data->refresh_token, $after->data->refresh_token);
    }

    public function testClientCreateFolderLegacy()
    {
        $root = self::getRoot();

        $folder1 = self::$client->createFolder('Test folder #1', null, null);
        $this->assertInstanceOf(Folder::class, $folder1);
        $this->assertEquals('Test folder #1', $folder1->getName());

        $children = array_map(function (DriveItemProxy $driveItem) {
            return $driveItem->id;
        }, $root->children);

        $this->assertContains($folder1->getId(), $children);
        self::deleteLegacy($folder1);

        $folder2 = self::$client->createFolder('Test folder #2', null, 'Test description folder #2');
        $this->assertInstanceOf(Folder::class, $folder2);
        $this->assertEquals('Test folder #2', $folder2->getName());

        $children = array_map(function (DriveItemProxy $driveItem) {
            return $driveItem->id;
        }, $root->children);

        $this->assertContains($folder2->getId(), $children);
        $this->assertEquals('Test description folder #2', $folder2->getDescription());
        self::deleteLegacy($folder2);

        self::withSandbox(__FUNCTION__, function (DriveItemProxy $parent) {
            $folder3 = self::$client->createFolder('Test folder #3', $parent->id, null);
            $this->assertInstanceOf(Folder::class, $folder3);
            $this->assertEquals('Test folder #3', $folder3->getName());
            $this->assertEquals($parent->id, $folder3->getParentId());

            $children = array_map(function (DriveItemProxy $driveItem) {
                return $driveItem->id;
            }, $parent->children);

            $this->assertContains($folder3->getId(), $children);
        });
    }

    public function testClientCreateFileLegacy()
    {
        self::withSandbox(__FUNCTION__, function (DriveItemProxy $parent) {
            // Test with a text file.
            $file1 = self::$client->createFile('Test file #1.txt', $parent->id, 'Test content');
            $this->assertInstanceOf(File::class, $file1);
            $this->assertEquals('Test file #1.txt', $file1->getName());
            $this->assertEquals($parent->id, $file1->getParentId());
            $this->assertEquals('Test content', $file1->fetchContent());

            $children = array_map(function (DriveItemProxy $driveItem) {
                return $driveItem->id;
            }, $parent->children);

            $this->assertContains($file1->getId(), $children);

            // Test with a binary file.
            $content = base64_decode(self::PHP_LOGO_PNG_BASE64);
            $file2   = self::$client->createFile('Test file #2.png', $parent->id, $content);
            $this->assertInstanceOf(File::class, $file2);
            $this->assertEquals('Test file #2.png', $file2->getName());
            $this->assertEquals($parent->id, $file2->getParentId());
            $this->assertEquals($content, $file2->fetchContent());

            $children = array_map(function (DriveItemProxy $driveItem) {
                return $driveItem->id;
            }, $parent->children);

            $this->assertContains($file2->getId(), $children);
        });
    }

    public function testClientFetchDriveItemLegacy()
    {
        $root = self::getRoot();
        $item = self::$client->fetchDriveItem($root->id);
        $this->assertInstanceOf(Folder::class, $item);
        $this->assertContains($item->getName(), ['root', 'SkyDrive']);
        $this->assertInternalType('string', $item->getParentId());
    }

    public function testClientFetchRootLegacy()
    {
        $root = self::$client->fetchRoot();
        $this->assertInstanceOf(Folder::class, $root);
        $this->assertContains($root->getName(), ['root', 'SkyDrive']);
        $this->assertInternalType('string', $root->getParentId());
    }

    public function testClientFetchCameraRollLegacy()
    {
        $pics       = self::getPhotosFolder();
        $cameraRoll = self::$client->fetchCameraRoll();
        $this->assertInstanceOf(Folder::class, $cameraRoll);
        $this->assertEquals('Camera Roll', $cameraRoll->getName());

        $children = array_map(function (DriveItemProxy $driveItem) {
            return $driveItem->name;
        }, $pics->children);

        $this->assertContains($cameraRoll->getName(), $children);
    }

    public function testClientFetchDocsLegacy()
    {
        $root = self::getRoot();
        $docs = self::$client->fetchDocs();
        $this->assertInstanceOf(Folder::class, $docs);
        $this->assertEquals('Documents', $docs->getName());

        $children = array_map(function (DriveItemProxy $driveItem) {
            return $driveItem->name;
        }, $root->children);

        $this->assertContains($docs->getName(), $children);
    }

    public function testClientFetchPicsLegacy()
    {
        $root = self::getRoot();
        $pics = self::$client->fetchPics();
        $this->assertInstanceOf(Folder::class, $pics);
        $this->assertEquals('Pictures', $pics->getName());

        $children = array_map(function (DriveItemProxy $driveItem) {
            return $driveItem->name;
        }, $root->children);

        $this->assertContains($pics->getName(), $children);
    }

    public function testClientFetchPropertiesLegacy()
    {
        $root       = self::getRoot();
        $properties = self::$client->fetchProperties($root->id);
        $this->assertInternalType('object', $properties);
        $this->assertInternalType('string', $properties->id);
        $this->assertInternalType('object', $properties->from);
        $this->assertNull($properties->from->name);
        $this->assertNull($properties->from->id);
        $this->assertContains($properties->name, ['root', 'SkyDrive']);

        if (property_exists($properties, 'description')) {
            $this->fail();
        }

        $this->assertInternalType('string', $properties->parent_id);
        $this->assertGreaterThanOrEqual(0, $properties->size);
        $this->assertRegExp(self::DATETIME_REGEX, $properties->created_time);
        $this->assertRegExp(self::DATETIME_REGEX, $properties->updated_time);
    }

    public function testClientFetchDriveItemsLegacy()
    {
        $root  = self::getRoot();
        $items = self::$client->fetchDriveItems($root->id);
        $this->assertInternalType('array', $items);

        foreach ($items as $item) {
            $this->assertThat(
                $item,
                $this->logicalOr(
                    $this->isInstanceOf(Folder::class, $item),
                    $this->isInstanceOf(File::class, $item)
                )
            );
        }
    }

    public function testClientUpdateDriveItemLegacy()
    {
        $root = self::getRoot();
        $item = self::createFolder($root, 'Test folder');

        self::$client->updateDriveItem(
            $item->id,
            [
                'name'        => 'Test folder (renamed)',
                'description' => 'Test description folder',
            ],
            false
        );

        $item = self::getDriveItemById($item->id);
        $this->assertEquals('Test folder (renamed)', $item->name);
        $this->assertEquals('Test description folder', $item->description);

        self::delete($item);
    }

    public function testClientMoveDriveItemLegacy()
    {
        self::withSandbox(__FUNCTION__, function (DriveItemProxy $parent) {
            $item        = self::createFolder($parent, 'Test item');
            $destination = self::createFolder($parent, 'Test destination');
            self::$client->moveDriveItem($item->id, $destination->id);

            $children = array_map(function (DriveItemProxy $driveItem) {
                return $driveItem->id;
            }, $parent->children);

            $this->assertFalse(in_array($item->id, $children));

            $children = array_map(function (DriveItemProxy $driveItem) {
                return $driveItem->id;
            }, $destination->children);

            $this->assertContains($item->id, $children);

            $item = self::getDriveItemById($item->id);
            $this->assertEquals($destination->id, $item->parentReference->id);
        });
    }

    public function testClientCopyDriveItemLegacy()
    {
        self::withSandbox(__FUNCTION__, function (DriveItemProxy $parent) {
            $item        = self::upload($parent, 'Test item');
            $destination = self::createFolder($parent, 'Test destination');
            self::$client->copyFile($item->id, $destination->id);

            $children = array_map(function (DriveItemProxy $driveItem) {
                return $driveItem->id;
            }, $parent->children);

            $this->assertContains($item->id, $children);
            $this->assertDriveItemProxyWillHaveChild($destination, 'Test item');
        });
    }

    public function testClientDeleteDriveItemLegacy()
    {
        self::withSandbox(__FUNCTION__, function (DriveItemProxy $parent) {
            $item = self::createFolder($parent, 'Test folder');
            self::$client->deleteDriveItem($item->id);

            $children = array_map(function (DriveItemProxy $driveItem) {
                return $driveItem->id;
            }, $parent->children);

            $this->assertFalse(in_array($item->id, $children));
        });
    }

    public function testClientFetchQuotaLegacy()
    {
        $quota = self::$client->fetchQuota();
        $this->assertInternalType('object', $quota);
        $this->assertGreaterThanOrEqual(0, $quota->quota);
        $this->assertGreaterThanOrEqual(0, $quota->available);
    }

    public function testClientFetchRecentDocsLegacy()
    {
        $recentDocs = self::$client->fetchRecentDocs();
        $this->assertInternalType('object', $recentDocs);
        $this->assertInternalType('array', $recentDocs->data);

        foreach ($recentDocs->data as $data) {
            $this->assertInternalType('object', $data);
        }
    }

    public function testClientFetchSharedLegacy()
    {
        $shared = self::$client->fetchShared();
        $this->assertInternalType('object', $shared);
        $this->assertInternalType('array', $shared->data);

        foreach ($shared->data as $data) {
            $this->assertInternalType('object', $data);
        }
    }

    public function testDriveItemMoveLegacy()
    {
        self::withSandbox(__FUNCTION__, function (DriveItemProxy $parent) {
            $item        = self::upload($parent, 'Test item');
            $destination = self::createFolder($parent, 'Test destination');

            (new File(self::$client, $item->id))->move($destination->id);

            $children = array_map(function (DriveItemProxy $driveItem) {
                return $driveItem->id;
            }, $parent->children);

            $this->assertFalse(in_array($item->id, $children));

            $children = array_map(function (DriveItemProxy $driveItem) {
                return $driveItem->id;
            }, $destination->children);

            $this->assertContains($item->id, $children);

            $item = self::getDriveItemById($item->id);
            $this->assertEquals($destination->id, $item->parentReference->id);
        });
    }

    public function testFileFetchContentLegacy()
    {
        self::withSandbox(__FUNCTION__, function (DriveItemProxy $parent) {
            $item   = self::upload($parent, 'Test item', 'Test content');
            $item   = new File(self::$client, $item->id);
            $actual = $item->fetchContent();
            $this->assertEquals('Test content', $actual);
        });
    }

    public function testFileCopyLegacy()
    {
        self::withSandbox(__FUNCTION__, function (DriveItemProxy $parent) {
            $item        = self::upload($parent, 'Test item');
            $destination = self::createFolder($parent, 'Test destination');
            (new File(self::$client, $item->id))->copy($destination->id);

            $children = array_map(function (DriveItemProxy $driveItem) {
                return $driveItem->name;
            }, $parent->children);

            $this->assertContains($item->name, $children);
            $this->assertDriveItemProxyWillHaveChild($destination, $item->name);

            $copy = array_filter($destination->children, function (DriveItemProxy $driveItem) use ($item) {
                return $driveItem->name == $item->name;
            });

            $this->assertCount(1, $copy);
            $copy = self::getDriveItemById($copy[0]->id);
            $this->assertEquals($destination->id, $copy->parentReference->id);
        });
    }

    public function testFolderFetchDriveItemsLegacy()
    {
        $root  = self::getRoot();
        $root  = new Folder(self::$client, $root->id);
        $items = $root->fetchDriveItems();
        $this->assertInternalType('array', $items);

        foreach ($items as $item) {
            $this->assertThat(
                $item,
                $this->logicalOr(
                    $this->isInstanceOf(Folder::class, $item),
                    $this->isInstanceOf(File::class, $item)
                )
            );
        }
    }

    public function testFolderFetchChildDriveItemsLegacy()
    {
        $root  = self::getRoot();
        $root  = new Folder(self::$client, $root->id);
        $items = $root->fetchChildDriveItems();
        $this->assertInternalType('array', $items);

        foreach ($items as $item) {
            $this->assertThat(
                $item,
                $this->logicalOr(
                    $this->isInstanceOf(Folder::class, $item),
                    $this->isInstanceOf(File::class, $item)
                )
            );
        }
    }

    public function testFolderCreateFolderLegacy()
    {
        self::withSandbox(__FUNCTION__, function (DriveItemProxy $parent) {
            $item = (new Folder(self::$client, $parent->id))->createFolder('Test folder', 'Test description');
            $this->assertInstanceOf(Folder::class, $item);
            $this->assertEquals($parent->id, $item->getParentId());
            $this->assertEquals('Test folder', $item->getName());
            $this->assertEquals('Test description', $item->getDescription());

            $children = array_map(function (DriveItemProxy $driveItem) {
                return $driveItem->id;
            }, $parent->children);

            $this->assertContains($item->getId(), $children);
        });
    }

    public function testFolderCreateFileLegacy()
    {
        self::withSandbox(__FUNCTION__, function (DriveItemProxy $parent) {
            $item = (new Folder(self::$client, $parent->id))->createFile('Test file', 'Test content');
            $this->assertInstanceOf(File::class, $item);
            $this->assertEquals($parent->id, $item->getParentId());
            $this->assertEquals('Test file', $item->getName());
            $this->assertEquals('Test content', $item->fetchContent());

            $children = array_map(function (DriveItemProxy $driveItem) {
                return $driveItem->id;
            }, $parent->children);

            $this->assertContains($item->getId(), $children);
        });
    }

    private function assertDriveItemProxyWillHaveChild(DriveItemProxy $item, $name)
    {
        $now = time();

        $items = array_filter($item->children, function (DriveItemProxy $item) use ($name) {
            return $item->name == $name;
        });

        while (count($items) != 1) {
            sleep(self::ASYNC_POLL_INTERVAL);

            if (time() - $now > self::ASYNC_POLL_TIMEOUT) {
                $this->fail('Assertion failed after timeout');
            }

            $items = array_filter($item->children, function (DriveItemProxy $item) use ($name) {
                return $item->name == $name;
            });
        }

        return $items[0];
    }

    private static function getRoot()
    {
        return self::$client->getRoot();
    }

    private static function getDriveItemById($id)
    {
        return self::$client->getDriveItemById($id);
    }

    private static function getPhotosFolder()
    {
        return self::$client->getSpecialFolder(SpecialFolderName::PHOTOS);
    }

    private static function createFolder(DriveItemProxy $item, $name)
    {
        return $item->createFolder($name);
    }

    private static function upload(DriveItemProxy $item, $name, $content = '')
    {
        return $item->upload(
            $name,
            $content,
            []
        );
    }

    private static function delete(DriveItemProxy $item)
    {
        $item->delete();
    }

    private static function deleteLegacy(DriveItem $item)
    {
        self::$client->deleteDriveItem($item->getId());
    }

    private static function withSandbox($name, callable $function)
    {
        $root   = self::getRoot();
        $suffix = sprintf('%08X', rand(0x00000000, 0xffffffff));
        $name   = $name . '_' . gmdate('YmdHis') . "_$suffix";
        $folder = self::createFolder($root, $name);

        try {
            return $function($folder);
        } catch (\Exception $exception) {
            throw $exception;
        } finally {
            self::delete($folder);
        }
    }
}
