<?php

namespace Tests\E2E\Services\Databases\Transactions;

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

trait ACIDBase
{
    use SchemaPolling;
    /**
     * Test atomicity - all operations succeed or all fail
     */
    public function testAtomicity(): void
    {
        // Create database
        $database = $this->client->call(Client::METHOD_POST, $this->getDatabaseUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'databaseId' => ID::unique(),
            'name' => 'AtomicityTestDB'
        ]);

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

        // Create collection with unique constraint
        $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' => 'AtomicityTest',
            $this->getSecurityParam() => false,
            'permissions' => [
                Permission::create(Role::any()),
                Permission::read(Role::any()),
            ],
        ]);

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

        if ($this->getSupportForAttributes()) {
            // Add unique 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' => 'email',
                'size' => 256,
                'required' => true,
            ]);

            $this->waitForAllAttributes($databaseId, $collectionId);
        }

        // Add unique 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' => 'unique_email',
            'type' => Database::INDEX_UNIQUE,
            $this->getIndexAttributesParam() => ['email']
        ]);

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

        // Create first document outside transaction
        $doc1 = $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' => 'existing@example.com'
            ]
        ]);

        $this->assertEquals(201, $doc1['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'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->assertEquals(201, $transaction['headers']['status-code'], 'Transaction creation should succeed. Response: ' . json_encode($transaction));
        $this->assertArrayHasKey('$id', $transaction['body'], 'Transaction response should have $id. Response body: ' . json_encode($transaction['body']));
        $transactionId = $transaction['body']['$id'];

        // Add operations - second one will fail due to unique constraint
        $response = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transactionId) . "/operations", array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'operations' => [
                [
                    'databaseId' => $databaseId,
                    $this->getContainerIdParam() => $collectionId,
                    'action' => 'create',
                    $this->getRecordIdParam() => ID::unique(),
                    'data' => [
                        'email' => 'newuser@example.com' // This should succeed
                    ]
                ],
                [
                    'databaseId' => $databaseId,
                    $this->getContainerIdParam() => $collectionId,
                    'action' => 'create',
                    $this->getRecordIdParam() => ID::unique(),
                    'data' => [
                        'email' => 'existing@example.com' // This will fail - duplicate
                    ]
                ],
                [
                    'databaseId' => $databaseId,
                    $this->getContainerIdParam() => $collectionId,
                    'action' => 'create',
                    $this->getRecordIdParam() => ID::unique(),
                    'data' => [
                        'email' => 'anotheruser@example.com' // This should not be created due to atomicity
                    ]
                ]
            ]
        ]);

        $this->assertEquals(201, $response['headers']['status-code'], 'Add operations failed. Response: ' . json_encode($response['body']));

        // Attempt to commit - should fail due to unique constraint violation
        $response = $this->client->call(Client::METHOD_PATCH, $this->getTransactionUrl($transactionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'commit' => true
        ]);

        if ($response['headers']['status-code'] === 200) {
            // If transaction succeeded, all documents should be created
            $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
                'content-type' => 'application/json',
                'x-appwrite-project' => $this->getProject()['$id'],
            ], $this->getHeaders()));

            // Should have 4 documents total (1 original + 3 from transaction)
            // But since we have a unique constraint violation, this might fail
            $this->assertGreaterThanOrEqual(1, $documents['body']['total']);
        } else {
            $this->assertEquals(409, $response['headers']['status-code']); // Conflict error

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

            $this->assertEquals(1, $documents['body']['total']); // Only the original document
            $this->assertEquals('existing@example.com', $documents['body'][$this->getRecordResource()][0]['email']);
        }
    }

    /**
     * Test consistency - schema validation and constraints
     */
    public function testConsistency(): void
    {
        if (!$this->getSupportForAttributes()) {
            $this->markTestSkipped('This adapter does not support attributes; schema constraint consistency cannot be tested.');
        }

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

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

        // Create collection with required fields and constraints
        $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' => 'ConsistencyTest',
            $this->getSecurityParam() => false,
            'permissions' => [
                Permission::create(Role::any()),
                Permission::read(Role::any()),
            ],
        ]);

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

        // Add required 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' => 'required_field',
            'size' => 256,
            'required' => true,
        ]);

        // Add integer attribute with min/max constraints
        $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' => 'age',
            'required' => true,
            'min' => 18,
            'max' => 100
        ]);

        $this->waitForAllAttributes($databaseId, $collectionId);

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

        $transactionId = $transaction['body']['$id'];

        // Add operations with both valid and invalid data
        $response = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transactionId) . "/operations", array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'operations' => [
                [
                    'databaseId' => $databaseId,
                    $this->getContainerIdParam() => $collectionId,
                    'action' => 'create',
                    $this->getRecordIdParam() => ID::unique(),
                    'data' => [
                        'required_field' => 'Valid User',
                        'age' => 25 // Valid age
                    ]
                ],
                [
                    'databaseId' => $databaseId,
                    $this->getContainerIdParam() => $collectionId,
                    'action' => 'create',
                    $this->getRecordIdParam() => ID::unique(),
                    'data' => [
                        'required_field' => 'Too Young User',
                        'age' => 10 // Below minimum - will fail constraint
                    ]
                ],
                [
                    'databaseId' => $databaseId,
                    $this->getContainerIdParam() => $collectionId,
                    'action' => 'create',
                    $this->getRecordIdParam() => ID::unique(),
                    'data' => [
                        'required_field' => 'Another Valid User',
                        'age' => 30 // Valid but should not be created due to transaction failure
                    ]
                ]
            ]
        ]);

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

        // Attempt to commit - should fail due to constraint violation
        $response = $this->client->call(Client::METHOD_PATCH, $this->getTransactionUrl($transactionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'commit' => true
        ]);

        $this->assertEquals(400, $response['headers']['status-code'], 'Transaction commit should fail with 400 due to constraint violation. Response: ' . json_encode($response['body']));

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

        $this->assertEquals(0, $documents['body']['total']);
    }

    /**
     * Test isolation - concurrent transactions on same data
     */
    public function testIsolation(): void
    {
        // Create database
        $database = $this->client->call(Client::METHOD_POST, $this->getDatabaseUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'databaseId' => ID::unique(),
            'name' => 'IsolationTestDB'
        ]);

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

        // Create collection
        $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' => 'IsolationTest',
            $this->getSecurityParam() => false,
            'permissions' => [
                Permission::create(Role::any()),
                Permission::read(Role::any()),
                Permission::update(Role::any()),
            ],
        ]);

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

        if ($this->getSupportForAttributes()) {
            // Add counter 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' => 'counter',
                'required' => true,
                'min' => 0,
                'max' => 1000000
            ]);

            $this->waitForAllAttributes($databaseId, $collectionId);
        }

        // Create initial document with counter
        $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() => 'shared_counter',
            'data' => [
                'counter' => 0
            ]
        ]);

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

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

        $this->assertEquals(201, $transaction1['headers']['status-code'], 'Transaction 1 creation should succeed');
        $this->assertArrayHasKey('$id', $transaction1['body'], 'Transaction 1 response should have $id');
        $transactionId1 = $transaction1['body']['$id'];

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

        $this->assertEquals(201, $transaction2['headers']['status-code'], 'Transaction 2 creation should succeed');
        $this->assertArrayHasKey('$id', $transaction2['body'], 'Transaction 2 response should have $id');
        $transactionId2 = $transaction2['body']['$id'];

        // Transaction 1: Increment counter by 10
        $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transactionId1) . "/operations", array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'operations' => [
                [
                    'databaseId' => $databaseId,
                    $this->getContainerIdParam() => $collectionId,
                    $this->getRecordIdParam() => 'shared_counter',
                    'action' => 'increment',
                    'data' => [
                        $this->getSchemaParam() => 'counter',
                        'value' => 10
                    ]
                ]
            ]
        ]);

        // Transaction 2: Increment counter by 5
        $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transactionId2) . "/operations", array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'operations' => [
                [
                    'databaseId' => $databaseId,
                    $this->getContainerIdParam() => $collectionId,
                    $this->getRecordIdParam() => 'shared_counter',
                    'action' => 'increment',
                    'data' => [
                        $this->getSchemaParam() => 'counter',
                        'value' => 5
                    ]
                ]
            ]
        ]);

        // Commit first transaction
        $response1 = $this->client->call(Client::METHOD_PATCH, $this->getTransactionUrl($transactionId1), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'commit' => true
        ]);

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

        // Commit second transaction
        $response2 = $this->client->call(Client::METHOD_PATCH, $this->getTransactionUrl($transactionId2), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'commit' => true
        ]);

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

        // Check final value - both increments should be applied
        $document = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId, 'shared_counter'), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        // Both increments should be applied: 0 + 10 + 5 = 15
        $this->assertEquals(15, $document['body']['counter']);
    }

    /**
     * Test durability - committed data persists
     */
    public function testDurability(): void
    {
        // Create database
        $database = $this->client->call(Client::METHOD_POST, $this->getDatabaseUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'databaseId' => ID::unique(),
            'name' => 'DurabilityTestDB'
        ]);

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

        // Create collection
        $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' => 'DurabilityTest',
            $this->getSecurityParam() => false,
            'permissions' => [
                Permission::create(Role::any()),
                Permission::read(Role::any()),
                Permission::update(Role::any()),
                Permission::delete(Role::any()),
            ],
        ]);

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

        if ($this->getSupportForAttributes()) {
            // Add 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' => 'data',
                'size' => 256,
                'required' => true,
            ]);

            $this->waitForAllAttributes($databaseId, $collectionId);
        }

        // Create and commit transaction with multiple operations
        $transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]));

        $this->assertEquals(201, $transaction['headers']['status-code'], 'Transaction creation should succeed');
        $this->assertArrayHasKey('$id', $transaction['body'], 'Transaction response should have $id');
        $transactionId = $transaction['body']['$id'];

        // Add multiple operations
        $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transactionId) . "/operations", array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'operations' => [
                [
                    'databaseId' => $databaseId,
                    $this->getContainerIdParam() => $collectionId,
                    'action' => 'create',
                    $this->getRecordIdParam() => 'durable_doc_1',
                    'data' => [
                        'data' => 'Important data 1'
                    ]
                ],
                [
                    'databaseId' => $databaseId,
                    $this->getContainerIdParam() => $collectionId,
                    'action' => 'create',
                    $this->getRecordIdParam() => 'durable_doc_2',
                    'data' => [
                        'data' => 'Important data 2'
                    ]
                ],
                [
                    'databaseId' => $databaseId,
                    $this->getContainerIdParam() => $collectionId,
                    'action' => 'update',
                    $this->getRecordIdParam() => 'durable_doc_1',
                    'data' => [
                        'data' => 'Updated important data 1'
                    ]
                ]
            ]
        ]);

        // Commit transaction
        $response = $this->client->call(Client::METHOD_PATCH, $this->getTransactionUrl($transactionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
            'x-appwrite-key' => $this->getProject()['apiKey']
        ]), [
            'commit' => true
        ]);

        $this->assertEquals(200, $response['headers']['status-code'], 'Commit should succeed. Response: ' . json_encode($response['body']));
        $this->assertEquals('committed', $response['body']['status']);

        // List all documents to see what was created
        $allDocs = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        $this->assertGreaterThan(0, $allDocs['body']['total'], 'Should have created documents. Found: ' . json_encode($allDocs['body']));

        // Verify documents exist and have correct data
        $document1 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId, 'durable_doc_1'), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()));

        $this->assertEquals(200, $document1['headers']['status-code']);
        $this->assertEquals('Updated important data 1', $document1['body']['data']);

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

        $this->assertEquals(200, $document2['headers']['status-code']);
        $this->assertEquals('Important data 2', $document2['body']['data']);

        // Further update outside transaction to ensure persistence
        $update = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, 'durable_doc_1'), array_merge([
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ], $this->getHeaders()), [
            'data' => [
                'data' => 'Modified outside transaction'
            ]
        ]);

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

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

        $this->assertEquals('Modified outside transaction', $document1['body']['data']);

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

        $this->assertEquals(2, $documents['body']['total']);
    }
}
