<?php

namespace Tests\E2E\Services\Databases\Transactions;

use Tests\E2E\Client;
use Tests\E2E\Scopes\SchemaPolling;
use Tests\E2E\Traits\DatabasesUrlHelpers;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;

trait TransactionPermissionsBase
{
    use DatabasesUrlHelpers;
    use SchemaPolling;

    protected static string $permissionsDatabase = '';

    /**
     * Set up database once for all permission tests in this class
     */
    public static function setUpBeforeClass(): void
    {
        parent::setUpBeforeClass();
    }

    /**
     * Initialize the permissions database if not already done
     */
    protected function ensurePermissionsDatabase(): void
    {
        if (!empty(self::$permissionsDatabase)) {
            return;
        }

        $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' => 'PermissionsTestDB'
        ]);

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

    /**
     * Get the permissions database ID, creating it if needed
     */
    protected function getPermissionsDatabase(): string
    {
        $this->ensurePermissionsDatabase();
        return self::$permissionsDatabase;
    }

    /**
     * Clean up database after all tests in this class
     */
    public static function tearDownAfterClass(): void
    {
        // Database cleanup is handled by the test framework
        self::$permissionsDatabase = '';
        parent::tearDownAfterClass();
    }

    /**
     * Test collection-level create permission check on staging
     */
    public function testCollectionCreatePermissionDenied(): void
    {
        // Create a collection with no create permission for current user
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($this->getPermissionsDatabase()), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => 'permTest1',
            'name' => 'Permission Test 1',
            'permissions' => [
                Permission::read(Role::any()),
                Permission::update(Role::any()),
                Permission::delete(Role::any()),
            ],
            $this->getSecurityParam() => false,
        ]);

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

        if ($this->getSupportForAttributes()) {
            $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], 'string'), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                'key' => 'title',
                'size' => 255,
                'required' => true,
            ]);

            $this->assertEquals(202, $attribute['headers']['status-code']);
            $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
        }

        // Create transaction
        $transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

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

        // Try to stage a create operation without permission, should fail
        $staged = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transaction['body']['$id']) . '/operations', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'operations' => [[
                'action' => 'create',
                'databaseId' => $this->getPermissionsDatabase(),
                $this->getContainerIdParam() => $collection['body']['$id'],
                $this->getRecordIdParam() => 'testDoc1',
                'data' => ['title' => 'Test Document'],
            ]]
        ]);

        // This should fail with 401 Unauthorized
        $this->assertEquals(401, $staged['headers']['status-code']);
    }

    /**
     * Test collection-level update permission check on staging
     */
    public function testCollectionUpdatePermissionDenied(): void
    {
        // Create a collection with create but no update permission
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($this->getPermissionsDatabase()), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => 'permTest2',
            'name' => 'Permission Test 2',
            'permissions' => [
                Permission::create(Role::any()),
                Permission::read(Role::any()),
                Permission::delete(Role::any()),
            ],
            $this->getSecurityParam() => false,
        ]);

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

        if ($this->getSupportForAttributes()) {
            $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], 'string'), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                'key' => 'title',
                'size' => 255,
                'required' => true,
            ]);

            $this->assertEquals(202, $attribute['headers']['status-code']);
            $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
        }

        // Create a document first with API key
        $doc = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($this->getPermissionsDatabase(), $collection['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getRecordIdParam() => 'testDoc2',
            'data' => ['title' => 'Original Title'],
        ]);

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

        // Create transaction
        $transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

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

        // Try to stage an update operation without permission, should fail
        $staged = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transaction['body']['$id']) . '/operations', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'operations' => [[
                'action' => 'update',
                'databaseId' => $this->getPermissionsDatabase(),
                $this->getContainerIdParam() => $collection['body']['$id'],
                $this->getRecordIdParam() => 'testDoc2',
                'data' => ['title' => 'Updated Title'],
            ]]
        ]);

        // This should fail with 401 Unauthorized
        $this->assertEquals(401, $staged['headers']['status-code']);
    }

    /**
     * Test collection-level delete permission check on staging
     */
    public function testCollectionDeletePermissionDenied(): void
    {
        // Create a collection with create, read but no delete permission
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($this->getPermissionsDatabase()), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => 'permTest3',
            'name' => 'Permission Test 3',
            'permissions' => [
                Permission::create(Role::any()),
                Permission::read(Role::any()),
                Permission::update(Role::any()),
            ],
            $this->getSecurityParam() => false,
        ]);

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

        if ($this->getSupportForAttributes()) {
            $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], 'string'), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                'key' => 'title',
                'size' => 255,
                'required' => true,
            ]);

            $this->assertEquals(202, $attribute['headers']['status-code']);
            $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
        }

        $doc = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($this->getPermissionsDatabase(), $collection['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getRecordIdParam() => 'testDoc3',
            'data' => ['title' => 'To Be Deleted'],
        ]);

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

        // Create transaction
        $transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

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

        // Try to stage a delete operation without permission, should fail
        $staged = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transaction['body']['$id']) . '/operations', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'operations' => [[
                'action' => 'delete',
                'databaseId' => $this->getPermissionsDatabase(),
                $this->getContainerIdParam() => $collection['body']['$id'],
                $this->getRecordIdParam() => 'testDoc3',
                'data' => [],
            ]]
        ]);

        // This should fail with 401 Unauthorized
        $this->assertEquals(401, $staged['headers']['status-code']);
    }

    /**
     * Test document-level update permission grants access when rowSecurity is enabled
     * Collection has no update permission, but document does, should succeed
     */
    public function testDocumentLevelUpdatePermissionGranted(): void
    {
        // Create collection with rowSecurity enabled but no update permission at collection level
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($this->getPermissionsDatabase()), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => 'permTest4',
            'name' => 'Permission Test 4',
            'permissions' => [
                Permission::read(Role::any()),
                Permission::create(Role::any()),
            ],
            $this->getSecurityParam() => true,
        ]);

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

        if ($this->getSupportForAttributes()) {
            $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], 'string'), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                'key' => 'title',
                'size' => 255,
                'required' => true,
            ]);

            $this->assertEquals(202, $attribute['headers']['status-code']);
            $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
        }

        // Create a document with update permission at document level
        $doc = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($this->getPermissionsDatabase(), $collection['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getRecordIdParam() => 'testDoc4',
            'data' => ['title' => 'Protected Document'],
            'permissions' => [
                Permission::read(Role::any()),
                Permission::update(Role::any()),
            ],
        ]);

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

        // Create transaction
        $transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

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

        // Stage an update, should succeed because document has update permission
        $staged = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transaction['body']['$id']) . '/operations', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'operations' => [[
                'action' => 'update',
                'databaseId' => $this->getPermissionsDatabase(),
                $this->getContainerIdParam() => $collection['body']['$id'],
                $this->getRecordIdParam() => 'testDoc4',
                'data' => ['title' => 'Trying to Update'],
            ]]
        ]);

        // This should succeed with 201 because document has update permission
        $this->assertEquals(201, $staged['headers']['status-code']);
    }

    /**
     * Test document-level delete permission grants access when rowSecurity is enabled
     * Collection has no delete permission, but document does, should succeed
     */
    public function testDocumentLevelDeletePermissionGranted(): void
    {
        // Create collection with rowSecurity enabled but no delete permission at collection level
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($this->getPermissionsDatabase()), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => 'permTest5',
            'name' => 'Permission Test 5',
            'permissions' => [
                Permission::create(Role::any()),
                Permission::read(Role::any()),
                Permission::update(Role::any()),
            ],
            $this->getSecurityParam() => true,
        ]);

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

        if ($this->getSupportForAttributes()) {
            $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], 'string'), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                'key' => 'title',
                'size' => 255,
                'required' => true,
            ]);

            $this->assertEquals(202, $attribute['headers']['status-code']);
            $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
        }

        // Create a document with delete permission at document level
        $doc = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($this->getPermissionsDatabase(), $collection['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getRecordIdParam() => 'testDoc5',
            'data' => ['title' => 'Can Delete Me'],
            'permissions' => [
                Permission::read(Role::any()),
                Permission::update(Role::any()),
                Permission::delete(Role::any()),
            ],
        ]);

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

        // Create transaction
        $transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

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

        // Stage a delete should succeed because document has delete permission
        $staged = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transaction['body']['$id']) . '/operations', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'operations' => [[
                'action' => 'delete',
                'databaseId' => $this->getPermissionsDatabase(),
                $this->getContainerIdParam() => $collection['body']['$id'],
                $this->getRecordIdParam() => 'testDoc5',
                'data' => [],
            ]]
        ]);

        // This should succeed with 201 because document has DELETE permission
        $this->assertEquals(201, $staged['headers']['status-code']);
    }

    /**
     * Test that users cannot set permissions for roles they don't have
     */
    public function testCannotSetUnauthorizedRolePermissions(): void
    {
        // Create a collection
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($this->getPermissionsDatabase()), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => 'permTest6',
            'name' => 'Permission Test 6',
            'permissions' => [
                Permission::read(Role::any()),
                Permission::create(Role::any()),
                Permission::update(Role::any()),
                Permission::delete(Role::any()),
            ],
            $this->getSecurityParam() => true,
        ]);

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

        // Add attribute
        if ($this->getSupportForAttributes()) {
            $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], 'string'), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                'key' => 'title',
                'size' => 255,
                'required' => true,
            ]);

            $this->assertEquals(202, $attribute['headers']['status-code']);
            $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
        }

        // Create transaction
        $transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

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

        // Try to stage a create with team permissions, current user is not in team
        $staged = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transaction['body']['$id']) . '/operations', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'operations' => [[
                'action' => 'create',
                'databaseId' => $this->getPermissionsDatabase(),
                $this->getContainerIdParam() => $collection['body']['$id'],
                $this->getRecordIdParam() => 'testDoc6',
                'data' => [
                    'title' => 'Admin Only Doc',
                    '$permissions' => [
                        Permission::read(Role::any()),
                        Permission::update(Role::team('adminTeam')),
                    ],
                ],
            ]]
        ]);

        // This should fail with 401 Unauthorized, cannot set permissions for roles you don't have
        $this->assertEquals(401, $staged['headers']['status-code']);
        $this->assertArrayHasKey('message', $staged['body']);
        $this->assertStringContainsString('Permissions must be one of', $staged['body']['message']);
    }

    /**
     * Test successful staging when user has the required permissions
     */
    public function testSuccessfulStagingWithProperPermissions(): void
    {
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($this->getPermissionsDatabase()), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => 'permTest7',
            'name' => 'Permission Test 7',
            'permissions' => [
                Permission::read(Role::any()),
                Permission::create(Role::any()),
                Permission::update(Role::any()),
                Permission::delete(Role::any()),
            ],
            $this->getSecurityParam() => true,
        ]);

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

        if ($this->getSupportForAttributes()) {
            $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], 'string'), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                'key' => 'title',
                'size' => 255,
                'required' => true,
            ]);

            $this->assertEquals(202, $attribute['headers']['status-code']);
            $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
        }

        // Create transaction
        $transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

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

        // Stage a create with permissions for current user's roles, should succeed
        $staged = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transaction['body']['$id']) . '/operations', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'operations' => [[
                'action' => 'create',
                'databaseId' => $this->getPermissionsDatabase(),
                $this->getContainerIdParam() => $collection['body']['$id'],
                $this->getRecordIdParam() => 'testDoc7',
                'data' => [
                    'title' => 'Valid Document',
                    '$permissions' => [
                        Permission::read(Role::any()),
                        Permission::update(Role::user($this->getUser()['$id'])),
                    ],
                ],
            ]]
        ]);

        // This should succeed
        $this->assertEquals(201, $staged['headers']['status-code']);
        $this->assertEquals(1, $staged['body']['operations']);
    }

    /**
     * Test that non-existent documents cannot be updated in transactions
     */
    public function testCannotUpdateNonExistentDocument(): void
    {
        // Create a collection
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($this->getPermissionsDatabase()), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => 'permTest8',
            'name' => 'Permission Test 8',
            'permissions' => [
                Permission::read(Role::any()),
                Permission::create(Role::any()),
                Permission::update(Role::any()),
                Permission::delete(Role::any()),
            ],
            $this->getSecurityParam() => false,
        ]);

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

        if ($this->getSupportForAttributes()) {
            $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], 'string'), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                'key' => 'title',
                'size' => 255,
                'required' => true,
            ]);

            $this->assertEquals(202, $attribute['headers']['status-code']);
            $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
        }

        // Create transaction
        $transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

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

        // Try to update a document that doesn't exist - should fail
        $staged = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transaction['body']['$id']) . '/operations', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'operations' => [[
                'action' => 'update',
                'databaseId' => $this->getPermissionsDatabase(),
                $this->getContainerIdParam() => $collection['body']['$id'],
                $this->getRecordIdParam() => 'nonExistentDoc',
                'data' => ['title' => 'Trying to Update'],
            ]]
        ]);

        // This should fail with 404 Not Found
        $this->assertEquals(404, $staged['headers']['status-code']);
    }

    /**
     * Test that non-existent documents cannot be deleted in transactions
     */
    public function testCannotDeleteNonExistentDocument(): void
    {
        // Create a collection
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($this->getPermissionsDatabase()), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => 'permTest9',
            'name' => 'Permission Test 9',
            'permissions' => [
                Permission::read(Role::any()),
                Permission::create(Role::any()),
                Permission::update(Role::any()),
                Permission::delete(Role::any()),
            ],
            $this->getSecurityParam() => false,
        ]);

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

        if ($this->getSupportForAttributes()) {
            $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], 'string'), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                'key' => 'title',
                'size' => 255,
                'required' => true,
            ]);

            $this->assertEquals(202, $attribute['headers']['status-code']);
            $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
        }

        // Create transaction
        $transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

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

        // Try to delete a document that doesn't exist, should fail
        $staged = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transaction['body']['$id']) . '/operations', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'operations' => [[
                'action' => 'delete',
                'databaseId' => $this->getPermissionsDatabase(),
                $this->getContainerIdParam() => $collection['body']['$id'],
                $this->getRecordIdParam() => 'nonExistentDoc',
                'data' => [],
            ]]
        ]);

        // This should fail with 404 Not Found
        $this->assertEquals(404, $staged['headers']['status-code']);
    }

    /**
     * Test that a document created in one batch can be updated in a subsequent batch within the same transaction
     * This validates the transactionState->getDocument() fix for cross-batch dependencies
     */
    public function testCanUpdateDocumentCreatedInPreviousBatch(): void
    {
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($this->getPermissionsDatabase()), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => 'permTest10',
            'name' => 'Permission Test 10',
            'permissions' => [
                Permission::read(Role::any()),
                Permission::create(Role::any()),
                Permission::update(Role::any()),
                Permission::delete(Role::any()),
            ],
            $this->getSecurityParam() => false,
        ]);

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

        if ($this->getSupportForAttributes()) {
            $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], 'string'), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                'key' => 'title',
                'size' => 255,
                'required' => true,
            ]);

            $this->assertEquals(202, $attribute['headers']['status-code']);
            $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
        }

        // Create transaction
        $transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

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

        // Batch 1: Create a document
        $batch1 = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transaction['body']['$id']) . '/operations', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'operations' => [[
                'action' => 'create',
                'databaseId' => $this->getPermissionsDatabase(),
                $this->getContainerIdParam() => $collection['body']['$id'],
                $this->getRecordIdParam() => 'crossBatchDoc',
                'data' => [
                    'title' => 'Initial Title',
                ],
            ]]
        ]);

        $this->assertEquals(201, $batch1['headers']['status-code']);
        $this->assertEquals(1, $batch1['body']['operations']);

        // Batch 2: Update the document created in batch 1
        $batch2 = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transaction['body']['$id']) . '/operations', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'operations' => [[
                'action' => 'update',
                'databaseId' => $this->getPermissionsDatabase(),
                $this->getContainerIdParam() => $collection['body']['$id'],
                $this->getRecordIdParam() => 'crossBatchDoc',
                'data' => [
                    'title' => 'Updated Title',
                ],
            ]]
        ]);

        // This should succeed with 201 because transactionState finds the staged document from batch 1
        $this->assertEquals(201, $batch2['headers']['status-code']);
        $this->assertEquals(2, $batch2['body']['operations']);

        // Batch 3: Delete the same document
        $batch3 = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transaction['body']['$id']) . '/operations', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'operations' => [[
                'action' => 'delete',
                'databaseId' => $this->getPermissionsDatabase(),
                $this->getContainerIdParam() => $collection['body']['$id'],
                $this->getRecordIdParam() => 'crossBatchDoc',
                'data' => [],
            ]]
        ]);

        // This should also succeed with 201
        $this->assertEquals(201, $batch3['headers']['status-code']);
        $this->assertEquals(3, $batch3['body']['operations']);

        // Rollback to clean up
        $rollback = $this->client->call(Client::METHOD_PATCH, $this->getTransactionUrl($transaction['body']['$id']), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'rollback' => true,
        ]);

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

    /**
     * Test that one user cannot read another user's transaction
     */
    public function testUserCannotReadAnotherUsersTransaction(): void
    {
        // Create user 1 (fresh) and their transaction
        $user1 = $this->getUser(true);
        $user1Headers = [
            'origin' => 'http://localhost',
            'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user1['session'],
        ];

        $transaction1 = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $user1Headers));

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

        // Create user 2 (fresh)
        $user2 = $this->getUser(true); // Fresh user
        $user2Headers = [
            'origin' => 'http://localhost',
            'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user2['session'],
        ];

        // User 2 tries to read User 1's transaction - should fail
        $readAttempt = $this->client->call(Client::METHOD_GET, $this->getTransactionUrl($transactionId1), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $user2Headers));

        // This should fail with 404 Not Found (transaction doesn't exist for this user)
        $this->assertEquals(404, $readAttempt['headers']['status-code']);

        // Verify User 1 can still read their own transaction
        $readOwn = $this->client->call(Client::METHOD_GET, $this->getTransactionUrl($transactionId1), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $user1Headers));

        $this->assertEquals(200, $readOwn['headers']['status-code']);
        $this->assertEquals($transactionId1, $readOwn['body']['$id']);
    }

    /**
     * Test that one user cannot list another user's transactions
     */
    public function testUserCannotListAnotherUsersTransactions(): void
    {
        // Create user 1 (fresh) with transactions
        $user1 = $this->getUser(true);
        $user1Headers = [
            'origin' => 'http://localhost',
            'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user1['session'],
        ];

        $transaction1 = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $user1Headers));

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

        $transaction2 = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $user1Headers));

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

        // Create user 2 (fresh) with their own transaction
        $user2 = $this->getUser(true); // Fresh user
        $user2Headers = [
            'origin' => 'http://localhost',
            'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user2['session'],
        ];

        $transaction3 = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $user2Headers));

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

        // User 2 lists transactions - should only see their own
        $listUser2 = $this->client->call(Client::METHOD_GET, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $user2Headers));

        $this->assertEquals(200, $listUser2['headers']['status-code']);
        $this->assertEquals(1, $listUser2['body']['total']);
        $this->assertEquals($transaction3['body']['$id'], $listUser2['body']['transactions'][0]['$id']);

        // User 1 lists transactions - should only see their own (2 transactions)
        $listUser1 = $this->client->call(Client::METHOD_GET, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $user1Headers));

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

        // Verify neither of user1's transactions appear in user2's list
        $user2TransactionIds = array_column($listUser2['body']['transactions'], '$id');
        $this->assertNotContains($transaction1['body']['$id'], $user2TransactionIds);
        $this->assertNotContains($transaction2['body']['$id'], $user2TransactionIds);
    }

    /**
     * Test that one user cannot update another user's transaction
     */
    public function testUserCannotUpdateAnotherUsersTransaction(): void
    {
        // Create user 1 (fresh) and their transaction
        $user1 = $this->getUser(true);
        $user1Headers = [
            'origin' => 'http://localhost',
            'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user1['session'],
        ];

        $transaction1 = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $user1Headers));

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

        // Create user 2 (fresh)
        $user2 = $this->getUser(true); // Fresh user
        $user2Headers = [
            'origin' => 'http://localhost',
            'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user2['session'],
        ];

        // User 2 tries to commit User 1's transaction - should fail
        $commitAttempt = $this->client->call(Client::METHOD_PATCH, $this->getTransactionUrl($transactionId1), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $user2Headers), [
            'commit' => true,
        ]);

        // This should fail with 404 Not Found
        $this->assertEquals(404, $commitAttempt['headers']['status-code']);

        // User 2 tries to rollback User 1's transaction - should also fail
        $rollbackAttempt = $this->client->call(Client::METHOD_PATCH, $this->getTransactionUrl($transactionId1), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $user2Headers), [
            'rollback' => true,
        ]);

        // This should also fail with 404 Not Found
        $this->assertEquals(404, $rollbackAttempt['headers']['status-code']);

        // Verify User 1 can still commit their own transaction
        $commitOwn = $this->client->call(Client::METHOD_PATCH, $this->getTransactionUrl($transactionId1), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $user1Headers), [
            'commit' => true,
        ]);

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

    /**
     * Test that one user cannot delete another user's transaction
     */
    public function testUserCannotDeleteAnotherUsersTransaction(): void
    {
        // Create user 1 (fresh) and their transaction
        $user1 = $this->getUser(true);
        $user1Headers = [
            'origin' => 'http://localhost',
            'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user1['session'],
        ];

        $transaction1 = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $user1Headers));

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

        // Create user 2 (fresh)
        $user2 = $this->getUser(true); // Fresh user
        $user2Headers = [
            'origin' => 'http://localhost',
            'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user2['session'],
        ];

        // User 2 tries to delete User 1's transaction - should fail
        $deleteAttempt = $this->client->call(Client::METHOD_DELETE, $this->getTransactionUrl($transactionId1), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $user2Headers));

        // This should fail with 404 Not Found
        $this->assertEquals(404, $deleteAttempt['headers']['status-code']);

        // Verify User 1 can still access their transaction
        $readOwn = $this->client->call(Client::METHOD_GET, $this->getTransactionUrl($transactionId1), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $user1Headers));

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

        // User 1 can delete their own transaction
        $deleteOwn = $this->client->call(Client::METHOD_DELETE, $this->getTransactionUrl($transactionId1), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $user1Headers));

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

    /**
     * Test that one user cannot add operations to another user's transaction
     */
    public function testUserCannotAddOperationsToAnotherUsersTransaction(): void
    {
        // Create a collection for testing
        $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($this->getPermissionsDatabase()), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            $this->getContainerIdParam() => 'permTest11',
            'name' => 'Permission Test 11',
            'permissions' => [
                Permission::read(Role::any()),
                Permission::create(Role::any()),
                Permission::update(Role::any()),
                Permission::delete(Role::any()),
            ],
            $this->getSecurityParam() => false,
        ]);

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

        if ($this->getSupportForAttributes()) {
            $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], 'string'), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
                'x-appwrite-key' => $this->getProject()['apiKey']
            ]), [
                'key' => 'title',
                'size' => 255,
                'required' => true,
            ]);

            $this->assertEquals(202, $attribute['headers']['status-code']);
            $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
        }

        // Create user 1 (fresh) and their transaction
        $user1 = $this->getUser(true);
        $user1Headers = [
            'origin' => 'http://localhost',
            'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user1['session'],
        ];

        $transaction1 = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $user1Headers));

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

        // Create user 2 (fresh)
        $user2 = $this->getUser(true); // Fresh user
        $user2Headers = [
            'origin' => 'http://localhost',
            'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user2['session'],
        ];

        // User 2 tries to add operations to User 1's transaction - should fail
        $operationAttempt = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transactionId1) . '/operations', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $user2Headers), [
            'operations' => [[
                'action' => 'create',
                'databaseId' => $this->getPermissionsDatabase(),
                $this->getContainerIdParam() => $collection['body']['$id'],
                $this->getRecordIdParam() => 'maliciousDoc',
                'data' => ['title' => 'Malicious Document'],
            ]]
        ]);

        // This should fail with 404 Not Found
        $this->assertEquals(404, $operationAttempt['headers']['status-code']);

        // Verify User 1 can still add operations to their own transaction
        $operationOwn = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transactionId1) . '/operations', array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $user1Headers), [
            'operations' => [[
                'action' => 'create',
                'databaseId' => $this->getPermissionsDatabase(),
                $this->getContainerIdParam() => $collection['body']['$id'],
                $this->getRecordIdParam() => 'legitimateDoc',
                'data' => ['title' => 'Legitimate Document'],
            ]]
        ]);

        $this->assertEquals(201, $operationOwn['headers']['status-code']);
        $this->assertEquals(1, $operationOwn['body']['operations']);
    }

    /**
     * Test that an authenticated user can successfully list their own transactions
     */
    public function testAuthenticatedUserCanListTheirOwnTransactions(): void
    {
        // Create an authenticated user
        $user = $this->getUser();
        $userHeaders = [
            'origin' => 'http://localhost',
            'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user['session'],
        ];

        // Create multiple transactions for this user
        $transactionIds = [];
        for ($i = 0; $i < 3; $i++) {
            $transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $userHeaders));

            $this->assertEquals(201, $transaction['headers']['status-code']);
            $this->assertNotEmpty($transaction['body']['$id']);
            $transactionIds[] = $transaction['body']['$id'];
        }

        // List transactions
        $list = $this->client->call(Client::METHOD_GET, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $userHeaders));

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

        // Verify all created transactions are in the list
        $listedIds = array_column($list['body']['transactions'], '$id');
        foreach ($transactionIds as $transactionId) {
            $this->assertContains($transactionId, $listedIds);
        }

        // Verify transaction structure
        foreach ($list['body']['transactions'] as $transaction) {
            $this->assertArrayHasKey('$id', $transaction);
            $this->assertArrayHasKey('$createdAt', $transaction);
            $this->assertArrayHasKey('$updatedAt', $transaction);
            $this->assertArrayHasKey('status', $transaction);
            $this->assertArrayHasKey('operations', $transaction);
        }
    }

    /**
     * Test that an authenticated user can successfully delete their own transaction
     */
    public function testAuthenticatedUserCanDeleteTheirOwnTransaction(): void
    {
        // Create an authenticated user
        $user = $this->getUser();
        $userHeaders = [
            'origin' => 'http://localhost',
            'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user['session'],
        ];

        // Create a transaction
        $transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $userHeaders));

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

        // Verify transaction exists by reading it
        $read = $this->client->call(Client::METHOD_GET, $this->getTransactionUrl($transactionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $userHeaders));

        $this->assertEquals(200, $read['headers']['status-code']);
        $this->assertEquals($transactionId, $read['body']['$id']);

        // Delete the transaction
        $delete = $this->client->call(Client::METHOD_DELETE, $this->getTransactionUrl($transactionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $userHeaders));

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

        // Verify transaction is deleted by trying to read it again
        $readAfterDelete = $this->client->call(Client::METHOD_GET, $this->getTransactionUrl($transactionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $userHeaders));

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

        // Create another transaction and verify it can also be deleted
        $transaction2 = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $userHeaders));

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

        $delete2 = $this->client->call(Client::METHOD_DELETE, $this->getTransactionUrl($transactionId2), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $userHeaders));

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