<?php

namespace Tests\E2E\Services\Databases;

use Appwrite\Extend\Exception;
use Tests\E2E\Client;
use Tests\E2E\Scopes\SchemaPolling;
use Tests\E2E\Traits\DatabasesUrlHelpers;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Operator;
use Utopia\Database\Query;
use Utopia\Database\Validator\Datetime as DatetimeValidator;

trait DatabasesBase
{
    use DatabasesUrlHelpers;
    use SchemaPolling;

    /**
     * Static caches for test data - keyed by project ID to support parallel test runs
     */
    private static array $databaseCache = [];
    private static array $collectionCache = [];
    private static array $attributesCache = [];
    private static array $indexesCache = [];
    private static array $documentsCache = [];
    private static array $oneToOneCache = [];
    private static array $oneToManyCache = [];
    private static array $fulltextDocsCache = [];

    /**
     * Get cache key for current test instance (based on project ID)
     */
    protected function getCacheKey(): string
    {
        return $this->getProject()['$id'] ?? 'default';
    }

    /**
     * Setup: Create database and return data
     * Uses static caching to avoid recreating resources
     */
    protected function setupDatabase(): array
    {
        $cacheKey = $this->getCacheKey();
        if (!empty(self::$databaseCache[$cacheKey])) {
            return self::$databaseCache[$cacheKey];
        }

        $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'Test Database'
        ]);

        $this->assertNotEmpty($database['body']['$id']);
        $this->assertEquals(201, $database['headers']['status-code']);

        self::$databaseCache[$cacheKey] = ['databaseId' => $database['body']['$id']];
        return self::$databaseCache[$cacheKey];
    }

    /**
     * Helper to create an attribute on a collection.
     *
     * @param string $databaseId
     * @param string $collectionId
     * @param string $type
     * @param array<string, mixed> $payload
     *
     * @return array<string, mixed>
     */
    protected function createAttribute(string $databaseId, string $collectionId, string $type, array $payload): array
    {
        return $this->client->call(
            Client::METHOD_POST,
            $this->getSchemaUrl($databaseId, $collectionId) . '/' . $type,
            [
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey'],
            ],
            $payload
        );
    }


    /**
     * Setup: Create database and collections
     * Uses static caching to avoid recreating resources
     */
    protected function setupCollection(): array
    {
        $cacheKey = $this->getCacheKey();
        if (!empty(self::$collectionCache[$cacheKey])) {
            return self::$collectionCache[$cacheKey];
        }

        $data = $this->setupDatabase();
        $databaseId = $data['databaseId'];

        $movies = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Movies',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $this->assertEquals(201, $movies['headers']['status-code']);

        $actors = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Actors',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $this->assertEquals(201, $actors['headers']['status-code']);

        $books = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Books',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $this->assertEquals(201, $books['headers']['status-code']);

        self::$collectionCache[$cacheKey] = [
            'databaseId' => $databaseId,
            'moviesId' => $movies['body']['$id'],
            'actorsId' => $actors['body']['$id'],
            'booksId' => $books['body']['$id'],
        ];
        return self::$collectionCache[$cacheKey];
    }

    /**
     * Setup: Create database, collections, and attributes
     * Uses static caching to avoid recreating resources
     */
    protected function setupAttributes(): array
    {
        $cacheKey = $this->getCacheKey();
        if (!empty(self::$attributesCache[$cacheKey])) {
            return self::$attributesCache[$cacheKey];
        }

        $data = $this->setupCollection();
        $databaseId = $data['databaseId'];

        if (!$this->getSupportForAttributes()) {
            self::$attributesCache[$cacheKey] = $data;
            return self::$attributesCache[$cacheKey];
        }
        $title = $this->createAttribute($databaseId, $data['moviesId'], 'string', [
            'key' => 'title',
            'size' => 256,
            'required' => true,
        ]);

        $description = $this->createAttribute($databaseId, $data['moviesId'], 'string', [
            'key' => 'description',
            'size' => 512,
            'required' => false,
            'default' => '',
        ]);

        $tagline = $this->createAttribute($databaseId, $data['moviesId'], 'string', [
            'key' => 'tagline',
            'size' => 512,
            'required' => false,
            'default' => '',
        ]);

        $releaseYear = $this->createAttribute($databaseId, $data['moviesId'], 'integer', [
            'key' => 'releaseYear',
            'required' => true,
            'min' => 1900,
            'max' => 2200,
        ]);

        $duration = $this->createAttribute($databaseId, $data['moviesId'], 'integer', [
            'key' => 'duration',
            'required' => false,
            'min' => 60,
        ]);

        $actors = $this->createAttribute($databaseId, $data['moviesId'], 'string', [
            'key' => 'actors',
            'size' => 256,
            'required' => false,
            'array' => true,
        ]);

        $datetime = $this->createAttribute($databaseId, $data['moviesId'], 'datetime', [
            'key' => 'birthDay',
            'required' => false,
        ]);

        $relationship = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/relationship', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getRelatedIdParam() => $data['actorsId'],
            'type' => 'oneToMany',
            'twoWay' => true,
            'key' => 'starringActors',
            'twoWayKey' => 'movie'
        ]);

        $integers = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/integer', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'integers',
            'required' => false,
            'array' => true,
            'min' => 10,
            'max' => 99,
        ]);

        $this->assertEquals(202, $title['headers']['status-code']);
        $this->assertEquals(202, $description['headers']['status-code']);
        $this->assertEquals(202, $tagline['headers']['status-code']);
        $this->assertEquals(202, $releaseYear['headers']['status-code']);
        $this->assertEquals(202, $duration['headers']['status-code']);
        $this->assertEquals(202, $actors['headers']['status-code']);
        $this->assertEquals(202, $datetime['headers']['status-code']);
        $this->assertEquals(202, $relationship['headers']['status-code']);
        $this->assertEquals(202, $integers['headers']['status-code']);

        // Books collection attributes (for fulltext search tests)
        $bookTitle = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['booksId']) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'title',
            'size' => 256,
            'required' => true,
        ]);

        $bookDescription = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['booksId']) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'description',
            'size' => 2048,
            'required' => true,
        ]);

        $this->assertEquals(202, $bookTitle['headers']['status-code']);
        $this->assertEquals(202, $bookDescription['headers']['status-code']);

        // Cache before waiting so that if waitForAllAttributes times out,
        // subsequent calls don't try to re-create the same attributes (causing 409)
        self::$attributesCache[$cacheKey] = $data;

        // wait for database worker to create attributes
        $this->waitForAllAttributes($databaseId, $data['moviesId']);
        $this->waitForAllAttributes($databaseId, $data['booksId']);

        return self::$attributesCache[$cacheKey];
    }

    /**
     * Setup: Create database, collections, attributes, and indexes
     * Uses static caching to avoid recreating resources
     */
    protected function setupIndexes(): array
    {
        $cacheKey = $this->getCacheKey();
        if (!empty(self::$indexesCache[$cacheKey])) {
            return self::$indexesCache[$cacheKey];
        }

        $data = $this->setupAttributes();
        $databaseId = $data['databaseId'];

        $titleIndex = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'titleIndex',
            'type' => 'fulltext',
            $this->getIndexAttributesParam() => ['title'],
        ]);

        $releaseYearIndex = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'releaseYear',
            'type' => 'key',
            $this->getIndexAttributesParam() => ['releaseYear'],
        ]);

        $releaseWithDate1 = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'releaseYearDated',
            'type' => 'key',
            $this->getIndexAttributesParam() => ['releaseYear', '$createdAt', '$updatedAt'],
        ]);

        $releaseWithDate2 = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'birthDay',
            'type' => 'key',
            $this->getIndexAttributesParam() => ['birthDay'],
        ]);

        // Fulltext index on Books.description (for testNotSearch)
        $booksFtsIndex = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $data['booksId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'fts_description',
            'type' => Database::INDEX_FULLTEXT,
            $this->getIndexAttributesParam() => ['description'],
        ]);

        $this->assertEquals(202, $titleIndex['headers']['status-code']);
        $this->assertEquals(202, $releaseYearIndex['headers']['status-code']);
        $this->assertEquals(202, $releaseWithDate1['headers']['status-code']);
        $this->assertEquals(202, $releaseWithDate2['headers']['status-code']);
        $this->assertEquals(202, $booksFtsIndex['headers']['status-code']);

        // Cache before waiting so that if waitForAllIndexes times out,
        // subsequent calls don't try to re-create the same indexes (causing 409)
        self::$indexesCache[$cacheKey] = $data;

        $this->waitForAllIndexes($databaseId, $data['moviesId']);
        $this->waitForAllIndexes($databaseId, $data['booksId']);

        return self::$indexesCache[$cacheKey];
    }

    /**
     * Setup: Create database, collections, attributes, indexes, and documents
     * Uses static caching to avoid recreating resources
     */
    protected function setupDocuments(): array
    {
        $cacheKey = $this->getCacheKey();
        if (!empty(self::$documentsCache[$cacheKey])) {
            return self::$documentsCache[$cacheKey];
        }

        $data = $this->setupIndexes();
        $databaseId = $data['databaseId'];

        $document1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Captain America',
                'releaseYear' => 1944,
                'birthDay' => '1975-06-12 14:12:55+02:00',
                'actors' => [
                    'Chris Evans',
                    'Samuel Jackson',
                ]
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ]
        ]);

        $document2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Spider-Man: Far From Home',
                'releaseYear' => 2019,
                'birthDay' => null,
                'actors' => [
                    'Tom Holland',
                    'Zendaya Maree Stoermer',
                    'Samuel Jackson',
                ],
                'integers' => [50, 60]
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ]
        ]);

        $document3 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Spider-Man: Homecoming',
                'releaseYear' => 2017,
                'birthDay' => '1975-06-12 14:12:55 America/New_York',
                'duration' => 65,
                'actors' => [
                    'Tom Holland',
                    'Zendaya Maree Stoermer',
                ],
                'integers' => [50]
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ]
        ]);

        $this->assertEquals(201, $document1['headers']['status-code']);
        $this->assertEquals(201, $document2['headers']['status-code']);
        $this->assertEquals(201, $document3['headers']['status-code']);

        $data['documentIds'] = [
            $document1['body']['$id'],
            $document2['body']['$id'],
            $document3['body']['$id'],
        ];

        self::$documentsCache[$cacheKey] = $data;
        return self::$documentsCache[$cacheKey];
    }

    /**
     * Setup: Create one-to-one relationship collections
     * Uses static caching to avoid recreating resources
     */
    protected function setupOneToOneRelationship(): array
    {
        $cacheKey = $this->getCacheKey();
        if (!empty(self::$oneToOneCache[$cacheKey])) {
            return self::$oneToOneCache[$cacheKey];
        }

        $data = $this->setupDatabase();
        $databaseId = $data['databaseId'];

        $person = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'person',
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
            $this->getSecurityParam() => true,
        ]);

        $this->assertEquals(201, $person['headers']['status-code']);

        $library = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'library',
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
            $this->getSecurityParam() => true,
        ]);

        $this->assertEquals(201, $library['headers']['status-code']);

        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $person['body']['$id']) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'fullName',
            'size' => 255,
            'required' => false,
        ]);

        $this->waitForAttribute($databaseId, $person['body']['$id'], 'fullName');

        $libraryName = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $library['body']['$id']) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'libraryName',
            'size' => 255,
            'required' => true,
        ]);

        $this->waitForAttribute($databaseId, $library['body']['$id'], 'libraryName');
        $this->assertEquals(202, $libraryName['headers']['status-code']);

        self::$oneToOneCache[$cacheKey] = [
            'databaseId' => $databaseId,
            'personCollection' => $person['body']['$id'],
            'libraryCollection' => $library['body']['$id'],
        ];
        return self::$oneToOneCache[$cacheKey];
    }

    /**
     * Setup: Create one-to-many relationship collections (extends one-to-one)
     * Uses static caching to avoid recreating resources
     */
    protected function setupOneToManyRelationship(): array
    {
        $cacheKey = $this->getCacheKey();
        if (!empty(self::$oneToManyCache[$cacheKey])) {
            return self::$oneToManyCache[$cacheKey];
        }

        $data = $this->setupOneToOneRelationship();
        $databaseId = $data['databaseId'];
        $personCollection = $data['personCollection'];
        $libraryCollection = $data['libraryCollection'];

        // One person can own several libraries
        $relation = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $personCollection) . '/relationship', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getRelatedIdParam() => $libraryCollection,
            'type' => Database::RELATION_ONE_TO_MANY,
            'twoWay' => true,
            'key' => 'libraries',
            'twoWayKey' => 'person_one_to_many',
        ]);

        // Handle 409 if relationship already exists (possible race condition in parallel mode)
        if ($relation['headers']['status-code'] === 409) {
            // Relationship already exists, just wait for it to be available
        } else {
            $this->assertEquals(202, $relation['headers']['status-code'], 'Relationship creation failed: ' . \json_encode($relation['body'] ?? 'no body'));
        }

        // Wait for both the relationship attribute and its twoWayKey to be available
        $this->waitForAttribute($databaseId, $personCollection, 'libraries');
        $this->waitForAttribute($databaseId, $libraryCollection, 'person_one_to_many');

        $serverHeaders = [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey'],
        ];

        // Create a person with libraries
        $person = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $personCollection), $serverHeaders, [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'fullName' => 'Stevie Wonder',
                'libraries' => [
                    [
                        '$id' => ID::unique(),
                        '$permissions' => [
                            Permission::read(Role::any()),
                            Permission::update(Role::any()),
                            Permission::delete(Role::any()),
                        ],
                        'libraryName' => 'Library 10',
                    ],
                    [
                        '$id' => ID::unique(),
                        '$permissions' => [
                            Permission::read(Role::any()),
                            Permission::update(Role::any()),
                            Permission::delete(Role::any()),
                        ],
                        'libraryName' => 'Library 11',
                    ]
                ],
            ],
            'permissions' => [
                Permission::read(Role::any()),
                Permission::update(Role::any()),
                Permission::delete(Role::any()),
            ]
        ]);

        $this->assertEquals(201, $person['headers']['status-code'], 'Person with libraries creation failed: ' . \json_encode($person['body'] ?? 'no body'));

        // Create two person documents with null fullName for isNull query testing
        $nullPerson1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $personCollection), $serverHeaders, [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'fullName' => null,
            ],
            'permissions' => [
                Permission::read(Role::any()),
                Permission::update(Role::any()),
                Permission::delete(Role::any()),
            ]
        ]);
        $this->assertEquals(201, $nullPerson1['headers']['status-code'], 'Null person 1 creation failed: ' . \json_encode($nullPerson1['body'] ?? 'no body'));

        $nullPerson2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $personCollection), $serverHeaders, [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'fullName' => null,
            ],
            'permissions' => [
                Permission::read(Role::any()),
                Permission::update(Role::any()),
                Permission::delete(Role::any()),
            ]
        ]);
        $this->assertEquals(201, $nullPerson2['headers']['status-code'], 'Null person 2 creation failed: ' . \json_encode($nullPerson2['body'] ?? 'no body'));

        // Update onDelete to cascade
        $this->client->call(Client::METHOD_PATCH, $this->getSchemaUrl($databaseId, $personCollection, 'relationship', 'libraries'), $serverHeaders, [
            'onDelete' => Database::RELATION_MUTATE_CASCADE,
        ]);

        self::$oneToManyCache[$cacheKey] = ['databaseId' => $databaseId, 'personCollection' => $personCollection, 'libraryCollection' => $libraryCollection];
        return self::$oneToManyCache[$cacheKey];
    }

    /**
     * Setup: Insert fulltext search test documents into the cached Books collection.
     * Uses static caching to avoid inserting duplicate documents when multiple
     * test classes share the same worker process in ParaTest --functional mode.
     */
    protected function setupFulltextSearchDocuments(): array
    {
        $cacheKey = $this->getCacheKey();
        if (!empty(self::$fulltextDocsCache[$cacheKey])) {
            return self::$fulltextDocsCache[$cacheKey];
        }

        $data = $this->setupIndexes();
        $databaseId = $data['databaseId'];
        $booksId = $data['booksId'];

        $row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $booksId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Science Fiction Adventures',
                'description' => 'A thrilling journey through space and time',
            ],
            'permissions' => [
                Permission::read(Role::any()),
            ]
        ]);
        $this->assertEquals(201, $row1['headers']['status-code']);

        $row2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $booksId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Romance Novel',
                'description' => 'A love story set in modern times',
            ],
            'permissions' => [
                Permission::read(Role::any()),
            ]
        ]);
        $this->assertEquals(201, $row2['headers']['status-code']);

        $row3 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $booksId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Mystery Thriller',
                'description' => 'A detective solves complex crimes',
            ],
            'permissions' => [
                Permission::read(Role::any()),
            ]
        ]);
        $this->assertEquals(201, $row3['headers']['status-code']);

        self::$fulltextDocsCache[$cacheKey] = $data;
        return self::$fulltextDocsCache[$cacheKey];
    }

    /**
     * Helper: Get list of documents (for tests that need document data)
     */
    protected function getDocumentsList(): array
    {
        $data = $this->setupDocuments();
        $databaseId = $data['databaseId'];

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::orderDesc('releaseYear')->toString(),
            ],
        ]);

        return [$this->getRecordResource() => $documents['body'][$this->getRecordResource()], 'databaseId' => $databaseId];
    }

    public function testCreateDatabase(): void
    {
        /**
         * Test for SUCCESS
         */
        $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'Test Database'
        ]);

        $this->assertNotEmpty($database['body']['$id']);
        $this->assertEquals(201, $database['headers']['status-code']);
        $this->assertEquals('Test Database', $database['body']['name']);
        $this->assertEquals($this->getDatabaseType(), $database['body']['type']);
    }

    public function testCreateCollection(): void
    {
        $data = $this->setupDatabase();
        $databaseId = $data['databaseId'];
        /**
         * Test for SUCCESS
         */
        $movies = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Movies',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $this->assertEquals(201, $movies['headers']['status-code']);
        $this->assertEquals($movies['body']['name'], 'Movies');
        $this->assertArrayHasKey('bytesMax', $movies['body']);
        $this->assertArrayHasKey('bytesUsed', $movies['body']);
        $this->assertIsInt($movies['body']['bytesMax']);
        $this->assertIsInt($movies['body']['bytesUsed']);
        $this->assertGreaterThanOrEqual(0, $movies['body']['bytesMax']);
        $this->assertGreaterThanOrEqual(0, $movies['body']['bytesUsed']);

        $actors = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Actors',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $this->assertEquals(201, $actors['headers']['status-code']);
        $this->assertEquals($actors['body']['name'], 'Actors');
    }

    public function testConsoleProject(): void
    {
        if ($this->getSide() === 'server') {
            // Server side can't get past the invalid key check anyway
            $this->expectNotToPerformAssertions();
            return;
        }

        $data = $this->setupCollection();

        $response = $this->client->call(
            Client::METHOD_GET,
            $this->getApiBasePath() . '/console/' . $this->getContainerResource() . '/' . $data['moviesId'] . '/' . $this->getRecordResource(),
            array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => 'console',
            ], $this->getHeaders())
        );

        // Access should be denied - 401 (forbidden) or 400 (invalid request) are both acceptable
        $this->assertContains($response['headers']['status-code'], [400, 401], 'Console project access should be denied');
        if ($response['headers']['status-code'] === 401) {
            $this->assertEquals('general_access_forbidden', $response['body']['type']);
            $this->assertEquals('This endpoint is not available for the console project. The Appwrite Console is a reserved project ID and cannot be used with the Appwrite SDKs and APIs. Please check if your project ID is correct.', $response['body']['message']);
        }

        $response = $this->client->call(
            Client::METHOD_GET,
            $this->getApiBasePath() . '/console/' . $this->getContainerResource() . '/' . $data['moviesId'] . '/' . $this->getRecordResource(),
            array_merge([
                'content-type' => 'application/json',
                // 'x-appwrite-project' => '', empty header
            ], $this->getHeaders())
        );
        // Request without project should be denied
        $this->assertContains($response['headers']['status-code'], [400, 401], 'Request without project should be denied');
        if ($response['headers']['status-code'] === 401) {
            $this->assertEquals('No Appwrite project was specified. Please specify your project ID when initializing your Appwrite SDK.', $response['body']['message']);
        }
    }

    public function testDisableCollection(): void
    {
        $data = $this->setupCollection();
        $databaseId = $data['databaseId'];
        /**
         * Test for SUCCESS
         */
        $response = $this->client->call(Client::METHOD_PUT, $this->getContainerUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'name' => 'Movies',
            'enabled' => false,
            $this->getSecurityParam() => true,
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertFalse($response['body']['enabled']);
        $this->assertArrayHasKey('bytesMax', $response['body']);
        $this->assertArrayHasKey('bytesUsed', $response['body']);

        if ($this->getSide() === 'client') {
            $responseCreateDocument = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()), [
                $this->getRecordIdParam() => ID::unique(),
                'data' => [
                    'title' => 'Captain America',
                ],
                'permissions' => [
                    Permission::read(Role::user($this->getUser()['$id'])),
                    Permission::update(Role::user($this->getUser()['$id'])),
                    Permission::delete(Role::user($this->getUser()['$id'])),
                ],
            ]);

            $this->assertEquals(404, $responseCreateDocument['headers']['status-code']);

            $responseListDocument = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()));

            $this->assertEquals(404, $responseListDocument['headers']['status-code']);

            $responseGetDocument = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $data['moviesId']) . '/' . $this->getRecordResource() . '/someID', array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()));

            $this->assertEquals(404, $responseGetDocument['headers']['status-code']);
        }

        $response = $this->client->call(Client::METHOD_PUT, $this->getContainerUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'name' => 'Movies',
            'enabled' => true,
            $this->getSecurityParam() => true,
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertTrue($response['body']['enabled']);
    }

    public function testCreateAttributes(): void
    {
        if (!$this->getSupportForAttributes()) {
            $this->markTestSkipped('Attributes are not supported by this database adapter');
        }
        // Use dedicated collections for this test to avoid conflicts with setupAttributes()
        $data = $this->setupDatabase();
        $databaseId = $data['databaseId'];

        // Create dedicated collections for attribute testing (separate from shared collections)
        $movies = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'AttribTestMovies',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
        ]);
        $this->assertEquals(201, $movies['headers']['status-code']);
        $moviesId = $movies['body']['$id'];

        $actors = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'AttribTestActors',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
        ]);
        $this->assertEquals(201, $actors['headers']['status-code']);
        $actorsId = $actors['body']['$id'];

        $title = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $moviesId) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'title',
            'size' => 256,
            'required' => true,
        ]);

        $description = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $moviesId) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'description',
            'size' => 500,
            'required' => false,
            'default' => '',
        ]);

        $tagline = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $moviesId) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'tagline',
            'size' => 600,
            'required' => false,
            'default' => '',
        ]);

        $releaseYear = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $moviesId) . '/integer', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'releaseYear',
            'required' => true,
            'min' => 1900,
            'max' => 2200,
        ]);

        $duration = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $moviesId) . '/integer', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'duration',
            'required' => false,
            'min' => 60,
        ]);

        $actorsAttr = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $moviesId) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'actors',
            'size' => 256,
            'required' => false,
            'array' => true,
        ]);

        $datetime = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $moviesId) . '/datetime', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'birthDay',
            'required' => false,
        ]);

        $relationship = null;
        if ($this->getSupportForRelationships()) {
            $relationship = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $moviesId) . '/relationship', array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                $this->getRelatedIdParam() => $actorsId,
                'type' => 'oneToMany',
                'twoWay' => true,
                'key' => 'starringActors',
                'twoWayKey' => 'movie'
            ]);
        }

        $integers = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $moviesId) . '/integer', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'integers',
            'required' => false,
            'array' => true,
            'min' => 10,
            'max' => 99,
        ]);

        $this->assertEquals(202, $title['headers']['status-code']);
        $this->assertEquals($title['body']['key'], 'title');
        $this->assertEquals($title['body']['type'], 'string');
        $this->assertEquals($title['body']['size'], 256);
        $this->assertEquals($title['body']['required'], true);
        $this->assertFalse($title['body']['encrypt']);
        $this->assertEquals(202, $description['headers']['status-code']);
        $this->assertEquals($description['body']['key'], 'description');
        $this->assertEquals($description['body']['type'], 'string');
        $this->assertEquals($description['body']['required'], false);
        $this->assertEquals($description['body']['default'], '');

        $this->assertEquals(202, $tagline['headers']['status-code']);
        $this->assertEquals($tagline['body']['key'], 'tagline');
        $this->assertEquals($tagline['body']['type'], 'string');
        $this->assertEquals($tagline['body']['required'], false);
        $this->assertEquals($tagline['body']['default'], '');

        $this->assertEquals(202, $releaseYear['headers']['status-code']);
        $this->assertEquals($releaseYear['body']['key'], 'releaseYear');
        $this->assertEquals($releaseYear['body']['type'], 'integer');
        $this->assertEquals($releaseYear['body']['required'], true);

        $this->assertEquals(202, $duration['headers']['status-code']);
        $this->assertEquals($duration['body']['key'], 'duration');
        $this->assertEquals($duration['body']['type'], 'integer');
        $this->assertEquals($duration['body']['required'], false);

        $this->assertEquals(202, $actorsAttr['headers']['status-code']);
        $this->assertEquals($actorsAttr['body']['key'], 'actors');
        $this->assertEquals($actorsAttr['body']['type'], 'string');
        $this->assertEquals($actorsAttr['body']['size'], 256);
        $this->assertEquals($actorsAttr['body']['required'], false);
        $this->assertEquals($actorsAttr['body']['array'], true);

        $this->assertEquals($datetime['headers']['status-code'], 202);
        $this->assertEquals($datetime['body']['key'], 'birthDay');
        $this->assertEquals($datetime['body']['type'], 'datetime');
        $this->assertEquals($datetime['body']['required'], false);

        // to meet mongodb duplicate attributes index limit
        $integers2 = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $moviesId) . '/integer', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'integers2',
            'required' => false,
            'array' => true,
            'min' => 10,
            'max' => 99,
        ]);


        $this->assertEquals($description['body']['size'], 500);
        $this->assertEquals($tagline['body']['size'], 600);
        if ($this->getSupportForRelationships()) {
            $this->assertEquals($relationship['headers']['status-code'], 202);
            $this->assertEquals($relationship['body']['key'], 'starringActors');
            $this->assertEquals($relationship['body']['type'], 'relationship');
            $this->assertEquals($relationship['body'][$this->getRelatedResourceKey()], $actorsId);
            $this->assertEquals($relationship['body']['relationType'], 'oneToMany');
            $this->assertEquals($relationship['body']['twoWay'], true);
            $this->assertEquals($relationship['body']['twoWayKey'], 'movie');
        }

        $this->assertEquals(202, $integers['headers']['status-code']);
        $this->assertEquals($integers['body']['key'], 'integers');
        $this->assertEquals($integers['body']['type'], 'integer');
        $this->assertArrayNotHasKey('size', $integers['body']);
        $this->assertEquals($integers['body']['required'], false);
        $this->assertEquals($integers['body']['array'], true);

        $this->assertEquals(202, $integers2['headers']['status-code']);
        $this->assertEquals($integers2['body']['key'], 'integers2');
        $this->assertEquals($integers2['body']['type'], 'integer');
        $this->assertArrayNotHasKey('size', $integers2['body']);
        $this->assertEquals($integers2['body']['required'], false);
        $this->assertEquals($integers2['body']['array'], true);

        // wait for database worker to create attributes
        $this->waitForAllAttributes($databaseId, $moviesId);

        $movies = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $moviesId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $schemaResource = $this->getSchemaResource();
        $this->assertIsArray($movies['body'][$schemaResource]);
        $this->assertCount($this->getSupportForRelationships() ? 10 : 9, $movies['body'][$schemaResource]);
        $this->assertArrayHasKey('bytesMax', $movies['body']);
        $this->assertArrayHasKey('bytesUsed', $movies['body']);
        $this->assertGreaterThanOrEqual(0, $movies['body']['bytesUsed']);
        $this->assertEquals($movies['body'][$schemaResource][0]['key'], $title['body']['key']);
        $this->assertEquals($movies['body'][$schemaResource][1]['key'], $description['body']['key']);
        $this->assertEquals($movies['body'][$schemaResource][2]['key'], $tagline['body']['key']);
        $this->assertEquals($movies['body'][$schemaResource][3]['key'], $releaseYear['body']['key']);
        $this->assertEquals($movies['body'][$schemaResource][4]['key'], $duration['body']['key']);
        $this->assertEquals($movies['body'][$schemaResource][5]['key'], $actorsAttr['body']['key']);
        $this->assertEquals($movies['body'][$schemaResource][6]['key'], $datetime['body']['key']);
        if (!$this->getSupportForRelationships()) {
            $this->assertEquals($movies['body'][$schemaResource][7]['key'], $integers['body']['key']);
            $this->assertEquals($movies['body'][$schemaResource][8]['key'], $integers2['body']['key']);
        } else {
            $this->assertEquals($movies['body'][$schemaResource][7]['key'], $relationship['body']['key']);
            $this->assertEquals($movies['body'][$schemaResource][8]['key'], $integers['body']['key']);
            $this->assertEquals($movies['body'][$schemaResource][9]['key'], $integers2['body']['key']);
        }
    }

    public function testListAttributes(): void
    {
        if (!$this->getSupportForAttributes()) {
            $this->markTestSkipped('Attributes are not supported by this database adapter');
        }
        $data = $this->setupAttributes();
        $databaseId = $data['databaseId'];
        $response = $this->client->call(Client::METHOD_GET, $this->getSchemaUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey'],
        ]), [
            'queries' => [
                Query::equal('type', ['string'])->toString(),
                Query::limit(2)->toString(),
                Query::cursorAfter(new Document(['$id' => 'title']))->toString()
            ],
        ]);
        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals(2, \count($response['body'][$this->getSchemaResource()]));
        $response = $this->client->call(Client::METHOD_GET, $this->getSchemaUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey'],
        ]), [
            'queries' => [Query::select(['key'])->toString()],
        ]);
        $this->assertEquals(Exception::GENERAL_ARGUMENT_INVALID, $response['body']['type']);
        $this->assertEquals(400, $response['headers']['status-code']);
    }

    public function testPatchAttribute(): void
    {
        if (!$this->getSupportForAttributes()) {
            $this->markTestSkipped('Attributes are not supported by this database adapter');
        }
        $data = $this->setupDatabase();
        $databaseId = $data['databaseId'];

        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'patch',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $this->assertEquals(201, $collection['headers']['status-code']);
        $this->assertEquals($collection['body']['name'], 'patch');

        $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collection['body']['$id']) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'title',
            'required' => true,
            'size' => 100,
        ]);
        $this->assertEquals(202, $attribute['headers']['status-code']);
        $this->assertEquals($attribute['body']['size'], 100);

        $this->waitForAttribute($databaseId, $collection['body']['$id'], 'title');

        $index = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collection['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'titleIndex',
            'type' => 'key',
            $this->getIndexAttributesParam() => ['title'],
        ]);
        $this->assertEquals(202, $index['headers']['status-code']);

        $this->waitForIndex($databaseId, $collection['body']['$id'], 'titleIndex');

        /**
         * Update attribute size to exceed Index maximum length
         */
        $attribute = $this->client->call(Client::METHOD_PATCH, $this->getSchemaUrl($databaseId, $collection['body']['$id']) . '/string/'.$attribute['body']['key'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey'],
        ]), [
            'required' => true,
            'default' => null,
            'size' => 2000, // updated to exceed index maximum length also for mongodb
        ]);

        $this->assertEquals(400, $attribute['headers']['status-code']);
        $this->assertStringContainsString('Index length is longer than the maximum:', $attribute['body']['message']);
    }

    public function testUpdateAttributeEnum(): void
    {
        if (!$this->getSupportForAttributes()) {
            $this->markTestSkipped('Attributes are not supported by this database adapter');
        }
        $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'Test Database 2'
        ]);

        $players = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($database['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Players',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
        ]);

        // Create enum attribute
        $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($database['body']['$id'], $players['body']['$id']) . '/enum', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey'],
        ]), [
            'key' => 'position',
            'elements' => ['goalkeeper', 'defender', 'midfielder', 'forward'],
            'required' => true,
            'array' => false,
        ]);

        $this->assertEquals(202, $attribute['headers']['status-code']);
        $this->assertEquals($attribute['body']['key'], 'position');
        $this->assertEquals($attribute['body']['elements'], ['goalkeeper', 'defender', 'midfielder', 'forward']);

        $this->waitForAttribute($database['body']['$id'], $players['body']['$id'], 'position');

        // Update enum attribute
        $attribute = $this->client->call(Client::METHOD_PATCH, $this->getSchemaUrl($database['body']['$id'], $players['body']['$id']) . '/enum/' . $attribute['body']['key'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey'],
        ]), [
            'elements' => ['goalkeeper', 'defender', 'midfielder', 'forward', 'coach'],
            'required' => true,
            'default' => null
        ]);

        $this->assertEquals(200, $attribute['headers']['status-code']);
        $this->assertEquals($attribute['body']['elements'], ['goalkeeper', 'defender', 'midfielder', 'forward', 'coach']);
    }

    public function testAttributeResponseModels(): void
    {
        if (!$this->getSupportForAttributes()) {
            $this->markTestSkipped('Attributes are not supported by this database adapter');
        }
        $data = $this->setupAttributes();
        $databaseId = $data['databaseId'];
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Response Models',
            // 'permissions' missing on purpose to make sure it's optional
            $this->getSecurityParam() => true,
        ]);

        $this->assertEquals(201, $collection['headers']['status-code']);
        $this->assertEquals($collection['body']['name'], 'Response Models');

        $collectionId = $collection['body']['$id'];

        $attributesPath = $this->getSchemaUrl($databaseId, $collectionId);

        $string = $this->client->call(Client::METHOD_POST, $attributesPath . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'string',
            'size' => 16,
            'required' => false,
            'default' => 'default',
        ]);

        $email = $this->client->call(Client::METHOD_POST, $attributesPath . '/email', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'email',
            'required' => false,
            'default' => 'default@example.com',
        ]);

        $enum = $this->client->call(Client::METHOD_POST, $attributesPath . '/enum', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'enum',
            'elements' => ['yes', 'no', 'maybe'],
            'required' => false,
            'default' => 'maybe',
        ]);

        $ip = $this->client->call(Client::METHOD_POST, $attributesPath . '/ip', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'ip',
            'required' => false,
            'default' => '192.0.2.0',
        ]);

        $url = $this->client->call(Client::METHOD_POST, $attributesPath . '/url', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'url',
            'required' => false,
            'default' => 'http://example.com',
        ]);

        $integer = $this->client->call(Client::METHOD_POST, $attributesPath . '/integer', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'integer',
            'required' => false,
            'min' => 1,
            'max' => 5,
            'default' => 3
        ]);

        $float = $this->client->call(Client::METHOD_POST, $attributesPath . '/float', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'float',
            'required' => false,
            'min' => 1.5,
            'max' => 5.5,
            'default' => 3.5
        ]);

        $boolean = $this->client->call(Client::METHOD_POST, $attributesPath . '/boolean', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'boolean',
            'required' => false,
            'default' => true,
        ]);

        $datetime = $this->client->call(Client::METHOD_POST, $attributesPath . '/datetime', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'datetime',
            'required' => false,
            'default' => null,
        ]);

        $relationship = null;
        if ($this->getSupportForRelationships()) {
            $relationship = $this->client->call(Client::METHOD_POST, $attributesPath . '/relationship', array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                $this->getRelatedIdParam() => $data['actorsId'],
                'type' => 'oneToMany',
                'twoWay' => true,
                'key' => 'relationship',
                'twoWayKey' => 'twoWayKey'
            ]);
        }

        $strings = $this->client->call(Client::METHOD_POST, $attributesPath . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'names',
            'size' => 512,
            'required' => false,
            'array' => true,
        ]);

        $integers = $this->client->call(Client::METHOD_POST, $attributesPath . '/integer', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'numbers',
            'required' => false,
            'array' => true,
            'min' => 1,
            'max' => 999,
        ]);

        $this->assertEquals(202, $string['headers']['status-code']);
        $this->assertEquals('string', $string['body']['key']);
        $this->assertEquals('string', $string['body']['type']);
        $this->assertEquals(false, $string['body']['required']);
        $this->assertEquals(false, $string['body']['array']);
        $this->assertEquals(16, $string['body']['size']);
        $this->assertEquals('default', $string['body']['default']);

        $this->assertEquals(202, $email['headers']['status-code']);
        $this->assertEquals('email', $email['body']['key']);
        $this->assertEquals('string', $email['body']['type']);
        $this->assertEquals(false, $email['body']['required']);
        $this->assertEquals(false, $email['body']['array']);
        $this->assertEquals('email', $email['body']['format']);
        $this->assertEquals('default@example.com', $email['body']['default']);

        $this->assertEquals(202, $enum['headers']['status-code']);
        $this->assertEquals('enum', $enum['body']['key']);
        $this->assertEquals('string', $enum['body']['type']);
        $this->assertEquals(false, $enum['body']['required']);
        $this->assertEquals(false, $enum['body']['array']);
        $this->assertEquals('enum', $enum['body']['format']);
        $this->assertEquals('maybe', $enum['body']['default']);
        $this->assertIsArray($enum['body']['elements']);
        $this->assertEquals(['yes', 'no', 'maybe'], $enum['body']['elements']);

        $this->assertEquals(202, $ip['headers']['status-code']);
        $this->assertEquals('ip', $ip['body']['key']);
        $this->assertEquals('string', $ip['body']['type']);
        $this->assertEquals(false, $ip['body']['required']);
        $this->assertEquals(false, $ip['body']['array']);
        $this->assertEquals('ip', $ip['body']['format']);
        $this->assertEquals('192.0.2.0', $ip['body']['default']);

        $this->assertEquals(202, $url['headers']['status-code']);
        $this->assertEquals('url', $url['body']['key']);
        $this->assertEquals('string', $url['body']['type']);
        $this->assertEquals(false, $url['body']['required']);
        $this->assertEquals(false, $url['body']['array']);
        $this->assertEquals('url', $url['body']['format']);
        $this->assertEquals('http://example.com', $url['body']['default']);

        $this->assertEquals(202, $integer['headers']['status-code']);
        $this->assertEquals('integer', $integer['body']['key']);
        $this->assertEquals('integer', $integer['body']['type']);
        $this->assertEquals(false, $integer['body']['required']);
        $this->assertEquals(false, $integer['body']['array']);
        $this->assertEquals(1, $integer['body']['min']);
        $this->assertEquals(5, $integer['body']['max']);
        $this->assertEquals(3, $integer['body']['default']);

        $this->assertEquals(202, $float['headers']['status-code']);
        $this->assertEquals('float', $float['body']['key']);
        $this->assertEquals('double', $float['body']['type']);
        $this->assertEquals(false, $float['body']['required']);
        $this->assertEquals(false, $float['body']['array']);
        $this->assertEquals(1.5, $float['body']['min']);
        $this->assertEquals(5.5, $float['body']['max']);
        $this->assertEquals(3.5, $float['body']['default']);

        $this->assertEquals(202, $boolean['headers']['status-code']);
        $this->assertEquals('boolean', $boolean['body']['key']);
        $this->assertEquals('boolean', $boolean['body']['type']);
        $this->assertEquals(false, $boolean['body']['required']);
        $this->assertEquals(false, $boolean['body']['array']);
        $this->assertEquals(true, $boolean['body']['default']);

        $this->assertEquals(202, $datetime['headers']['status-code']);
        $this->assertEquals('datetime', $datetime['body']['key']);
        $this->assertEquals('datetime', $datetime['body']['type']);
        $this->assertEquals(false, $datetime['body']['required']);
        $this->assertEquals(false, $datetime['body']['array']);
        $this->assertEquals(null, $datetime['body']['default']);

        if ($this->getSupportForRelationships()) {
            $this->assertEquals(202, $relationship['headers']['status-code']);
            $this->assertEquals('relationship', $relationship['body']['key']);
            $this->assertEquals('relationship', $relationship['body']['type']);
            $this->assertEquals(false, $relationship['body']['required']);
            $this->assertEquals(false, $relationship['body']['array']);
            $this->assertEquals($data['actorsId'], $relationship['body'][$this->getRelatedResourceKey()]);
            $this->assertEquals('oneToMany', $relationship['body']['relationType']);
            $this->assertEquals(true, $relationship['body']['twoWay']);
            $this->assertEquals('twoWayKey', $relationship['body']['twoWayKey']);
        }

        $this->assertEquals(202, $strings['headers']['status-code']);
        $this->assertEquals('names', $strings['body']['key']);
        $this->assertEquals('string', $strings['body']['type']);
        $this->assertEquals(false, $strings['body']['required']);
        $this->assertEquals(true, $strings['body']['array']);
        $this->assertEquals(null, $strings['body']['default']);

        $this->assertEquals(202, $integers['headers']['status-code']);
        $this->assertEquals('numbers', $integers['body']['key']);
        $this->assertEquals('integer', $integers['body']['type']);
        $this->assertEquals(false, $integers['body']['required']);
        $this->assertEquals(true, $integers['body']['array']);
        $this->assertEquals(1, $integers['body']['min']);
        $this->assertEquals(999, $integers['body']['max']);
        $this->assertEquals(null, $integers['body']['default']);

        // Wait for database worker to create attributes
        $this->waitForAllAttributes($databaseId, $collectionId);

        $stringResponse = $this->client->call(Client::METHOD_GET, $attributesPath . '/' . $string['body']['key'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $emailResponse = $this->client->call(Client::METHOD_GET, $attributesPath . '/' . $email['body']['key'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $enumResponse = $this->client->call(Client::METHOD_GET, $attributesPath . '/' . $enum['body']['key'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $ipResponse = $this->client->call(Client::METHOD_GET, $attributesPath . '/' . $ip['body']['key'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $urlResponse = $this->client->call(Client::METHOD_GET, $attributesPath . '/' . $url['body']['key'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $integerResponse = $this->client->call(Client::METHOD_GET, $attributesPath . '/' . $integer['body']['key'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $floatResponse = $this->client->call(Client::METHOD_GET, $attributesPath . '/' . $float['body']['key'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $booleanResponse = $this->client->call(Client::METHOD_GET, $attributesPath . '/' . $boolean['body']['key'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $datetimeResponse = $this->client->call(Client::METHOD_GET, $attributesPath . '/' . $datetime['body']['key'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $relationshipResponse = null;
        if ($this->getSupportForRelationships()) {
            $relationshipResponse = $this->client->call(Client::METHOD_GET, $attributesPath . '/' . $relationship['body']['key'], array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]));
        }

        $stringsResponse = $this->client->call(Client::METHOD_GET, $attributesPath . '/' . $strings['body']['key'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $integersResponse = $this->client->call(Client::METHOD_GET, $attributesPath . '/' . $integers['body']['key'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->assertEquals(200, $stringResponse['headers']['status-code']);
        $this->assertEquals($string['body']['key'], $stringResponse['body']['key']);
        $this->assertEquals($string['body']['type'], $stringResponse['body']['type']);
        $this->assertEquals('available', $stringResponse['body']['status']);
        $this->assertEquals($string['body']['required'], $stringResponse['body']['required']);
        $this->assertEquals($string['body']['array'], $stringResponse['body']['array']);
        $this->assertEquals(16, $stringResponse['body']['size']);
        $this->assertEquals($string['body']['default'], $stringResponse['body']['default']);

        $this->assertEquals(200, $emailResponse['headers']['status-code']);
        $this->assertEquals($email['body']['key'], $emailResponse['body']['key']);
        $this->assertEquals($email['body']['type'], $emailResponse['body']['type']);
        $this->assertEquals('available', $emailResponse['body']['status']);
        $this->assertEquals($email['body']['required'], $emailResponse['body']['required']);
        $this->assertEquals($email['body']['array'], $emailResponse['body']['array']);
        $this->assertEquals($email['body']['format'], $emailResponse['body']['format']);
        $this->assertEquals($email['body']['default'], $emailResponse['body']['default']);

        $this->assertEquals(200, $enumResponse['headers']['status-code']);
        $this->assertEquals($enum['body']['key'], $enumResponse['body']['key']);
        $this->assertEquals($enum['body']['type'], $enumResponse['body']['type']);
        $this->assertEquals('available', $enumResponse['body']['status']);
        $this->assertEquals($enum['body']['required'], $enumResponse['body']['required']);
        $this->assertEquals($enum['body']['array'], $enumResponse['body']['array']);
        $this->assertEquals($enum['body']['format'], $enumResponse['body']['format']);
        $this->assertEquals($enum['body']['default'], $enumResponse['body']['default']);
        $this->assertEquals($enum['body']['elements'], $enumResponse['body']['elements']);

        $this->assertEquals(200, $ipResponse['headers']['status-code']);
        $this->assertEquals($ip['body']['key'], $ipResponse['body']['key']);
        $this->assertEquals($ip['body']['type'], $ipResponse['body']['type']);
        $this->assertEquals('available', $ipResponse['body']['status']);
        $this->assertEquals($ip['body']['required'], $ipResponse['body']['required']);
        $this->assertEquals($ip['body']['array'], $ipResponse['body']['array']);
        $this->assertEquals($ip['body']['format'], $ipResponse['body']['format']);
        $this->assertEquals($ip['body']['default'], $ipResponse['body']['default']);

        $this->assertEquals(200, $urlResponse['headers']['status-code']);
        $this->assertEquals($url['body']['key'], $urlResponse['body']['key']);
        $this->assertEquals($url['body']['type'], $urlResponse['body']['type']);
        $this->assertEquals('available', $urlResponse['body']['status']);
        $this->assertEquals($url['body']['required'], $urlResponse['body']['required']);
        $this->assertEquals($url['body']['array'], $urlResponse['body']['array']);
        $this->assertEquals($url['body']['format'], $urlResponse['body']['format']);
        $this->assertEquals($url['body']['default'], $urlResponse['body']['default']);

        $this->assertEquals(200, $integerResponse['headers']['status-code']);
        $this->assertEquals($integer['body']['key'], $integerResponse['body']['key']);
        $this->assertEquals($integer['body']['type'], $integerResponse['body']['type']);
        $this->assertEquals('available', $integerResponse['body']['status']);
        $this->assertEquals($integer['body']['required'], $integerResponse['body']['required']);
        $this->assertEquals($integer['body']['array'], $integerResponse['body']['array']);
        $this->assertEquals($integer['body']['min'], $integerResponse['body']['min']);
        $this->assertEquals($integer['body']['max'], $integerResponse['body']['max']);
        $this->assertEquals($integer['body']['default'], $integerResponse['body']['default']);

        $this->assertEquals(200, $floatResponse['headers']['status-code']);
        $this->assertEquals($float['body']['key'], $floatResponse['body']['key']);
        $this->assertEquals($float['body']['type'], $floatResponse['body']['type']);
        $this->assertEquals('available', $floatResponse['body']['status']);
        $this->assertEquals($float['body']['required'], $floatResponse['body']['required']);
        $this->assertEquals($float['body']['array'], $floatResponse['body']['array']);
        $this->assertEquals($float['body']['min'], $floatResponse['body']['min']);
        $this->assertEquals($float['body']['max'], $floatResponse['body']['max']);
        $this->assertEquals($float['body']['default'], $floatResponse['body']['default']);

        $this->assertEquals(200, $booleanResponse['headers']['status-code']);
        $this->assertEquals($boolean['body']['key'], $booleanResponse['body']['key']);
        $this->assertEquals($boolean['body']['type'], $booleanResponse['body']['type']);
        $this->assertEquals('available', $booleanResponse['body']['status']);
        $this->assertEquals($boolean['body']['required'], $booleanResponse['body']['required']);
        $this->assertEquals($boolean['body']['array'], $booleanResponse['body']['array']);
        $this->assertEquals($boolean['body']['default'], $booleanResponse['body']['default']);

        $this->assertEquals(200, $datetimeResponse['headers']['status-code']);
        $this->assertEquals($datetime['body']['key'], $datetimeResponse['body']['key']);
        $this->assertEquals($datetime['body']['type'], $datetimeResponse['body']['type']);
        $this->assertEquals('available', $datetimeResponse['body']['status']);
        $this->assertEquals($datetime['body']['required'], $datetimeResponse['body']['required']);
        $this->assertEquals($datetime['body']['array'], $datetimeResponse['body']['array']);
        $this->assertEquals($datetime['body']['default'], $datetimeResponse['body']['default']);

        if ($this->getSupportForRelationships()) {
            $this->assertEquals(200, $relationshipResponse['headers']['status-code']);
            $this->assertEquals($relationship['body']['key'], $relationshipResponse['body']['key']);
            $this->assertEquals($relationship['body']['type'], $relationshipResponse['body']['type']);
            $this->assertEquals('available', $relationshipResponse['body']['status']);
            $this->assertEquals($relationship['body']['required'], $relationshipResponse['body']['required']);
            $this->assertEquals($relationship['body']['array'], $relationshipResponse['body']['array']);
            $this->assertEquals($relationship['body'][$this->getRelatedResourceKey()], $relationshipResponse['body'][$this->getRelatedResourceKey()]);
            $this->assertEquals($relationship['body']['relationType'], $relationshipResponse['body']['relationType']);
            $this->assertEquals($relationship['body']['twoWay'], $relationshipResponse['body']['twoWay']);
            $this->assertEquals($relationship['body']['twoWayKey'], $relationshipResponse['body']['twoWayKey']);
        }

        $attributes = $this->client->call(Client::METHOD_GET, $this->getSchemaUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->assertEquals(200, $attributes['headers']['status-code']);
        $this->assertEquals($this->getSupportForRelationships() ? 12 : 11, $attributes['body']['total']);

        /**
         * Test for SUCCESS with total=false
         */
        $attributesWithIncludeTotalFalse = $this->client->call(Client::METHOD_GET, $this->getSchemaUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'total' => false
        ]);

        $this->assertEquals(200, $attributesWithIncludeTotalFalse['headers']['status-code']);
        $this->assertIsArray($attributesWithIncludeTotalFalse['body']);
        $this->assertIsArray($attributesWithIncludeTotalFalse['body'][$this->getSchemaResource()]);
        $this->assertIsInt($attributesWithIncludeTotalFalse['body']['total']);
        $this->assertEquals(0, $attributesWithIncludeTotalFalse['body']['total']);
        $this->assertGreaterThan(0, count($attributesWithIncludeTotalFalse['body'][$this->getSchemaResource()]));

        $attributes = $attributes['body'][$this->getSchemaResource()];
        $this->assertIsArray($attributes);

        $this->assertEquals($stringResponse['body']['key'], $attributes[0]['key']);
        $this->assertEquals($stringResponse['body']['type'], $attributes[0]['type']);
        $this->assertEquals($stringResponse['body']['status'], $attributes[0]['status']);
        $this->assertEquals($stringResponse['body']['required'], $attributes[0]['required']);
        $this->assertEquals($stringResponse['body']['array'], $attributes[0]['array']);
        $this->assertEquals($stringResponse['body']['size'], $attributes[0]['size']);
        $this->assertEquals($stringResponse['body']['default'], $attributes[0]['default']);

        $this->assertEquals($emailResponse['body']['key'], $attributes[1]['key']);
        $this->assertEquals($emailResponse['body']['type'], $attributes[1]['type']);
        $this->assertEquals($emailResponse['body']['status'], $attributes[1]['status']);
        $this->assertEquals($emailResponse['body']['required'], $attributes[1]['required']);
        $this->assertEquals($emailResponse['body']['array'], $attributes[1]['array']);
        $this->assertEquals($emailResponse['body']['default'], $attributes[1]['default']);
        $this->assertEquals($emailResponse['body']['format'], $attributes[1]['format']);

        $this->assertEquals($enumResponse['body']['key'], $attributes[2]['key']);
        $this->assertEquals($enumResponse['body']['type'], $attributes[2]['type']);
        $this->assertEquals($enumResponse['body']['status'], $attributes[2]['status']);
        $this->assertEquals($enumResponse['body']['required'], $attributes[2]['required']);
        $this->assertEquals($enumResponse['body']['array'], $attributes[2]['array']);
        $this->assertEquals($enumResponse['body']['default'], $attributes[2]['default']);
        $this->assertEquals($enumResponse['body']['format'], $attributes[2]['format']);
        $this->assertEquals($enumResponse['body']['elements'], $attributes[2]['elements']);

        $this->assertEquals($ipResponse['body']['key'], $attributes[3]['key']);
        $this->assertEquals($ipResponse['body']['type'], $attributes[3]['type']);
        $this->assertEquals($ipResponse['body']['status'], $attributes[3]['status']);
        $this->assertEquals($ipResponse['body']['required'], $attributes[3]['required']);
        $this->assertEquals($ipResponse['body']['array'], $attributes[3]['array']);
        $this->assertEquals($ipResponse['body']['default'], $attributes[3]['default']);
        $this->assertEquals($ipResponse['body']['format'], $attributes[3]['format']);

        $this->assertEquals($urlResponse['body']['key'], $attributes[4]['key']);
        $this->assertEquals($urlResponse['body']['type'], $attributes[4]['type']);
        $this->assertEquals($urlResponse['body']['status'], $attributes[4]['status']);
        $this->assertEquals($urlResponse['body']['required'], $attributes[4]['required']);
        $this->assertEquals($urlResponse['body']['array'], $attributes[4]['array']);
        $this->assertEquals($urlResponse['body']['default'], $attributes[4]['default']);
        $this->assertEquals($urlResponse['body']['format'], $attributes[4]['format']);

        $this->assertEquals($integerResponse['body']['key'], $attributes[5]['key']);
        $this->assertEquals($integerResponse['body']['type'], $attributes[5]['type']);
        $this->assertEquals($integerResponse['body']['status'], $attributes[5]['status']);
        $this->assertEquals($integerResponse['body']['required'], $attributes[5]['required']);
        $this->assertEquals($integerResponse['body']['array'], $attributes[5]['array']);
        $this->assertEquals($integerResponse['body']['default'], $attributes[5]['default']);
        $this->assertEquals($integerResponse['body']['min'], $attributes[5]['min']);
        $this->assertEquals($integerResponse['body']['max'], $attributes[5]['max']);

        $this->assertEquals($floatResponse['body']['key'], $attributes[6]['key']);
        $this->assertEquals($floatResponse['body']['type'], $attributes[6]['type']);
        $this->assertEquals($floatResponse['body']['status'], $attributes[6]['status']);
        $this->assertEquals($floatResponse['body']['required'], $attributes[6]['required']);
        $this->assertEquals($floatResponse['body']['array'], $attributes[6]['array']);
        $this->assertEquals($floatResponse['body']['default'], $attributes[6]['default']);
        $this->assertEquals($floatResponse['body']['min'], $attributes[6]['min']);
        $this->assertEquals($floatResponse['body']['max'], $attributes[6]['max']);

        $this->assertEquals($booleanResponse['body']['key'], $attributes[7]['key']);
        $this->assertEquals($booleanResponse['body']['type'], $attributes[7]['type']);
        $this->assertEquals($booleanResponse['body']['status'], $attributes[7]['status']);
        $this->assertEquals($booleanResponse['body']['required'], $attributes[7]['required']);
        $this->assertEquals($booleanResponse['body']['array'], $attributes[7]['array']);
        $this->assertEquals($booleanResponse['body']['default'], $attributes[7]['default']);

        $this->assertEquals($datetimeResponse['body']['key'], $attributes[8]['key']);
        $this->assertEquals($datetimeResponse['body']['type'], $attributes[8]['type']);
        $this->assertEquals($datetimeResponse['body']['status'], $attributes[8]['status']);
        $this->assertEquals($datetimeResponse['body']['required'], $attributes[8]['required']);
        $this->assertEquals($datetimeResponse['body']['array'], $attributes[8]['array']);
        $this->assertEquals($datetimeResponse['body']['default'], $attributes[8]['default']);

        $expectedCount = $this->getSupportForRelationships() ? 12 : 11;
        $this->assertCount($expectedCount, $attributes);
        // Relationship attribute assertions - only when relationships are supported
        $stringsIndex = 9;
        $integersIndex = 10;
        if ($this->getSupportForRelationships()) {
            $this->assertEquals($relationshipResponse['body']['key'], $attributes[9]['key']);
            $this->assertEquals($relationshipResponse['body']['type'], $attributes[9]['type']);
            $this->assertEquals($relationshipResponse['body']['status'], $attributes[9]['status']);
            $this->assertEquals($relationshipResponse['body']['required'], $attributes[9]['required']);
            $this->assertEquals($relationshipResponse['body']['array'], $attributes[9]['array']);
            $this->assertEquals($relationshipResponse['body'][$this->getRelatedResourceKey()], $attributes[9][$this->getRelatedResourceKey()]);
            $this->assertEquals($relationshipResponse['body']['relationType'], $attributes[9]['relationType']);
            $this->assertEquals($relationshipResponse['body']['twoWay'], $attributes[9]['twoWay']);
            $this->assertEquals($relationshipResponse['body']['twoWayKey'], $attributes[9]['twoWayKey']);
            $stringsIndex = 10;
            $integersIndex = 11;
        }

        $collection = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->assertEquals(200, $collection['headers']['status-code']);

        $attributes = $collection['body'][$this->getSchemaResource()];

        $this->assertIsArray($attributes);

        $this->assertEquals($stringResponse['body']['key'], $attributes[0]['key']);
        $this->assertEquals($stringResponse['body']['type'], $attributes[0]['type']);
        $this->assertEquals($stringResponse['body']['status'], $attributes[0]['status']);
        $this->assertEquals($stringResponse['body']['required'], $attributes[0]['required']);
        $this->assertEquals($stringResponse['body']['array'], $attributes[0]['array']);
        $this->assertEquals($stringResponse['body']['size'], $attributes[0]['size']);
        $this->assertEquals($stringResponse['body']['default'], $attributes[0]['default']);

        $this->assertEquals($emailResponse['body']['key'], $attributes[1]['key']);
        $this->assertEquals($emailResponse['body']['type'], $attributes[1]['type']);
        $this->assertEquals($emailResponse['body']['status'], $attributes[1]['status']);
        $this->assertEquals($emailResponse['body']['required'], $attributes[1]['required']);
        $this->assertEquals($emailResponse['body']['array'], $attributes[1]['array']);
        $this->assertEquals($emailResponse['body']['default'], $attributes[1]['default']);
        $this->assertEquals($emailResponse['body']['format'], $attributes[1]['format']);

        $this->assertEquals($enumResponse['body']['key'], $attributes[2]['key']);
        $this->assertEquals($enumResponse['body']['type'], $attributes[2]['type']);
        $this->assertEquals($enumResponse['body']['status'], $attributes[2]['status']);
        $this->assertEquals($enumResponse['body']['required'], $attributes[2]['required']);
        $this->assertEquals($enumResponse['body']['array'], $attributes[2]['array']);
        $this->assertEquals($enumResponse['body']['default'], $attributes[2]['default']);
        $this->assertEquals($enumResponse['body']['format'], $attributes[2]['format']);
        $this->assertEquals($enumResponse['body']['elements'], $attributes[2]['elements']);

        $this->assertEquals($ipResponse['body']['key'], $attributes[3]['key']);
        $this->assertEquals($ipResponse['body']['type'], $attributes[3]['type']);
        $this->assertEquals($ipResponse['body']['status'], $attributes[3]['status']);
        $this->assertEquals($ipResponse['body']['required'], $attributes[3]['required']);
        $this->assertEquals($ipResponse['body']['array'], $attributes[3]['array']);
        $this->assertEquals($ipResponse['body']['default'], $attributes[3]['default']);
        $this->assertEquals($ipResponse['body']['format'], $attributes[3]['format']);

        $this->assertEquals($urlResponse['body']['key'], $attributes[4]['key']);
        $this->assertEquals($urlResponse['body']['type'], $attributes[4]['type']);
        $this->assertEquals($urlResponse['body']['status'], $attributes[4]['status']);
        $this->assertEquals($urlResponse['body']['required'], $attributes[4]['required']);
        $this->assertEquals($urlResponse['body']['array'], $attributes[4]['array']);
        $this->assertEquals($urlResponse['body']['default'], $attributes[4]['default']);
        $this->assertEquals($urlResponse['body']['format'], $attributes[4]['format']);

        $this->assertEquals($integerResponse['body']['key'], $attributes[5]['key']);
        $this->assertEquals($integerResponse['body']['type'], $attributes[5]['type']);
        $this->assertEquals($integerResponse['body']['status'], $attributes[5]['status']);
        $this->assertEquals($integerResponse['body']['required'], $attributes[5]['required']);
        $this->assertEquals($integerResponse['body']['array'], $attributes[5]['array']);
        $this->assertEquals($integerResponse['body']['default'], $attributes[5]['default']);
        $this->assertEquals($integerResponse['body']['min'], $attributes[5]['min']);
        $this->assertEquals($integerResponse['body']['max'], $attributes[5]['max']);

        $this->assertEquals($floatResponse['body']['key'], $attributes[6]['key']);
        $this->assertEquals($floatResponse['body']['type'], $attributes[6]['type']);
        $this->assertEquals($floatResponse['body']['status'], $attributes[6]['status']);
        $this->assertEquals($floatResponse['body']['required'], $attributes[6]['required']);
        $this->assertEquals($floatResponse['body']['array'], $attributes[6]['array']);
        $this->assertEquals($floatResponse['body']['default'], $attributes[6]['default']);
        $this->assertEquals($floatResponse['body']['min'], $attributes[6]['min']);
        $this->assertEquals($floatResponse['body']['max'], $attributes[6]['max']);

        $this->assertEquals($booleanResponse['body']['key'], $attributes[7]['key']);
        $this->assertEquals($booleanResponse['body']['type'], $attributes[7]['type']);
        $this->assertEquals($booleanResponse['body']['status'], $attributes[7]['status']);
        $this->assertEquals($booleanResponse['body']['required'], $attributes[7]['required']);
        $this->assertEquals($booleanResponse['body']['array'], $attributes[7]['array']);
        $this->assertEquals($booleanResponse['body']['default'], $attributes[7]['default']);

        $this->assertEquals($datetimeResponse['body']['key'], $attributes[8]['key']);
        $this->assertEquals($datetimeResponse['body']['type'], $attributes[8]['type']);
        $this->assertEquals($datetimeResponse['body']['status'], $attributes[8]['status']);
        $this->assertEquals($datetimeResponse['body']['required'], $attributes[8]['required']);
        $this->assertEquals($datetimeResponse['body']['array'], $attributes[8]['array']);
        $this->assertEquals($datetimeResponse['body']['default'], $attributes[8]['default']);


        $this->assertEquals($stringsResponse['body']['key'], $attributes[$stringsIndex]['key']);
        $this->assertEquals($stringsResponse['body']['type'], $attributes[$stringsIndex]['type']);
        $this->assertEquals($stringsResponse['body']['status'], $attributes[$stringsIndex]['status']);
        $this->assertEquals($stringsResponse['body']['required'], $attributes[$stringsIndex]['required']);
        $this->assertEquals($stringsResponse['body']['array'], $attributes[$stringsIndex]['array']);
        $this->assertEquals($stringsResponse['body']['default'], $attributes[$stringsIndex]['default']);

        $this->assertEquals($integersResponse['body']['key'], $attributes[$integersIndex]['key']);
        $this->assertEquals($integersResponse['body']['type'], $attributes[$integersIndex]['type']);
        $this->assertEquals($integersResponse['body']['status'], $attributes[$integersIndex]['status']);
        $this->assertEquals($integersResponse['body']['required'], $attributes[$integersIndex]['required']);
        $this->assertEquals($integersResponse['body']['array'], $attributes[$integersIndex]['array']);
        $this->assertEquals($integersResponse['body']['default'], $attributes[$integersIndex]['default']);
        $this->assertEquals($integersResponse['body']['min'], $attributes[$integersIndex]['min']);
        $this->assertEquals($integersResponse['body']['max'], $attributes[$integersIndex]['max']);
        $expectedCount = $this->getSupportForRelationships() ? 12 : 11;
        $this->assertCount($expectedCount, $attributes);
        // Relationship attribute assertions - only when relationships are supported
        $stringsIndex = 9;
        $integersIndex = 10;
        if ($this->getSupportForRelationships()) {
            $this->assertEquals($relationshipResponse['body']['key'], $attributes[9]['key']);
            $this->assertEquals($relationshipResponse['body']['type'], $attributes[9]['type']);
            $this->assertEquals($relationshipResponse['body']['status'], $attributes[9]['status']);
            $this->assertEquals($relationshipResponse['body']['required'], $attributes[9]['required']);
            $this->assertEquals($relationshipResponse['body']['array'], $attributes[9]['array']);
            $this->assertEquals($relationshipResponse['body'][$this->getRelatedResourceKey()], $attributes[9][$this->getRelatedResourceKey()]);
            $this->assertEquals($relationshipResponse['body']['relationType'], $attributes[9]['relationType']);
            $this->assertEquals($relationshipResponse['body']['twoWay'], $attributes[9]['twoWay']);
            $this->assertEquals($relationshipResponse['body']['twoWayKey'], $attributes[9]['twoWayKey']);
            $stringsIndex = 10;
            $integersIndex = 11;
        }

        /**
         * Test for FAILURE
         */
        $badEnum = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/enum', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'enum',
            'elements' => ['yes', 'no', ''],
            'required' => false,
            'default' => 'maybe',
        ]);

        $this->assertEquals(400, $badEnum['headers']['status-code']);
        $this->assertEquals('Invalid `elements` param: Value must a valid array no longer than 100 items and Value must be a valid string and at least 1 chars and no longer than 255 chars', $badEnum['body']['message']);
    }

    public function testCreateIndexes(): void
    {
        // Use dedicated collection for index testing to avoid conflicts with setupIndexes()
        $data = $this->setupDatabase();
        $databaseId = $data['databaseId'];

        // Create dedicated collection for index testing
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'IndexTestCollection',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
        ]);
        $this->assertEquals(201, $collection['headers']['status-code']);
        $collectionId = $collection['body']['$id'];

        // Create attributes needed for index testing (only when supported).
        // DocumentsDB can still create indexes without a predefined schema.
        if ($this->getSupportForAttributes()) {
            $title = $this->createAttribute($databaseId, $collectionId, 'string', [
                'key' => 'title',
                'size' => 256,
                'required' => true,
            ]);
            $this->assertEquals(202, $title['headers']['status-code']);

            $description = $this->createAttribute($databaseId, $collectionId, 'string', [
                'key' => 'description',
                'size' => 512,
                'required' => false,
                'default' => '',
            ]);
            $this->assertEquals(202, $description['headers']['status-code']);

            $tagline = $this->createAttribute($databaseId, $collectionId, 'string', [
                'key' => 'tagline',
                'size' => 512,
                'required' => false,
                'default' => '',
            ]);
            $this->assertEquals(202, $tagline['headers']['status-code']);

            $releaseYear = $this->createAttribute($databaseId, $collectionId, 'integer', [
                'key' => 'releaseYear',
                'required' => true,
                'min' => 1900,
                'max' => 2200,
            ]);
            $this->assertEquals(202, $releaseYear['headers']['status-code']);

            $actors = $this->createAttribute($databaseId, $collectionId, 'string', [
                'key' => 'actors',
                'size' => 256,
                'required' => false,
                'array' => true,
            ]);
            $this->assertEquals(202, $actors['headers']['status-code']);

            $birthDay = $this->createAttribute($databaseId, $collectionId, 'datetime', [
                'key' => 'birthDay',
                'required' => false,
            ]);
            $this->assertEquals(202, $birthDay['headers']['status-code']);

            $integers = $this->createAttribute($databaseId, $collectionId, 'integer', [
                'key' => 'integers',
                'required' => false,
                'array' => true,
                'min' => 10,
                'max' => 99,
            ]);
            $this->assertEquals(202, $integers['headers']['status-code']);

            $integers2 = $this->createAttribute($databaseId, $collectionId, 'integer', [
                'key' => 'integers2',
                'required' => false,
                'array' => true,
                'min' => 10,
                'max' => 99,
            ]);
            $this->assertEquals(202, $integers2['headers']['status-code']);

            // Wait for attributes to be ready
            $this->waitForAllAttributes($databaseId, $collectionId);
        }

        $titleIndex = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'titleIndex',
            'type' => 'fulltext',
            $this->getIndexAttributesParam() => ['title'],
        ]);

        $this->assertEquals(202, $titleIndex['headers']['status-code']);
        $this->assertEquals('titleIndex', $titleIndex['body']['key']);
        $this->assertEquals('fulltext', $titleIndex['body']['type']);
        $this->assertCount(1, $titleIndex['body'][$this->getSchemaResource()]);
        $this->assertEquals('title', $titleIndex['body'][$this->getSchemaResource()][0]);

        $releaseYearIndex = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'releaseYear',
            'type' => 'key',
            $this->getIndexAttributesParam() => ['releaseYear'],
        ]);

        $this->assertEquals(202, $releaseYearIndex['headers']['status-code']);
        $this->assertEquals('releaseYear', $releaseYearIndex['body']['key']);
        $this->assertEquals('key', $releaseYearIndex['body']['type']);
        $this->assertCount(1, $releaseYearIndex['body'][$this->getSchemaResource()]);
        $this->assertEquals('releaseYear', $releaseYearIndex['body'][$this->getSchemaResource()][0]);

        $releaseWithDate1 = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'releaseYearDated',
            'type' => 'key',
            $this->getIndexAttributesParam() => ['releaseYear', '$createdAt', '$updatedAt'],
        ]);

        $this->assertEquals(202, $releaseWithDate1['headers']['status-code']);
        $this->assertEquals('releaseYearDated', $releaseWithDate1['body']['key']);
        $this->assertEquals('key', $releaseWithDate1['body']['type']);
        $this->assertCount(3, $releaseWithDate1['body'][$this->getSchemaResource()]);
        $this->assertEquals('releaseYear', $releaseWithDate1['body'][$this->getSchemaResource()][0]);
        $this->assertEquals('$createdAt', $releaseWithDate1['body'][$this->getSchemaResource()][1]);
        $this->assertEquals('$updatedAt', $releaseWithDate1['body'][$this->getSchemaResource()][2]);

        $releaseWithDate2 = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'birthDay',
            'type' => 'key',
            $this->getIndexAttributesParam() => ['birthDay'],
        ]);

        $this->assertEquals(202, $releaseWithDate2['headers']['status-code']);
        $this->assertEquals('birthDay', $releaseWithDate2['body']['key']);
        $this->assertEquals('key', $releaseWithDate2['body']['type']);
        $this->assertCount(1, $releaseWithDate2['body'][$this->getSchemaResource()]);
        $this->assertEquals('birthDay', $releaseWithDate2['body'][$this->getSchemaResource()][0]);

        // Test for failure
        $fulltextReleaseYear = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'releaseYearDated',
            'type' => 'fulltext',
            $this->getIndexAttributesParam() => ['releaseYear'],
        ]);

        $this->assertEquals(400, $fulltextReleaseYear['headers']['status-code']);

        $noAttributes = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'none',
            'type' => 'key',
            $this->getIndexAttributesParam() => [],
        ]);

        $this->assertEquals(400, $noAttributes['headers']['status-code']);
        $this->assertEquals($noAttributes['body']['message'], 'No attributes provided for index');

        $duplicates = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'duplicate',
            'type' => 'fulltext',
            $this->getIndexAttributesParam() => ['releaseYear', 'releaseYear'],
        ]);

        $this->assertEquals(400, $duplicates['headers']['status-code']);
        $this->assertEquals($duplicates['body']['message'], 'Duplicate attributes provided');

        $tooLong = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey'],
        ]), [
            'key' => 'tooLong',
            'type' => 'key',
            $this->getIndexAttributesParam() => ['description', 'tagline'],
        ]);

        // documentsdb isn't aware of the size so it will create
        if ($this->getSupportForAttributes()) {
            if ($this->getMaxIndexLength() < 1024) {
                // Only SQL-based adapters (MariaDB, PostgreSQL) enforce byte-level index length limits
                $this->assertEquals(400, $tooLong['headers']['status-code']);
                $this->assertStringContainsString('Index length is longer than the maximum', $tooLong['body']['message']);
            } else {
                // MongoDB (maxIndexLength=1024) doesn't exceed the limit with 512+512
                $this->assertEquals(202, $tooLong['headers']['status-code']);
            }
        }

        $fulltextArray = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey'],
        ]), [
            'key' => 'ft',
            'type' => 'fulltext',
            $this->getIndexAttributesParam() => ['actors'],
        ]);

        $this->assertEquals(400, $fulltextArray['headers']['status-code']);
        $errorMessage = $this->getSupportForAttributes() ? "Creating indexes on array attributes is not currently supported." : "There is already a fulltext index in the collection";
        $this->assertEquals($errorMessage, $fulltextArray['body']['message']);

        if ($this->getSupportForAttributes()) {

            $actorsArray = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey'],
            ]), [
                'key' => 'index-actors',
                'type' => 'key',
                $this->getIndexAttributesParam() => ['actors'],
            ]);

            $this->assertEquals(400, $actorsArray['headers']['status-code']);
            $this->assertEquals('Creating indexes on array attributes is not currently supported.', $actorsArray['body']['message']);

            $twoLevelsArray = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey'],
            ]), [
                'key' => 'index-ip-actors',
                'type' => 'key',
                $this->getIndexAttributesParam() => ['releaseYear', 'actors'], // 2 levels
                'orders' => ['DESC', 'DESC'],
            ]);

            $this->assertEquals(400, $twoLevelsArray['headers']['status-code']);
            $this->assertEquals('Creating indexes on array attributes is not currently supported.', $twoLevelsArray['body']['message']);

            $unknown = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey'],
            ]), [
                'key' => 'index-unknown',
                'type' => 'key',
                $this->getIndexAttributesParam() => ['Unknown'],
            ]);

            $this->assertEquals(400, $unknown['headers']['status-code']);
            $this->assertStringContainsString('\'Unknown\' required for the index could not be found', $unknown['body']['message']);
            $index1 = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey'],
            ]), [
                'key' => 'integers-order',
                'type' => 'key',
                $this->getIndexAttributesParam() => ['integers'], // array attribute
                'orders' => ['DESC'], // Check order is removed in API
            ]);

            $this->assertEquals(400, $index1['headers']['status-code']);
            $this->assertEquals('Creating indexes on array attributes is not currently supported.', $index1['body']['message']);

            $index2 = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey'],
            ]), [
                'key' => 'integers-size',
                'type' => 'key',
                $this->getIndexAttributesParam() => ['integers2'], // array attribute
            ]);

            $this->assertEquals(400, $index2['headers']['status-code']);
            $this->assertEquals('Creating indexes on array attributes is not currently supported.', $index2['body']['message']);

            if (!$this->getSupportForMultipleFulltextIndexes()) {
                // Some databases only allow one fulltext index per collection
                $this->assertEquals('There is already a fulltext index in the collection', $fulltextReleaseYear['body']['message']);
            } else {
                $this->assertEquals('Attribute "releaseYear" cannot be part of a fulltext index, must be of type string', $fulltextReleaseYear['body']['message']);
            }

            /**
             * Create Indexes by worker
             */
            $this->waitForAllIndexes($databaseId, $collectionId);

            $collectionResponse = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $collectionId), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), []);

            $this->assertIsArray($collectionResponse['body']['indexes']);
            $expectedIndexCount = $this->getMaxIndexLength() < 1024 ? 4 : 5; // MongoDB accepts tooLong index
            $this->assertCount($expectedIndexCount, $collectionResponse['body']['indexes']);
            $indexKeys = array_column($collectionResponse['body']['indexes'], 'key');
            $this->assertContains($titleIndex['body']['key'], $indexKeys);
            $this->assertContains($releaseYearIndex['body']['key'], $indexKeys);
            $this->assertContains($releaseWithDate1['body']['key'], $indexKeys);
            $this->assertContains($releaseWithDate2['body']['key'], $indexKeys);

            $this->assertEventually(function () use ($databaseId, $collectionId) {
                $collResp = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $collectionId), array_merge([
                    'content-type' => 'application/json',
                    'x-appwrite-project' => $this->getProject()['$id'],
                    'x-appwrite-key' => $this->getProject()['apiKey']
                ]));

                foreach ($collResp['body']['indexes'] as $index) {
                    $this->assertEquals('available', $index['status']);
                }

                return true;
            }, 60000, 500);
        }
    }

    public function testGetIndexByKeyWithLengths(): void
    {
        if (!$this->getSupportForAttributes()) {
            $this->expectNotToPerformAssertions();
            return;
        }
        $data = $this->setupAttributes();
        $databaseId = $data['databaseId'];
        $collectionId = $data['moviesId'];

        // Test case for valid lengths
        $create = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'key' => 'lengthTestIndex',
            'type' => 'key',
            $this->getIndexAttributesParam() => ['title','description'],
            'lengths' => [128,200]
        ]);
        $this->assertEquals(202, $create['headers']['status-code']);

        // Fetch index and check correct lengths
        $index = $this->client->call(Client::METHOD_GET, $this->getIndexUrl($databaseId, $collectionId, "lengthTestIndex"), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]);
        $this->assertEquals(200, $index['headers']['status-code']);
        $this->assertEquals('lengthTestIndex', $index['body']['key']);
        $this->assertEquals([128, 200], $index['body']['lengths']);

        // Test case for array attribute index (should be blocked)
        $create = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'key' => 'lengthOverrideTestIndex',
            'type' => 'key',
            $this->getIndexAttributesParam() => ['actors'],
            'lengths' => [120],
        ]);
        $this->assertEquals(400, $create['headers']['status-code']);
        $this->assertEquals('Creating indexes on array attributes is not currently supported.', $create['body']['message']);

        // Test case for count of lengths greater than attributes (should throw 400)
        $create = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'key' => 'lengthCountExceededIndex',
            'type' => 'key',
            $this->getIndexAttributesParam() => ['title'],
            'lengths' => [128, 128]
        ]);
        $this->assertEquals(400, $create['headers']['status-code']);

        // Test case for lengths exceeding total of 768/1024
        $indexLength = 256;
        if (!$this->getSupportForRelationships()) {
            $indexLength = 500;
        }
        $create = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'key' => 'lengthTooLargeIndex',
            'type' => 'key',
            $this->getIndexAttributesParam() => ['title','description','tagline','actors'],
            'lengths' => [$indexLength, $indexLength, $indexLength, 20],
        ]);

        $this->assertEquals(400, $create['headers']['status-code']);

        // Test case for negative length values
        $create = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'key' => 'negativeLengthIndex',
            'type' => 'key',
            $this->getIndexAttributesParam() => ['title'],
            'lengths' => [-1]
        ]);
        $this->assertEquals(400, $create['headers']['status-code']);
    }

    public function testListIndexes(): void
    {
        $data = $this->setupIndexes();
        $databaseId = $data['databaseId'];
        $response = $this->client->call(Client::METHOD_GET, $this->getIndexUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey'],
        ]), [
            'queries' => [
                Query::equal('type', ['key'])->toString(),
                Query::limit(2)->toString()
            ],
        ]);
        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals(2, \count($response['body']['indexes']));
        $response = $this->client->call(Client::METHOD_GET, $this->getIndexUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey'],
        ]), [
            'queries' => [
                Query::select(['key'])->toString(),
            ],
        ]);
        $this->assertEquals(Exception::GENERAL_ARGUMENT_INVALID, $response['body']['type']);
        $this->assertEquals(400, $response['headers']['status-code']);
    }

    public function testCreateDocument(): void
    {
        $data = $this->setupIndexes();
        $databaseId = $data['databaseId'];
        $document1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Captain America',
                'releaseYear' => 1944,
                'birthDay' => '1975-06-12 14:12:55+02:00',
                'actors' => [
                    'Chris Evans',
                    'Samuel Jackson',
                ]
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ]
        ]);

        $document2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Spider-Man: Far From Home',
                'releaseYear' => 2019,
                'birthDay' => null,
                'actors' => [
                    'Tom Holland',
                    'Zendaya Maree Stoermer',
                    'Samuel Jackson',
                ],
                'integers' => [50,60]
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ]
        ]);

        $document3 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Spider-Man: Homecoming',
                'releaseYear' => 2017,
                'birthDay' => '1975-06-12 14:12:55 America/New_York',
                'duration' => 65,
                'actors' => [
                    'Tom Holland',
                    'Zendaya Maree Stoermer',
                ],
                'integers' => [50]
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ]
        ]);

        $document4 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'releaseYear' => 2020, // Missing title, expect an 400 error
                'birthDay' => null // adding null here as documentsdb will require it as for documentsdb this document will be created
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ]
        ]);

        $this->assertEquals(201, $document1['headers']['status-code']);
        $this->assertEquals($data['moviesId'], $document1['body'][$this->getContainerIdResponseKey()]);
        $this->assertArrayNotHasKey('$collection', $document1['body']);
        $this->assertEquals($databaseId, $document1['body']['$databaseId']);
        $this->assertEquals($document1['body']['title'], 'Captain America');
        $this->assertEquals($document1['body']['releaseYear'], 1944);
        $this->assertIsArray($document1['body']['$permissions']);
        $this->assertCount(3, $document1['body']['$permissions']);
        $this->assertCount(2, $document1['body']['actors']);
        $this->assertEquals($document1['body']['actors'][0], 'Chris Evans');
        $this->assertEquals($document1['body']['actors'][1], 'Samuel Jackson');
        if ($this->getSupportForAttributes()) {
            $this->assertEquals($document1['body']['birthDay'], '1975-06-12T12:12:55.000+00:00');
        } else {
            $this->assertEquals($document1['body']['birthDay'], '1975-06-12 14:12:55+02:00');
        }
        $this->assertTrue(array_key_exists('$sequence', $document1['body']));

        $this->assertIsString($document1['body']['$sequence']);

        $this->assertEquals(201, $document2['headers']['status-code']);
        $this->assertEquals($data['moviesId'], $document2['body'][$this->getContainerIdResponseKey()]);
        $this->assertArrayNotHasKey('$collection', $document2['body']);
        $this->assertEquals($databaseId, $document2['body']['$databaseId']);
        $this->assertEquals($document2['body']['title'], 'Spider-Man: Far From Home');
        $this->assertEquals($document2['body']['releaseYear'], 2019);
        $this->assertEquals($document2['body']['duration'], null);
        $this->assertIsArray($document2['body']['$permissions']);
        $this->assertCount(3, $document2['body']['$permissions']);
        $this->assertCount(3, $document2['body']['actors']);
        $this->assertEquals($document2['body']['actors'][0], 'Tom Holland');
        $this->assertEquals($document2['body']['actors'][1], 'Zendaya Maree Stoermer');
        $this->assertEquals($document2['body']['actors'][2], 'Samuel Jackson');
        $this->assertEquals($document2['body']['birthDay'], null);
        $this->assertEquals($document2['body']['integers'][0], 50);
        $this->assertEquals($document2['body']['integers'][1], 60);
        $this->assertTrue(array_key_exists('$sequence', $document2['body']));

        $this->assertEquals(201, $document3['headers']['status-code']);
        $this->assertEquals($data['moviesId'], $document3['body'][$this->getContainerIdResponseKey()]);
        $this->assertArrayNotHasKey('$collection', $document3['body']);
        $this->assertEquals($databaseId, $document3['body']['$databaseId']);
        $this->assertEquals($document3['body']['title'], 'Spider-Man: Homecoming');
        $this->assertEquals($document3['body']['releaseYear'], 2017);
        $this->assertEquals($document3['body']['duration'], 65);
        $this->assertIsArray($document3['body']['$permissions']);
        $this->assertCount(3, $document3['body']['$permissions']);
        $this->assertCount(2, $document3['body']['actors']);
        $this->assertEquals($document3['body']['actors'][0], 'Tom Holland');
        $this->assertEquals($document3['body']['actors'][1], 'Zendaya Maree Stoermer');
        if ($this->getSupportForAttributes()) {
            $this->assertEquals($document3['body']['birthDay'], '1975-06-12T18:12:55.000+00:00'); // UTC for NY
        } else {
            $this->assertEquals($document1['body']['birthDay'], '1975-06-12 14:12:55+02:00');
        }
        $this->assertTrue(array_key_exists('$sequence', $document3['body']));

        if ($this->getSupportForAttributes()) {
            $this->assertEquals(400, $document4['headers']['status-code']);
        } else {
            $this->assertEquals(201, $document4['headers']['status-code']);
        }
    }

    public function testUpsertDocument(): void
    {
        $data = $this->setupIndexes();
        $databaseId = $data['databaseId'];
        $documentId = ID::unique();

        $document = $this->client->call(Client::METHOD_PUT, $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'title' => 'Thor: Ragnarok',
                'releaseYear' => 2000
            ],
            'permissions' => [
                Permission::read(Role::users()),
                Permission::update(Role::users()),
                Permission::delete(Role::users()),
            ],
        ]);

        $this->assertEquals(200, $document['headers']['status-code']);
        $this->assertCount(3, $document['body']['$permissions']);

        $document = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        $this->assertEquals('Thor: Ragnarok', $document['body']['title']);

        /**
         * Resubmit same document, nothing to update
         */
        $this->assertIsString($document['body']['$sequence']);

        $upsertData = [
            'title' => 'Thor: Ragnarok',
            'releaseYear' => 2000,
            'integers' => [],
            'birthDay' => null,
            'duration' => null,
            'actors' => [],
            'tagline' => '',
            'description' => '',
        ];
        if ($this->getSupportForRelationships()) {
            $upsertData['starringActors'] = [];
        }
        $document = $this->client->call(Client::METHOD_PUT, $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => $upsertData,
            'permissions' => [
                Permission::read(Role::users()),
                Permission::update(Role::users()),
                Permission::delete(Role::users()),
            ],
        ]);

        $this->assertEquals(200, $document['headers']['status-code']);
        $this->assertEquals('Thor: Ragnarok', $document['body']['title']);
        $this->assertCount(3, $document['body']['$permissions']);

        $document = $this->client->call(Client::METHOD_PUT, $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'title' => 'Thor: Love and Thunder',
                'releaseYear' => 2000
            ],
            'permissions' => [
                Permission::read(Role::users()),
                Permission::update(Role::users()),
                Permission::delete(Role::users()),
            ],
        ]);

        $this->assertEquals(200, $document['headers']['status-code']);
        $this->assertEquals('Thor: Love and Thunder', $document['body']['title']);

        $document = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        $this->assertEquals('Thor: Love and Thunder', $document['body']['title']);

        // removing permission to read and delete
        $document = $this->client->call(Client::METHOD_PUT, $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'title' => 'Thor: Love and Thunder',
                'releaseYear' => 2000
            ],
            'permissions' => [
                Permission::update(Role::users())
            ],
        ]);
        // shouldn't be able to read as no read permission
        $document = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));
        switch ($this->getSide()) {
            case 'client':
                $this->assertEquals(404, $document['headers']['status-code']);
                break;
            case 'server':
                $this->assertEquals(200, $document['headers']['status-code']);
                break;
        }
        // shouldn't be able to delete as no delete permission
        $document = $this->client->call(Client::METHOD_DELETE, $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));
        // simulating for the client
        // the document should not be allowed to be deleted as needed downward
        if ($this->getSide() === 'client') {
            $this->assertEquals(401, $document['headers']['status-code']);
        }
        // giving the delete permission
        $document = $this->client->call(Client::METHOD_PUT, $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'title' => 'Thor: Love and Thunder',
                'releaseYear' => 2000
            ],
            'permissions' => [
                Permission::read(Role::users()),
                Permission::update(Role::users()),
                Permission::delete(Role::users())
            ],
        ]);
        $document = $this->client->call(Client::METHOD_DELETE, $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));
        $this->assertEquals(204, $document['headers']['status-code']);

        // relationship behaviour - only test on databases that support relationships
        /** @var array<string, mixed>|null $person */
        $person = null;
        /** @var array<string, mixed>|null $library */
        $library = null;
        if ($this->getSupportForRelationships()) {
            $person = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                $this->getContainerIdParam() => 'person-upsert',
                'name' => 'person',
                'permissions' => [
                    Permission::read(Role::users()),
                    Permission::update(Role::users()),
                    Permission::delete(Role::users()),
                    Permission::create(Role::users()),
                ],
                $this->getSecurityParam() => true,
            ]);

            $this->assertEquals(201, $person['headers']['status-code']);

            $library = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                $this->getContainerIdParam() => 'library-upsert',
                'name' => 'library',
                'permissions' => [
                    Permission::read(Role::users()),
                    Permission::update(Role::users()),
                    Permission::create(Role::users()),
                    Permission::delete(Role::users()),
                ],
                $this->getSecurityParam() => true,
            ]);

            $this->assertEquals(201, $library['headers']['status-code']);

            $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $person['body']['$id']) . '/string', array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                'key' => 'fullName',
                'size' => 255,
                'required' => false,
            ]);

            $this->waitForAttribute($databaseId, $person['body']['$id'], 'fullName');

            $relation = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $person['body']['$id']) . '/relationship', array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                $this->getRelatedIdParam() => 'library-upsert',
                'type' => Database::RELATION_ONE_TO_ONE,
                'key' => 'library',
                'twoWay' => true,
                'onDelete' => Database::RELATION_MUTATE_CASCADE,
            ]);

            $this->waitForAttribute($databaseId, $person['body']['$id'], 'library');

            $libraryName = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $library['body']['$id']) . '/string', array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                'key' => 'libraryName',
                'size' => 255,
                'required' => true,
            ]);

            $this->waitForAttribute($databaseId, $library['body']['$id'], 'libraryName');

            $this->assertEquals(202, $libraryName['headers']['status-code']);

            // upserting values
            $documentId = ID::unique();
            $person1 = $this->client->call(Client::METHOD_PUT, $this->getRecordUrl($databaseId, $person['body']['$id'], $documentId), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()), [
                'data' => [
                    'library' => [
                        '$id' => 'library1',
                        '$permissions' => [
                            Permission::read(Role::users()),
                            Permission::update(Role::users()),
                            Permission::delete(Role::users()),
                        ],
                        'libraryName' => 'Library 1',
                    ],
                ],
                'permissions' => [
                    Permission::read(Role::users()),
                    Permission::update(Role::users()),
                    Permission::delete(Role::users()),
                ]
            ]);

            $this->assertEquals('Library 1', $person1['body']['library']['libraryName']);
            $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $person['body']['$id']), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()), [
                'queries' => [
                    Query::select(['fullName', 'library.*'])->toString(),
                    Query::equal('library', ['library1'])->toString(),
                ],
            ]);

            $this->assertEquals(1, $documents['body']['total']);
            $this->assertEquals('Library 1', $documents['body'][$this->getRecordResource()][0]['library']['libraryName']);


            $person1 = $this->client->call(Client::METHOD_PUT, $this->getRecordUrl($databaseId, $person['body']['$id'], $documentId), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()), [
                'data' => [
                    'library' => [
                        '$id' => 'library1',
                        '$permissions' => [
                            Permission::read(Role::users()),
                            Permission::update(Role::users()),
                            Permission::delete(Role::users()),
                        ],
                        'libraryName' => 'Library 2',
                    ],
                ],
                'permissions' => [
                    Permission::read(Role::users()),
                    Permission::update(Role::users()),
                    Permission::delete(Role::users()),
                ]
            ]);

            // data should get updated
            $this->assertEquals('Library 2', $person1['body']['library']['libraryName']);
            $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $person['body']['$id']), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()), [
                'queries' => [
                    Query::select(['fullName', 'library.*'])->toString(),
                    Query::equal('library', ['library1'])->toString(),
                ],
            ]);

            $this->assertEquals(1, $documents['body']['total']);
            $this->assertEquals('Library 2', $documents['body'][$this->getRecordResource()][0]['library']['libraryName']);


            // data should get added
            $person1 = $this->client->call(Client::METHOD_PUT, $this->getRecordUrl($databaseId, $person['body']['$id'], ID::unique()), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()), [
                'data' => [
                    'library' => [
                        '$id' => 'library2',
                        '$permissions' => [
                            Permission::read(Role::users()),
                            Permission::update(Role::users()),
                            Permission::delete(Role::users()),
                        ],
                        'libraryName' => 'Library 2',
                    ],
                ],
                'permissions' => [
                    Permission::read(Role::users()),
                    Permission::update(Role::users()),
                    Permission::delete(Role::users()),
                ]
            ]);

            $this->assertEquals('Library 2', $person1['body']['library']['libraryName']);
            $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $person['body']['$id']), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()), [
                'queries' => [
                    Query::select(['fullName', 'library.*'])->toString()
                ],
            ]);
            $this->assertEquals(2, $documents['body']['total']);
        }

        // test without passing permissions
        $document = $this->client->call(Client::METHOD_PUT, $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'title' => 'Thor: Ragnarok',
                'releaseYear' => 2000
            ]
        ]);

        $this->assertEquals(200, $document['headers']['status-code']);
        $this->assertEquals('Thor: Ragnarok', $document['body']['title']);

        $document = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->assertEquals(200, $document['headers']['status-code']);

        $deleteResponse = $this->client->call(Client::METHOD_DELETE, $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->assertEquals(204, $deleteResponse['headers']['status-code']);

        if ($this->getSide() === 'client') {
            // Skipped on server side: Creating a document with no permissions results in an empty permissions array, whereas on client side it assigns permissions to the current user

            // test without passing permissions
            $document = $this->client->call(Client::METHOD_PUT, $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()), [
                'data' => [
                    'title' => 'Thor: Ragnarok',
                    'releaseYear' => 2000
                ]
            ]);

            $this->assertEquals(200, $document['headers']['status-code']);
            $this->assertEquals('Thor: Ragnarok', $document['body']['title']);
            $this->assertCount(3, $document['body']['$permissions']);
            $permissionsCreated = $document['body']['$permissions'];
            // checking the default created permission
            $defaultPermission = [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id']))
            ];
            // ignoring the order of the permission and checking the permissions
            $this->assertEqualsCanonicalizing($defaultPermission, $permissionsCreated);

            $document = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id']
            ], $this->getHeaders()));

            $this->assertEquals(200, $document['headers']['status-code']);

            // updating the created doc
            $document = $this->client->call(Client::METHOD_PUT, $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()), [
                'data' => [
                    'title' => 'Thor: Ragnarok',
                    'releaseYear' => 2002
                ]
            ]);
            $this->assertEquals(200, $document['headers']['status-code']);
            $this->assertEquals('Thor: Ragnarok', $document['body']['title']);
            $this->assertEquals(2002, $document['body']['releaseYear']);
            $this->assertCount(3, $document['body']['$permissions']);
            $this->assertEquals($permissionsCreated, $document['body']['$permissions']);

            // removing the delete permission
            $document = $this->client->call(Client::METHOD_PUT, $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()), [
                'data' => [
                    'title' => 'Thor: Ragnarok',
                    'releaseYear' => 2002
                ],
                'permissions' => [
                    Permission::update(Role::user($this->getUser()['$id']))
                ]
            ]);
            $this->assertEquals(200, $document['headers']['status-code']);
            $this->assertEquals('Thor: Ragnarok', $document['body']['title']);
            $this->assertEquals(2002, $document['body']['releaseYear']);
            $this->assertCount(1, $document['body']['$permissions']);

            $deleteResponse = $this->client->call(Client::METHOD_DELETE, $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id']
            ], $this->getHeaders()));

            $this->assertEquals(401, $deleteResponse['headers']['status-code']);

            // giving the delete permission
            $document = $this->client->call(Client::METHOD_PUT, $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()), [
                'data' => [
                    'title' => 'Thor: Ragnarok',
                    'releaseYear' => 2002
                ],
                'permissions' => [
                    Permission::update(Role::user($this->getUser()['$id'])),
                    Permission::delete(Role::user($this->getUser()['$id']))
                ]
            ]);
            $this->assertEquals(200, $document['headers']['status-code']);
            $this->assertEquals('Thor: Ragnarok', $document['body']['title']);
            $this->assertEquals(2002, $document['body']['releaseYear']);
            $this->assertCount(2, $document['body']['$permissions']);

            $deleteResponse = $this->client->call(Client::METHOD_DELETE, $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id']
            ], $this->getHeaders()));

            $this->assertEquals(204, $deleteResponse['headers']['status-code']);

            // upsertion for the related document without passing permissions - only for databases that support relationships
            if ($this->getSupportForRelationships() && $person !== null && $library !== null) {
                // data should get added
                $newPersonId = ID::unique();
                $personNoPerm = $this->client->call(Client::METHOD_PUT, $this->getRecordUrl($databaseId, $person['body']['$id'], $newPersonId), array_merge([
                    'content-type' => 'application/json',
                    'x-appwrite-project' => $this->getProject()['$id'],
                ], $this->getHeaders()), [
                    'data' => [
                        'library' => [
                            '$id' => 'library3',
                            'libraryName' => 'Library 3',
                        ],
                    ],
                ]);

                $this->assertEquals('Library 3', $personNoPerm['body']['library']['libraryName']);
                $this->assertCount(3, $personNoPerm['body']['library']['$permissions']);
                $this->assertCount(3, $personNoPerm['body']['$permissions']);
                $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $person['body']['$id']), array_merge([
                    'content-type' => 'application/json',
                    'x-appwrite-project' => $this->getProject()['$id'],
                ], $this->getHeaders()), [
                    'queries' => [
                        Query::select(['fullName', 'library.*'])->toString()
                    ],
                ]);
                $this->assertGreaterThanOrEqual(1, $documents['body']['total']);
                $recordResource = $this->getRecordResource();
                $documentsDetails = $documents['body'][$recordResource];
                foreach ($documentsDetails as $doc) {
                    $this->assertCount(3, $doc['$permissions']);
                }
                $found = false;
                foreach ($documents['body'][$recordResource] as $doc) {
                    if (isset($doc['library']['libraryName']) && $doc['library']['libraryName'] === 'Library 3') {
                        $found = true;
                        break;
                    }
                }
                $this->assertTrue($found, 'Library 3 should be present in the upserted documents.');

                // Fetch the related library and assert on its permissions (should be default/inherited)
                $library3 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $library['body']['$id'], 'library3'), array_merge([
                    'content-type' => 'application/json',
                    'x-appwrite-project' => $this->getProject()['$id'],
                ], $this->getHeaders()));

                $this->assertEquals(200, $library3['headers']['status-code']);
                $this->assertEquals('Library 3', $library3['body']['libraryName']);
                $this->assertArrayHasKey('$permissions', $library3['body']);
                $this->assertCount(3, $library3['body']['$permissions']);
                $this->assertNotEmpty($library3['body']['$permissions']);

                // Readonly attributes are ignored
                $personNoPerm = $this->client->call(Client::METHOD_PUT, $this->getRecordUrl($databaseId, $person['body']['$id'], $newPersonId), array_merge([
                    'content-type' => 'application/json',
                    'x-appwrite-project' => $this->getProject()['$id'],
                ], $this->getHeaders()), [
                    'data' => [
                        '$id' => 'some-other-id',
                        $this->getContainerIdResponseKey() => 'some-other-collection',
                        '$databaseId' => 'some-other-database',
                        '$createdAt' => '2024-01-01T00:00:00Z',
                        '$updatedAt' => '2024-01-01T00:00:00Z',
                        'library' => [
                            '$id' => 'library3',
                            'libraryName' => 'Library 3',
                            '$createdAt' => '2024-01-01T00:00:00Z',
                            '$updatedAt' => '2024-01-01T00:00:00Z',
                        ],
                    ],
                ]);

                $update = $personNoPerm;
                $update['body']['$id'] = 'random';
                $update['body']['$sequence'] = 123;
                $update['body']['$databaseId'] = 'random';
                $update['body'][$this->getContainerIdResponseKey()] = 'random';
                $update['body']['$createdAt'] = '2024-01-01T00:00:00.000+00:00';
                $update['body']['$updatedAt'] = '2024-01-01T00:00:00.000+00:00';

                $upserted = $this->client->call(Client::METHOD_PUT, $this->getRecordUrl($databaseId, $person['body']['$id'], $newPersonId), array_merge([
                    'content-type' => 'application/json',
                    'x-appwrite-project' => $this->getProject()['$id'],
                ], $this->getHeaders()), [
                    'data' => $update['body']
                ]);

                $this->assertEquals(200, $upserted['headers']['status-code']);
                $this->assertEquals($personNoPerm['body']['$id'], $upserted['body']['$id']);
                $this->assertEquals($personNoPerm['body'][$this->getContainerIdResponseKey()], $upserted['body'][$this->getContainerIdResponseKey()]);
                $this->assertEquals($personNoPerm['body']['$databaseId'], $upserted['body']['$databaseId']);
                $this->assertEquals($personNoPerm['body']['$sequence'], $upserted['body']['$sequence']);
            }
        }
    }

    public function testListDocuments(): void
    {
        $data = $this->setupDocuments();
        $databaseId = $data['databaseId'];
        $docIds = $data['documentIds'];
        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$id', $docIds)->toString(),
                Query::orderAsc('releaseYear')->toString(),
            ],
        ]);

        $this->assertEquals(200, $documents['headers']['status-code']);
        $this->assertEquals(1944, $documents['body'][$this->getRecordResource()][0]['releaseYear']);
        $this->assertEquals(2017, $documents['body'][$this->getRecordResource()][1]['releaseYear']);
        $this->assertEquals(2019, $documents['body'][$this->getRecordResource()][2]['releaseYear']);
        $this->assertTrue(array_key_exists('$sequence', $documents['body'][$this->getRecordResource()][0]));
        $this->assertTrue(array_key_exists('$sequence', $documents['body'][$this->getRecordResource()][1]));
        $this->assertTrue(array_key_exists('$sequence', $documents['body'][$this->getRecordResource()][2]));
        $this->assertCount(3, $documents['body'][$this->getRecordResource()]);

        foreach ($documents['body'][$this->getRecordResource()] as $document) {
            $this->assertEquals($data['moviesId'], $document[$this->getContainerIdResponseKey()]);
            $this->assertArrayNotHasKey('$collection', $document);
            $this->assertEquals($databaseId, $document['$databaseId']);
        }

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$id', $docIds)->toString(),
                Query::orderDesc('releaseYear')->toString(),
            ],
        ]);

        $this->assertEquals(200, $documents['headers']['status-code']);
        $this->assertEquals(1944, $documents['body'][$this->getRecordResource()][2]['releaseYear']);
        $this->assertEquals(2017, $documents['body'][$this->getRecordResource()][1]['releaseYear']);
        $this->assertEquals(2019, $documents['body'][$this->getRecordResource()][0]['releaseYear']);
        $this->assertCount(3, $documents['body'][$this->getRecordResource()]);

        // changing description attribute to be null by default instead of empty string
        $patchNull = $this->client->call(Client::METHOD_PATCH, $this->getSchemaUrl($databaseId, $data['moviesId'], 'string', 'description'), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'default' => null,
            'required' => false,
        ]);
        // creating a dummy doc with null description
        $document1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Dummy',
                'releaseYear' => 1944,
                'birthDay' => '1975-06-12 14:12:55+02:00',
                'actors' => [
                    'Dummy',
                ],
            ]
        ]);

        $this->assertEquals(201, $document1['headers']['status-code']);
        // fetching docs with cursor after the dummy doc with order attr description which is null
        $documentsPaginated = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::orderAsc('dummy')->toString(),
                Query::cursorAfter(new Document(['$id' => $document1['body']['$id']]))->toString()
            ],
        ]);
        // should throw 400 as the order attr description of the selected doc is null
        $this->assertEquals(400, $documentsPaginated['headers']['status-code']);

        // deleting the dummy doc created
        $this->client->call(Client::METHOD_DELETE, $this->getContainerUrl($databaseId, $data['moviesId']) . '/' . $this->getRecordResource() . '/' . $document1['body']['$id'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));
    }

    public function testListDocumentsWithCache(): void
    {
        if (!$this->getSupportForAttributes()) {
            $this->markTestSkipped('Attributes are not supported by this database adapter');
        }
        $data = $this->setupDocuments();
        $databaseId = $data['databaseId'];
        $docIds = $data['documentIds'];

        // Filter to setup documents only, since other tests may have created additional docs in this collection.
        $baseQueries = [
            Query::equal('$id', $docIds)->toString(),
            Query::select(['title', 'releaseYear', '$id'])->toString(),
            Query::orderAsc('releaseYear')->toString(),
        ];

        // 1. Using cache with select queries, first request should miss cache.
        $documents1 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => $baseQueries,
            'ttl' => 30,
        ]);

        $this->assertEquals(200, $documents1['headers']['status-code']);
        $this->assertEquals(3, $documents1['body']['total']);
        $this->assertCount(3, $documents1['body'][$this->getRecordResource()]);
        $this->assertEquals(1944, $documents1['body'][$this->getRecordResource()][0]['releaseYear']);
        $this->assertEquals(2017, $documents1['body'][$this->getRecordResource()][1]['releaseYear']);
        $this->assertEquals(2019, $documents1['body'][$this->getRecordResource()][2]['releaseYear']);
        $this->assertArrayHasKey('title', $documents1['body'][$this->getRecordResource()][0]);
        $this->assertArrayHasKey('releaseYear', $documents1['body'][$this->getRecordResource()][0]);
        $this->assertArrayHasKey('$id', $documents1['body'][$this->getRecordResource()][0]);
        $this->assertArrayHasKey('x-appwrite-cache', $documents1['headers']);
        $this->assertEquals('miss', $documents1['headers']['x-appwrite-cache']);

        // 2. Using cache with same select queries, should return cached results.
        $documents2 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => $baseQueries,
            'ttl' => 30,
        ]);

        $this->assertEquals(200, $documents2['headers']['status-code']);
        $this->assertSame(3, $documents2['body']['total']);
        $this->assertCount(3, $documents2['body'][$this->getRecordResource()]);
        $this->assertEquals($documents1['body'][$this->getRecordResource()][0]['$id'], $documents2['body'][$this->getRecordResource()][0]['$id']);
        $this->assertEquals($documents1['body'][$this->getRecordResource()][0]['title'], $documents2['body'][$this->getRecordResource()][0]['title']);
        $this->assertEquals($documents1['body'][$this->getRecordResource()][0]['releaseYear'], $documents2['body'][$this->getRecordResource()][0]['releaseYear']);
        $this->assertArrayHasKey('x-appwrite-cache', $documents2['headers']);
        $this->assertEquals('hit', $documents2['headers']['x-appwrite-cache']);

        // 3. Using cache with same select queries but total is false, should return cached results just for documents.
        $documents3 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => $baseQueries,
            'ttl' => 30,
            'total' => false,
        ]);

        $this->assertEquals(200, $documents3['headers']['status-code']);
        $this->assertCount(3, $documents3['body'][$this->getRecordResource()]);
        $this->assertEquals($documents3['body'][$this->getRecordResource()][0]['$id'], $documents1['body'][$this->getRecordResource()][0]['$id']);
        $this->assertEquals($documents3['body'][$this->getRecordResource()][0]['title'], $documents1['body'][$this->getRecordResource()][0]['title']);
        $this->assertEquals($documents3['body'][$this->getRecordResource()][0]['releaseYear'], $documents1['body'][$this->getRecordResource()][0]['releaseYear']);
        $this->assertEquals(0, $documents3['body']['total']);
        $this->assertArrayHasKey('x-appwrite-cache', $documents3['headers']);
        $this->assertEquals('hit', $documents3['headers']['x-appwrite-cache']);

        // 4. Using cache with different select queries, should miss cache.
        $documents4 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$id', $docIds)->toString(),
                Query::select(['title'])->toString(),
                Query::orderAsc('releaseYear')->toString(),
            ],
            'ttl' => 10,
        ]);

        $this->assertEquals(200, $documents4['headers']['status-code']);
        $this->assertEquals(3, $documents4['body']['total']);
        $this->assertCount(3, $documents4['body'][$this->getRecordResource()]);
        $this->assertEquals($documents4['body'][$this->getRecordResource()][0]['title'], $documents1['body'][$this->getRecordResource()][0]['title']);
        $this->assertEquals($documents4['body'][$this->getRecordResource()][1]['title'], $documents1['body'][$this->getRecordResource()][1]['title']);
        $this->assertEquals($documents4['body'][$this->getRecordResource()][2]['title'], $documents1['body'][$this->getRecordResource()][2]['title']);
        $this->assertArrayHasKey('x-appwrite-cache', $documents4['headers']);
        $this->assertEquals('miss', $documents4['headers']['x-appwrite-cache']);

        // 5. Not using cache at all
        $documents5 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$id', $docIds)->toString(),
                Query::select(['title', 'releaseYear', '$id'])->toString(),
                Query::orderAsc('releaseYear')->toString(),
            ],
        ]);

        $this->assertEquals(200, $documents5['headers']['status-code']);
        $this->assertCount(3, $documents5['body'][$this->getRecordResource()]);
        $this->assertEquals(1944, $documents5['body'][$this->getRecordResource()][0]['releaseYear']);
        $this->assertArrayNotHasKey('x-appwrite-cache', $documents5['headers']);

        sleep(10);

        // 6. Using cache with same select queries but passed ttl time, should miss cache.
        $documents6 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => $baseQueries,
            'ttl' => 10,
        ]);

        $this->assertEquals(200, $documents6['headers']['status-code']);
        $this->assertCount(3, $documents6['body'][$this->getRecordResource()]);
        $this->assertArrayHasKey('title', $documents6['body'][$this->getRecordResource()][0]);
        $this->assertArrayHasKey('releaseYear', $documents6['body'][$this->getRecordResource()][0]);
        $this->assertArrayHasKey('$id', $documents6['body'][$this->getRecordResource()][0]);
        $this->assertArrayHasKey('x-appwrite-cache', $documents6['headers']);
        $this->assertEquals('miss', $documents6['headers']['x-appwrite-cache']);
    }

    public function testListDocumentsCacheBustedByAttributeChange(): void
    {
        if (!$this->getSupportForAttributes()) {
            $this->markTestSkipped('Attributes are not supported by this database adapter');
        }
        $data = $this->setupDocuments();
        $databaseId = $data['databaseId'];
        $docIds = $data['documentIds'];

        // Use different select queries from testListDocumentsWithCache to avoid cache key collision.
        $queries = [
            Query::equal('$id', $docIds)->toString(),
            Query::select(['title', '$id'])->toString(),
            Query::orderAsc('$createdAt')->toString(),
        ];

        // 1. First request should miss cache.
        $documents1 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => $queries,
            'ttl' => 300,
        ]);

        $this->assertEquals(200, $documents1['headers']['status-code']);
        $this->assertArrayHasKey('x-appwrite-cache', $documents1['headers']);
        $this->assertEquals('miss', $documents1['headers']['x-appwrite-cache']);

        // 2. Same request should hit cache.
        $documents2 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => $queries,
            'ttl' => 300,
        ]);

        $this->assertEquals(200, $documents2['headers']['status-code']);
        $this->assertArrayHasKey('x-appwrite-cache', $documents2['headers']);
        $this->assertEquals('hit', $documents2['headers']['x-appwrite-cache']);

        // 3. Add a new attribute to the collection, which updates the collection's $updatedAt.
        $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'cacheTestAttr',
            'size' => 64,
            'required' => false,
        ]);

        $this->assertEquals(202, $attribute['headers']['status-code']);

        // Wait for the attribute to be ready
        $this->waitForAttribute($databaseId, $data['moviesId'], 'cacheTestAttr');

        // 4. Same request should now miss cache because collection $updatedAt changed.
        $documents3 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => $queries,
            'ttl' => 300,
        ]);

        $this->assertEquals(200, $documents3['headers']['status-code']);
        $this->assertArrayHasKey('x-appwrite-cache', $documents3['headers']);
        $this->assertEquals('miss', $documents3['headers']['x-appwrite-cache']);
    }

    public function testListDocumentsCachedWithoutSelectQuery(): void
    {
        if (!$this->getSupportForAttributes()) {
            $this->markTestSkipped('Attributes are not supported by this database adapter');
        }
        $data = $this->setupDocuments();
        $databaseId = $data['databaseId'];
        $docIds = $data['documentIds'];

        // No Query::select(...) at all — ttl alone should enable caching.
        $queries = [
            Query::equal('$id', $docIds)->toString(),
            Query::orderAsc('releaseYear')->toString(),
        ];

        // 1. First request populates the cache.
        $documents1 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => $queries,
            'ttl' => 60,
        ]);

        $this->assertEquals(200, $documents1['headers']['status-code']);
        $this->assertArrayHasKey('x-appwrite-cache', $documents1['headers']);
        $this->assertEquals('miss', $documents1['headers']['x-appwrite-cache']);

        // 2. Same request hits cache — proves the gate is ttl > 0, not the presence of a select query.
        $documents2 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => $queries,
            'ttl' => 60,
        ]);

        $this->assertEquals(200, $documents2['headers']['status-code']);
        $this->assertArrayHasKey('x-appwrite-cache', $documents2['headers']);
        $this->assertEquals('hit', $documents2['headers']['x-appwrite-cache']);
        $this->assertSame(
            $documents1['body'][$this->getRecordResource()],
            $documents2['body'][$this->getRecordResource()]
        );
    }

    public function testListDocumentsCachePurgedByUpdate(): void
    {
        if (!$this->getSupportForAttributes()) {
            $this->markTestSkipped('Attributes are not supported by this database adapter');
        }
        $data = $this->setupDocuments();
        $databaseId = $data['databaseId'];
        $docIds = $data['documentIds'];

        // Use different select queries from other cache tests to avoid cache key collision.
        $queries = [
            Query::equal('$id', $docIds)->toString(),
            Query::select(['title', 'tagline', '$id'])->toString(),
            Query::orderAsc('$createdAt')->toString(),
        ];

        // 1. First request populates the cache.
        $documents1 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => $queries,
            'ttl' => 300,
        ]);

        $this->assertEquals(200, $documents1['headers']['status-code']);
        $this->assertEquals('miss', $documents1['headers']['x-appwrite-cache']);

        // 2. Same request hits cache.
        $documents2 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => $queries,
            'ttl' => 300,
        ]);

        $this->assertEquals(200, $documents2['headers']['status-code']);
        $this->assertEquals('hit', $documents2['headers']['x-appwrite-cache']);

        // 3. Update the collection/table with purge=true to invalidate all cached list responses.
        $update = $this->client->call(Client::METHOD_PUT, $this->getContainerUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey'],
        ]), [
            'name' => 'Movies',
            'enabled' => true,
            $this->getSecurityParam() => true,
            'purge' => true,
        ]);

        $this->assertEquals(200, $update['headers']['status-code']);

        // 4. Same request should now miss cache because purge=true cleared the hash.
        $documents3 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => $queries,
            'ttl' => 300,
        ]);

        $this->assertEquals(200, $documents3['headers']['status-code']);
        $this->assertEquals('miss', $documents3['headers']['x-appwrite-cache']);

        // 5. Re-reading without purge should hit the freshly populated cache.
        $documents4 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => $queries,
            'ttl' => 300,
        ]);

        $this->assertEquals(200, $documents4['headers']['status-code']);
        $this->assertEquals('hit', $documents4['headers']['x-appwrite-cache']);

        // 6. Update without purge=true must NOT invalidate the cache.
        $update2 = $this->client->call(Client::METHOD_PUT, $this->getContainerUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey'],
        ]), [
            'name' => 'Movies',
            'enabled' => true,
            $this->getSecurityParam() => true,
        ]);

        $this->assertEquals(200, $update2['headers']['status-code']);

        $documents5 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => $queries,
            'ttl' => 300,
        ]);

        $this->assertEquals(200, $documents5['headers']['status-code']);
        $this->assertEquals('hit', $documents5['headers']['x-appwrite-cache']);
    }

    public function testGetDocument(): void
    {
        $data = $this->getDocumentsList();
        $databaseId = $data['databaseId'];
        foreach ($data[$this->getRecordResource()] as $document) {
            $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $document[$this->getContainerIdResponseKey()], $document['$id']), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()));

            $this->assertEquals(200, $response['headers']['status-code']);
            $this->assertEquals($response['body']['$id'], $document['$id']);
            $this->assertEquals($document[$this->getContainerIdResponseKey()], $response['body'][$this->getContainerIdResponseKey()]);
            $this->assertArrayNotHasKey('$collection', $response['body']);
            $this->assertEquals($document['$databaseId'], $response['body']['$databaseId']);
            $this->assertEquals($response['body']['title'], $document['title']);
            $this->assertEquals($response['body']['releaseYear'], $document['releaseYear']);
            $this->assertEquals($response['body']['$permissions'], $document['$permissions']);
            $this->assertEquals($response['body']['birthDay'], $document['birthDay']);
            $this->assertTrue(array_key_exists('$sequence', $response['body']));
            $this->assertFalse(array_key_exists('$tenant', $response['body']));
        }
    }

    public function testGetDocumentWithQueries(): void
    {
        $data = $this->getDocumentsList();
        $databaseId = $data['databaseId'];
        $document = $data[$this->getRecordResource()][0];

        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $document[$this->getContainerIdResponseKey()], $document['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
            ],
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals($document['title'], $response['body']['title']);
        $this->assertEquals($document['releaseYear'], $response['body']['releaseYear']);
        $this->assertArrayHasKey('birthDay', $response['body']);
        $this->assertArrayHasKey('$sequence', $response['body']);

        // Query by sequence on get single document route
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $document[$this->getContainerIdResponseKey()], $document['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::select(['title', 'releaseYear', '$id', '$sequence'])->toString(),
            ],
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
    }

    public function testQueryBySequenceType(): void
    {
        $data = $this->setupDocuments();
        $databaseId = $data['databaseId'];

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$id', $data['documentIds'])->toString(),
            ],
        ]);

        $this->assertEquals(200, $documents['headers']['status-code']);
        $this->assertGreaterThan(0, count($documents['body'][$this->getRecordResource()]));

        $sequence = $documents['body'][$this->getRecordResource()][0]['$sequence'];
        $this->assertIsString($sequence);

        // Query with string $sequence value (supported by all adapters)
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$sequence', [$sequence])->toString(),
            ],
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertCount(1, $response['body'][$this->getRecordResource()]);
        $this->assertIsString($response['body'][$this->getRecordResource()][0]['$sequence']);
        $this->assertSame($sequence, $response['body'][$this->getRecordResource()][0]['$sequence']);

        // Query with int $sequence value (supported by SQL adapters, rejected by MongoDB)
        $intSequence = (int)$sequence;
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$sequence', [$intSequence])->toString(),
            ],
        ]);

        $adapter = getenv('_APP_DB_ADAPTER');
        if ($adapter === 'mongodb' || !$this->getSupportForAttributes()) {
            $this->assertEquals(400, $response['headers']['status-code']);
        } else {
            $this->assertEquals(200, $response['headers']['status-code']);
            $this->assertCount(1, $response['body'][$this->getRecordResource()]);
            $this->assertIsString($response['body'][$this->getRecordResource()][0]['$sequence']);
        }
    }

    public function testListDocumentsAfterPagination(): void
    {
        $data = $this->setupDocuments();
        $databaseId = $data['databaseId'];
        $docIds = $data['documentIds'];
        /**
         * Test after without order.
         */
        $base = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$id', $docIds)->toString(),
            ],
        ]);

        $this->assertEquals(200, $base['headers']['status-code']);
        $this->assertEquals('Captain America', $base['body'][$this->getRecordResource()][0]['title']);
        $this->assertEquals('Spider-Man: Far From Home', $base['body'][$this->getRecordResource()][1]['title']);
        $this->assertEquals('Spider-Man: Homecoming', $base['body'][$this->getRecordResource()][2]['title']);
        $this->assertCount(3, $base['body'][$this->getRecordResource()]);

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$id', $docIds)->toString(),
                Query::cursorAfter(new Document(['$id' => $base['body'][$this->getRecordResource()][0]['$id']]))->toString()
            ],
        ]);

        $this->assertEquals(200, $documents['headers']['status-code']);
        $this->assertEquals($base['body'][$this->getRecordResource()][1]['$id'], $documents['body'][$this->getRecordResource()][0]['$id']);
        $this->assertEquals($base['body'][$this->getRecordResource()][2]['$id'], $documents['body'][$this->getRecordResource()][1]['$id']);
        $this->assertCount(2, $documents['body'][$this->getRecordResource()]);

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$id', $docIds)->toString(),
                Query::cursorAfter(new Document(['$id' => $base['body'][$this->getRecordResource()][2]['$id']]))->toString()
            ],
        ]);

        $this->assertEquals(200, $documents['headers']['status-code']);
        $this->assertEmpty($documents['body'][$this->getRecordResource()]);

        /**
         * Test with ASC order and after.
         */
        $base = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$id', $docIds)->toString(),
                Query::orderAsc('releaseYear')->toString()
            ],
        ]);

        $this->assertEquals(200, $base['headers']['status-code']);
        $this->assertEquals(1944, $base['body'][$this->getRecordResource()][0]['releaseYear']);
        $this->assertEquals(2017, $base['body'][$this->getRecordResource()][1]['releaseYear']);
        $this->assertEquals(2019, $base['body'][$this->getRecordResource()][2]['releaseYear']);
        $this->assertCount(3, $base['body'][$this->getRecordResource()]);

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$id', $docIds)->toString(),
                Query::cursorAfter(new Document(['$id' => $base['body'][$this->getRecordResource()][1]['$id']]))->toString(),
                Query::orderAsc('releaseYear')->toString()
            ],
        ]);

        $this->assertEquals(200, $documents['headers']['status-code']);
        $this->assertEquals($base['body'][$this->getRecordResource()][2]['$id'], $documents['body'][$this->getRecordResource()][0]['$id']);
        $this->assertCount(1, $documents['body'][$this->getRecordResource()]);

        /**
         * Test with DESC order and after.
         */
        $base = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$id', $docIds)->toString(),
                Query::orderDesc('releaseYear')->toString()
            ],
        ]);

        $this->assertEquals(200, $base['headers']['status-code']);
        $this->assertEquals(1944, $base['body'][$this->getRecordResource()][2]['releaseYear']);
        $this->assertEquals(2017, $base['body'][$this->getRecordResource()][1]['releaseYear']);
        $this->assertEquals(2019, $base['body'][$this->getRecordResource()][0]['releaseYear']);
        $this->assertCount(3, $base['body'][$this->getRecordResource()]);

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$id', $docIds)->toString(),
                Query::cursorAfter(new Document(['$id' => $base['body'][$this->getRecordResource()][1]['$id']]))->toString(),
                Query::orderDesc('releaseYear')->toString()
            ],
        ]);

        $this->assertEquals(200, $documents['headers']['status-code']);
        $this->assertEquals($base['body'][$this->getRecordResource()][2]['$id'], $documents['body'][$this->getRecordResource()][0]['$id']);
        $this->assertCount(1, $documents['body'][$this->getRecordResource()]);

        /**
         * Test after with unknown document.
         */
        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::cursorAfter(new Document(['$id' => 'unknown']))->toString(),
            ],
        ]);

        $this->assertEquals(400, $documents['headers']['status-code']);

        /**
         * Test null value for cursor
         */

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                '{"method":"cursorAfter","values":[null]}',
            ],
        ]);

        $this->assertEquals(400, $documents['headers']['status-code']);
    }

    public function testListDocumentsBeforePagination(): void
    {
        $data = $this->setupDocuments();
        $databaseId = $data['databaseId'];
        $docIds = $data['documentIds'];
        /**
         * Test before without order.
         */
        $base = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$id', $docIds)->toString(),
            ],
        ]);

        $this->assertEquals(200, $base['headers']['status-code']);
        $this->assertEquals('Captain America', $base['body'][$this->getRecordResource()][0]['title']);
        $this->assertEquals('Spider-Man: Far From Home', $base['body'][$this->getRecordResource()][1]['title']);
        $this->assertEquals('Spider-Man: Homecoming', $base['body'][$this->getRecordResource()][2]['title']);
        $this->assertCount(3, $base['body'][$this->getRecordResource()]);

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$id', $docIds)->toString(),
                Query::cursorBefore(new Document(['$id' => $base['body'][$this->getRecordResource()][2]['$id']]))->toString(),
            ],
        ]);

        $this->assertEquals(200, $documents['headers']['status-code']);
        $this->assertEquals($base['body'][$this->getRecordResource()][0]['$id'], $documents['body'][$this->getRecordResource()][0]['$id']);
        $this->assertEquals($base['body'][$this->getRecordResource()][1]['$id'], $documents['body'][$this->getRecordResource()][1]['$id']);
        $this->assertCount(2, $documents['body'][$this->getRecordResource()]);

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$id', $docIds)->toString(),
                Query::cursorBefore(new Document(['$id' => $base['body'][$this->getRecordResource()][0]['$id']]))->toString(),
            ],
        ]);

        $this->assertEquals(200, $documents['headers']['status-code']);
        $this->assertEmpty($documents['body'][$this->getRecordResource()]);

        /**
         * Test with ASC order and before.
         */
        $base = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$id', $docIds)->toString(),
                Query::orderAsc('releaseYear')->toString(),
            ],
        ]);

        $this->assertEquals(200, $base['headers']['status-code']);
        $this->assertEquals(1944, $base['body'][$this->getRecordResource()][0]['releaseYear']);
        $this->assertEquals(2017, $base['body'][$this->getRecordResource()][1]['releaseYear']);
        $this->assertEquals(2019, $base['body'][$this->getRecordResource()][2]['releaseYear']);
        $this->assertCount(3, $base['body'][$this->getRecordResource()]);

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$id', $docIds)->toString(),
                Query::cursorBefore(new Document(['$id' => $base['body'][$this->getRecordResource()][1]['$id']]))->toString(),
                Query::orderAsc('releaseYear')->toString(),
            ],
        ]);

        $this->assertEquals(200, $documents['headers']['status-code']);
        $this->assertEquals($base['body'][$this->getRecordResource()][0]['$id'], $documents['body'][$this->getRecordResource()][0]['$id']);
        $this->assertCount(1, $documents['body'][$this->getRecordResource()]);

        /**
         * Test with DESC order and before.
         */
        $base = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$id', $docIds)->toString(),
                Query::orderDesc('releaseYear')->toString(),
            ],
        ]);

        $this->assertEquals(200, $base['headers']['status-code']);
        $this->assertEquals(1944, $base['body'][$this->getRecordResource()][2]['releaseYear']);
        $this->assertEquals(2017, $base['body'][$this->getRecordResource()][1]['releaseYear']);
        $this->assertEquals(2019, $base['body'][$this->getRecordResource()][0]['releaseYear']);
        $this->assertCount(3, $base['body'][$this->getRecordResource()]);

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$id', $docIds)->toString(),
                Query::cursorBefore(new Document(['$id' => $base['body'][$this->getRecordResource()][1]['$id']]))->toString(),
                Query::orderDesc('releaseYear')->toString(),
            ],
        ]);

        $this->assertEquals(200, $documents['headers']['status-code']);
        $this->assertEquals($base['body'][$this->getRecordResource()][0]['$id'], $documents['body'][$this->getRecordResource()][0]['$id']);
        $this->assertCount(1, $documents['body'][$this->getRecordResource()]);
    }

    public function testListDocumentsLimitAndOffset(): void
    {
        $data = $this->setupDocuments();
        $databaseId = $data['databaseId'];
        $docIds = $data['documentIds'];
        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$id', $docIds)->toString(),
                Query::orderAsc('releaseYear')->toString(),
                Query::limit(1)->toString(),
            ],
        ]);

        $this->assertEquals(200, $documents['headers']['status-code']);
        $this->assertEquals(1944, $documents['body'][$this->getRecordResource()][0]['releaseYear']);
        $this->assertCount(1, $documents['body'][$this->getRecordResource()]);

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$id', $docIds)->toString(),
                Query::orderAsc('releaseYear')->toString(),
                Query::limit(2)->toString(),
                Query::offset(1)->toString(),
            ],
        ]);

        $this->assertEquals(200, $documents['headers']['status-code']);
        $this->assertEquals(2017, $documents['body'][$this->getRecordResource()][0]['releaseYear']);
        $this->assertEquals(2019, $documents['body'][$this->getRecordResource()][1]['releaseYear']);
        $this->assertCount(2, $documents['body'][$this->getRecordResource()]);
    }

    public function testDocumentsListQueries(): void
    {
        $data = $this->setupDocuments();
        $databaseId = $data['databaseId'];
        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::search('title', 'Captain America')->toString(),
            ],
        ]);

        $this->assertEquals(200, $documents['headers']['status-code']);
        $this->assertEquals(1944, $documents['body'][$this->getRecordResource()][0]['releaseYear']);
        $this->assertGreaterThanOrEqual(1, count($documents['body'][$this->getRecordResource()]));

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('$id', [$documents['body'][$this->getRecordResource()][0]['$id']])->toString(),
            ],
        ]);

        $this->assertEquals(200, $documents['headers']['status-code']);
        $this->assertEquals(1944, $documents['body'][$this->getRecordResource()][0]['releaseYear']);
        $this->assertCount(1, $documents['body'][$this->getRecordResource()]);

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::search('title', 'Homecoming')->toString(),
            ],
        ]);

        $this->assertEquals(200, $documents['headers']['status-code']);
        $this->assertEquals(2017, $documents['body'][$this->getRecordResource()][0]['releaseYear']);
        $this->assertGreaterThanOrEqual(1, count($documents['body'][$this->getRecordResource()]));

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::search('title', 'spider')->toString(),
            ],
        ]);

        $this->assertEquals(200, $documents['headers']['status-code']);
        $this->assertGreaterThanOrEqual(2, count($documents['body'][$this->getRecordResource()]));
        $releaseYears = array_column($documents['body'][$this->getRecordResource()], 'releaseYear');
        $this->assertContains(2019, $releaseYears);
        $this->assertContains(2017, $releaseYears);

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                '{"method":"contains","attribute":"title","values":[bad]}'
            ],
        ]);

        $this->assertEquals(400, $documents['headers']['status-code']);
        $this->assertEquals('Invalid query: Syntax error', $documents['body']['message']);

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::contains('title', ['spi'])->toString(), // like query
            ],
        ]);

        $this->assertEquals(200, $documents['headers']['status-code']);
        $this->assertGreaterThanOrEqual(2, $documents['body']['total']);

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('releaseYear', [1944])->toString(),
            ],
        ]);

        $this->assertGreaterThanOrEqual(1, count($documents['body'][$this->getRecordResource()]));
        $this->assertEquals('Captain America', $documents['body'][$this->getRecordResource()][0]['title']);

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::notEqual('releaseYear', 1944)->toString(),
            ],
        ]);

        $this->assertGreaterThanOrEqual(2, count($documents['body'][$this->getRecordResource()]));

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::greaterThan('$createdAt', '1976-06-12')->toString(),
            ],
        ]);

        $this->assertGreaterThanOrEqual(3, count($documents['body'][$this->getRecordResource()]));

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::lessThan('$createdAt', '1976-06-12')->toString(),
            ],
        ]);

        $this->assertCount(0, $documents['body'][$this->getRecordResource()]);

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::contains('actors', ['Tom Holland', 'Samuel Jackson'])->toString(),
            ],
        ]);

        $this->assertEquals(200, $documents['headers']['status-code']);
        $this->assertGreaterThanOrEqual(3, $documents['body']['total']);

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::contains('actors', ['Tom'])->toString(), // Full-match not like
            ],
        ]);

        $this->assertEquals(200, $documents['headers']['status-code']);

        // for tablesdb/legacy it is full match , for docsdb inner pattern is matched
        if ($this->getSupportForAttributes()) {
            $this->assertEquals(0, $documents['body']['total']);
        } else {
            $this->assertGreaterThan(0, $documents['body']['total']);
        }

        if ($this->getSupportForAttributes()) {
            $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()), [
                'queries' => [
                    Query::greaterThan('birthDay', '16/01/2024 12:00:00AM')->toString(),
                ],
            ]);

            $this->assertEquals(400, $documents['headers']['status-code']);
            $this->assertEquals('Invalid query: Query value is invalid for attribute "birthDay"', $documents['body']['message']);

            $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()), [
                'queries' => [
                    Query::greaterThan('birthDay', '1960-01-01 10:10:10+02:30')->toString(),
                ],
            ]);

            $this->assertEquals(200, $documents['headers']['status-code']);
            $this->assertGreaterThanOrEqual(2, count($documents['body'][$this->getRecordResource()]));
            $birthDays = array_column($documents['body'][$this->getRecordResource()], 'birthDay');
            $this->assertContains('1975-06-12T12:12:55.000+00:00', $birthDays);
            $this->assertContains('1975-06-12T18:12:55.000+00:00', $birthDays);
        }


        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::isNull('integers')->toString(),
            ],
        ]);

        $this->assertEquals(200, $documents['headers']['status-code']);
        $this->assertGreaterThanOrEqual(1, $documents['body']['total']);

        /**
         * Test for Failure
         */
        $conditions = [];

        for ($i = 0; $i < APP_DATABASE_QUERY_MAX_VALUES + 1; $i++) {
            $conditions[] = $i;
        }

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('releaseYear', $conditions)->toString(),
            ],
        ]);
        $this->assertEquals(400, $documents['headers']['status-code']);
        $this->assertEquals('Invalid query: Query on attribute has greater than '.APP_DATABASE_QUERY_MAX_VALUES.' values: releaseYear', $documents['body']['message']);

        $value = '';

        for ($i = 0; $i < 101; $i++) {
            $value .= "[" . $i . "] Too long title to cross 2k chars query limit ";
        }

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::search('title', $value)->toString(),
            ],
        ]);

        // Todo: Not sure what to do we with Query length Test VS old? JSON validator will fails if query string will be truncated?
        //$this->assertEquals(400, $documents['headers']['status-code']);

        // Todo: Disabled for CL - Uncomment after ProxyDatabase cleanup for find method
        // $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
        //     'content-type' => 'application/json',
        //     'x-appwrite-project' => $this->getProject()['$id'],
        // ], $this->getHeaders()), [
        //     'queries' => [
        //         Query::search('actors', 'Tom')->toString(),
        //     ],
        // ]);
        // $this->assertEquals(400, $documents['headers']['status-code']);
        // $this->assertEquals('Invalid query: Cannot query search on attribute "actors" because it is an array.', $documents['body']['message']);
    }

    public function testUpdateDocument(): void
    {
        $data = $this->setupDocuments();
        $databaseId = $data['databaseId'];
        $document = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Thor: Ragnaroc',
                'releaseYear' => 2017,
                'birthDay' => '1976-06-12 14:12:55',
                'actors' => [],
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $id = $document['body']['$id'];

        $this->assertEquals(201, $document['headers']['status-code']);
        $this->assertEquals($data['moviesId'], $document['body'][$this->getContainerIdResponseKey()]);
        $this->assertArrayNotHasKey('$collection', $document['body']);
        $this->assertEquals($databaseId, $document['body']['$databaseId']);
        $this->assertEquals($document['body']['title'], 'Thor: Ragnaroc');
        $this->assertEquals($document['body']['releaseYear'], 2017);
        $dateValidator = new DatetimeValidator();
        $this->assertEquals(true, $dateValidator->isValid($document['body']['$createdAt']));
        $this->assertEquals(true, $dateValidator->isValid($document['body']['birthDay']));
        $this->assertContains(Permission::read(Role::user($this->getUser()['$id'])), $document['body']['$permissions']);
        $this->assertContains(Permission::update(Role::user($this->getUser()['$id'])), $document['body']['$permissions']);
        $this->assertContains(Permission::delete(Role::user($this->getUser()['$id'])), $document['body']['$permissions']);

        $document = $this->client->call(Client::METHOD_PATCH, $this->getContainerUrl($databaseId, $data['moviesId']) . '/' . $this->getRecordResource() . '/' . $id, array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'title' => 'Thor: Ragnarok',
            ],
            'permissions' => [
                Permission::read(Role::users()),
                Permission::update(Role::users()),
                Permission::delete(Role::users()),
            ],
        ]);

        $this->assertEquals(200, $document['headers']['status-code']);
        $this->assertEquals($document['body']['$id'], $id);
        $this->assertEquals($data['moviesId'], $document['body'][$this->getContainerIdResponseKey()]);
        $this->assertArrayNotHasKey('$collection', $document['body']);
        $this->assertEquals($databaseId, $document['body']['$databaseId']);
        $this->assertEquals($document['body']['title'], 'Thor: Ragnarok');
        $this->assertEquals($document['body']['releaseYear'], 2017);
        $this->assertContains(Permission::read(Role::users()), $document['body']['$permissions']);
        $this->assertContains(Permission::update(Role::users()), $document['body']['$permissions']);
        $this->assertContains(Permission::delete(Role::users()), $document['body']['$permissions']);

        $document = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $data['moviesId']) . '/' . $this->getRecordResource() . '/' . $id, array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        $id = $document['body']['$id'];

        $this->assertEquals(200, $document['headers']['status-code']);
        $this->assertEquals($data['moviesId'], $document['body'][$this->getContainerIdResponseKey()]);
        $this->assertArrayNotHasKey('$collection', $document['body']);
        $this->assertEquals($databaseId, $document['body']['$databaseId']);
        $this->assertEquals($document['body']['title'], 'Thor: Ragnarok');
        $this->assertEquals($document['body']['releaseYear'], 2017);

        $response = $this->client->call(Client::METHOD_PATCH, $this->getContainerUrl($databaseId, $data['moviesId']) . '/' . $this->getRecordResource() . '/' . $id, array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-timestamp' => DateTime::formatTz(DateTime::now()),
        ], $this->getHeaders()), [
            'data' => [
                'title' => 'Thor: Ragnarok',
            ],
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);

        // Test readonly attributes are ignored
        $response = $this->client->call(Client::METHOD_PATCH, $this->getContainerUrl($databaseId, $data['moviesId']) . '/' . $this->getRecordResource() . '/' . $id, array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-timestamp' => DateTime::formatTz(DateTime::now()),
        ], $this->getHeaders()), [
            'data' => [
                '$id' => 'newId',
                '$sequence' => 9999,
                $this->getContainerIdResponseKey() => 'newContainerId',
                '$databaseId' => 'newDatabaseId',
                '$createdAt' => '2024-01-01T00:00:00.000+00:00',
                '$updatedAt' => '2024-01-01T00:00:00.000+00:00',
                'title' => 'Thor: Ragnarok',
            ],
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals($id, $response['body']['$id']);
        $this->assertEquals($data['moviesId'], $response['body'][$this->getContainerIdResponseKey()]);
        $this->assertEquals($databaseId, $response['body']['$databaseId']);

        if ($this->getSide() === 'client') {
            $this->assertNotEquals('2024-01-01T00:00:00.000+00:00', $response['body']['$createdAt']);
            $this->assertNotEquals('2024-01-01T00:00:00.000+00:00', $response['body']['$updatedAt']);
        } else {
            $this->assertEquals('2024-01-01T00:00:00.000+00:00', $response['body']['$createdAt']);
            $this->assertEquals('2024-01-01T00:00:00.000+00:00', $response['body']['$updatedAt']);
        }
    }

    public function testOperators(): void
    {
        if (!$this->getSupportForOperators()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        // Create database
        $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'Test Database for Operators'
        ]);

        $this->assertEquals(201, $database['headers']['status-code']);
        $databaseId = $database['body']['$id'];

        // Create collection
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Operator Tests',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $this->assertEquals(201, $collection['headers']['status-code']);
        $collectionId = $collection['body']['$id'];

        // Create attributes
        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/string', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'key' => 'title',
            'size' => 256,
            'required' => true,
        ]);

        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/integer', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'key' => 'releaseYear',
            'required' => true,
        ]);

        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/integer', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'key' => 'duration',
            'required' => false,
        ]);


        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/string', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'key' => 'actors',
            'size' => 256,
            'required' => false,
            'array' => true,
        ]);

        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/integer', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'key' => 'integers',
            'required' => false,
            'array' => true,
        ]);

        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/string', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'key' => 'tagline',
            'size' => 512,
            'required' => false,
        ]);

        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/datetime', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'key' => 'birthDay',
            'required' => false,
        ]);

        // Wait for attributes to be created
        $this->waitForAllAttributes($databaseId, $collectionId);

        // Create a document to test operators
        $document = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Operator Test',
                'releaseYear' => 2020,
                'duration' => 120,
                'actors' => ['Actor1', 'Actor2'],
                'integers' => [10, 20],
                'tagline' => 'Original',
                'birthDay' => '2020-01-01 12:00:00',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $this->assertEquals(201, $document['headers']['status-code']);
        $documentId = $document['body']['$id'];

        // Test increment operator on integer
        $updated = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'releaseYear' => Operator::increment(5)->toString(),
                'duration' => Operator::increment(10)->toString(),
            ],
        ]);

        $this->assertEquals(200, $updated['headers']['status-code']);
        $this->assertEquals(2025, $updated['body']['releaseYear']);
        $this->assertEquals(130, $updated['body']['duration']);

        // Test decrement operator
        $updated = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'releaseYear' => Operator::decrement(3)->toString(),
            ],
        ]);

        $this->assertEquals(200, $updated['headers']['status-code']);
        $this->assertEquals(2022, $updated['body']['releaseYear']);

        // Test array append operator
        $updated = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'actors' => Operator::arrayAppend(['Actor3'])->toString(),
            ],
        ]);

        $this->assertEquals(200, $updated['headers']['status-code']);
        $this->assertEquals(['Actor1', 'Actor2', 'Actor3'], $updated['body']['actors']);

        // Test array prepend operator
        $updated = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'actors' => Operator::arrayPrepend(['Actor0'])->toString(),
            ],
        ]);

        $this->assertEquals(200, $updated['headers']['status-code']);
        $this->assertEquals(['Actor0', 'Actor1', 'Actor2', 'Actor3'], $updated['body']['actors']);

        // Test string concat operator
        $updated = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'tagline' => Operator::stringConcat(' Appended')->toString(),
            ],
        ]);

        $this->assertEquals(200, $updated['headers']['status-code']);
        $this->assertEquals('Original Appended', $updated['body']['tagline']);

        // Test multiple operators in a single update
        $updated = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'releaseYear' => Operator::increment(1)->toString(),
                'integers' => Operator::arrayAppend([30])->toString(),
            ],
        ]);

        $this->assertEquals(200, $updated['headers']['status-code']);
        $this->assertEquals(2023, $updated['body']['releaseYear']);
        $this->assertEquals([10, 20, 30], $updated['body']['integers']);

        // Test upsert with operators
        $upsertId = ID::unique();
        $upserted = $this->client->call(Client::METHOD_PUT, $this->getContainerUrl($databaseId, $collectionId) . '/' . $this->getRecordResource() . '/' . $upsertId, array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'title' => 'Upsert Test',
                'releaseYear' => 2020,
                'actors' => [],
                'birthDay' => '2020-01-01 12:00:00',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $this->assertEquals(200, $upserted['headers']['status-code']);

        $upserted = $this->client->call(Client::METHOD_PUT, $this->getContainerUrl($databaseId, $collectionId) . '/' . $this->getRecordResource() . '/' . $upsertId, array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'title' => 'Upsert Test Updated',
                'releaseYear' => Operator::increment(5)->toString(),
                'actors' => [],
                'birthDay' => '2020-01-01 12:00:00',
            ],
        ]);

        $this->assertEquals(200, $upserted['headers']['status-code']);
        $this->assertEquals(2025, $upserted['body']['releaseYear']);
    }

    public function testBulkOperators(): void
    {
        if (!$this->getSupportForOperators()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        // Create database
        $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'Test Database for Bulk Operators'
        ]);

        $this->assertEquals(201, $database['headers']['status-code']);
        $databaseId = $database['body']['$id'];

        // Create collection
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Bulk Operator Tests',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::users()),
            ],
        ]);

        $this->assertEquals(201, $collection['headers']['status-code']);
        $collectionId = $collection['body']['$id'];

        // Create attributes
        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/string', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'key' => 'title',
            'size' => 256,
            'required' => true,
        ]);

        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/integer', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'key' => 'releaseYear',
            'required' => true,
        ]);


        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/string', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'key' => 'actors',
            'size' => 256,
            'required' => false,
            'array' => true,
        ]);

        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/datetime', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'key' => 'birthDay',
            'required' => false,
        ]);

        // Wait for attributes to be created
        $this->waitForAllAttributes($databaseId, $collectionId);

        // Create multiple documents
        $document1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Bulk Test 1',
                'releaseYear' => 2020,
                'actors' => ['Actor1'],
                'birthDay' => '2020-01-01 12:00:00',
            ],
            'permissions' => [
                Permission::read(Role::users()),
                Permission::update(Role::users()),
                Permission::delete(Role::users()),
            ],
        ]);

        $document2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Bulk Test 2',
                'releaseYear' => 2021,
                'actors' => ['Actor2'],
                'birthDay' => '2020-01-01 12:00:00',
            ],
            'permissions' => [
                Permission::read(Role::users()),
                Permission::update(Role::users()),
                Permission::delete(Role::users()),
            ],
        ]);

        $this->assertEquals(201, $document1['headers']['status-code']);
        $this->assertEquals(201, $document2['headers']['status-code']);

        // Test bulk update with operators
        $bulkUpdate = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'data' => [
                'releaseYear' => Operator::increment(10)->toString(),
            ],
            'queries' => [
                Query::startsWith('title', 'Bulk Test')->toString(),
            ],
        ]);

        $this->assertEquals(200, $bulkUpdate['headers']['status-code']);
        $this->assertGreaterThanOrEqual(2, $bulkUpdate['body']['total']);

        // Verify the updates
        $verify1 = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $collectionId) . '/' . $this->getRecordResource() . '/' . $document1['body']['$id'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        $this->assertEquals(200, $verify1['headers']['status-code']);
        $this->assertEquals(2030, $verify1['body']['releaseYear']);

        $verify2 = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $collectionId) . '/' . $this->getRecordResource() . '/' . $document2['body']['$id'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        $this->assertEquals(200, $verify2['headers']['status-code']);
        $this->assertEquals(2031, $verify2['body']['releaseYear']);
    }

    public function testDeleteDocument(): void
    {
        $data = $this->setupDocuments();
        $databaseId = $data['databaseId'];
        $document = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Thor: Ragnarok',
                'releaseYear' => 2017,
                'birthDay' => '1975-06-12 14:12:55',
                'actors' => [],
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ]
        ]);

        $id = $document['body']['$id'];

        $this->assertEquals(201, $document['headers']['status-code']);

        $document = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $data['moviesId']) . '/' . $this->getRecordResource() . '/' . $id, array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        $this->assertEquals(200, $document['headers']['status-code']);

        $document = $this->client->call(Client::METHOD_DELETE, $this->getContainerUrl($databaseId, $data['moviesId']) . '/' . $this->getRecordResource() . '/' . $id, array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        $this->assertEquals(204, $document['headers']['status-code']);

        $document = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $data['moviesId']) . '/' . $this->getRecordResource() . '/' . $id, array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        $this->assertEquals(404, $document['headers']['status-code']);
    }

    public function testInvalidDocumentStructure(): void
    {
        if (!$this->getSupportForAttributes()) {
            $this->markTestSkipped('Attributes are not supported by this database adapter');
        }
        $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'databaseId' => ID::unique(),
            'name' => 'InvalidDocumentDatabase',
        ]);
        $this->assertEquals(201, $database['headers']['status-code']);
        $this->assertEquals('InvalidDocumentDatabase', $database['body']['name']);

        $databaseId = $database['body']['$id'];
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'invalidDocumentStructure',
            'permissions' => [
                Permission::create(Role::any()),
                Permission::read(Role::any()),
            ],
            $this->getSecurityParam() => true,
        ]);

        $this->assertEquals(201, $collection['headers']['status-code']);
        $this->assertEquals('invalidDocumentStructure', $collection['body']['name']);

        $collectionId = $collection['body']['$id'];

        $email = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/email', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'email',
            'required' => false,
        ]);

        $enum = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/enum', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'enum',
            'elements' => ['yes', 'no', 'maybe'],
            'required' => false,
        ]);

        $ip = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/ip', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'ip',
            'required' => false,
        ]);

        $url = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/url', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'url',
            'size' => 256,
            'required' => false,
        ]);

        $range = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/integer', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'range',
            'required' => false,
            'min' => 1,
            'max' => 10,
        ]);

        // TODO@kodumbeats min and max are rounded in error message
        $floatRange = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/float', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'floatRange',
            'required' => false,
            'min' => 1.1,
            'max' => 1.4,
        ]);

        $probability = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/float', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'probability',
            'required' => false,
            'default' => 0,
            'min' => 0,
            'max' => 1,
        ]);

        $upperBound = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/integer', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'upperBound',
            'required' => false,
            'max' => 10,
        ]);

        $lowerBound = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/integer', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'lowerBound',
            'required' => false,
            'min' => 5,
        ]);

        /**
         * Test for failure
         */

        $invalidRange = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/integer', array_merge([
            'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'invalidRange',
            'required' => false,
            'min' => 4,
            'max' => 3,
        ]);

        $defaultArray = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/integer', array_merge([
            'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'defaultArray',
            'required' => false,
            'default' => 42,
            'array' => true,
        ]);

        $defaultRequired = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/integer', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'attributeId' => ID::custom('defaultRequired'),
            'required' => true,
            'default' => 12
        ]);

        $enumDefault = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/enum', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'attributeId' => ID::custom('enumDefault'),
            'elements' => ['north', 'west'],
            'default' => 'south'
        ]);

        $enumDefaultStrict = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/enum', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'attributeId' => ID::custom('enumDefault'),
            'elements' => ['north', 'west'],
            'default' => 'NORTH'
        ]);

        $goodDatetime = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/datetime', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'birthDay',
            'required' => false,
            'default' => null
        ]);

        $datetimeDefault = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/datetime', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'badBirthDay',
            'required' => false,
            'default' => 'bad'
        ]);

        $this->assertEquals(202, $email['headers']['status-code']);
        $this->assertEquals(202, $ip['headers']['status-code']);
        $this->assertEquals(202, $url['headers']['status-code']);
        $this->assertEquals(202, $range['headers']['status-code']);
        $this->assertEquals(202, $floatRange['headers']['status-code']);
        $this->assertEquals(202, $probability['headers']['status-code']);
        $this->assertEquals(202, $upperBound['headers']['status-code']);
        $this->assertEquals(202, $lowerBound['headers']['status-code']);
        $this->assertEquals(202, $enum['headers']['status-code']);
        $this->assertEquals(202, $goodDatetime['headers']['status-code']);
        $this->assertEquals(400, $invalidRange['headers']['status-code']);
        $this->assertEquals(400, $defaultArray['headers']['status-code']);
        $this->assertEquals(400, $defaultRequired['headers']['status-code']);
        $this->assertEquals(400, $enumDefault['headers']['status-code']);
        $this->assertEquals(400, $enumDefaultStrict['headers']['status-code']);
        $this->assertEquals('Minimum value must be lesser than maximum value', $invalidRange['body']['message']);
        $this->assertEquals('Cannot set default value for array ' . $this->getSchemaResource(), $defaultArray['body']['message']);
        $this->assertEquals(400, $datetimeDefault['headers']['status-code']);
        // wait for worker to add attributes
        $this->waitForAllAttributes($databaseId, $collectionId);

        $collection = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey'],
        ]), []);

        $this->assertCount(10, $collection['body'][$this->getSchemaResource()]);

        /**
         * Test for successful validation
         */

        $goodEmail = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'email' => 'user@example.com',
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        $goodEnum = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'enum' => 'yes',
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        $goodIp = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'ip' => '1.1.1.1',
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        $goodUrl = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'url' => 'http://www.example.com',
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        $goodRange = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'range' => 3,
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        $goodFloatRange = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'floatRange' => 1.4,
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        $goodProbability = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'probability' => 0.99999,
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        $notTooHigh = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'upperBound' => 8,
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        $notTooLow = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'lowerBound' => 8,
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        $this->assertEquals(201, $goodEmail['headers']['status-code']);
        $this->assertEquals(201, $goodEnum['headers']['status-code']);
        $this->assertEquals(201, $goodIp['headers']['status-code']);
        $this->assertEquals(201, $goodUrl['headers']['status-code']);
        $this->assertEquals(201, $goodRange['headers']['status-code']);
        $this->assertEquals(201, $goodFloatRange['headers']['status-code']);
        $this->assertEquals(201, $goodProbability['headers']['status-code']);
        $this->assertEquals(201, $notTooHigh['headers']['status-code']);
        $this->assertEquals(201, $notTooLow['headers']['status-code']);

        /*
         * Test that custom validators reject documents
         */

        $badEmail = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'email' => 'user@@example.com',
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        $badEnum = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'enum' => 'badEnum',
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        $badIp = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'ip' => '1.1.1.1.1',
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        $badUrl = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'url' => 'example...com',
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        $badRange = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'range' => 11,
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        $badFloatRange = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'floatRange' => 2.5,
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        $badProbability = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'probability' => 1.1,
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        $tooHigh = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'upperBound' => 11,
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        $tooLow = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'lowerBound' => 3,
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        $badTime = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => 'unique()',
            'data' => [
                'birthDay' => '2020-10-10 27:30:10+01:00',
            ],
            'read' => ['user:' . $this->getUser()['$id']],
            'write' => ['user:' . $this->getUser()['$id']],
        ]);

        $this->assertEquals(400, $badEmail['headers']['status-code']);
        $this->assertEquals(400, $badEnum['headers']['status-code']);
        $this->assertEquals(400, $badIp['headers']['status-code']);
        $this->assertEquals(400, $badUrl['headers']['status-code']);
        $this->assertEquals(400, $badRange['headers']['status-code']);
        $this->assertEquals(400, $badFloatRange['headers']['status-code']);
        $this->assertEquals(400, $badProbability['headers']['status-code']);
        $this->assertEquals(400, $tooHigh['headers']['status-code']);
        $this->assertEquals(400, $tooLow['headers']['status-code']);
        $this->assertEquals(400, $badTime['headers']['status-code']);
        $this->assertEquals('Invalid document structure: Attribute "email" has invalid format. Value must be a valid email address', $badEmail['body']['message']);
        $this->assertEquals('Invalid document structure: Attribute "enum" has invalid format. Value must be one of (yes, no, maybe)', $badEnum['body']['message']);
        $this->assertEquals('Invalid document structure: Attribute "ip" has invalid format. Value must be a valid IP address', $badIp['body']['message']);
        $this->assertEquals('Invalid document structure: Attribute "url" has invalid format. Value must be a valid URL', $badUrl['body']['message']);
        $this->assertEquals('Invalid document structure: Attribute "range" has invalid format. Value must be a valid range between 1 and 10', $badRange['body']['message']);
        $this->assertEquals('Invalid document structure: Attribute "floatRange" has invalid format. Value must be a valid range between 1 and 1', $badFloatRange['body']['message']);
        $this->assertEquals('Invalid document structure: Attribute "probability" has invalid format. Value must be a valid range between 0 and 1', $badProbability['body']['message']);
        $this->assertEquals('Invalid document structure: Attribute "upperBound" has invalid format. Value must be a valid range between -9,223,372,036,854,775,808 and 10', $tooHigh['body']['message']);
        $this->assertEquals('Invalid document structure: Attribute "lowerBound" has invalid format. Value must be a valid range between 5 and 9,223,372,036,854,775,807', $tooLow['body']['message']);
    }

    public function testDefaultPermissions(): void
    {
        $data = $this->setupDocuments();
        $databaseId = $data['databaseId'];
        $document = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Captain America',
                'releaseYear' => 1944,
                'actors' => [],
            ],
        ]);

        $id = $document['body']['$id'];

        $this->assertEquals(201, $document['headers']['status-code']);
        $this->assertEquals($document['body']['title'], 'Captain America');
        $this->assertEquals($document['body']['releaseYear'], 1944);
        $this->assertIsArray($document['body']['$permissions']);

        if ($this->getSide() == 'client') {
            $this->assertCount(3, $document['body']['$permissions']);
            $this->assertContains(Permission::read(Role::user($this->getUser()['$id'])), $document['body']['$permissions']);
            $this->assertContains(Permission::update(Role::user($this->getUser()['$id'])), $document['body']['$permissions']);
            $this->assertContains(Permission::delete(Role::user($this->getUser()['$id'])), $document['body']['$permissions']);
        }

        if ($this->getSide() == 'server') {
            $this->assertCount(0, $document['body']['$permissions']);
            $this->assertEquals([], $document['body']['$permissions']);
        }

        // Updated Permissions

        $document = $this->client->call(Client::METHOD_PATCH, $this->getContainerUrl($databaseId, $data['moviesId']) . '/' . $this->getRecordResource() . '/' . $id, array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'title' => 'Captain America 2',
                'releaseYear' => 1945,
                'actors' => [],
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id']))
            ],
        ]);

        $this->assertEquals(200, $document['headers']['status-code']);
        $this->assertEquals($document['body']['title'], 'Captain America 2');
        $this->assertEquals($document['body']['releaseYear'], 1945);

        // This differs from the old permissions model because we don't inherit
        // existing document permissions on update, unless none were supplied,
        // so that specific types can be removed if wanted.
        $this->assertCount(2, $document['body']['$permissions']);
        $this->assertEquals([
            Permission::read(Role::user($this->getUser()['$id'])),
            Permission::update(Role::user($this->getUser()['$id'])),
        ], $document['body']['$permissions']);

        $document = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $data['moviesId']) . '/' . $this->getRecordResource() . '/' . $id, array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        $this->assertEquals(200, $document['headers']['status-code']);
        $this->assertEquals($document['body']['title'], 'Captain America 2');
        $this->assertEquals($document['body']['releaseYear'], 1945);

        $this->assertCount(2, $document['body']['$permissions']);
        $this->assertEquals([
            Permission::read(Role::user($this->getUser()['$id'])),
            Permission::update(Role::user($this->getUser()['$id'])),
        ], $document['body']['$permissions']);

        // Reset Permissions

        $document = $this->client->call(Client::METHOD_PATCH, $this->getContainerUrl($databaseId, $data['moviesId']) . '/' . $this->getRecordResource() . '/' . $id, array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'title' => 'Captain America 3',
                'releaseYear' => 1946,
                'actors' => [],
            ],
            'permissions' => [],
        ]);

        $this->assertEquals(200, $document['headers']['status-code']);
        $this->assertEquals($document['body']['title'], 'Captain America 3');
        $this->assertEquals($document['body']['releaseYear'], 1946);
        $this->assertCount(0, $document['body']['$permissions']);
        $this->assertEquals([], $document['body']['$permissions']);

        // Check client side can no longer read the document.
        $document = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $data['moviesId']) . '/' . $this->getRecordResource() . '/' . $id, array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        switch ($this->getSide()) {
            case 'client':
                $this->assertEquals(404, $document['headers']['status-code']);
                break;
            case 'server':
                $this->assertEquals(200, $document['headers']['status-code']);
                break;
        }
    }

    public function testEnforceCollectionAndDocumentPermissions(): void
    {
        $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'databaseId' => ID::unique(),
            'name' => 'EnforceCollectionAndDocumentPermissions',
        ]);
        $this->assertEquals(201, $database['headers']['status-code']);
        $this->assertEquals('EnforceCollectionAndDocumentPermissions', $database['body']['name']);

        $databaseId = $database['body']['$id'];
        $user = $this->getUser()['$id'];
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'enforceCollectionAndDocumentPermissions',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::read(Role::user($user)),
                Permission::create(Role::user($user)),
                Permission::update(Role::user($user)),
                Permission::delete(Role::user($user)),
            ],
        ]);

        $this->assertEquals(201, $collection['headers']['status-code']);
        $this->assertEquals($collection['body']['name'], 'enforceCollectionAndDocumentPermissions');
        $this->assertEquals($collection['body'][$this->getSecurityResponseKey()], true);

        $collectionId = $collection['body']['$id'];
        if ($this->getSupportForAttributes()) {
            $attribute = $this->createAttribute($databaseId, $collectionId, 'string', [
                'key' => 'attribute',
                'size' => 64,
                'required' => true,
            ]);
            $this->assertEquals(202, $attribute['headers']['status-code'], 202);
            $this->assertEquals('attribute', $attribute['body']['key']);

            // wait for db to add attribute
            $this->waitForAttribute($databaseId, $collectionId, 'attribute');
        }

        $index = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'key_attribute',
            'type' => 'key',
            $this->getIndexAttributesParam() => ['attribute'],
        ]);

        $this->assertEquals(202, $index['headers']['status-code']);
        $this->assertEquals('key_attribute', $index['body']['key']);

        // wait for db to add index
        $this->waitForIndex($databaseId, $collectionId, 'key_attribute');

        $document1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'attribute' => 'one',
            ],
            'permissions' => [
                Permission::read(Role::user($user)),
                Permission::update(Role::user($user)),
                Permission::delete(Role::user($user)),
            ]
        ]);

        $this->assertEquals(201, $document1['headers']['status-code']);

        $document2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'attribute' => 'one',
            ],
            'permissions' => [
                Permission::update(Role::user($user)),
                Permission::delete(Role::user($user)),
            ]
        ]);

        $this->assertEquals(201, $document2['headers']['status-code']);

        $document3 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'attribute' => 'one',
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom('other'))),
                Permission::update(Role::user(ID::custom('other'))),
            ],
        ]);

        $this->assertEquals(201, $document3['headers']['status-code']);

        $documentsUser1 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        // Current user has read permission on the collection so can get any document
        $this->assertEquals(3, $documentsUser1['body']['total']);
        $this->assertCount(3, $documentsUser1['body'][$this->getRecordResource()]);

        $document3GetWithCollectionRead = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $collectionId) . '/' . $this->getRecordResource() . '/' . $document3['body']['$id'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        // Current user has read permission on the collection so can get any document
        $this->assertEquals(200, $document3GetWithCollectionRead['headers']['status-code']);

        $email = uniqid() . 'user@localhost.test';
        $password = 'password';
        $name = 'User Name';
        $this->client->call(Client::METHOD_POST, '/account', [
            'origin' => 'http://localhost',
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], [
            'userId' => ID::custom('other'),
            'email' => $email,
            'password' => $password,
            'name' => $name,
        ]);
        $session2 = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
            'origin' => 'http://localhost',
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], [
            'email' => $email,
            'password' => $password,
        ]);
        $session2 = $session2['cookies']['a_session_' . $this->getProject()['$id']];

        $document3GetWithDocumentRead = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $collectionId) . '/' . $this->getRecordResource() . '/' . $document3['body']['$id'], [
            'origin' => 'http://localhost',
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session2,
        ]);

        // Current user has no collection permissions but has read permission for this document
        $this->assertEquals(200, $document3GetWithDocumentRead['headers']['status-code']);

        $document2GetFailure = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $collectionId) . '/' . $this->getRecordResource() . '/' . $document2['body']['$id'], [
            'origin' => 'http://localhost',
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session2,
        ]);

        // Current user has no collection or document permissions for this document
        $this->assertEquals(404, $document2GetFailure['headers']['status-code']);

        $documentsUser2 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), [
            'origin' => 'http://localhost',
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session2,
        ]);

        // Current user has no collection permissions but has read permission for one document
        $this->assertEquals(1, $documentsUser2['body']['total']);
        $this->assertCount(1, $documentsUser2['body'][$this->getRecordResource()]);
    }

    public function testEnforceCollectionPermissions(): void
    {
        $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'databaseId' => ID::unique(),
            'name' => 'EnforceCollectionPermissions',
        ]);
        $this->assertEquals(201, $database['headers']['status-code']);
        $this->assertEquals('EnforceCollectionPermissions', $database['body']['name']);

        $databaseId = $database['body']['$id'];
        $user = $this->getUser()['$id'];
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'enforceCollectionPermissions',
            'permissions' => [
                Permission::read(Role::user($user)),
                Permission::create(Role::user($user)),
                Permission::update(Role::user($user)),
                Permission::delete(Role::user($user)),
            ],
        ]);

        $this->assertEquals(201, $collection['headers']['status-code']);
        $this->assertEquals($collection['body']['name'], 'enforceCollectionPermissions');
        $this->assertEquals($collection['body'][$this->getSecurityResponseKey()], false);

        $collectionId = $collection['body']['$id'];
        if ($this->getSupportForAttributes()) {
            $attribute = $this->createAttribute($databaseId, $collectionId, 'string', [
                'key' => 'attribute',
                'size' => 64,
                'required' => true,
            ]);
            $this->assertEquals(202, $attribute['headers']['status-code'], 202);
            $this->assertEquals('attribute', $attribute['body']['key']);

            $this->waitForAttribute($databaseId, $collectionId, 'attribute');
        }

        $index = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'key_attribute',
            'type' => 'key',
            $this->getIndexAttributesParam() => ['attribute'],
        ]);

        $this->assertEquals(202, $index['headers']['status-code'], 'Index creation failed: ' . json_encode($index['body'] ?? []));
        $this->assertEquals('key_attribute', $index['body']['key']);

        $this->waitForIndex($databaseId, $collectionId, 'key_attribute');

        $document1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'attribute' => 'one',
            ],
            'permissions' => [
                Permission::read(Role::user($user)),
                Permission::update(Role::user($user)),
                Permission::delete(Role::user($user)),
            ]
        ]);

        $this->assertEquals(201, $document1['headers']['status-code']);

        $document2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'attribute' => 'one',
            ],
            'permissions' => [
                Permission::update(Role::user($user)),
                Permission::delete(Role::user($user)),
            ]
        ]);

        $this->assertEquals(201, $document2['headers']['status-code']);

        $document3 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'attribute' => 'one',
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom('other2'))),
                Permission::update(Role::user(ID::custom('other2'))),
            ],
        ]);

        $this->assertEquals(201, $document3['headers']['status-code']);

        $documentsUser1 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        // Current user has read permission on the collection so can get any document
        $this->assertEquals(3, $documentsUser1['body']['total']);
        $this->assertCount(3, $documentsUser1['body'][$this->getRecordResource()]);

        $document3GetWithCollectionRead = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $collectionId) . '/' . $this->getRecordResource() . '/' . $document3['body']['$id'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        // Current user has read permission on the collection so can get any document
        $this->assertEquals(200, $document3GetWithCollectionRead['headers']['status-code']);

        $email = uniqid() . 'user2@localhost.test';
        $password = 'password';
        $name = 'User Name';
        $this->client->call(Client::METHOD_POST, '/account', [
            'origin' => 'http://localhost',
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], [
            'userId' => ID::custom('other2'),
            'email' => $email,
            'password' => $password,
            'name' => $name,
        ]);
        $session2 = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
            'origin' => 'http://localhost',
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], [
            'email' => $email,
            'password' => $password,
        ]);
        $session2 = $session2['cookies']['a_session_' . $this->getProject()['$id']];

        $document3GetWithDocumentRead = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $collectionId) . '/' . $this->getRecordResource() . '/' . $document3['body']['$id'], [
            'origin' => 'http://localhost',
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session2,
        ]);

        // other2 has no collection permissions and document permissions are disabled
        // $this->assertEquals(404, $document3GetWithDocumentRead['headers']['status-code']);

        $documentsUser2 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), [
            'origin' => 'http://localhost',
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session2,
        ]);

        // other2 has no collection permissions and document permissions are disabled
        $this->assertEquals(401, $documentsUser2['headers']['status-code']);

        // Enable document permissions
        $collection = $this->client->call(Client::METHOD_PUT, $this->getContainerUrl($databaseId, $collectionId), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'name' => $collection['body']['name'],
            $this->getSecurityParam() => true,
        ]);

        $documentsUser2 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), [
            'origin' => 'http://localhost',
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session2,
        ]);

        // Current user has no collection permissions read access to one document
        $this->assertEquals(1, $documentsUser2['body']['total']);
        $this->assertCount(1, $documentsUser2['body'][$this->getRecordResource()]);
    }

    public function testUniqueIndexDuplicate(): void
    {
        // Use a dedicated collection for this test to avoid interference from other tests
        // that may add duplicate titles to the shared movies collection in the same process
        $data = $this->setupDatabase();
        $databaseId = $data['databaseId'];
        $serverHeaders = [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey'],
        ];

        // Create a dedicated collection for unique index testing
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), $serverHeaders, [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'uniqueIndexTest',
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
            $this->getSecurityParam() => true,
        ]);
        $this->assertEquals(201, $collection['headers']['status-code']);
        $collectionId = $collection['body']['$id'];

        // Add a title attribute
        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/string', $serverHeaders, [
            'key' => 'title',
            'size' => 256,
            'required' => true,
        ]);
        $this->waitForAttribute($databaseId, $collectionId, 'title');

        // Add two documents with unique titles
        $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => ['title' => 'Unique Title A'],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);
        $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => ['title' => 'Unique Title B'],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        // Create unique index on title
        $uniqueIndex = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), $serverHeaders, [
            'key' => 'unique_title',
            'type' => 'unique',
            $this->getIndexAttributesParam() => ['title'],
            'orders' => [Database::ORDER_DESC],
        ]);

        $this->assertEquals(202, $uniqueIndex['headers']['status-code']);

        $this->waitForIndex($databaseId, $collectionId, 'unique_title');

        // test for failure - inserting duplicate title
        $duplicate = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Unique Title A',
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        $this->assertEquals(409, $duplicate['headers']['status-code']);

        // Test for exception when inserting new doc and then updating to conflict
        $document = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Unique Title C',
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        $this->assertEquals(201, $document['headers']['status-code']);

        // Test for exception when updating document to conflict
        $duplicate = $this->client->call(Client::METHOD_PATCH, $this->getContainerUrl($databaseId, $collectionId) . '/' . $this->getRecordResource() . '/' . $document['body']['$id'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Unique Title A',
            ],
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        $this->assertEquals(409, $duplicate['headers']['status-code']);
    }

    public function testPersistentCreatedAt(): void
    {
        $data = $this->setupDocuments();
        $headers = $this->getSide() === 'client' ? array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()) : [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ];

        $document = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($data['databaseId'], $data['moviesId']), $headers, [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Creation Date Test',
                'releaseYear' => 2000
            ]
        ]);

        $this->assertEquals($document['body']['title'], 'Creation Date Test');

        $documentId = $document['body']['$id'];
        $createdAt = $document['body']['$createdAt'];
        $updatedAt = $document['body']['$updatedAt'];

        \usleep(500000);

        $document = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($data['databaseId'], $data['moviesId'], $documentId), $headers, [
            'data' => [
                'title' => 'Updated Date Test',
            ]
        ]);

        $updatedAtSecond = $document['body']['$updatedAt'];

        $this->assertEquals($document['body']['title'], 'Updated Date Test');
        $this->assertEquals($document['body']['$createdAt'], $createdAt);
        $this->assertNotEquals($document['body']['$updatedAt'], $updatedAt);

        \usleep(500000);

        $document = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($data['databaseId'], $data['moviesId'], $documentId), $headers, [
            'data' => [
                'title' => 'Again Updated Date Test',
                '$createdAt' => '2022-08-01 13:09:23.040',
                '$updatedAt' => '2022-08-01 13:09:23.050'
            ]
        ]);
        if ($this->getSide() === 'client') {
            $this->assertEquals($document['body']['title'], 'Again Updated Date Test');
            $this->assertNotEquals($document['body']['$createdAt'], DateTime::formatTz('2022-08-01 13:09:23.040'));
            $this->assertNotEquals($document['body']['$updatedAt'], DateTime::formatTz('2022-08-01 13:09:23.050'));
        } else {
            $this->assertEquals($document['body']['title'], 'Again Updated Date Test');
            $this->assertEquals($document['body']['$createdAt'], DateTime::formatTz('2022-08-01 13:09:23.040'));
            $this->assertEquals($document['body']['$updatedAt'], DateTime::formatTz('2022-08-01 13:09:23.050'));

        }
    }

    public function testUpdatePermissionsWithEmptyPayload(): void
    {
        // Create Database
        $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'databaseId' => ID::unique(),
            'name' => 'Empty Permissions',
        ]);
        $this->assertEquals(201, $database['headers']['status-code']);

        $databaseId = $database['body']['$id'];

        // Create collection
        $movies = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Movies',
            'permissions' => [
                Permission::create(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ],
            $this->getSecurityParam() => true,
        ]);

        $this->assertEquals(201, $movies['headers']['status-code']);
        $this->assertEquals($movies['body']['name'], 'Movies');

        $moviesId = $movies['body']['$id'];

        // create attribute
        if ($this->getSupportForAttributes()) {
            $title = $this->createAttribute($databaseId, $moviesId, 'string', [
                'key' => 'title',
                'size' => 256,
                'required' => true,
            ]);

            $this->assertEquals(202, $title['headers']['status-code']);

            // wait for database worker to create attributes
            $this->waitForAttribute($databaseId, $moviesId, 'title');
        }
        // add document
        $document = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $moviesId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Captain America',
            ],
            'permissions' => [
                Permission::read(Role::any()),
                Permission::update(Role::any()),
                Permission::delete(Role::any()),
            ],
        ]);

        $id = $document['body']['$id'];

        $this->assertEquals(201, $document['headers']['status-code']);
        $this->assertCount(3, $document['body']['$permissions']);
        $this->assertContains(Permission::read(Role::any()), $document['body']['$permissions']);
        $this->assertContains(Permission::update(Role::any()), $document['body']['$permissions']);
        $this->assertContains(Permission::delete(Role::any()), $document['body']['$permissions']);

        // Send only read permission
        $document = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $moviesId, $id), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'permissions' => [
                Permission::read(Role::user(ID::custom($this->getUser()['$id']))),
            ]
        ]);

        $this->assertEquals(200, $document['headers']['status-code']);
        $this->assertCount(1, $document['body']['$permissions']);

        // Send only mutation permissions
        $document = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $moviesId, $id), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'permissions' => [
                Permission::update(Role::user(ID::custom($this->getUser()['$id']))),
                Permission::delete(Role::user(ID::custom($this->getUser()['$id']))),
            ],
        ]);

        if ($this->getSide() == 'server') {
            $this->assertEquals(200, $document['headers']['status-code']);
            $this->assertCount(2, $document['body']['$permissions']);
            $this->assertContains(Permission::update(Role::user($this->getUser()['$id'])), $document['body']['$permissions']);
            $this->assertContains(Permission::delete(Role::user($this->getUser()['$id'])), $document['body']['$permissions']);
        }

        // remove collection
        $this->client->call(Client::METHOD_DELETE, $this->getContainerUrl($databaseId, $moviesId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));
    }

    public function testAttributeBooleanDefault(): void
    {
        if (!$this->getSupportForAttributes()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        $data = $this->setupDatabase();
        $databaseId = $data['databaseId'];

        /**
         * Test for SUCCESS
         */
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Boolean'
        ]);

        $this->assertEquals(201, $collection['headers']['status-code']);

        $collectionId = $collection['body']['$id'];

        $true = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/boolean', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'true',
            'required' => false,
            'default' => true
        ]);

        $this->assertEquals(202, $true['headers']['status-code']);

        $false = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/boolean', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'false',
            'required' => false,
            'default' => false
        ]);

        $this->assertEquals(202, $false['headers']['status-code']);
    }

    public function testOneToOneRelationship(): void
    {
        if (!$this->getSupportForRelationships()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        $data = $this->setupDatabase();
        $databaseId = $data['databaseId'];

        $person = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'person',
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
            $this->getSecurityParam() => true,
        ]);

        $this->assertEquals(201, $person['headers']['status-code']);

        $library = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'library',
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
            $this->getSecurityParam() => true,
        ]);

        $this->assertEquals(201, $library['headers']['status-code']);

        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $person['body']['$id']) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'fullName',
            'size' => 255,
            'required' => false,
        ]);

        $this->waitForAttribute($databaseId, $person['body']['$id'], 'fullName');

        $relation = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $person['body']['$id']) . '/relationship', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getRelatedIdParam() => $library['body']['$id'],
            'type' => Database::RELATION_ONE_TO_ONE,
            'key' => 'library',
            'twoWay' => true,
            'twoWayKey' => 'person',
            'onDelete' => Database::RELATION_MUTATE_CASCADE,
        ]);

        $this->waitForAttribute($databaseId, $person['body']['$id'], 'library');

        $libraryName = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $library['body']['$id']) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'libraryName',
            'size' => 255,
            'required' => true,
        ]);

        $this->waitForAttribute($databaseId, $library['body']['$id'], 'libraryName');

        $this->assertEquals(202, $libraryName['headers']['status-code']);
        $this->assertEquals(202, $relation['headers']['status-code']);
        $this->assertEquals('library', $relation['body']['key']);
        $this->assertEquals('relationship', $relation['body']['type']);
        $this->assertEquals('processing', $relation['body']['status']);

        $attributes = $this->client->call(Client::METHOD_GET, $this->getSchemaUrl($databaseId, $person['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->assertEquals(200, $attributes['headers']['status-code']);
        $this->assertEquals(2, $attributes['body']['total']);
        $attributes = $attributes['body'][$this->getSchemaResource()];
        $this->assertEquals($library['body']['$id'], $attributes[1][$this->getRelatedResourceKey()]);
        $this->assertEquals('oneToOne', $attributes[1]['relationType']);
        $this->assertEquals(true, $attributes[1]['twoWay']);
        $this->assertEquals('person', $attributes[1]['twoWayKey']);
        $this->assertEquals(Database::RELATION_MUTATE_CASCADE, $attributes[1]['onDelete']);

        $attribute = $this->client->call(Client::METHOD_GET, $this->getSchemaUrl($databaseId, $person['body']['$id'], '', 'library'), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->assertEquals(200, $attribute['headers']['status-code']);
        $this->assertEquals('available', $attribute['body']['status']);
        $this->assertEquals('library', $attribute['body']['key']);
        $this->assertEquals('relationship', $attribute['body']['type']);
        $this->assertEquals(false, $attribute['body']['required']);
        $this->assertEquals(false, $attribute['body']['array']);
        $this->assertEquals('oneToOne', $attribute['body']['relationType']);
        $this->assertEquals(true, $attribute['body']['twoWay']);
        $this->assertEquals('person', $attribute['body']['twoWayKey']);
        $this->assertEquals(Database::RELATION_MUTATE_CASCADE, $attribute['body']['onDelete']);

        $person1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $person['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'library' => [
                    '$id' => 'library1',
                    '$permissions' => [
                        Permission::read(Role::any()),
                    ],
                    'libraryName' => 'Library 1',
                ],
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ]
        ]);

        $this->assertEquals('Library 1', $person1['body']['library']['libraryName']);

        // Create without nested ID
        $person2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $person['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'library' => [
                    'libraryName' => 'Library 2',
                ],
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ]
        ]);

        $this->assertEquals('Library 2', $person2['body']['library']['libraryName']);

        // Ensure IDs were set and internal IDs removed
        $this->assertEquals($databaseId, $person1['body']['$databaseId']);
        $this->assertEquals($databaseId, $person1['body']['library']['$databaseId']);

        $this->assertEquals($person['body']['$id'], $person1['body'][$this->getContainerIdResponseKey()]);
        $this->assertEquals($library['body']['$id'], $person1['body']['library'][$this->getContainerIdResponseKey()]);

        $this->assertArrayNotHasKey('$collection', $person1['body']);
        $this->assertArrayNotHasKey('$collection', $person1['body']['library']);
        $this->assertArrayHasKey('$sequence', $person1['body']);
        $this->assertArrayHasKey('$sequence', $person1['body']['library']);

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $person['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::select(['fullName', 'library.*'])->toString(),
                Query::equal('library', ['library1'])->toString(),
            ],
        ]);

        $this->assertEquals(1, $documents['body']['total']);
        $this->assertEquals('Library 1', $documents['body'][$this->getRecordResource()][0]['library']['libraryName']);
        $this->assertArrayHasKey('fullName', $documents['body'][$this->getRecordResource()][0]);

        $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $person['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::select(['library.*'])->toString(),
                Query::equal('library.libraryName', ['Library 1'])->toString(),
            ],
        ]);

        $this->assertEquals(200, $documents['headers']['status-code']);
        $this->assertEquals(1, $documents['body']['total']);
        $this->assertCount(1, $documents['body'][$this->getRecordResource()]);
        $this->assertEquals('Library 1', $documents['body'][$this->getRecordResource()][0]['library']['libraryName']);
        $this->assertEquals($person1['body']['$id'], $documents['body'][$this->getRecordResource()][0]['$id']);

        $response = $this->client->call(Client::METHOD_DELETE, $this->getSchemaUrl($databaseId, $person['body']['$id'], '', 'library'), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->assertEquals(204, $response['headers']['status-code']);

        $this->assertEventually(function () use ($databaseId, $person) {
            $attribute = $this->client->call(Client::METHOD_GET, $this->getSchemaUrl($databaseId, $person['body']['$id'], '', 'library'), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]));
            $this->assertEquals(404, $attribute['headers']['status-code']);
            return true;
        }, 60000, 500);

        $attribute = $this->client->call(Client::METHOD_GET, $this->getSchemaUrl($databaseId, $person['body']['$id'], '', 'library'), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->assertEquals(404, $attribute['headers']['status-code']);

        $person1 = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $person['body']['$id']) . '/' . $this->getRecordResource() . '/' . $person1['body']['$id'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        $this->assertArrayNotHasKey('library', $person1['body']);

        //Test Deletion of related twoKey
        $attributes = $this->client->call(Client::METHOD_GET, $this->getSchemaUrl($databaseId, $library['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->assertEquals(200, $attributes['headers']['status-code']);
        $this->assertEquals(1, $attributes['body']['total']);
        $this->assertEquals('libraryName', $attributes['body'][$this->getSchemaResource()][0]['key']);
    }

    public function testOneToManyRelationship(): void
    {
        if (!$this->getSupportForRelationships()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        $data = $this->setupOneToManyRelationship();
        $databaseId = $data['databaseId'];
        $personCollection = $data['personCollection'];
        $libraryCollection = $data['libraryCollection'];

        $libraryAttributesResponse = $this->client->call(Client::METHOD_GET, $this->getSchemaUrl($databaseId, $libraryCollection), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->assertIsArray($libraryAttributesResponse['body'][$this->getSchemaResource()]);
        $this->assertGreaterThanOrEqual(2, $libraryAttributesResponse['body']['total']);

        $libraryCollectionResponse = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $libraryCollection), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->assertIsArray($libraryCollectionResponse['body'][$this->getSchemaResource()]);
        $this->assertGreaterThanOrEqual(2, count($libraryCollectionResponse['body'][$this->getSchemaResource()]));

        $attribute = $this->client->call(Client::METHOD_GET, $this->getSchemaUrl($databaseId, $personCollection, '', 'libraries'), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->assertEquals(200, $attribute['headers']['status-code']);
        $this->assertEquals('available', $attribute['body']['status']);
        $this->assertEquals('libraries', $attribute['body']['key']);
        $this->assertEquals('relationship', $attribute['body']['type']);
        $this->assertEquals(false, $attribute['body']['required']);
        $this->assertEquals(false, $attribute['body']['array']);
        $this->assertEquals('oneToMany', $attribute['body']['relationType']);
        $this->assertEquals(true, $attribute['body']['twoWay']);
        $this->assertEquals('person_one_to_many', $attribute['body']['twoWayKey']);

        $personDocId = ID::unique();
        $libraryDoc10Id = ID::unique();
        $libraryDoc11Id = ID::unique();

        $person2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $personCollection), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => $personDocId,
            'data' => [
                'fullName' => 'Ray Charles',
                'libraries' => [
                    [
                        '$id' => $libraryDoc10Id,
                        '$permissions' => [
                            Permission::read(Role::any()),
                            Permission::update(Role::any()),
                            Permission::delete(Role::any()),
                        ],
                        'libraryName' => 'Library 10',
                    ],
                    [
                        '$id' => $libraryDoc11Id,
                        '$permissions' => [
                            Permission::read(Role::any()),
                            Permission::update(Role::any()),
                            Permission::delete(Role::any()),
                        ],
                        'libraryName' => 'Library 11',
                    ]
                ],
            ],
            'permissions' => [
                Permission::read(Role::any()),
                Permission::update(Role::any()),
                Permission::delete(Role::any()),
            ]
        ]);

        $this->assertEquals(201, $person2['headers']['status-code']);
        $this->assertArrayHasKey('libraries', $person2['body']);
        $this->assertEquals(2, count($person2['body']['libraries']));

        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $personCollection, $person2['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::select(['*', 'libraries.*'])->toString()
            ]
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertArrayNotHasKey('$collection', $response['body']);
        $this->assertArrayHasKey('libraries', $response['body']);
        $this->assertEquals(2, count($response['body']['libraries']));

        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $libraryCollection, $libraryDoc11Id), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::select(['person_one_to_many.$id'])->toString()
            ]
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertArrayHasKey('person_one_to_many', $response['body']);
        $this->assertEquals($personDocId, $response['body']['person_one_to_many']['$id']);

        $response = $this->client->call(Client::METHOD_PATCH, $this->getSchemaUrl($databaseId, $personCollection, 'relationship', 'libraries'), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'onDelete' => Database::RELATION_MUTATE_CASCADE,
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);

        $attribute = $this->client->call(Client::METHOD_GET, $this->getSchemaUrl($databaseId, $personCollection, '', 'libraries'), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->assertEquals(200, $attribute['headers']['status-code']);
        $this->assertEquals('available', $attribute['body']['status']);
        $this->assertEquals('libraries', $attribute['body']['key']);
        $this->assertEquals('relationship', $attribute['body']['type']);
        $this->assertEquals(false, $attribute['body']['required']);
        $this->assertEquals(false, $attribute['body']['array']);
        $this->assertEquals('oneToMany', $attribute['body']['relationType']);
        $this->assertEquals(true, $attribute['body']['twoWay']);
        $this->assertEquals(Database::RELATION_MUTATE_CASCADE, $attribute['body']['onDelete']);
    }

    public function testManyToOneRelationship(): void
    {
        if (!$this->getSupportForRelationships()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        $data = $this->setupDatabase();
        $databaseId = $data['databaseId'];

        // Create album collection
        $albums = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Albums',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::read(Role::user($this->getUser()['$id'])),
            ],
        ]);

        // Create album name attribute
        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $albums['body']['$id']) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'name',
            'size' => 255,
            'required' => true,
        ]);

        // Create artist collection
        $artists = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Artists',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::read(Role::user($this->getUser()['$id'])),
            ],
        ]);

        // Create artist name attribute
        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $artists['body']['$id']) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'name',
            'size' => 255,
            'required' => true,
        ]);

        // Create relationship
        $response = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $albums['body']['$id']) . '/relationship', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getRelatedIdParam() => $artists['body']['$id'],
            'type' => Database::RELATION_MANY_TO_ONE,
            'twoWay' => true,
            'key' => 'artist',
            'twoWayKey' => 'albums',
        ]);
        $this->assertEquals(202, $response['headers']['status-code']);
        $this->assertEquals('artist', $response['body']['key']);
        $this->assertEquals('relationship', $response['body']['type']);
        $this->assertEquals(false, $response['body']['required']);
        $this->assertEquals(false, $response['body']['array']);
        $this->assertEquals('manyToOne', $response['body']['relationType']);
        $this->assertEquals(true, $response['body']['twoWay']);
        $this->assertEquals('albums', $response['body']['twoWayKey']);
        $this->assertEquals('restrict', $response['body']['onDelete']);

        $this->waitForAttribute($databaseId, $albums['body']['$id'], 'artist');

        $permissions = [
            Permission::read(Role::user($this->getUser()['$id'])),
            Permission::update(Role::user($this->getUser()['$id'])),
            Permission::delete(Role::user($this->getUser()['$id'])),
        ];

        // Create album
        $album = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $albums['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => 'album1',
            'permissions' => $permissions,
            'data' => [
                'name' => 'Album 1',
                'artist' => [
                    '$id' => ID::unique(),
                    'name' => 'Artist 1',
                ],
            ],
        ]);

        $this->assertEquals(201, $album['headers']['status-code']);
        $this->assertEquals('album1', $album['body']['$id']);
        $this->assertEquals('Album 1', $album['body']['name']);
        $this->assertEquals('Artist 1', $album['body']['artist']['name']);
        $this->assertEquals($permissions, $album['body']['$permissions']);
        $this->assertEquals($permissions, $album['body']['artist']['$permissions']);

        $album = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $albums['body']['$id'], 'album1'), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::select(['*', 'artist.name', 'artist.$permissions'])->toString()
            ]
        ]);

        $this->assertEquals(200, $album['headers']['status-code']);
        $this->assertEquals('album1', $album['body']['$id']);
        $this->assertEquals('Album 1', $album['body']['name']);
        $this->assertEquals('Artist 1', $album['body']['artist']['name']);
        $this->assertEquals($permissions, $album['body']['$permissions']);
        $this->assertEquals($permissions, $album['body']['artist']['$permissions']);

        $artist = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $artists['body']['$id'], $album['body']['artist']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::select(['*', 'albums.$id', 'albums.name', 'albums.$permissions'])->toString()
            ]
        ]);

        $this->assertEquals(200, $artist['headers']['status-code']);
        $this->assertEquals('Artist 1', $artist['body']['name']);
        $this->assertEquals($permissions, $artist['body']['$permissions']);
        $this->assertEquals(1, count($artist['body']['albums']));
        $this->assertEquals('album1', $artist['body']['albums'][0]['$id']);
        $this->assertEquals('Album 1', $artist['body']['albums'][0]['name']);
        $this->assertEquals($permissions, $artist['body']['albums'][0]['$permissions']);
    }

    public function testManyToManyRelationship(): void
    {
        if (!$this->getSupportForRelationships()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        $data = $this->setupDatabase();
        $databaseId = $data['databaseId'];

        // Create sports collection
        $sports = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Sports',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::read(Role::user($this->getUser()['$id'])),
            ],
        ]);

        // Create sport name attribute
        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $sports['body']['$id']) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'name',
            'size' => 255,
            'required' => true,
        ]);

        // Create player collection
        $players = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Players',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::read(Role::user($this->getUser()['$id'])),
            ],
        ]);

        // Create player name attribute
        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $players['body']['$id']) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'name',
            'size' => 255,
            'required' => true,
        ]);

        // Create relationship
        $response = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $sports['body']['$id']) . '/relationship', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getRelatedIdParam() => $players['body']['$id'],
            'type' => Database::RELATION_MANY_TO_MANY,
            'twoWay' => true,
            'key' => 'players',
            'twoWayKey' => 'sports',
            'onDelete' => Database::RELATION_MUTATE_SET_NULL,
        ]);

        $this->assertEquals(202, $response['headers']['status-code']);
        $this->assertEquals('players', $response['body']['key']);
        $this->assertEquals('relationship', $response['body']['type']);
        $this->assertEquals(false, $response['body']['required']);
        $this->assertEquals(false, $response['body']['array']);
        $this->assertEquals('manyToMany', $response['body']['relationType']);
        $this->assertEquals(true, $response['body']['twoWay']);
        $this->assertEquals('sports', $response['body']['twoWayKey']);
        $this->assertEquals('setNull', $response['body']['onDelete']);

        $this->waitForAttribute($databaseId, $sports['body']['$id'], 'players');

        $permissions = [
            Permission::read(Role::user($this->getUser()['$id'])),
            Permission::update(Role::user($this->getUser()['$id'])),
        ];

        // Create sport
        $sport = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $sports['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => 'sport1',
            'permissions' => $permissions,
            'data' => [
                'name' => 'Sport 1',
                'players' => [
                    [
                        '$id' => 'player1',
                        'name' => 'Player 1',
                    ],
                    [
                        '$id' => 'player2',
                        'name' => 'Player 2',
                    ]
                ],
            ],
        ]);

        $this->assertEquals(201, $sport['headers']['status-code']);
        $this->assertEquals('sport1', $sport['body']['$id']);
        $this->assertEquals('Sport 1', $sport['body']['name']);
        $this->assertEquals('Player 1', $sport['body']['players'][0]['name']);
        $this->assertEquals('Player 2', $sport['body']['players'][1]['name']);
        $this->assertEquals($permissions, $sport['body']['$permissions']);
        $this->assertEquals($permissions, $sport['body']['players'][0]['$permissions']);
        $this->assertEquals($permissions, $sport['body']['players'][1]['$permissions']);

        $sport = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $sports['body']['$id'], 'sport1'), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::select(['*', 'players.name', 'players.$permissions'])->toString()
            ]
        ]);

        $this->assertEquals(200, $sport['headers']['status-code']);
        $this->assertEquals('sport1', $sport['body']['$id']);
        $this->assertEquals('Sport 1', $sport['body']['name']);
        $this->assertEquals('Player 1', $sport['body']['players'][0]['name']);
        $this->assertEquals('Player 2', $sport['body']['players'][1]['name']);
        $this->assertEquals($permissions, $sport['body']['$permissions']);
        $this->assertEquals($permissions, $sport['body']['players'][0]['$permissions']);
        $this->assertEquals($permissions, $sport['body']['players'][1]['$permissions']);

        $player = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $players['body']['$id'], $sport['body']['players'][0]['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::select(['*', 'sports.$id', 'sports.name', 'sports.$permissions'])->toString()
            ]
        ]);

        $this->assertEquals(200, $player['headers']['status-code']);
        $this->assertEquals('Player 1', $player['body']['name']);
        $this->assertEquals($permissions, $player['body']['$permissions']);
        $this->assertEquals(1, count($player['body']['sports']));
        $this->assertEquals('sport1', $player['body']['sports'][0]['$id']);
        $this->assertEquals('Sport 1', $player['body']['sports'][0]['name']);
        $this->assertEquals($permissions, $player['body']['sports'][0]['$permissions']);
    }

    public function testValidateOperators(): void
    {
        if (!$this->getSupportForRelationships()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        $data = $this->setupOneToManyRelationship();

        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($data['databaseId'], $data['personCollection']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::isNotNull('$id')->toString(),
                Query::select(['*', 'libraries.*'])->toString(),
                Query::startsWith('fullName', 'Stevie')->toString(),
                Query::endsWith('fullName', 'Wonder')->toString(),
                Query::between('$createdAt', '1975-12-06', '2050-12-01')->toString(),
            ],
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals(1, count($response['body'][$this->getRecordResource()]));
        $this->assertNotEmpty($response['body'][$this->getRecordResource()][0]['$id']);
        $this->assertEquals('Stevie Wonder', $response['body'][$this->getRecordResource()][0]['fullName']);
        $this->assertEquals(2, count($response['body'][$this->getRecordResource()][0]['libraries']));

        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($data['databaseId'], $data['personCollection']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::isNotNull('$id')->toString(),
                Query::isNull('fullName')->toString(),
                Query::select(['fullName'])->toString(),
            ],
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertGreaterThanOrEqual(2, count($response['body'][$this->getRecordResource()]));
        $this->assertEquals(null, $response['body'][$this->getRecordResource()][0]['fullName']);
        $this->assertArrayNotHasKey("libraries", $response['body'][$this->getRecordResource()][0]);
        $this->assertArrayHasKey('$databaseId', $response['body'][$this->getRecordResource()][0]);
        $this->assertArrayHasKey($this->getContainerIdResponseKey(), $response['body'][$this->getRecordResource()][0]);
    }

    public function testSelectQueries(): void
    {
        if (!$this->getSupportForRelationships()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        $data = $this->setupOneToManyRelationship();

        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($data['databaseId'], $data['personCollection']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::equal('fullName', ['Stevie Wonder'])->toString(),
                Query::select(['fullName'])->toString(),
            ],
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertArrayNotHasKey('libraries', $response['body'][$this->getRecordResource()][0]);
        $this->assertArrayHasKey('$databaseId', $response['body'][$this->getRecordResource()][0]);
        $this->assertArrayHasKey($this->getContainerIdResponseKey(), $response['body'][$this->getRecordResource()][0]);

        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($data['databaseId'], $data['personCollection']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::select(['libraries.*', '$id'])->toString(),
            ],
        ]);
        $document = $response['body'][$this->getRecordResource()][0];
        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertArrayHasKey('libraries', $document);
        $this->assertArrayHasKey('$databaseId', $document);
        $this->assertArrayHasKey($this->getContainerIdResponseKey(), $document);

        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($data['databaseId'], $data['personCollection'], $document['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::select(['fullName', '$id'])->toString(),
            ],
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertArrayHasKey('fullName', $response['body']);
        $this->assertArrayNotHasKey('libraries', $response['body']);
    }

    /**
     * @throws \Utopia\Database\Exception
     * @throws \Utopia\Database\Exception\Query
     */
    public function testOrQueries(): void
    {
        // Create database
        $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'Or queries'
        ]);

        $this->assertNotEmpty($database['body']['$id']);
        $this->assertEquals(201, $database['headers']['status-code']);
        $this->assertEquals('Or queries', $database['body']['name']);

        $databaseId = $database['body']['$id'];

        // Create Collection
        $presidents = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'USA Presidents',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $this->assertEquals(201, $presidents['headers']['status-code']);
        $this->assertEquals($presidents['body']['name'], 'USA Presidents');

        // Create Attributes (only for adapters that support attributes)
        if ($this->getSupportForAttributes()) {
            $firstName = $this->createAttribute($databaseId, $presidents['body']['$id'], 'string', [
                'key' => 'first_name',
                'size' => 256,
                'required' => true,
            ]);
            $this->assertEquals(202, $firstName['headers']['status-code']);

            $lastName = $this->createAttribute($databaseId, $presidents['body']['$id'], 'string', [
                'key' => 'last_name',
                'size' => 256,
                'required' => true,
            ]);
            $this->assertEquals(202, $lastName['headers']['status-code']);

            // Wait for worker
            $this->waitForAllAttributes($databaseId, $presidents['body']['$id']);
        }

        $document1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $presidents['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'first_name' => 'Donald',
                'last_name' => 'Trump',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $document1['headers']['status-code']);

        $document2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $presidents['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'first_name' => 'George',
                'last_name' => 'Bush',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $document2['headers']['status-code']);

        $document3 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $presidents['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'first_name' => 'Joe',
                'last_name' => 'Biden',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);

        $this->assertEquals(201, $document3['headers']['status-code']);

        $documents = $this->client->call(
            Client::METHOD_GET,
            $this->getRecordUrl($databaseId, $presidents['body']['$id']),
            array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()),
            [
                'queries' => [
                    Query::select(['first_name', 'last_name'])->toString(),
                    Query::or([
                        Query::equal('first_name', ['Donald']),
                        Query::equal('last_name', ['Bush'])
                    ])->toString(),
                    Query::limit(999)->toString(),
                    Query::offset(0)->toString()
                ],
            ]
        );

        $this->assertEquals(200, $documents['headers']['status-code']);
        $this->assertCount(2, $documents['body'][$this->getRecordResource()]);
    }

    /**
     * @return void
     * @throws \Exception
     */
    public function testUpdateWithExistingRelationships(): void
    {
        if (!$this->getSupportForRelationships()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        $data = $this->setupDatabase();
        $databaseId = $data['databaseId'];

        $collection1 = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Collection1',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::read(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $collection2 = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Collection2',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::read(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $collection1 = $collection1['body']['$id'];
        $collection2 = $collection2['body']['$id'];

        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collection1) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'name',
            'size' => '49',
            'required' => true,
        ]);

        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collection2) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'name',
            'size' => '49',
            'required' => true,
        ]);

        $this->waitForAttribute($databaseId, $collection1, 'name');
        $this->waitForAttribute($databaseId, $collection2, 'name');

        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collection1) . '/relationship', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getRelatedIdParam() => $collection2,
            'type' => Database::RELATION_ONE_TO_MANY,
            'twoWay' => true,
            'key' => 'collection2'
        ]);

        $this->waitForAttribute($databaseId, $collection1, 'collection2');

        $document = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collection1), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id']
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'name' => 'Document 1',
                'collection2' => [
                    [
                        'name' => 'Document 2',
                    ],
                ],
            ],
        ]);

        $update = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collection1, $document['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id']
        ], $this->getHeaders()), [
            'data' => [
                'name' => 'Document 1 Updated',
            ],
        ]);

        $this->assertEquals(200, $update['headers']['status-code']);
    }

    public function testTimeout(): void
    {
        $data = $this->setupDatabase();
        $databaseId = $data['databaseId'];

        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Slow Queries',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $this->assertEquals(201, $collection['headers']['status-code']);

        $data = [
            '$id' => $collection['body']['$id'],
            'databaseId' => $databaseId,
        ];

        // Create attribute only on adapters that support attributes; DocumentsDB can still store the field schemalessly
        if ($this->getSupportForAttributes()) {
            $longtext = $this->createAttribute($data['databaseId'], $data['$id'], 'string', [
                'key' => 'longtext',
                'size' => 100000000,
                'required' => false,
                'default' => null,
            ]);

            $this->assertEquals(202, $longtext['headers']['status-code']);

            $this->waitForAttribute($data['databaseId'], $data['$id'], 'longtext');
        }

        for ($i = 0; $i < 10; $i++) {
            $this->client->call(Client::METHOD_POST, $this->getRecordUrl($data['databaseId'], $data['$id']), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()), [
                $this->getRecordIdParam() => ID::unique(),
                'data' => [
                    'longtext' => file_get_contents(__DIR__ . '/../../../resources/longtext.txt'),
                ],
                'permissions' => [
                    Permission::read(Role::user($this->getUser()['$id'])),
                    Permission::update(Role::user($this->getUser()['$id'])),
                    Permission::delete(Role::user($this->getUser()['$id'])),
                ]
            ]);
        }

        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($data['databaseId'], $data['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-timeout' => 1,
        ], $this->getHeaders()), [
            'queries' => [
                Query::notEqual('longtext', 'appwrite')->toString(),
            ],
        ]);

        $this->assertEquals(408, $response['headers']['status-code']);

        $this->client->call(Client::METHOD_DELETE, $this->getContainerUrl($data['databaseId'], $data['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));
    }

    /**
     * @throws \Exception
     */
    public function testIncrementAttribute(): void
    {
        $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'CounterDatabase'
        ]);
        $databaseId = $database['body']['$id'];

        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'CounterCollection',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::read(Role::user($this->getUser()['$id'])),
            ],
        ]);
        $collectionId = $collection['body']['$id'];

        // Add integer attribute only when supported; schemaless adapters (e.g. documentsdb)
        // can still store the field without a predefined attribute.
        if ($this->getSupportForAttributes()) {
            $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/integer', array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                'key' => 'count',
                'required' => true,
            ]);

            $this->waitForAttribute($databaseId, $collectionId, 'count');
        }

        // Create document with initial count = 5
        $doc = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'count' => 5
            ],
            'permissions' => [
                Permission::read(Role::any()),
                Permission::update(Role::any()),
            ],
        ]);
        $this->assertEquals(201, $doc['headers']['status-code']);

        $docId = $doc['body']['$id'];

        // Increment by default 1
        $inc = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $docId) . "/count/increment", array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ]));
        $this->assertEquals(200, $inc['headers']['status-code']);
        $this->assertEquals(6, $inc['body']['count']);
        $this->assertEquals($collectionId, $inc['body'][$this->getContainerIdResponseKey()]);
        $this->assertEquals($databaseId, $inc['body']['$databaseId']);

        // Verify count = 6
        $get = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId, $docId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));
        $this->assertEquals(6, $get['body']['count']);

        // Increment by custom value 4
        $inc2 = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $docId) . "/count/increment", array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ]), [
            'value' => 4
        ]);
        $this->assertEquals(200, $inc2['headers']['status-code']);
        $this->assertEquals(10, $inc2['body']['count']);

        $get2 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId, $docId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));
        $this->assertEquals(10, $get2['body']['count']);

        // Test max limit exceeded
        $err = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $docId) . "/count/increment", array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ]), ['max' => 8]);
        $this->assertEquals(400, $err['headers']['status-code']);

        // Test attribute not found
        $notFound = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $docId) . "/unknown/increment", array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ]));
        $this->assertEquals(
            $this->getSupportForAttributes() ? 404 : 200,
            $notFound['headers']['status-code']
        );

        // Test increment with value 0
        $inc3 = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $docId) . "/count/increment", array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ]), [
            'value' => 0
        ]);
        $this->assertEquals(400, $inc3['headers']['status-code']);
    }

    public function testDecrementAttribute(): void
    {
        $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'CounterDatabase'
        ]);

        $databaseId = $database['body']['$id'];

        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'CounterCollection',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::read(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $collectionId = $collection['body']['$id'];

        // Add integer attribute only when supported; schemaless adapters can still
        // store the field without a predefined attribute.
        if ($this->getSupportForAttributes()) {
            $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/integer', array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                'key' => 'count',
                'required' => true,
            ]);

            $this->waitForAttribute($databaseId, $collectionId, 'count');
        }

        // Create document with initial count = 10
        $doc = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => ['count' => 10],
            'permissions' => [
                Permission::read(Role::any()),
                Permission::update(Role::any()),
            ],
        ]);

        $documentId = $doc['body']['$id'];

        // Decrement by default 1 (count = 10 -> 9)
        $dec = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $documentId) . '/count/decrement', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ]));
        $this->assertEquals(200, $dec['headers']['status-code']);
        $this->assertEquals(9, $dec['body']['count']);
        $this->assertEquals($collectionId, $dec['body'][$this->getContainerIdResponseKey()]);
        $this->assertEquals($databaseId, $dec['body']['$databaseId']);

        $get = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId, $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));
        $this->assertEquals(9, $get['body']['count']);

        // Decrement by custom value 3 (count 9 -> 6)
        $dec2 = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $documentId) . '/count/decrement', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ]), [
            'value' => 3
        ]);
        $this->assertEquals(200, $dec2['headers']['status-code']);
        $this->assertEquals(6, $dec2['body']['count']);

        $get2 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId, $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));
        $this->assertEquals(6, $get2['body']['count']);

        // Test min limit exceeded
        $err = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $documentId) . '/count/decrement', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ]), ['min' => 7]);
        $this->assertEquals(400, $err['headers']['status-code']);

        // Test min limit exceeded with custom value
        $err = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $documentId) . '/count/decrement', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ]), [
            'value' => 3,
            'min' => 5,
        ]);
        $this->assertEquals(400, $err['headers']['status-code']);

        // Test min limit 0
        $err = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $documentId) . '/count/decrement', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ]), [
            'value' => 10,
            'min' => 0,
        ]);
        $this->assertEquals(400, $err['headers']['status-code']);

        // Test type error on non-numeric attribute
        $typeErr = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $documentId) . '/count/decrement', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ]), ['value' => 'not-a-number']);
        $this->assertEquals(400, $typeErr['headers']['status-code']);

        // Test decrement with value 0
        $inc3 = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $documentId) . "/count/increment", array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ]), [
            'value' => 0
        ]);
        $this->assertEquals(400, $inc3['headers']['status-code']);
    }

    public function testSpatialPointAttributes(): void
    {

        if (!$this->getSupportForSpatials()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        $database = $this->client->call(Client::METHOD_POST, '/databases', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'Spatial Point Test Database'
        ]);

        $databaseId = $database['body']['$id'];

        // Create collection with spatial and non-spatial attributes
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Spatial Point Collection',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $collectionId = $collection['body']['$id'];

        // Create string attribute
        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'name',
            'size' => 256,
            'required' => true,
        ]);

        // Create point attribute - handle both 201 (created) and 200 (already exists)
        $response = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/point', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'location',
            'required' => true,
        ]);

        $this->assertEquals(202, $response['headers']['status-code']);

        $this->waitForAllAttributes($databaseId, $collectionId);

        // Test 1: Create document with point attribute
        $response = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'name' => 'Test Location',
                'location' => [40.7128, -74.0060] // New York coordinates
            ]
        ]);

        $this->assertEquals(201, $response['headers']['status-code']);
        $this->assertEquals([40.7128, -74.0060], $response['body']['location']);
        $documentId = $response['body']['$id'];

        // Test 2: Read document with point attribute
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId, $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals([40.7128, -74.0060], $response['body']['location']);

        // Test 3: Update document with new point coordinates
        $response = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'location' => [40.7589, -73.9851] // Times Square coordinates
            ]
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals([40.7589, -73.9851], $response['body']['location']);

        // Test 4: Upsert document with point attribute
        $response = $this->client->call(Client::METHOD_PUT, $this->getContainerUrl($databaseId, $collectionId) . '/' . $this->getRecordResource() . '/' . ID::unique(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'name' => 'Upserted Location',
                'location' => [34.0522, -80] // Los Angeles coordinates
            ]
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals([34.0522, -80], $response['body']['location']);

        // Test 5: Create document without permissions (should fail)
        $response = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'name' => 'Unauthorized Location',
                'location' => [0, 0]
            ]
        ]);

        $this->assertEquals(401, $response['headers']['status-code']);

        // Cleanup
        $this->client->call(Client::METHOD_DELETE, $this->getContainerUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->client->call(Client::METHOD_DELETE, $this->getDatabaseUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));
    }

    public function testSpatialLineAttributes(): void
    {

        if (!$this->getSupportForSpatials()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        $database = $this->client->call(Client::METHOD_POST, '/databases', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'Spatial Line Test Database'
        ]);

        $databaseId = $database['body']['$id'];

        // Create collection with spatial and non-spatial attributes
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Spatial Line Collection',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $collectionId = $collection['body']['$id'];

        // Create integer attribute
        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/integer', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'distance',
            'required' => true,
        ]);

        // Create line attribute
        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/line', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'route',
            'required' => true,
        ]);

        $this->waitForAllAttributes($databaseId, $collectionId);

        // Test 1: Create document with line attribute
        $response = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'distance' => 100,
                'route' => [[40.7128, -74.0060], [40.7589, -73.9851]] // Line from Downtown to Times Square
            ]
        ]);

        $this->assertEquals(201, $response['headers']['status-code']);
        $this->assertEquals([[40.7128, -74.0060], [40.7589, -73.9851]], $response['body']['route']);
        $documentId = $response['body']['$id'];

        // Test 2: Read document with line attribute
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId, $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals([[40.7128, -74.0060], [40.7589, -73.9851]], $response['body']['route']);

        // Test 3: Update document with new line coordinates
        $response = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'route' => [[40.7128, -74.0060], [40.7589, -73.9851], [40.7505, -73.9934]] // Extended route
            ]
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals([[40.7128, -74.0060], [40.7589, -73.9851], [40.7505, -73.9934]], $response['body']['route']);

        // Test 4: Upsert document with line attribute
        $response = $this->client->call(Client::METHOD_PUT, $this->getContainerUrl($databaseId, $collectionId) . '/' . $this->getRecordResource() . '/' . ID::unique(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'distance' => 200,
                'route' => [[34.0522, -80], [34.0736, -90]] // LA route
            ]
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals([[34.0522, -80], [34.0736, -90]], $response['body']['route']);

        // Test 5: Delete document
        $response = $this->client->call(Client::METHOD_DELETE, $this->getRecordUrl($databaseId, $collectionId, $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        $this->assertEquals(204, $response['headers']['status-code']);

        // Test 6: Verify document is deleted
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId, $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        $this->assertEquals(404, $response['headers']['status-code']);

        // Cleanup
        $this->client->call(Client::METHOD_DELETE, $this->getContainerUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->client->call(Client::METHOD_DELETE, $this->getDatabaseUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));
    }

    public function testSpatialPolygonAttributes(): void
    {

        if (!$this->getSupportForSpatials()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        $database = $this->client->call(Client::METHOD_POST, '/databases', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'Spatial Polygon Test Database'
        ]);

        $databaseId = $database['body']['$id'];

        // Create collection with spatial and non-spatial attributes
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Spatial Polygon Collection',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $collectionId = $collection['body']['$id'];

        // Create boolean attribute
        $boolResponse = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/boolean', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'active',
            'required' => true,
        ]);

        $this->assertEquals(202, $boolResponse['headers']['status-code']);

        // Create polygon attribute
        $polyResponse = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/polygon', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'area',
            'required' => true,
        ]);

        $this->assertEquals(202, $polyResponse['headers']['status-code']);

        $this->waitForAllAttributes($databaseId, $collectionId);

        // Test 1: Create document with polygon attribute
        $response = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'active' => true,
                'area' => [[[40.7128, -74.0060], [40.7589, -74.0060], [40.7589, -73.9851], [40.7128, -73.9851], [40.7128, -74.0060]]] // Manhattan area
            ]
        ]);

        $this->assertEquals(201, $response['headers']['status-code']);
        $this->assertEquals([[[40.7128, -74.0060], [40.7589, -74.0060], [40.7589, -73.9851], [40.7128, -73.9851], [40.7128, -74.0060]]], $response['body']['area']);
        $documentId = $response['body']['$id'];

        // Test 2: Read document with polygon attribute
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId, $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals([[[40.7128, -74.0060], [40.7589, -74.0060], [40.7589, -73.9851], [40.7128, -73.9851], [40.7128, -74.0060]]], $response['body']['area']);

        // Test 3: Update document with new polygon coordinates
        $response = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'area' => [[[40.7128, -74.0060], [40.7589, -74.0060], [40.7589, -73.9851], [40.7128, -73.9851], [40.7505, -73.9934], [40.7128, -74.0060]]] // Extended area
            ]
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals([[[40.7128, -74.0060], [40.7589, -74.0060], [40.7589, -73.9851], [40.7128, -73.9851], [40.7505, -73.9934], [40.7128, -74.0060]]], $response['body']['area']);

        // Test 4: Upsert document with polygon attribute
        $response = $this->client->call(Client::METHOD_PUT, $this->getContainerUrl($databaseId, $collectionId) . '/' . $this->getRecordResource() . '/' . ID::unique(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'active' => false,
                'area' => [[[34.0522, -80], [34.0736, -80], [34.0736, -90], [34.0522, -90], [34.0522, -80]]] // LA area
            ]
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals([[[34.0522, -80], [34.0736, -80], [34.0736, -90], [34.0522, -90], [34.0522, -80]]], $response['body']['area']);

        // Test 5: Create document without required polygon attribute (should fail)
        $response = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'active' => true
                // Missing required 'area' attribute
            ]
        ]);

        $this->assertEquals(400, $response['headers']['status-code']);

        // Cleanup
        $this->client->call(Client::METHOD_DELETE, $this->getContainerUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->client->call(Client::METHOD_DELETE, $this->getDatabaseUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));
    }

    public function testSpatialAttributesMixedCollection(): void
    {

        if (!$this->getSupportForSpatials()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        $database = $this->client->call(Client::METHOD_POST, '/databases', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'Mixed Spatial Test Database'
        ]);

        $databaseId = $database['body']['$id'];

        // Create collection with multiple spatial and non-spatial attributes
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Mixed Spatial Collection',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $collectionId = $collection['body']['$id'];

        // Create multiple attributes
        $nameAttr = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'name',
            'size' => 256,
            'required' => true,
        ]);
        $this->assertEquals(202, $nameAttr['headers']['status-code']);

        $centerAttr = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/point', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'center',
            'required' => true,
        ]);
        $this->assertEquals(202, $centerAttr['headers']['status-code']);

        $boundaryAttr = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/line', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'boundary',
            'required' => false,
        ]);
        $this->assertEquals(202, $boundaryAttr['headers']['status-code']);

        $coverageAttr = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/polygon', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'coverage',
            'required' => true,
        ]);
        $this->assertEquals(202, $coverageAttr['headers']['status-code']);

        $this->waitForAllAttributes($databaseId, $collectionId);

        // Test 1: Create document with all spatial attributes
        $response = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'name' => 'Central Park',
                'center' => [40.7829, -73.9654],
                'boundary' => [[40.7649, -73.9814], [40.8009, -73.9494]],
                'coverage' => [[[40.7649, -73.9814], [40.8009, -73.9814], [40.8009, -73.9494], [40.7649, -73.9494], [40.7649, -73.9814]]]
            ]
        ]);

        $this->assertEquals(201, $response['headers']['status-code']);
        $this->assertEquals([40.7829, -73.9654], $response['body']['center']);
        $this->assertEquals([[40.7649, -73.9814], [40.8009, -73.9494]], $response['body']['boundary']);
        $this->assertEquals([[[40.7649, -73.9814], [40.8009, -73.9814], [40.8009, -73.9494], [40.7649, -73.9494], [40.7649, -73.9814]]], $response['body']['coverage']);
        $documentId = $response['body']['$id'];

        // Test 2: Update document with new spatial data
        $response = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $documentId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'center' => [40.7505, -73.9934],
                'boundary' => [[40.7305, -74.0134], [40.7705, -73.9734]],
                'coverage' => [[[40.7305, -74.0134], [40.7705, -74.0134], [40.7705, -73.9734], [40.7305, -73.9734], [40.7305, -74.0134]]]
            ]
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals([40.7505, -73.9934], $response['body']['center']);
        $this->assertEquals([[40.7305, -74.0134], [40.7705, -73.9734]], $response['body']['boundary']);
        $this->assertEquals([[[40.7305, -74.0134], [40.7705, -74.0134], [40.7705, -73.9734], [40.7305, -73.9734], [40.7305, -74.0134]]], $response['body']['coverage']);

        // Test 3: Create document with minimal required attributes
        $response = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'name' => 'Minimal Location',
                'center' => [0, 0],
                'coverage' => [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]]
            ]
        ]);

        $this->assertEquals(201, $response['headers']['status-code']);
        $this->assertEquals([0, 0], $response['body']['center']);

        // Test 4: Test permission validation - create without user context
        $response = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'name' => 'Unauthorized Location',
                'center' => [0, 0],
                'coverage' => [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]]
            ]
        ]);

        $this->assertEquals(401, $response['headers']['status-code']);

        // Cleanup
        $this->client->call(Client::METHOD_DELETE, $this->getContainerUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->client->call(Client::METHOD_DELETE, $this->getDatabaseUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));
    }

    public function testUpdateSpatialAttributes(): void
    {

        if (!$this->getSupportForSpatials()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        $database = $this->client->call(Client::METHOD_POST, '/databases', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'Update Spatial Attributes Test Database'
        ]);

        $databaseId = $database['body']['$id'];

        // Create collection with spatial attributes
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Update Spatial Attributes Collection',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $collectionId = $collection['body']['$id'];

        // Create string attribute
        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'name',
            'size' => 256,
            'required' => true,
        ]);

        // Create point attribute
        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/point', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'location',
            'required' => true,
        ]);

        // Create line attribute
        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/line', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'route',
            'required' => false,
        ]);

        // Create polygon attribute
        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/polygon', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'area',
            'required' => true,
        ]);

        $this->waitForAllAttributes($databaseId, $collectionId);

        // Test 1: Update point attribute - change required status
        $response = $this->client->call(Client::METHOD_PATCH, $this->getSchemaUrl($databaseId, $collectionId) . '/point/location', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'required' => false,
            'default' => null,
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals(false, $response['body']['required']);

        // Test 2: Update line attribute - change required status and add default value
        $response = $this->client->call(Client::METHOD_PATCH, $this->getSchemaUrl($databaseId, $collectionId) . '/line/route', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'required' => false,
            'default' => [[0, 0], [1, 1]],
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals(false, $response['body']['required']);
        $this->assertEquals([[0, 0], [1, 1]], $response['body']['default']);

        // Test 3: Update polygon attribute - change key name
        $response = $this->client->call(Client::METHOD_PATCH, $this->getSchemaUrl($databaseId, $collectionId) . '/polygon/area', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'newKey' => 'coverage',
            'default' => null,
            'required' => false
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals('coverage', $response['body']['key']);

        // Test 4: Update point attribute - add default value
        $response = $this->client->call(Client::METHOD_PATCH, $this->getSchemaUrl($databaseId, $collectionId) . '/point/location', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'default' => [0, 0],
            'required' => false
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals([0, 0], $response['body']['default']);

        // Test 5: Verify attribute updates by creating a document
        $response = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'name' => 'Test Location',
                'coverage' => [[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]]
            ]
        ]);

        $this->assertEquals(201, $response['headers']['status-code']);
        $this->assertEquals([0, 0], $response['body']['location']); // Should use default value
        $this->assertEquals([[0, 0], [1, 1]], $response['body']['route']); // Should use default value

        // Cleanup
        $this->client->call(Client::METHOD_DELETE, $this->getContainerUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->client->call(Client::METHOD_DELETE, $this->getDatabaseUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));
    }

    public function testSpatialQuery(): void
    {

        if (!$this->getSupportForSpatials()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        $database = $this->client->call(Client::METHOD_POST, '/databases', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'Spatial Query Test Database'
        ]);

        $this->assertNotEmpty($database['body']['$id']);
        $databaseId = $database['body']['$id'];

        // Create collection with spatial attributes
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Spatial Query Collection',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::any()),
                Permission::read(Role::any()),
                Permission::delete(Role::any()),
                Permission::update(Role::any()),
            ],
        ]);

        $this->assertEquals(201, $collection['headers']['status-code']);
        $collectionId = $collection['body']['$id'];

        // Create string attribute
        $nameAttribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'name',
            'size' => 256,
            'required' => true,
        ]);

        $this->assertEquals(202, $nameAttribute['headers']['status-code']);

        // Create point attribute
        $pointAttribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/point', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'pointAttr',
            'required' => true,
        ]);

        $this->assertEquals(202, $pointAttribute['headers']['status-code']);

        // Create line attribute
        $lineAttribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/line', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'lineAttr',
            'required' => true,
        ]);

        $this->assertEquals(202, $lineAttribute['headers']['status-code']);

        // Create polygon attribute
        $polygonAttribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/polygon', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'polyAttr',
            'required' => true,
        ]);

        $this->assertEquals(202, $polygonAttribute['headers']['status-code']);

        // Wait for attributes to be created
        $this->waitForAllAttributes($databaseId, $collectionId);

        // Create test documents with spatial data
        $documents = [
            [
                '$id' => 'doc1',
                'name' => 'Test Document 1',
                'pointAttr' => [6.0, 6.0],
                'lineAttr' => [[1.0, 1.0], [1.1,1.1] , [2.0, 2.0]],
                'polyAttr' => [[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0], [0.0, 0.0]]]
            ],
            [
                '$id' => 'doc2',
                'name' => 'Test Document 2',
                'pointAttr' => [7.0, 6.0],
                'lineAttr' => [[10.0, 10.0], [20.0, 20.0]],
                'polyAttr' => [[[20.0, 20.0], [30.0, 20.0], [30.0, 30.0], [20.0, 30.0], [20.0, 20.0]]]
            ],
            [
                '$id' => 'doc3',
                'name' => 'Test Document 3',
                'pointAttr' => [25.0, 25.0],
                'lineAttr' => [[25.0, 25.0], [35.0, 35.0]],
                'polyAttr' => [[[40.0, 40.0], [50.0, 40.0], [50.0, 50.0], [40.0, 50.0], [40.0, 40.0]]]
            ]
        ];

        foreach ($documents as $doc) {
            $response = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()), [
                $this->getRecordIdParam() => $doc['$id'],
                'data' => [
                    'name' => $doc['name'],
                    'pointAttr' => $doc['pointAttr'],
                    'lineAttr' => $doc['lineAttr'],
                    'polyAttr' => $doc['polyAttr']
                ]
            ]);

            $this->assertEquals(201, $response['headers']['status-code']);
        }

        // Test 1: Equality on non-spatial attribute (name)
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::equal('name', ['Test Document 1'])->toString()]
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertCount(1, $response['body'][$this->getRecordResource()]);
        $this->assertEquals('doc1', $response['body'][$this->getRecordResource()][0]['$id']);

        // Test 3: Polygon attribute queries
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::equal('polyAttr', [[[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0], [0.0, 0.0]]]])->toString()]
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertCount(1, $response['body'][$this->getRecordResource()]);
        $this->assertEquals('doc1', $response['body'][$this->getRecordResource()][0]['$id']);

        // Test 4: Not equal queries
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::notEqual('pointAttr', [[6.0, 6.0]])->toString()]
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertCount(2, $response['body'][$this->getRecordResource()]);

        // Test 4.1: contains on line (point on line)
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::contains('lineAttr', [[1.1, 1.1]])->toString()]
        ]);
        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertCount(1, $response['body'][$this->getRecordResource()]);
        $this->assertEquals('doc1', $response['body'][$this->getRecordResource()][0]['$id']);


        // Test 4.2: notContains on polygon (point outside all polygons)
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::notContains('polyAttr', [[15.0, 15.0]])->toString()]
        ]);
        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals(3, $response['body']['total']);

        // Test 4.3: intersects on polygon (point inside doc1 polygon)
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::intersects('polyAttr', [5.0, 5.0])->toString()]
        ]);
        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals(1, $response['body']['total']);
        $this->assertEquals('doc1', $response['body'][$this->getRecordResource()][0]['$id']);

        // Test 4.4: notIntersects on polygon (point outside all polygons)
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::notIntersects('polyAttr', [60.0, 60.0])->toString()]
        ]);
        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals(3, $response['body']['total']);

        // Test 4.5: overlaps on polygon (polygon overlapping doc1)
        $overlapPoly = [[[5.0, 5.0], [12.0, 5.0], [12.0, 12.0], [5.0, 12.0], [5.0, 5.0]]];
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::overlaps('polyAttr', $overlapPoly)->toString()]
        ]);
        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals(1, $response['body']['total']);
        $this->assertEquals('doc1', $response['body'][$this->getRecordResource()][0]['$id']);

        // Test 4.6: notOverlaps on polygon (polygon that overlaps none)
        $noOverlapPoly = [[[60.0, 60.0], [70.0, 60.0], [70.0, 70.0], [60.0, 70.0], [60.0, 60.0]]];
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::notOverlaps('polyAttr', $noOverlapPoly)->toString()]
        ]);
        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals(3, $response['body']['total']);

        // Test 4.7: distance (equals) on point
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::distanceEqual('pointAttr', [6.0, 6.0], 1.0)->toString()]
        ]);
        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals(1, $response['body']['total']);
        $this->assertEquals('doc2', $response['body'][$this->getRecordResource()][0]['$id']);

        // Test 4.8: notDistance (outside radius) on point
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::distanceNotEqual('pointAttr', [6.0, 6.0], 1.0)->toString()]
        ]);
        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals(2, $response['body']['total']);

        // Test 4.9: distanceGreaterThan
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::distanceGreaterThan('pointAttr', [6.0, 6.0], 5.0)->toString()]
        ]);
        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals(1, $response['body']['total']);

        // Test 4.10: distanceLessThan
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::distanceLessThan('pointAttr', [6.0, 6.0], 0.5)->toString()]
        ]);
        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals(1, $response['body']['total']);

        // Test 4.11: crosses on line (query line crosses doc1 line)
        $crossLine = [[1.0, 2.0], [2.0, 1.0]];
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::crosses('lineAttr', $crossLine)->toString()]
        ]);
        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals(1, $response['body']['total']);
        $this->assertEquals('doc1', $response['body'][$this->getRecordResource()][0]['$id']);

        // Test 4.12: notCrosses on line (query line does not cross any stored lines)
        $nonCrossLine = [[0.0, 1.0], [0.0, 2.0]];
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::notCrosses('lineAttr', $nonCrossLine)->toString()]
        ]);
        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals(3, $response['body']['total']);

        // Test 4.13: touches on polygon (query polygon touches doc1 polygon at corner)
        $touchPoly = [[[10.0, 10.0], [20.0, 10.0], [20.0, 20.0], [10.0, 20.0], [10.0, 10.0]]];
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::touches('polyAttr', $touchPoly)->toString()]
        ]);
        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals(2, $response['body']['total']);
        $this->assertEquals('doc1', $response['body'][$this->getRecordResource()][0]['$id']);

        // Test 4.14: notTouches on polygon (polygon far away should not touch)
        $farPoly = [[[60.0, 60.0], [70.0, 60.0], [70.0, 70.0], [60.0, 70.0], [60.0, 60.0]]];
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::notTouches('polyAttr', $farPoly)->toString()]
        ]);
        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertEquals(3, $response['body']['total']);

        // Test 5: Select specific attributes
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::select(['name', 'pointAttr'])->toString()]
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertCount(3, $response['body'][$this->getRecordResource()]);

        foreach ($response['body'][$this->getRecordResource()] as $doc) {
            $this->assertArrayHasKey('name', $doc);
            $this->assertArrayHasKey('pointAttr', $doc);
            $this->assertArrayNotHasKey('lineAttr', $doc);
            $this->assertArrayNotHasKey('polyAttr', $doc);
        }

        // Test 6: Order by name
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::orderAsc('name')->toString()]
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertCount(3, $response['body'][$this->getRecordResource()]);
        $this->assertEquals('Test Document 1', $response['body'][$this->getRecordResource()][0]['name']);
        $this->assertEquals('Test Document 2', $response['body'][$this->getRecordResource()][1]['name']);
        $this->assertEquals('Test Document 3', $response['body'][$this->getRecordResource()][2]['name']);

        // Test 7: Limit results
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::limit(2)->toString()]
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertCount(2, $response['body'][$this->getRecordResource()]);

        // Test 8: Offset results
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::offset(1)->toString(), Query::limit(2)->toString()]
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertCount(2, $response['body'][$this->getRecordResource()]);

        // Test 9: Complex query with multiple conditions
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::select(['name', 'pointAttr'])->toString(),
                Query::orderAsc('name')->toString(),
                Query::limit(1)->toString()
            ]
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertCount(1, $response['body'][$this->getRecordResource()]);
        $this->assertEquals('Test Document 1', $response['body'][$this->getRecordResource()][0]['name']);

        // Test 11: Query with no results
        $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::equal('name', ['Non-existent Document'])->toString()]
        ]);

        $this->assertEquals(200, $response['headers']['status-code']);
        $this->assertCount(0, $response['body'][$this->getRecordResource()]);

        // Cleanup
        $this->client->call(Client::METHOD_DELETE, $this->getContainerUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->client->call(Client::METHOD_DELETE, $this->getDatabaseUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));
    }

    public function testSpatialRelationshipOneToOne(): void
    {
        if (!$this->getSupportForRelationships() || !$this->getSupportForSpatials()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        $database = $this->client->call(Client::METHOD_POST, '/databases', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'Spatial OneToOne Test DB'
        ]);

        $databaseId = $database['body']['$id'];

        $place = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Place',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ],
        ]);
        $placeId = $place['body']['$id'];

        $location = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Location',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ],
        ]);
        $locationId = $location['body']['$id'];

        // attributes
        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $placeId) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'name',
            'size' => 255,
            'required' => true,
        ]);

        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $locationId) . '/point', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'coordinates',
            'required' => true,
        ]);

        $this->waitForAttribute($databaseId, $locationId, 'coordinates');

        // relationship: place.oneToOne -> location
        $relation = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $placeId) . '/relationship', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getRelatedIdParam() => $locationId,
            'type' => Database::RELATION_ONE_TO_ONE,
            'key' => 'location',
            'twoWay' => true,
            'twoWayKey' => 'place',
            'onDelete' => Database::RELATION_MUTATE_CASCADE,
        ]);
        $this->assertEquals(202, $relation['headers']['status-code']);

        $this->waitForAttribute($databaseId, $placeId, 'location');

        // create doc with nested spatial related doc
        $doc = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $placeId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'name' => 'Museum',
                'location' => [
                    '$id' => ID::unique(),
                    'coordinates' => [40.7794, -73.9632],
                ],
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $doc['headers']['status-code']);
        $this->assertEquals([40.7794, -73.9632], $doc['body']['location']['coordinates']);

        // fetch with select to ensure relationship shape
        $fetched = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $placeId) . '/' . $this->getRecordResource() . '/' . $doc['body']['$id'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::select(['name', 'location.coordinates'])->toString()
            ]
        ]);
        $this->assertEquals(200, $fetched['headers']['status-code']);
        $this->assertEquals([40.7794, -73.9632], $fetched['body']['location']['coordinates']);

        // cleanup
        $this->client->call(Client::METHOD_DELETE, $this->getContainerUrl($databaseId, $placeId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));
        $this->client->call(Client::METHOD_DELETE, $this->getContainerUrl($databaseId, $locationId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));
        $this->client->call(Client::METHOD_DELETE, $this->getDatabaseUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));
    }

    public function testSpatialRelationshipOneToMany(): void
    {
        if (!$this->getSupportForRelationships() || !$this->getSupportForSpatials()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        $database = $this->client->call(Client::METHOD_POST, '/databases', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'Spatial OneToMany Test DB'
        ]);
        $databaseId = $database['body']['$id'];

        $person = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Person',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ],
        ]);
        $personId = $person['body']['$id'];

        $visit = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Visit',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ],
        ]);
        $visitId = $visit['body']['$id'];

        // attributes
        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $personId) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'fullName',
            'size' => 255,
            'required' => true,
        ]);
        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $visitId) . '/point', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'point',
            'required' => true,
        ]);

        $this->waitForAttribute($databaseId, $visitId, 'point');

        // relationship person.oneToMany -> visit
        $rel = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $personId) . '/relationship', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getRelatedIdParam() => $visitId,
            'type' => Database::RELATION_ONE_TO_MANY,
            'key' => 'visits',
            'twoWay' => true,
            'twoWayKey' => 'person',
        ]);
        $this->assertEquals(202, $rel['headers']['status-code']);

        $this->waitForAttribute($databaseId, $personId, 'visits');

        $personDoc = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $personId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => 'person-spatial-1',
            'data' => [
                'fullName' => 'Alice',
                'visits' => [
                    [ '$id' => 'visit-1', 'point' => [40.7589, -73.9851] ],
                    [ '$id' => 'visit-2', 'point' => [40.7505, -73.9934] ],
                ],
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $personDoc['headers']['status-code']);
        $this->assertCount(2, $personDoc['body']['visits']);
        $this->assertEquals([40.7589, -73.9851], $personDoc['body']['visits'][0]['point']);

        $visitDoc = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $visitId) . '/' . $this->getRecordResource() . '/visit-2', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::select(['point', 'person.$id'])->toString()
            ]
        ]);
        $this->assertEquals(200, $visitDoc['headers']['status-code']);
        $this->assertEquals('person-spatial-1', $visitDoc['body']['person']['$id']);

        // cleanup
        $this->client->call(Client::METHOD_DELETE, $this->getContainerUrl($databaseId, $personId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));
        $this->client->call(Client::METHOD_DELETE, $this->getContainerUrl($databaseId, $visitId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));
        $this->client->call(Client::METHOD_DELETE, $this->getDatabaseUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));
    }

    public function testSpatialRelationshipManyToOne(): void
    {

        if (!$this->getSupportForRelationships() || !$this->getSupportForSpatials()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        $database = $this->client->call(Client::METHOD_POST, '/databases', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'Spatial ManyToOne Test DB'
        ]);
        $databaseId = $database['body']['$id'];

        $cities = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'City',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::read(Role::user($this->getUser()['$id'])),
            ],
        ]);
        $citiesId = $cities['body']['$id'];

        $stores = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Store',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::read(Role::user($this->getUser()['$id'])),
            ],
        ]);
        $storesId = $stores['body']['$id'];

        // attributes
        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $citiesId) . '/polygon', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'area',
            'required' => true,
        ]);
        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $storesId) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'name',
            'size' => 255,
            'required' => true,
        ]);

        $this->waitForAttribute($databaseId, $storesId, 'name');

        // relationship stores.manyToOne -> cities
        $rel = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $storesId) . '/relationship', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getRelatedIdParam() => $citiesId,
            'type' => Database::RELATION_MANY_TO_ONE,
            'key' => 'city',
            'twoWay' => true,
            'twoWayKey' => 'stores',
        ]);
        $this->assertEquals(202, $rel['headers']['status-code']);

        $this->waitForAttribute($databaseId, $storesId, 'city');

        $store = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $storesId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => 'store-1',
            'data' => [
                'name' => 'Main Store',
                'city' => [
                    '$id' => ID::unique(),
                    'area' => [[[40.7128, -74.0060], [40.7589, -74.0060], [40.7589, -73.9851], [40.7128, -73.9851], [40.7128, -74.0060]]]
                ],
            ]
        ]);
        $this->assertEquals(201, $store['headers']['status-code']);
        $this->assertEquals('Main Store', $store['body']['name']);
        $this->assertEquals([[[40.7128, -74.0060], [40.7589, -74.0060], [40.7589, -73.9851], [40.7128, -73.9851], [40.7128, -74.0060]]], $store['body']['city']['area']);

        $city = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $citiesId) . '/' . $this->getRecordResource() . '/' . $store['body']['city']['$id'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::select(['stores.$id'])->toString()
            ]
        ]);
        $this->assertEquals(200, $city['headers']['status-code']);
        $this->assertEquals('store-1', $city['body']['stores'][0]['$id']);

        // cleanup
        $this->client->call(Client::METHOD_DELETE, $this->getContainerUrl($databaseId, $storesId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));
        $this->client->call(Client::METHOD_DELETE, $this->getContainerUrl($databaseId, $citiesId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));
        $this->client->call(Client::METHOD_DELETE, $this->getDatabaseUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));
    }

    public function testSpatialRelationshipManyToMany(): void
    {
        if (!$this->getSupportForRelationships() || !$this->getSupportForSpatials()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        $database = $this->client->call(Client::METHOD_POST, '/databases', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'Spatial ManyToMany Test DB'
        ]);
        $databaseId = $database['body']['$id'];

        $drivers = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Drivers',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::read(Role::user($this->getUser()['$id'])),
            ],
        ]);
        $driversId = $drivers['body']['$id'];

        $zones = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Zones',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::read(Role::user($this->getUser()['$id'])),
            ],
        ]);
        $zonesId = $zones['body']['$id'];

        // attributes
        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $driversId) . '/point', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'home',
            'required' => true,
        ]);
        $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $zonesId) . '/polygon', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'area',
            'required' => true,
        ]);

        $this->waitForAttribute($databaseId, $zonesId, 'area');

        // relationship drivers.manyToMany <-> zones
        $rel = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $driversId) . '/relationship', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getRelatedIdParam() => $zonesId,
            'type' => Database::RELATION_MANY_TO_MANY,
            'key' => 'zones',
            'twoWay' => true,
            'twoWayKey' => 'drivers',
        ]);
        $this->assertEquals(202, $rel['headers']['status-code']);

        $this->waitForAttribute($databaseId, $driversId, 'zones');

        // create driver with two zones containing spatial polygons
        $driver = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $driversId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => 'driver-1',
            'data' => [
                'home' => [40.7128, -74.0060],
                'zones' => [
                    [ '$id' => 'zone-1', 'area' => [[[0,0],[10,0],[10,10],[0,10],[0,0]]]],
                    [ '$id' => 'zone-2', 'area' => [[[20,20],[30,20],[30,30],[20,30],[20,20]]]],
                ],
            ]
        ]);
        $this->assertEquals(201, $driver['headers']['status-code']);
        $this->assertCount(2, $driver['body']['zones']);
        $this->assertEquals([40.7128, -74.0060], $driver['body']['home']);

        $zone = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $zonesId) . '/' . $this->getRecordResource() . '/zone-1', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [
                Query::select(['drivers.$id'])->toString()
            ]
        ]);
        $this->assertEquals(200, $zone['headers']['status-code']);
        $this->assertEquals('driver-1', $zone['body']['drivers'][0]['$id']);

        // cleanup
        $this->client->call(Client::METHOD_DELETE, $this->getContainerUrl($databaseId, $driversId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));
        $this->client->call(Client::METHOD_DELETE, $this->getContainerUrl($databaseId, $zonesId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));
        $this->client->call(Client::METHOD_DELETE, $this->getDatabaseUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));
    }

    public function testSpatialIndex(): void
    {

        if (!$this->getSupportForSpatials()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        $database = $this->client->call(Client::METHOD_POST, '/databases', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'Spatial Index Test DB'
        ]);
        $this->assertEquals(201, $database['headers']['status-code']);
        $databaseId = $database['body']['$id'];

        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'SpatialIdx',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::any()),
                Permission::read(Role::any()),
                Permission::update(Role::any()),
                Permission::delete(Role::any()),
            ],
        ]);
        $this->assertEquals(201, $collection['headers']['status-code']);
        $collectionId = $collection['body']['$id'];

        // Create spatial attributes: one required, one optional
        $reqPoint = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/point', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'pRequired',
            'required' => true,
        ]);
        $this->assertEquals(202, $reqPoint['headers']['status-code']);

        $optPoint = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/point', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'pOptional',
            'required' => false,
        ]);
        $this->assertEquals(202, $optPoint['headers']['status-code']);

        // Ensure attributes are available
        $this->waitForAllAttributes($databaseId, $collectionId);

        // Create index on required spatial attribute (should succeed)
        $okIndex = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'idx_required_point',
            'type' => Database::INDEX_SPATIAL,
            $this->getIndexAttributesParam() => ['pRequired'],
        ]);
        $this->assertEquals(202, $okIndex['headers']['status-code']);

        // Create index on optional spatial attribute (should fail for adapters that don't support spatial index null)
        $badIndex = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'idx_optional_point',
            'type' => Database::INDEX_SPATIAL,
            $this->getIndexAttributesParam() => ['pOptional'],
        ]);

        if ($this->getSupportForSpatialIndexNull()) {
            // PostgreSQL allows spatial indexes on nullable columns
            $this->assertEquals(202, $badIndex['headers']['status-code']);
        } else {
            // MariaDB requires spatial indexed columns to be NOT NULL
            $this->assertEquals(400, $badIndex['headers']['status-code']);

            // updating the attribute to required to create index
            $updated = $this->client->call(Client::METHOD_PATCH, $this->getSchemaUrl($databaseId, $collectionId, 'point', 'pOptional'), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                'required' => true,
                'default' => null
            ]);
            $this->assertEquals(200, $updated['headers']['status-code']);

            $this->waitForAttribute($databaseId, $collectionId, 'pOptional');

            $retriedIndex = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                'key' => 'idx_optional_point',
                'type' => Database::INDEX_SPATIAL,
                $this->getIndexAttributesParam() => ['pOptional'],
            ]);
            $this->assertEquals(202, $retriedIndex['headers']['status-code']);
        }

        // Cleanup
        $this->client->call(Client::METHOD_DELETE, $this->getContainerUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));
    }

    public function testSpatialDistanceInMeter(): void
    {

        if (!$this->getSupportForSpatials()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        $database = $this->client->call(Client::METHOD_POST, '/databases', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'Spatial Distance Meters Database'
        ]);

        $databaseId = $database['body']['$id'];

        // Create collection with spatial attribute
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Spatial Distance Meters Collection',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::any()),
                Permission::read(Role::any()),
                Permission::update(Role::any()),
                Permission::delete(Role::any()),
            ],
        ]);

        $collectionId = $collection['body']['$id'];

        // Create point attribute
        $response = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/point', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'loc',
            'required' => true,
        ]);

        $this->assertEquals(202, $response['headers']['status-code']);
        $this->waitForAllAttributes($databaseId, $collectionId);

        // Create spatial index
        $indexResponse = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'idx_loc',
            'type' => Database::INDEX_SPATIAL,
            $this->getIndexAttributesParam() => ['loc'],
        ]);

        $this->assertEquals(202, $indexResponse['headers']['status-code']);
        $this->waitForIndex($databaseId, $collectionId, 'idx_loc');


        // Two points roughly ~1000 meters apart by latitude delta (~0.009 deg ≈ 1km)
        $p0 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => 'p0',
            'data' => [
                'loc' => [0.0000, 0.0000]
            ]
        ]);

        $p1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => 'p1',
            'data' => [
                'loc' => [0.0090, 0.0000]
            ]
        ]);

        $this->assertEquals(201, $p0['headers']['status-code']);
        $this->assertEquals(201, $p1['headers']['status-code']);

        // distanceLessThan with meters=true: within 1500m should include both
        $within1_5km = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::distanceLessThan('loc', [0.0000, 0.0000], 1500, true)->toString()]
        ]);

        $this->assertEquals(200, $within1_5km['headers']['status-code']);
        $this->assertCount(2, $within1_5km['body'][$this->getRecordResource()]);

        // Within 500m should include only p0 (exact point)
        $within500m = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::distanceLessThan('loc', [0.0000, 0.0000], 500, true)->toString()]
        ]);

        $this->assertEquals(200, $within500m['headers']['status-code']);
        $this->assertCount(1, $within500m['body'][$this->getRecordResource()]);
        $this->assertEquals('p0', $within500m['body'][$this->getRecordResource()][0]['$id']);

        // distanceGreaterThan 500m should include only p1
        $greater500m = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::distanceGreaterThan('loc', [0.0000, 0.0000], 500, true)->toString()]
        ]);

        $this->assertEquals(200, $greater500m['headers']['status-code']);
        $this->assertCount(1, $greater500m['body'][$this->getRecordResource()]);
        $this->assertEquals('p1', $greater500m['body'][$this->getRecordResource()][0]['$id']);

        // distanceEqual with 0m should return exact match p0
        $equalZero = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::distanceEqual('loc', [0.0000, 0.0000], 0, true)->toString()]
        ]);

        $this->assertEquals(200, $equalZero['headers']['status-code']);
        $this->assertEquals('p0', $equalZero['body'][$this->getRecordResource()][0]['$id']);

        // distanceNotEqual with 0m should return p1
        $notEqualZero = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'queries' => [Query::distanceNotEqual('loc', [0.0000, 0.0000], 0, true)->toString()]
        ]);

        $this->assertEquals(200, $notEqualZero['headers']['status-code']);
        $this->assertEquals('p1', $notEqualZero['body'][$this->getRecordResource()][0]['$id']);

        // Cleanup
        $this->client->call(Client::METHOD_DELETE, $this->getContainerUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->client->call(Client::METHOD_DELETE, $this->getDatabaseUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));
    }

    public function testSpatialColCreateOnExistingData(): void
    {

        if (!$this->getSupportForSpatials()) {
            $this->expectNotToPerformAssertions();
            return;
        }

        $database = $this->client->call(Client::METHOD_POST, '/databases', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'Spatial Distance Meters Database'
        ]);

        $databaseId = $database['body']['$id'];

        $colId = ID::unique();
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => $colId,
            'name' => 'spatial-test',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::any()),
                Permission::read(Role::any()),
            ],
        ]);

        $this->assertEquals(201, $collection['headers']['status-code']);

        $description = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $colId) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'description',
            'size' => 512,
            'required' => false,
            'default' => '',
        ]);

        $this->assertEquals(202, $description['headers']['status-code']);
        $this->waitForAttribute($databaseId, $colId, 'description');

        $document = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $colId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'description' => 'description'
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $document['headers']['status-code']);

        $point = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $colId) . '/point', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'loc',
            'required' => true,
        ]);

        if ($this->getSupportForSpatialIndexNull()) {
            $this->assertEquals(202, $point['headers']['status-code']);
        } else {
            $this->assertEquals(400, $point['headers']['status-code']);

            $point = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $colId) . '/point', array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                'key' => 'loc',
                'required' => false,
                'default' => null
            ]);

            $this->assertEquals(202, $point['headers']['status-code']);
        }

        $line = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $colId) . '/line', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'route',
            'required' => true,
        ]);

        if ($this->getSupportForSpatialIndexNull()) {
            $this->assertEquals(202, $line['headers']['status-code']);
        } else {
            $this->assertEquals(400, $line['headers']['status-code']);

            $line = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $colId) . '/line', array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                'key' => 'route',
                'required' => false,
                'default' => null
            ]);

            $this->assertEquals(202, $line['headers']['status-code']);
        }

        $poly = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $colId) . '/polygon', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'area',
            'required' => true,
        ]);

        if ($this->getSupportForSpatialIndexNull()) {
            $this->assertEquals(202, $poly['headers']['status-code']);
        } else {
            $this->assertEquals(400, $poly['headers']['status-code']);

            $poly = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $colId) . '/polygon', array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                'key' => 'area',
                'required' => false,
                'default' => null
            ]);

            $this->assertEquals(202, $poly['headers']['status-code']);
        }
    }

    public function testSpatialColCreateOnExistingDataWithDefaults(): void
    {

        if (!$this->getSupportForSpatials()) {
            $this->expectNotToPerformAssertions();
            return;
        }
        $database = $this->client->call(Client::METHOD_POST, '/databases', [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'Spatial With Defaults Database'
        ]);

        $databaseId = $database['body']['$id'];

        $colId = ID::unique();
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => $colId,
            'name' => 'spatial-test-defaults',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::any()),
                Permission::read(Role::any()),
            ],
        ]);

        $this->assertEquals(201, $collection['headers']['status-code']);

        $description = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $colId) . '/string', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'description',
            'size' => 512,
            'required' => false,
            'default' => '',
        ]);

        $this->assertEquals(202, $description['headers']['status-code']);
        $this->waitForAttribute($databaseId, $colId, 'description');

        $document = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $colId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'description' => 'description'
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $document['headers']['status-code']);

        // Test point with default value
        $point = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $colId) . '/point', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'loc',
            'required' => false,
            'default' => [0.0, 0.0]
        ]);

        $this->assertEquals(202, $point['headers']['status-code']);

        // Test line with default value
        $line = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $colId) . '/line', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'route',
            'required' => false,
            'default' => [[0.0, 0.0], [1.0, 1.0]]
        ]);

        $this->assertEquals(202, $line['headers']['status-code']);

        // Test polygon with default value
        $poly = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $colId) . '/polygon', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'key' => 'area',
            'required' => false,
            'default' => [[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]]]
        ]);

        $this->assertEquals(202, $poly['headers']['status-code']);

        // Wait for attributes to be available
        $this->waitForAllAttributes($databaseId, $colId);

        // Create a new document without spatial data to test default values
        $newDocument = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $colId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'description' => 'test default values'
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $newDocument['headers']['status-code']);

        $newDocumentId = $newDocument['body']['$id'];

        // Fetch the document to verify default values are applied
        $fetchedDocument = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $colId) . '/' . $this->getRecordResource() . '/' . $newDocumentId, array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        $this->assertEquals(200, $fetchedDocument['headers']['status-code']);

        // Verify default values are applied
        $this->assertEquals([0.0, 0.0], $fetchedDocument['body']['loc']);
        $this->assertEquals([[0.0, 0.0], [1.0, 1.0]], $fetchedDocument['body']['route']);
        $this->assertEquals([[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]]], $fetchedDocument['body']['area']);
    }

    public function testNotContains(): void
    {
        // Create database
        $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'NotContains test'
        ]);

        $this->assertNotEmpty($database['body']['$id']);
        $this->assertEquals(201, $database['headers']['status-code']);
        $this->assertEquals('NotContains test', $database['body']['name']);

        $databaseId = $database['body']['$id'];

        // Create Collection
        $movies = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Movies',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $this->assertEquals(201, $movies['headers']['status-code']);
        $this->assertEquals($movies['body']['name'], 'Movies');

        // Create Attributes (only when supported; DocumentsDB can still store fields schemalessly)
        if ($this->getSupportForAttributes()) {
            $title = $this->createAttribute($databaseId, $movies['body']['$id'], 'string', [
                'key' => 'title',
                'size' => 256,
                'required' => true,
            ]);
            $this->assertEquals(202, $title['headers']['status-code']);

            $genre = $this->createAttribute($databaseId, $movies['body']['$id'], 'string', [
                'key' => 'genre',
                'size' => 256,
                'required' => true,
            ]);
            $this->assertEquals(202, $genre['headers']['status-code']);

            // Wait for worker
            $this->waitForAllAttributes($databaseId, $movies['body']['$id']);
        }

        $row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $movies['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Spider-Man: Homecoming',
                'genre' => 'Action',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row1['headers']['status-code']);

        $row2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $movies['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'The Avengers',
                'genre' => 'Action',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row2['headers']['status-code']);

        $row3 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $movies['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Romantic Comedy',
                'genre' => 'Romance',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);

        $this->assertEquals(201, $row3['headers']['status-code']);

        // Test notContains query - should return movies that don't contain "Spider" in title
        $rows = $this->client->call(
            Client::METHOD_GET,
            $this->getRecordUrl($databaseId, $movies['body']['$id']),
            array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()),
            [
                'queries' => [
                    Query::select(['title', 'genre'])->toString(),
                    Query::notContains('title', ['Spider'])->toString(),
                    Query::limit(999)->toString(),
                    Query::offset(0)->toString()
                ],
            ]
        );

        $this->assertEquals(200, $rows['headers']['status-code']);
        $this->assertCount(2, $rows['body'][$this->getRecordResource()]);
        $this->assertEquals('The Avengers', $rows['body'][$this->getRecordResource()][0]['title']);
        $this->assertEquals('Romantic Comedy', $rows['body'][$this->getRecordResource()][1]['title']);
    }

    /**
     * @throws \Utopia\Database\Exception
     * @throws \Utopia\Database\Exception\Query
     */
    public function testNotSearch(): void
    {
        $data = $this->setupFulltextSearchDocuments();
        $databaseId = $data['databaseId'];
        $booksId = $data['booksId'];

        // Test notSearch query - should return books that don't have "space" in the description
        $rows = $this->client->call(
            Client::METHOD_GET,
            $this->getRecordUrl($databaseId, $booksId),
            array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]),
            [
                'queries' => [
                    Query::notSearch('description', 'space')->toString(),
                ],
            ]
        );

        $this->assertEquals(200, $rows['headers']['status-code']);
        $this->assertCount(2, $rows['body'][$this->getRecordResource()]);
        $this->assertEquals('Romance Novel', $rows['body'][$this->getRecordResource()][0]['title']);
        $this->assertEquals('Mystery Thriller', $rows['body'][$this->getRecordResource()][1]['title']);
    }

    /**
     * @throws \Utopia\Database\Exception
     * @throws \Utopia\Database\Exception\Query
     */
    public function testNotBetween(): void
    {
        // Create database
        $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'NotBetween test'
        ]);

        $this->assertNotEmpty($database['body']['$id']);
        $this->assertEquals(201, $database['headers']['status-code']);
        $this->assertEquals('NotBetween test', $database['body']['name']);

        $databaseId = $database['body']['$id'];

        // Create Collection
        $products = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Products',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $this->assertEquals(201, $products['headers']['status-code']);
        $this->assertEquals($products['body']['name'], 'Products');

        // Create Attributes (only when supported)
        if ($this->getSupportForAttributes()) {
            $name = $this->createAttribute($databaseId, $products['body']['$id'], 'string', [
                'key' => 'name',
                'size' => 256,
                'required' => true,
            ]);
            $this->assertEquals(202, $name['headers']['status-code']);

            $price = $this->createAttribute($databaseId, $products['body']['$id'], 'float', [
                'key' => 'price',
                'required' => true,
            ]);
            $this->assertEquals(202, $price['headers']['status-code']);

            // Wait for worker
            $this->waitForAllAttributes($databaseId, $products['body']['$id']);
        }

        $row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $products['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'name' => 'Cheap Product',
                'price' => 5.99,
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row1['headers']['status-code']);

        $row2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $products['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'name' => 'Mid Product',
                'price' => 25.00,
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row2['headers']['status-code']);

        $row3 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $products['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'name' => 'Expensive Product',
                'price' => 150.00,
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);

        $this->assertEquals(201, $row3['headers']['status-code']);

        // Test notBetween query - should return products NOT priced between 10 and 50
        $rows = $this->client->call(
            Client::METHOD_GET,
            $this->getRecordUrl($databaseId, $products['body']['$id']),
            array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()),
            [
                'queries' => [
                    Query::notBetween('price', 10, 50)->toString(),
                ],
            ]
        );

        $this->assertEquals(200, $rows['headers']['status-code']);
        $this->assertCount(2, $rows['body'][$this->getRecordResource()]);
        $this->assertEquals('Cheap Product', $rows['body'][$this->getRecordResource()][0]['name']);
        $this->assertEquals('Expensive Product', $rows['body'][$this->getRecordResource()][1]['name']);
    }

    /**
     * @throws \Utopia\Database\Exception
     * @throws \Utopia\Database\Exception\Query
     */
    public function testNotStartsWith(): void
    {
        // Create database
        $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'NotStartsWith test'
        ]);

        $this->assertNotEmpty($database['body']['$id']);
        $this->assertEquals(201, $database['headers']['status-code']);
        $this->assertEquals('NotStartsWith test', $database['body']['name']);

        $databaseId = $database['body']['$id'];

        // Create Collection
        $employees = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Employees',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $this->assertEquals(201, $employees['headers']['status-code']);
        $this->assertEquals($employees['body']['name'], 'Employees');

        // Create Attributes (only when supported)
        if ($this->getSupportForAttributes()) {
            $name = $this->createAttribute($databaseId, $employees['body']['$id'], 'string', [
                'key' => 'name',
                'size' => 256,
                'required' => true,
            ]);
            $this->assertEquals(202, $name['headers']['status-code']);

            $department = $this->createAttribute($databaseId, $employees['body']['$id'], 'string', [
                'key' => 'department',
                'size' => 256,
                'required' => true,
            ]);
            $this->assertEquals(202, $department['headers']['status-code']);

            // Wait for worker
            $this->waitForAllAttributes($databaseId, $employees['body']['$id']);
        }

        $row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $employees['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'name' => 'John Smith',
                'department' => 'Engineering',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row1['headers']['status-code']);

        $row2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $employees['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'name' => 'Jane Doe',
                'department' => 'Marketing',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row2['headers']['status-code']);

        $row3 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $employees['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'name' => 'Bob Johnson',
                'department' => 'Sales',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);

        $this->assertEquals(201, $row3['headers']['status-code']);

        // Test notStartsWith query - should return employees whose names don't start with "John"
        $rows = $this->client->call(
            Client::METHOD_GET,
            $this->getRecordUrl($databaseId, $employees['body']['$id']),
            array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()),
            [
                'queries' => [
                    Query::notStartsWith('name', 'John')->toString(),
                ],
            ]
        );

        $this->assertEquals(200, $rows['headers']['status-code']);
        $this->assertCount(2, $rows['body'][$this->getRecordResource()]);
        $this->assertEquals('Jane Doe', $rows['body'][$this->getRecordResource()][0]['name']);
        $this->assertEquals('Bob Johnson', $rows['body'][$this->getRecordResource()][1]['name']);
    }

    /**
     * @throws \Utopia\Database\Exception
     * @throws \Utopia\Database\Exception\Query
     */
    public function testNotEndsWith(): void
    {
        // Create database
        $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'NotEndsWith test'
        ]);

        $this->assertNotEmpty($database['body']['$id']);
        $this->assertEquals(201, $database['headers']['status-code']);
        $this->assertEquals('NotEndsWith test', $database['body']['name']);

        $databaseId = $database['body']['$id'];

        // Create Collection
        $files = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Files',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $this->assertEquals(201, $files['headers']['status-code']);
        $this->assertEquals($files['body']['name'], 'Files');

        // Create Attributes (only when supported)
        if ($this->getSupportForAttributes()) {
            $filename = $this->createAttribute($databaseId, $files['body']['$id'], 'string', [
                'key' => 'filename',
                'size' => 256,
                'required' => true,
            ]);
            $this->assertEquals(202, $filename['headers']['status-code']);

            $type = $this->createAttribute($databaseId, $files['body']['$id'], 'string', [
                'key' => 'type',
                'size' => 256,
                'required' => true,
            ]);
            $this->assertEquals(202, $type['headers']['status-code']);

            // Wait for worker
            $this->waitForAllAttributes($databaseId, $files['body']['$id']);
        }

        $row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $files['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'filename' => 'row.pdf',
                'type' => 'PDF',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row1['headers']['status-code']);

        $row2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $files['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'filename' => 'image.jpg',
                'type' => 'Image',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row2['headers']['status-code']);

        $row3 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $files['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'filename' => 'presentation.pptx',
                'type' => 'Presentation',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);

        $this->assertEquals(201, $row3['headers']['status-code']);

        // Test notEndsWith query - should return files that don't end with ".pdf"
        $rows = $this->client->call(
            Client::METHOD_GET,
            $this->getRecordUrl($databaseId, $files['body']['$id']),
            array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()),
            [
                'queries' => [
                    Query::notEndsWith('filename', '.pdf')->toString(),
                ],
            ]
        );

        $this->assertEquals(200, $rows['headers']['status-code']);
        $this->assertCount(2, $rows['body'][$this->getRecordResource()]);
        $this->assertEquals('image.jpg', $rows['body'][$this->getRecordResource()][0]['filename']);
        $this->assertEquals('presentation.pptx', $rows['body'][$this->getRecordResource()][1]['filename']);
    }

    /**
     * @throws \Utopia\Database\Exception
     * @throws \Utopia\Database\Exception\Query
     */
    public function testCreatedBefore(): void
    {
        // Create database
        $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'CreatedBefore test'
        ]);

        $this->assertNotEmpty($database['body']['$id']);
        $this->assertEquals(201, $database['headers']['status-code']);
        $this->assertEquals('CreatedBefore test', $database['body']['name']);

        $databaseId = $database['body']['$id'];

        // Create Collection
        $posts = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Posts',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $this->assertEquals(201, $posts['headers']['status-code']);
        $this->assertEquals($posts['body']['name'], 'Posts');

        // Create Attributes (only when supported)
        if ($this->getSupportForAttributes()) {
            $title = $this->createAttribute($databaseId, $posts['body']['$id'], 'string', [
                'key' => 'title',
                'size' => 256,
                'required' => true,
            ]);
            $this->assertEquals(202, $title['headers']['status-code']);

            $content = $this->createAttribute($databaseId, $posts['body']['$id'], 'string', [
                'key' => 'content',
                'size' => 512,
                'required' => true,
            ]);
            $this->assertEquals(202, $content['headers']['status-code']);

            // Wait for worker
            $this->waitForAllAttributes($databaseId, $posts['body']['$id']);
        }

        $row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $posts['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Old Post',
                'content' => 'This is an old post content',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row1['headers']['status-code']);

        // Ensure different creation times
        usleep(500000);

        $row2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $posts['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Recent Post',
                'content' => 'This is a recent post content',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row2['headers']['status-code']);

        // Get the creation time of the second post to use as boundary
        $secondPostCreatedAt = $row2['body']['$createdAt'];

        usleep(500000);

        $row3 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $posts['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Newest Post',
                'content' => 'This is the newest post content',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);

        $this->assertEquals(201, $row3['headers']['status-code']);

        // Test createdBefore query - should return posts created before the second post
        $rows = $this->client->call(
            Client::METHOD_GET,
            $this->getRecordUrl($databaseId, $posts['body']['$id']),
            array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()),
            [
                'queries' => [
                    Query::createdBefore($secondPostCreatedAt)->toString(),
                ],
            ]
        );

        $this->assertEquals(200, $rows['headers']['status-code']);
        $this->assertCount(1, $rows['body'][$this->getRecordResource()]);
        $this->assertEquals('Old Post', $rows['body'][$this->getRecordResource()][0]['title']);
    }

    /**
     * @throws \Utopia\Database\Exception
     * @throws \Utopia\Database\Exception\Query
     */
    public function testCreatedAfter(): void
    {
        // Create database
        $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'CreatedAfter test'
        ]);

        $this->assertNotEmpty($database['body']['$id']);
        $this->assertEquals(201, $database['headers']['status-code']);
        $this->assertEquals('CreatedAfter test', $database['body']['name']);

        $databaseId = $database['body']['$id'];

        // Create Collection
        $events = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Events',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $this->assertEquals(201, $events['headers']['status-code']);
        $this->assertEquals($events['body']['name'], 'Events');

        // Create Attributes (only when supported)
        if ($this->getSupportForAttributes()) {
            $name = $this->createAttribute($databaseId, $events['body']['$id'], 'string', [
                'key' => 'name',
                'size' => 256,
                'required' => true,
            ]);
            $this->assertEquals(202, $name['headers']['status-code']);

            $description = $this->createAttribute($databaseId, $events['body']['$id'], 'string', [
                'key' => 'description',
                'size' => 512,
                'required' => true,
            ]);
            $this->assertEquals(202, $description['headers']['status-code']);

            // Wait for worker
            $this->waitForAllAttributes($databaseId, $events['body']['$id']);
        }

        $row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $events['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'name' => 'Early Event',
                'description' => 'This is an early event',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row1['headers']['status-code']);

        // Ensure different creation times
        usleep(500000);

        $row2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $events['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'name' => 'Middle Event',
                'description' => 'This is a middle event',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row2['headers']['status-code']);

        // Get the creation time of the second event to use as boundary
        $secondEventCreatedAt = $row2['body']['$createdAt'];

        usleep(500000);

        $row3 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $events['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'name' => 'Latest Event',
                'description' => 'This is the latest event',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);

        $this->assertEquals(201, $row3['headers']['status-code']);

        // Test createdAfter query - should return events created after the second event
        $rows = $this->client->call(
            Client::METHOD_GET,
            $this->getRecordUrl($databaseId, $events['body']['$id']),
            array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()),
            [
                'queries' => [
                    Query::createdAfter($secondEventCreatedAt)->toString(),
                ],
            ]
        );

        $this->assertEquals(200, $rows['headers']['status-code']);
        $this->assertCount(1, $rows['body'][$this->getRecordResource()]);
        $this->assertEquals('Latest Event', $rows['body'][$this->getRecordResource()][0]['name']);
    }

    /**
     * @throws \Utopia\Database\Exception
     * @throws \Utopia\Database\Exception\Query
     */
    public function testCreatedBetween(): void
    {
        // Create database
        $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'CreatedBetween test'
        ]);

        $this->assertNotEmpty($database['body']['$id']);
        $this->assertEquals(201, $database['headers']['status-code']);
        $this->assertEquals('CreatedBetween test', $database['body']['name']);

        $databaseId = $database['body']['$id'];

        // Create Collection
        $articles = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Articles',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $this->assertEquals(201, $articles['headers']['status-code']);
        $this->assertEquals($articles['body']['name'], 'Articles');

        // Create Attributes (only when supported)
        if ($this->getSupportForAttributes()) {
            $title = $this->createAttribute($databaseId, $articles['body']['$id'], 'string', [
                'key' => 'title',
                'size' => 256,
                'required' => true,
            ]);
            $this->assertEquals(202, $title['headers']['status-code']);

            $content = $this->createAttribute($databaseId, $articles['body']['$id'], 'string', [
                'key' => 'content',
                'size' => 5000,
                'required' => true,
            ]);
            $this->assertEquals(202, $content['headers']['status-code']);

            // Wait for attributes to be available
            $this->waitForAllAttributes($databaseId, $articles['body']['$id']);
        }

        // Create first article
        $row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $articles['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'First Article',
                'content' => 'This is the first article content',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row1['headers']['status-code']);
        $firstArticleCreatedAt = $row1['body']['$createdAt'];

        // Ensure different timestamps
        usleep(500000);

        // Create second article
        $row2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $articles['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Second Article',
                'content' => 'This is the second article content',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row2['headers']['status-code']);
        $secondArticleCreatedAt = $row2['body']['$createdAt'];

        usleep(500000);

        // Create third article
        $row3 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $articles['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Third Article',
                'content' => 'This is the third article content',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row3['headers']['status-code']);
        $thirdArticleCreatedAt = $row3['body']['$createdAt'];

        usleep(500000);

        // Create fourth article
        $row4 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $articles['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Fourth Article',
                'content' => 'This is the fourth article content',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row4['headers']['status-code']);

        // Test createdBetween query - should return articles created between first and third (inclusive)
        $rows = $this->client->call(
            Client::METHOD_GET,
            $this->getRecordUrl($databaseId, $articles['body']['$id']),
            array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()),
            [
                'queries' => [
                    Query::createdBetween($firstArticleCreatedAt, $thirdArticleCreatedAt)->toString(),
                ],
            ]
        );

        $this->assertEquals(200, $rows['headers']['status-code']);
        $this->assertCount(3, $rows['body'][$this->getRecordResource()]);

        // Verify the returned articles are the correct ones
        $titles = array_column($rows['body'][$this->getRecordResource()], 'title');
        $this->assertContains('First Article', $titles);
        $this->assertContains('Second Article', $titles);
        $this->assertContains('Third Article', $titles);
        $this->assertNotContains('Fourth Article', $titles);

        // Test createdBetween query - should return only the second article when using its timestamp for both bounds
        $rows = $this->client->call(
            Client::METHOD_GET,
            $this->getRecordUrl($databaseId, $articles['body']['$id']),
            array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()),
            [
                'queries' => [
                    Query::createdBetween($secondArticleCreatedAt, $secondArticleCreatedAt)->toString(),
                ],
            ]
        );

        $this->assertEquals(200, $rows['headers']['status-code']);
        $this->assertCount(1, $rows['body'][$this->getRecordResource()]);
        $this->assertEquals('Second Article', $rows['body'][$this->getRecordResource()][0]['title']);
    }

    /**
     * @throws \Utopia\Database\Exception
     * @throws \Utopia\Database\Exception\Query
     */
    public function testUpdatedBefore(): void
    {
        // Create database
        $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'UpdatedBefore test'
        ]);

        $this->assertNotEmpty($database['body']['$id']);
        $this->assertEquals(201, $database['headers']['status-code']);
        $this->assertEquals('UpdatedBefore test', $database['body']['name']);

        $databaseId = $database['body']['$id'];

        // Create Collection
        $tasks = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Tasks',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $this->assertEquals(201, $tasks['headers']['status-code']);
        $this->assertEquals($tasks['body']['name'], 'Tasks');

        // Create Attributes (only when supported)
        if ($this->getSupportForAttributes()) {
            $title = $this->createAttribute($databaseId, $tasks['body']['$id'], 'string', [
                'key' => 'title',
                'size' => 256,
                'required' => true,
            ]);
            $this->assertEquals(202, $title['headers']['status-code']);

            $status = $this->createAttribute($databaseId, $tasks['body']['$id'], 'string', [
                'key' => 'status',
                'size' => 256,
                'required' => true,
            ]);
            $this->assertEquals(202, $status['headers']['status-code']);

            // Wait for worker
            $this->waitForAllAttributes($databaseId, $tasks['body']['$id']);
        }

        $row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $tasks['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Task One',
                'status' => 'pending',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row1['headers']['status-code']);
        $taskOneId = $row1['body']['$id'];

        $row2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $tasks['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Task Two',
                'status' => 'pending',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row2['headers']['status-code']);
        $taskTwoId = $row2['body']['$id'];

        $row3 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $tasks['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'title' => 'Task Three',
                'status' => 'pending',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row3['headers']['status-code']);
        $taskThreeId = $row3['body']['$id'];

        // Update first task
        usleep(500000);
        $this->client->call(Client::METHOD_PATCH, $this->getContainerUrl($databaseId, $tasks['body']['$id']) . '/' . $this->getRecordResource() . '/' . $taskOneId, array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'status' => 'completed',
            ]
        ]);

        // Update second task and get its updated time
        usleep(500000);
        $updatedTaskTwo = $this->client->call(Client::METHOD_PATCH, $this->getContainerUrl($databaseId, $tasks['body']['$id']) . '/' . $this->getRecordResource() . '/' . $taskTwoId, array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'status' => 'in_progress',
            ]
        ]);
        $secondTaskUpdatedAt = $updatedTaskTwo['body']['$updatedAt'];

        // Update third task
        usleep(500000);
        $this->client->call(Client::METHOD_PATCH, $this->getContainerUrl($databaseId, $tasks['body']['$id']) . '/' . $this->getRecordResource() . '/' . $taskThreeId, array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'status' => 'review',
            ]
        ]);

        // Test updatedBefore query - should return tasks updated before the second task's update time
        $rows = $this->client->call(
            Client::METHOD_GET,
            $this->getRecordUrl($databaseId, $tasks['body']['$id']),
            array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()),
            [
                'queries' => [
                    Query::updatedBefore($secondTaskUpdatedAt)->toString(),
                ],
            ]
        );

        $this->assertEquals(200, $rows['headers']['status-code']);
        $this->assertCount(1, $rows['body'][$this->getRecordResource()]);
        $this->assertEquals('Task One', $rows['body'][$this->getRecordResource()][0]['title']);
        $this->assertEquals('completed', $rows['body'][$this->getRecordResource()][0]['status']);
    }

    /**
     * @throws \Utopia\Database\Exception
     * @throws \Utopia\Database\Exception\Query
     */
    public function testUpdatedAfter(): void
    {
        // Create database
        $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'UpdatedAfter test'
        ]);

        $this->assertNotEmpty($database['body']['$id']);
        $this->assertEquals(201, $database['headers']['status-code']);
        $this->assertEquals('UpdatedAfter test', $database['body']['name']);

        $databaseId = $database['body']['$id'];

        // Create Collection
        $orders = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Orders',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $this->assertEquals(201, $orders['headers']['status-code']);
        $this->assertEquals($orders['body']['name'], 'Orders');

        // Create Attributes (only when supported)
        if ($this->getSupportForAttributes()) {
            $orderNumber = $this->createAttribute($databaseId, $orders['body']['$id'], 'string', [
                'key' => 'orderNumber',
                'size' => 256,
                'required' => true,
            ]);
            $this->assertEquals(202, $orderNumber['headers']['status-code']);

            $status = $this->createAttribute($databaseId, $orders['body']['$id'], 'string', [
                'key' => 'status',
                'size' => 256,
                'required' => true,
            ]);
            $this->assertEquals(202, $status['headers']['status-code']);

            // Wait for worker
            $this->waitForAllAttributes($databaseId, $orders['body']['$id']);
        }

        $row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $orders['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'orderNumber' => 'ORD-001',
                'status' => 'pending',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row1['headers']['status-code']);
        $orderOneId = $row1['body']['$id'];

        $row2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $orders['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'orderNumber' => 'ORD-002',
                'status' => 'pending',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row2['headers']['status-code']);
        $orderTwoId = $row2['body']['$id'];

        $row3 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $orders['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'orderNumber' => 'ORD-003',
                'status' => 'pending',
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row3['headers']['status-code']);
        $orderThreeId = $row3['body']['$id'];

        // Update first order
        usleep(500000);
        $this->client->call(Client::METHOD_PATCH, $this->getContainerUrl($databaseId, $orders['body']['$id']) . '/' . $this->getRecordResource() . '/' . $orderOneId, array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'status' => 'processing',
            ]
        ]);

        // Update second order and get its updated time
        usleep(500000);
        $updatedOrderTwo = $this->client->call(Client::METHOD_PATCH, $this->getContainerUrl($databaseId, $orders['body']['$id']) . '/' . $this->getRecordResource() . '/' . $orderTwoId, array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'status' => 'shipped',
            ]
        ]);
        $secondOrderUpdatedAt = $updatedOrderTwo['body']['$updatedAt'];

        // Update third order
        usleep(500000);
        $this->client->call(Client::METHOD_PATCH, $this->getContainerUrl($databaseId, $orders['body']['$id']) . '/' . $this->getRecordResource() . '/' . $orderThreeId, array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'status' => 'delivered',
            ]
        ]);

        // Test updatedAfter query - should return orders updated after the second order's update time
        $rows = $this->client->call(
            Client::METHOD_GET,
            $this->getRecordUrl($databaseId, $orders['body']['$id']),
            array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()),
            [
                'queries' => [
                    Query::updatedAfter($secondOrderUpdatedAt)->toString(),
                ],
            ]
        );

        $this->assertEquals(200, $rows['headers']['status-code']);
        $this->assertCount(1, $rows['body'][$this->getRecordResource()]);
        $this->assertEquals('ORD-003', $rows['body'][$this->getRecordResource()][0]['orderNumber']);
        $this->assertEquals('delivered', $rows['body'][$this->getRecordResource()][0]['status']);
    }

    /**
     * @throws \Utopia\Database\Exception
     * @throws \Utopia\Database\Exception\Query
     */
    public function testUpdatedBetween(): void
    {
        // Create database
        $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ], [
            'databaseId' => ID::unique(),
            'name' => 'UpdatedBetween test'
        ]);

        $this->assertNotEmpty($database['body']['$id']);
        $this->assertEquals(201, $database['headers']['status-code']);
        $this->assertEquals('UpdatedBetween test', $database['body']['name']);

        $databaseId = $database['body']['$id'];

        // Create Collection
        $products = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => ID::unique(),
            'name' => 'Products',
            $this->getSecurityParam() => true,
            'permissions' => [
                Permission::create(Role::user($this->getUser()['$id'])),
            ],
        ]);

        $this->assertEquals(201, $products['headers']['status-code']);
        $this->assertEquals($products['body']['name'], 'Products');

        // Create Attributes (only when supported)
        if ($this->getSupportForAttributes()) {
            $name = $this->createAttribute($databaseId, $products['body']['$id'], 'string', [
                'key' => 'name',
                'size' => 256,
                'required' => true,
            ]);
            $this->assertEquals(202, $name['headers']['status-code']);

            $price = $this->createAttribute($databaseId, $products['body']['$id'], 'float', [
                'key' => 'price',
                'required' => true,
            ]);
            $this->assertEquals(202, $price['headers']['status-code']);

            // Wait for attributes to be available
            $this->waitForAllAttributes($databaseId, $products['body']['$id']);
        }

        // Create first product
        $row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $products['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'name' => 'Product A',
                'price' => 99.99,
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row1['headers']['status-code']);

        // Ensure different timestamps
        usleep(500000);

        // Create second product
        $row2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $products['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'name' => 'Product B',
                'price' => 149.99,
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row2['headers']['status-code']);

        usleep(500000);

        // Create third product
        $row3 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $products['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'name' => 'Product C',
                'price' => 199.99,
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row3['headers']['status-code']);

        usleep(500000);

        // Create fourth product
        $row4 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $products['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [
                'name' => 'Product D',
                'price' => 249.99,
            ],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
            ]
        ]);
        $this->assertEquals(201, $row4['headers']['status-code']);

        // Now update products in sequence to get different updatedAt timestamps
        usleep(500000);

        // Update first product
        $update1 = $this->client->call(Client::METHOD_PATCH, $this->getContainerUrl($databaseId, $products['body']['$id']) . '/' . $this->getRecordResource() . '/' . $row1['body']['$id'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'price' => 89.99,
            ]
        ]);
        $this->assertEquals(200, $update1['headers']['status-code']);
        $firstProductUpdatedAt = $update1['body']['$updatedAt'];

        usleep(500000);

        // Update second product
        $update2 = $this->client->call(Client::METHOD_PATCH, $this->getContainerUrl($databaseId, $products['body']['$id']) . '/' . $this->getRecordResource() . '/' . $row2['body']['$id'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'price' => 139.99,
            ]
        ]);
        $this->assertEquals(200, $update2['headers']['status-code']);
        $secondProductUpdatedAt = $update2['body']['$updatedAt'];

        usleep(500000);

        // Update third product
        $update3 = $this->client->call(Client::METHOD_PATCH, $this->getContainerUrl($databaseId, $products['body']['$id']) . '/' . $this->getRecordResource() . '/' . $row3['body']['$id'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'price' => 189.99,
            ]
        ]);
        $this->assertEquals(200, $update3['headers']['status-code']);
        $thirdProductUpdatedAt = $update3['body']['$updatedAt'];

        usleep(500000);

        // Update fourth product
        $update4 = $this->client->call(Client::METHOD_PATCH, $this->getContainerUrl($databaseId, $products['body']['$id']) . '/' . $this->getRecordResource() . '/' . $row4['body']['$id'], array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'price' => 239.99,
            ]
        ]);
        $this->assertEquals(200, $update4['headers']['status-code']);

        // Test updatedBetween query - should return products updated between first and third (inclusive)
        $rows = $this->client->call(
            Client::METHOD_GET,
            $this->getRecordUrl($databaseId, $products['body']['$id']),
            array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()),
            [
                'queries' => [
                    Query::updatedBetween($firstProductUpdatedAt, $thirdProductUpdatedAt)->toString(),
                ],
            ]
        );

        $this->assertEquals(200, $rows['headers']['status-code']);
        $this->assertCount(3, $rows['body'][$this->getRecordResource()]);

        // Verify the returned products are the correct ones
        $names = array_column($rows['body'][$this->getRecordResource()], 'name');
        $this->assertContains('Product A', $names);
        $this->assertContains('Product B', $names);
        $this->assertContains('Product C', $names);
        $this->assertNotContains('Product D', $names);

        // Test updatedBetween query - should return only the second product when using its timestamp for both bounds
        $rows = $this->client->call(
            Client::METHOD_GET,
            $this->getRecordUrl($databaseId, $products['body']['$id']),
            array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()),
            [
                'queries' => [
                    Query::updatedBetween($secondProductUpdatedAt, $secondProductUpdatedAt)->toString(),
                ],
            ]
        );

        $this->assertEquals(200, $rows['headers']['status-code']);
        $this->assertCount(1, $rows['body'][$this->getRecordResource()]);
        $this->assertEquals('Product B', $rows['body'][$this->getRecordResource()][0]['name']);
        $this->assertEquals(139.99, $rows['body'][$this->getRecordResource()][0]['price']);
    }
    public function testDocumentWithEmptyPayload(): void
    {
        $data = $this->setupCollection();
        $databaseId = $data['databaseId'];
        $document = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            $this->getRecordIdParam() => ID::unique(),
            'data' => [],
            'permissions' => [
                Permission::read(Role::user($this->getUser()['$id'])),
                Permission::update(Role::user($this->getUser()['$id'])),
                Permission::delete(Role::user($this->getUser()['$id'])),
            ]
        ]);
        if ($this->getSupportForAttributes()) {
            $this->assertEquals(400, $document['headers']['status-code']);
        } else {
            $this->assertEquals(201, $document['headers']['status-code']);
            $this->assertEquals($data['moviesId'], $document['body'][$this->getContainerIdResponseKey()]);
            $this->assertArrayNotHasKey('$collection', $document['body']);
            $this->assertEquals($databaseId, $document['body']['$databaseId']);
            $this->assertTrue(array_key_exists('$sequence', $document['body']));
            $this->assertIsString($document['body']['$sequence']);

            $documentId = $document['body']['$id'];

            $fetched = $this->client->call(
                Client::METHOD_GET,
                $this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
                array_merge([
                    'content-type' => 'application/json',
                    'x-appwrite-project' => $this->getProject()['$id'],
                ], $this->getHeaders())
            );

            $this->assertEquals(200, $fetched['headers']['status-code']);
            $this->assertEqualsCanonicalizing([
                '$id',
                '$databaseId',
                '$createdAt',
                '$updatedAt',
                '$permissions',
                '$sequence',
                $this->getContainerIdResponseKey(),
            ], \array_keys($fetched['body']));
            $this->assertFalse(array_key_exists('$tenant', $fetched['body']));

            $updated = $this->client->call(
                Client::METHOD_PATCH,
                $this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
                array_merge([
                    'content-type' => 'application/json',
                    'x-appwrite-project' => $this->getProject()['$id'],
                ], $this->getHeaders()),
                [
                    'data' => [
                        'status' => 'draft',
                    ],
                ]
            );

            $this->assertEquals(200, $updated['headers']['status-code']);
            $this->assertEquals('draft', $updated['body']['status']);

            $refetched = $this->client->call(
                Client::METHOD_GET,
                $this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
                array_merge([
                    'content-type' => 'application/json',
                    'x-appwrite-project' => $this->getProject()['$id'],
                ], $this->getHeaders())
            );

            $this->assertEquals(200, $refetched['headers']['status-code']);
            $this->assertEquals('draft', $refetched['body']['status']);
        }
    }

    /**
     * API keys may set $createdAt / $updatedAt; invalid strings must return 400, not 500.
     * Assertions are HTTP status codes only (no error body matching).
     */
    public function testInvalidDate(): void
    {
        $data = $this->setupAttributes();
        $databaseId = $data['databaseId'];
        $invalidDatetime = '1dfs:12:55+sdf:00';
        $validUpdatedAt = '2024-01-01T00:00:00Z';

        $apiKeyHeaders = [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey'],
        ];

        $documentPayload = [
            'title' => 'Captain America',
            'releaseYear' => 1944,
            'actors' => [
                'Chris Evans',
                'Samuel Jackson',
            ],
        ];
        $permissions = [
            Permission::read(Role::user($this->getUser()['$id'])),
            Permission::update(Role::user($this->getUser()['$id'])),
            Permission::delete(Role::user($this->getUser()['$id'])),
        ];

        $invalidCreate = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), $apiKeyHeaders, [
            $this->getRecordIdParam() => ID::unique(),
            'data' => \array_merge($documentPayload, ['$updatedAt' => $invalidDatetime]),
            'permissions' => $permissions,
        ]);
        $this->assertEquals(400, $invalidCreate['headers']['status-code']);

        $document = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), $apiKeyHeaders, [
            $this->getRecordIdParam() => ID::unique(),
            'data' => $documentPayload,
            'permissions' => $permissions,
        ]);
        $this->assertEquals(201, $document['headers']['status-code']);
        $documentId = $document['body']['$id'];
        $this->assertNotEmpty($documentId);

        $invalidPatch = $this->client->call(
            Client::METHOD_PATCH,
            $this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
            $apiKeyHeaders,
            [
                'data' => [
                    '$updatedAt' => $invalidDatetime,
                ],
            ]
        );
        $this->assertEquals(400, $invalidPatch['headers']['status-code']);

        $updated = $this->client->call(
            Client::METHOD_PATCH,
            $this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
            $apiKeyHeaders,
            [
                'data' => [
                    '$updatedAt' => $validUpdatedAt,
                ],
            ]
        );
        $this->assertEquals(200, $updated['headers']['status-code']);

        $refetched = $this->client->call(
            Client::METHOD_GET,
            $this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
            $apiKeyHeaders
        );
        $this->assertEquals(200, $refetched['headers']['status-code']);
    }
}
