<?php
 namespace MongoDB\Tests\SpecTests;
 use LogicException;
 use MongoDB\Client;
 use MongoDB\Collection;
 use MongoDB\Database;
 use MongoDB\Driver\Cursor;
 use MongoDB\Driver\Exception\BulkWriteException;
 use MongoDB\Driver\Exception\Exception;
 use MongoDB\Driver\Server;
 use MongoDB\Driver\Session;
 use MongoDB\Driver\WriteConcern;
 use MongoDB\GridFS\Bucket;
 use MongoDB\Operation\FindOneAndReplace;
 use MongoDB\Operation\FindOneAndUpdate;
 use stdClass;
 use function array_diff_key;
 use function array_map;
 use function fclose;
 use function fopen;
 use function MongoDB\is_last_pipeline_operator_write;
 use function MongoDB\with_transaction;
 use function stream_get_contents;
 use function strtolower;
 final class Operation {
 const OBJECT_CLIENT = 'client';
 const OBJECT_COLLECTION = 'collection';
 const OBJECT_DATABASE = 'database';
 const OBJECT_GRIDFS_BUCKET = 'gridfsbucket';
 const OBJECT_SELECT_COLLECTION = 'selectCollection';
 const OBJECT_SELECT_DATABASE = 'selectDatabase';
 const OBJECT_SESSION0 = 'session0';
 const OBJECT_SESSION1 = 'session1';
 const OBJECT_TEST_RUNNER = 'testRunner';
 public $errorExpectation;
 public $resultExpectation;
 private $arguments = [];
 private $collectionName;
 private $collectionOptions = [];
 private $databaseName;
 private $name;
 private $object = self::OBJECT_COLLECTION;
 private function __construct(stdClass $operation) {
 $this->name = $operation->name;
 if (isset($operation->arguments)) {
 $this->arguments = (array) $operation->arguments;
 }
 if (isset($operation->object)) {
 $this->object = $operation->object;
 }
 }
 public static function fromChangeStreams(stdClass $operation) {
 $o = new self($operation);
 $writeConcern = new WriteConcern(WriteConcern::MAJORITY);
 $o->errorExpectation = ErrorExpectation::noError();
 if ($operation->name === 'rename') {
 $o->object = self::OBJECT_SELECT_DATABASE;
 $o->databaseName = 'admin';
 $o->name = 'runCommand';
 $o->arguments = [ 'command' => [ 'renameCollection' => $operation->database . '.' . $operation->collection, 'to' => $operation->database . '.' . $operation->arguments->to, 'writeConcern' => $writeConcern, ], ];
 return $o;
 }
 $o->databaseName = $operation->database;
 $o->collectionName = $operation->collection;
 $o->collectionOptions = ['writeConcern' => $writeConcern];
 $o->object = self::OBJECT_SELECT_COLLECTION;
 return $o;
 }
 public static function fromClientSideEncryption(stdClass $operation) {
 $o = new self($operation);
 $o->errorExpectation = ErrorExpectation::fromClientSideEncryption($operation);
 $o->resultExpectation = ResultExpectation::fromClientSideEncryption($operation, $o->getResultAssertionType());
 if (isset($operation->collectionOptions)) {
 $o->collectionOptions = (array) $operation->collectionOptions;
 }
 return $o;
 }
 public static function fromCommandMonitoring(stdClass $operation) {
 $o = new self($operation);
 if (isset($operation->collectionOptions)) {
 $o->collectionOptions = (array) $operation->collectionOptions;
 }
 return $o;
 }
 private static function fromConvenientTransactions(stdClass $operation) {
 $o = new self($operation);
 if (isset($operation->error)) {
 $o->errorExpectation = ErrorExpectation::fromTransactions($operation);
 }
 $o->resultExpectation = ResultExpectation::fromTransactions($operation, $o->getResultAssertionType());
 if (isset($operation->collectionOptions)) {
 $o->collectionOptions = (array) $operation->collectionOptions;
 }
 return $o;
 }
 public static function fromCrud(stdClass $operation) {
 $o = new self($operation);
 $o->errorExpectation = ErrorExpectation::fromCrud($operation);
 $o->resultExpectation = ResultExpectation::fromCrud($operation, $o->getResultAssertionType());
 if (isset($operation->collectionOptions)) {
 $o->collectionOptions = (array) $operation->collectionOptions;
 }
 return $o;
 }
 public static function fromRetryableReads(stdClass $operation) {
 $o = new self($operation);
 $o->errorExpectation = ErrorExpectation::fromRetryableReads($operation);
 $o->resultExpectation = ResultExpectation::fromRetryableReads($operation, $o->getResultAssertionType());
 return $o;
 }
 public static function fromRetryableWrites(stdClass $operation, stdClass $outcome) {
 $o = new self($operation);
 $o->errorExpectation = ErrorExpectation::fromRetryableWrites($outcome);
 $o->resultExpectation = ResultExpectation::fromRetryableWrites($outcome, $o->getResultAssertionType());
 return $o;
 }
 public static function fromTransactions(stdClass $operation) {
 $o = new self($operation);
 $o->errorExpectation = ErrorExpectation::fromTransactions($operation);
 $o->resultExpectation = ResultExpectation::fromTransactions($operation, $o->getResultAssertionType());
 if (isset($operation->collectionOptions)) {
 $o->collectionOptions = (array) $operation->collectionOptions;
 }
 return $o;
 }
 public function assert(FunctionalTestCase $test, Context $context, $bubbleExceptions = false) {
 $result = null;
 $exception = null;
 try {
 $result = $this->execute($test, $context);
 if ($result instanceof Cursor) {
 $result = $result->toArray();
 }
 }
 catch (Exception $e) {
 $exception = $e;
 }
 if ($exception instanceof BulkWriteException) {
 $result = $exception->getWriteResult();
 }
 if (isset($this->errorExpectation)) {
 $this->errorExpectation->assert($test, $exception);
 }
 if (isset($this->resultExpectation)) {
 $this->resultExpectation->assert($test, $result);
 }
 if ($exception && $bubbleExceptions) {
 throw $exception;
 }
 }
 private function execute(FunctionalTestCase $test, Context $context) {
 switch ($this->object) {
 case self::OBJECT_CLIENT: $client = $context->getClient();
 return $this->executeForClient($client, $context);
 case self::OBJECT_COLLECTION: $collection = $context->getCollection($this->collectionOptions);
 return $this->executeForCollection($collection, $context);
 case self::OBJECT_DATABASE: $database = $context->getDatabase();
 return $this->executeForDatabase($database, $context);
 case self::OBJECT_GRIDFS_BUCKET: $bucket = $context->getGridFSBucket();
 return $this->executeForGridFSBucket($bucket, $context);
 case self::OBJECT_SELECT_COLLECTION: $collection = $context->selectCollection($this->databaseName, $this->collectionName, $this->collectionOptions);
 return $this->executeForCollection($collection, $context);
 case self::OBJECT_SELECT_DATABASE: $database = $context->selectDatabase($this->databaseName);
 return $this->executeForDatabase($database, $context);
 case self::OBJECT_SESSION0: return $this->executeForSession($context->session0, $test, $context);
 case self::OBJECT_SESSION1: return $this->executeForSession($context->session1, $test, $context);
 case self::OBJECT_TEST_RUNNER: return $this->executeForTestRunner($test, $context);
 default: throw new LogicException('Unsupported object: ' . $this->object);
 }
 }
 private function executeForClient(Client $client, Context $context) {
 $args = $context->prepareOptions($this->arguments);
 $context->replaceArgumentSessionPlaceholder($args);
 switch ($this->name) {
 case 'listDatabases': return $client->listDatabases($args);
 case 'watch': return $client->watch( isset($args['pipeline']) ? $args['pipeline'] : [], array_diff_key($args, ['pipeline' => 1]) );
 default: throw new LogicException('Unsupported client operation: ' . $this->name);
 }
 }
 private function executeForCollection(Collection $collection, Context $context) {
 $args = $context->prepareOptions($this->arguments);
 $context->replaceArgumentSessionPlaceholder($args);
 switch ($this->name) {
 case 'aggregate': return $collection->aggregate( $args['pipeline'], array_diff_key($args, ['pipeline' => 1]) );
 case 'bulkWrite': $options = isset($args['options']) ? (array) $args['options'] : [];
 $options += array_diff_key($args, ['requests' => 1]);
 return $collection->bulkWrite( array_map([$this, 'prepareBulkWriteRequest'], $args['requests']), $options );
 case 'count': case 'countDocuments': case 'find': return $collection->{
$this->name}
( isset($args['filter']) ? $args['filter'] : [], array_diff_key($args, ['filter' => 1]) );
 case 'estimatedDocumentCount': return $collection->estimatedDocumentCount($args);
 case 'deleteMany': case 'deleteOne': case 'findOneAndDelete': return $collection->{
$this->name}
( $args['filter'], array_diff_key($args, ['filter' => 1]) );
 case 'distinct': return $collection->distinct( $args['fieldName'], isset($args['filter']) ? $args['filter'] : [], array_diff_key($args, ['fieldName' => 1, 'filter' => 1]) );
 case 'drop': return $collection->drop($args);
 case 'findOne': return $collection->findOne($args['filter'], array_diff_key($args, ['filter' => 1]));
 case 'findOneAndReplace': if (isset($args['returnDocument'])) {
 $args['returnDocument'] = 'after' === strtolower($args['returnDocument']) ? FindOneAndReplace::RETURN_DOCUMENT_AFTER : FindOneAndReplace::RETURN_DOCUMENT_BEFORE;
 }
 case 'replaceOne': return $collection->{
$this->name}
( $args['filter'], $args['replacement'], array_diff_key($args, ['filter' => 1, 'replacement' => 1]) );
 case 'findOneAndUpdate': if (isset($args['returnDocument'])) {
 $args['returnDocument'] = 'after' === strtolower($args['returnDocument']) ? FindOneAndUpdate::RETURN_DOCUMENT_AFTER : FindOneAndUpdate::RETURN_DOCUMENT_BEFORE;
 }
 case 'updateMany': case 'updateOne': return $collection->{
$this->name}
( $args['filter'], $args['update'], array_diff_key($args, ['filter' => 1, 'update' => 1]) );
 case 'insertMany': $options = isset($args['options']) ? (array) $args['options'] : [];
 $options += array_diff_key($args, ['documents' => 1]);
 return $collection->insertMany( $args['documents'], $options );
 case 'insertOne': return $collection->insertOne( $args['document'], array_diff_key($args, ['document' => 1]) );
 case 'listIndexes': return $collection->listIndexes($args);
 case 'mapReduce': return $collection->mapReduce( $args['map'], $args['reduce'], $args['out'], array_diff_key($args, ['map' => 1, 'reduce' => 1, 'out' => 1]) );
 case 'watch': return $collection->watch( isset($args['pipeline']) ? $args['pipeline'] : [], array_diff_key($args, ['pipeline' => 1]) );
 default: throw new LogicException('Unsupported collection operation: ' . $this->name);
 }
 }
 private function executeForDatabase(Database $database, Context $context) {
 $args = $context->prepareOptions($this->arguments);
 $context->replaceArgumentSessionPlaceholder($args);
 switch ($this->name) {
 case 'aggregate': return $database->aggregate( $args['pipeline'], array_diff_key($args, ['pipeline' => 1]) );
 case 'listCollections': return $database->listCollections($args);
 case 'runCommand': return $database->command( $args['command'], array_diff_key($args, ['command' => 1]) )->toArray()[0];
 case 'watch': return $database->watch( isset($args['pipeline']) ? $args['pipeline'] : [], array_diff_key($args, ['pipeline' => 1]) );
 default: throw new LogicException('Unsupported database operation: ' . $this->name);
 }
 }
 private function executeForGridFSBucket(Bucket $bucket, Context $context) {
 $args = $context->prepareOptions($this->arguments);
 $context->replaceArgumentSessionPlaceholder($args);
 switch ($this->name) {
 case 'download': $stream = fopen('php://memory', 'w+');
 try {
 $bucket->downloadToStream($args['id'], $stream);
 return stream_get_contents($stream);
 }
 finally {
 fclose($stream);
 }
 break;
 case 'download_by_name': $stream = fopen('php://memory', 'w+');
 try {
 $bucket->downloadToStreamByName($args['filename'], $stream, array_diff_key($args, ['filename' => 1]));
 return stream_get_contents($stream);
 }
 finally {
 fclose($stream);
 }
 break;
 default: throw new LogicException('Unsupported GridFS bucket operation: ' . $this->name);
 }
 }
 private function executeForSession(Session $session, FunctionalTestCase $test, Context $context) {
 switch ($this->name) {
 case 'abortTransaction': return $session->abortTransaction();
 case 'commitTransaction': return $session->commitTransaction();
 case 'startTransaction': $options = isset($this->arguments['options']) ? (array) $this->arguments['options'] : [];
 return $session->startTransaction($context->prepareOptions($options));
 case 'withTransaction': $callbackOperations = array_map(function ($operation) {
 return self::fromConvenientTransactions($operation);
 }
, $this->arguments['callback']->operations);
 $callback = function () use ($callbackOperations, $test, $context) {
 foreach ($callbackOperations as $operation) {
 $operation->assert($test, $context, true);
 }
 }
;
 $options = isset($this->arguments['options']) ? (array) $this->arguments['options'] : [];
 return with_transaction($session, $callback, $context->prepareOptions($options));
 default: throw new LogicException('Unsupported session operation: ' . $this->name);
 }
 }
 private function executeForTestRunner(FunctionalTestCase $test, Context $context) {
 $args = $context->prepareOptions($this->arguments);
 $context->replaceArgumentSessionPlaceholder($args);
 switch ($this->name) {
 case 'assertSessionPinned': $test->assertInstanceOf(Session::class, $args['session']);
 $test->assertInstanceOf(Server::class, $args['session']->getServer());
 return null;
 case 'assertSessionTransactionState': $test->assertInstanceOf(Session::class, $args['session']);
 $test->assertSame($this->arguments['state'], $args['session']->getTransactionState());
 return null;
 case 'assertSessionUnpinned': $test->assertInstanceOf(Session::class, $args['session']);
 $test->assertNull($args['session']->getServer());
 return null;
 case 'targetedFailPoint': $test->assertInstanceOf(Session::class, $args['session']);
 $test->configureFailPoint($this->arguments['failPoint'], $args['session']->getServer());
 return null;
 default: throw new LogicException('Unsupported test runner operation: ' . $this->name);
 }
 }
 private function getResultAssertionType() {
 switch ($this->object) {
 case self::OBJECT_CLIENT: return $this->getResultAssertionTypeForClient();
 case self::OBJECT_COLLECTION: return $this->getResultAssertionTypeForCollection();
 case self::OBJECT_DATABASE: return $this->getResultAssertionTypeForDatabase();
 case self::OBJECT_GRIDFS_BUCKET: return ResultExpectation::ASSERT_SAME;
 case self::OBJECT_SESSION0: case self::OBJECT_SESSION1: case self::OBJECT_TEST_RUNNER: return ResultExpectation::ASSERT_NOTHING;
 default: throw new LogicException('Unsupported object: ' . $this->object);
 }
 }
 private function getResultAssertionTypeForClient() {
 switch ($this->name) {
 case 'listDatabases': return ResultExpectation::ASSERT_SAME_DOCUMENTS;
 case 'watch': return ResultExpectation::ASSERT_SAME_DOCUMENTS;
 default: throw new LogicException('Unsupported client operation: ' . $this->name);
 }
 }
 private function getResultAssertionTypeForCollection() {
 switch ($this->name) {
 case 'aggregate': if (is_last_pipeline_operator_write($this->arguments['pipeline'])) {
 return ResultExpectation::ASSERT_NOTHING;
 }
 return ResultExpectation::ASSERT_SAME_DOCUMENTS;
 case 'bulkWrite': return ResultExpectation::ASSERT_BULKWRITE;
 case 'count': case 'countDocuments': case 'distinct': case 'estimatedDocumentCount': return ResultExpectation::ASSERT_SAME;
 case 'deleteMany': case 'deleteOne': return ResultExpectation::ASSERT_DELETE;
 case 'drop': return ResultExpectation::ASSERT_NOTHING;
 case 'findOne': case 'findOneAndDelete': case 'findOneAndReplace': case 'findOneAndUpdate': return ResultExpectation::ASSERT_SAME_DOCUMENT;
 case 'find': return ResultExpectation::ASSERT_SAME_DOCUMENTS;
 case 'insertMany': return ResultExpectation::ASSERT_INSERTMANY;
 case 'insertOne': return ResultExpectation::ASSERT_INSERTONE;
 case 'listIndexes': return ResultExpectation::ASSERT_SAME_DOCUMENTS;
 case 'mapReduce': return ResultExpectation::ASSERT_SAME_DOCUMENTS;
 case 'replaceOne': case 'updateMany': case 'updateOne': return ResultExpectation::ASSERT_UPDATE;
 case 'watch': return ResultExpectation::ASSERT_SAME_DOCUMENTS;
 default: throw new LogicException('Unsupported collection operation: ' . $this->name);
 }
 }
 private function getResultAssertionTypeForDatabase() {
 switch ($this->name) {
 case 'aggregate': case 'listCollections': return ResultExpectation::ASSERT_SAME_DOCUMENTS;
 case 'runCommand': return ResultExpectation::ASSERT_MATCHES_DOCUMENT;
 case 'watch': return ResultExpectation::ASSERT_SAME_DOCUMENTS;
 default: throw new LogicException('Unsupported database operation: ' . $this->name);
 }
 }
 private function prepareBulkWriteRequest(stdClass $request) {
 $args = (array) $request->arguments;
 switch ($request->name) {
 case 'deleteMany': case 'deleteOne': return [ $request->name => [ $args['filter'], array_diff_key($args, ['filter' => 1]), ], ];
 case 'insertOne': return [ 'insertOne' => [ $args['document'] ]];
 case 'replaceOne': return [ 'replaceOne' => [ $args['filter'], $args['replacement'], array_diff_key($args, ['filter' => 1, 'replacement' => 1]), ], ];
 case 'updateMany': case 'updateOne': return [ $request->name => [ $args['filter'], $args['update'], array_diff_key($args, ['filter' => 1, 'update' => 1]), ], ];
 default: throw new LogicException('Unsupported bulk write request: ' . $request->name);
 }
 }
 }
 