<?php
 namespace MongoDB\Tests\Collection;
 use IteratorIterator;
 use LogicException;
 use MongoDB\BulkWriteResult;
 use MongoDB\Collection;
 use MongoDB\DeleteResult;
 use MongoDB\Driver\Exception\BulkWriteException;
 use MongoDB\Driver\Exception\RuntimeException;
 use MongoDB\InsertManyResult;
 use MongoDB\InsertOneResult;
 use MongoDB\Operation\FindOneAndReplace;
 use MongoDB\UpdateResult;
 use MultipleIterator;
 use PHPUnit_Framework_SkippedTestError;
 use Symfony\Bridge\PhpUnit\SetUpTearDownTrait;
 use function array_diff_key;
 use function array_key_exists;
 use function array_map;
 use function file_get_contents;
 use function glob;
 use function json_decode;
 use function MongoDB\is_last_pipeline_operator_write;
 use function sprintf;
 use function str_replace;
 use function strtolower;
 use function version_compare;
 class CrudSpecFunctionalTest extends FunctionalTestCase {
 use SetUpTearDownTrait;
 private $expectedCollection;
 private function doSetUp() {
 parent::setUp();
 $this->expectedCollection = new Collection($this->manager, $this->getDatabaseName(), $this->getCollectionName() . '.expected');
 $this->expectedCollection->drop();
 }
 public function testSpecification(array $initialData, array $test, $minServerVersion, $maxServerVersion) {
 if (isset($minServerVersion) || isset($maxServerVersion)) {
 $this->checkServerVersion($minServerVersion, $maxServerVersion);
 }
 $expectedData = isset($test['outcome']['collection']['data']) ? $test['outcome']['collection']['data'] : null;
 $this->initializeData($initialData, $expectedData);
 if (isset($test['outcome']['collection']['name'])) {
 $outputCollection = new Collection($this->manager, $this->getDatabaseName(), $test['outcome']['collection']['name']);
 $outputCollection->drop();
 }
 $result = null;
 $exception = null;
 try {
 $result = $this->executeOperation($test['operation']);
 }
 catch (RuntimeException $e) {
 $exception = $e;
 }
 $this->executeOutcome($test['operation'], $test['outcome'], $result, $exception);
 }
 public function provideSpecificationTests() {
 $testArgs = [];
 foreach (glob(__DIR__ . '/spec-tests/*/*.json') as $filename) {
 $json = json_decode(file_get_contents($filename), true);
 $minServerVersion = isset($json['minServerVersion']) ? $json['minServerVersion'] : null;
 $maxServerVersion = isset($json['maxServerVersion']) ? $json['maxServerVersion'] : null;
 foreach ($json['tests'] as $test) {
 $name = str_replace(' ', '_', $test['description']);
 $testArgs[$name] = [$json['data'], $test, $minServerVersion, $maxServerVersion];
 }
 }
 return $testArgs;
 }
 private function assertEquivalentCollections($expectedCollection, $actualCollection) {
 $mi = new MultipleIterator(MultipleIterator::MIT_NEED_ANY);
 $mi->attachIterator(new IteratorIterator($expectedCollection->find()));
 $mi->attachIterator(new IteratorIterator($actualCollection->find()));
 foreach ($mi as $documents) {
 list($expectedDocument, $actualDocument) = $documents;
 $this->assertSameDocument($expectedDocument, $actualDocument);
 }
 }
 private function checkServerVersion($minServerVersion, $maxServerVersion) {
 $serverVersion = $this->getServerVersion();
 if (isset($minServerVersion) && version_compare($serverVersion, $minServerVersion, '<')) {
 $this->markTestSkipped(sprintf('Server version "%s" < minServerVersion "%s"', $serverVersion, $minServerVersion));
 }
 if (isset($maxServerVersion) && version_compare($serverVersion, $maxServerVersion, '>=')) {
 $this->markTestSkipped(sprintf('Server version "%s" >= maxServerVersion "%s"', $serverVersion, $maxServerVersion));
 }
 }
 private function executeOperation(array $operation) {
 switch ($operation['name']) {
 case 'aggregate': return $this->collection->aggregate( $operation['arguments']['pipeline'], array_diff_key($operation['arguments'], ['pipeline' => 1]) );
 case 'bulkWrite': return $this->collection->bulkWrite( array_map([$this, 'prepareBulkWriteRequest'], $operation['arguments']['requests']), isset($operation['arguments']['options']) ? $operation['arguments']['options'] : [] );
 case 'count': case 'countDocuments': case 'find': return $this->collection->{
$operation['name']}
( isset($operation['arguments']['filter']) ? $operation['arguments']['filter'] : [], array_diff_key($operation['arguments'], ['filter' => 1]) );
 case 'estimatedDocumentCount': return $this->collection->estimatedDocumentCount($operation['arguments']);
 case 'deleteMany': case 'deleteOne': case 'findOneAndDelete': return $this->collection->{
$operation['name']}
( $operation['arguments']['filter'], array_diff_key($operation['arguments'], ['filter' => 1]) );
 case 'distinct': return $this->collection->distinct( $operation['arguments']['fieldName'], isset($operation['arguments']['filter']) ? $operation['arguments']['filter'] : [], array_diff_key($operation['arguments'], ['fieldName' => 1, 'filter' => 1]) );
 case 'findOneAndReplace': $operation['arguments'] = $this->prepareFindAndModifyArguments($operation['arguments']);
 case 'replaceOne': return $this->collection->{
$operation['name']}
( $operation['arguments']['filter'], $operation['arguments']['replacement'], array_diff_key($operation['arguments'], ['filter' => 1, 'replacement' => 1]) );
 case 'findOneAndUpdate': $operation['arguments'] = $this->prepareFindAndModifyArguments($operation['arguments']);
 case 'updateMany': case 'updateOne': return $this->collection->{
$operation['name']}
( $operation['arguments']['filter'], $operation['arguments']['update'], array_diff_key($operation['arguments'], ['filter' => 1, 'update' => 1]) );
 case 'insertMany': return $this->collection->insertMany( $operation['arguments']['documents'], isset($operation['arguments']['options']) ? $operation['arguments']['options'] : [] );
 case 'insertOne': return $this->collection->insertOne( $operation['arguments']['document'], array_diff_key($operation['arguments'], ['document' => 1]) );
 default: throw new LogicException('Unsupported operation: ' . $operation['name']);
 }
 }
 private function executeOutcome(array $operation, array $outcome, $result, RuntimeException $exception = null) {
 $expectedError = array_key_exists('error', $outcome) ? $outcome['error'] : false;
 if ($expectedError) {
 $this->assertNull($result);
 $this->assertNotNull($exception);
 $result = $this->extractResultFromException($operation, $outcome, $exception);
 }
 if (array_key_exists('result', $outcome)) {
 $this->executeAssertResult($operation, $outcome['result'], $result);
 }
 if (isset($outcome['collection'])) {
 $actualCollection = isset($outcome['collection']['name']) ? new Collection($this->manager, $this->getDatabaseName(), $outcome['collection']['name']) : $this->collection;
 $this->assertEquivalentCollections($this->expectedCollection, $actualCollection);
 }
 }
 private function extractResultFromException(array $operation, array $outcome, RuntimeException $exception) {
 switch ($operation['name']) {
 case 'bulkWrite': $insertedIds = isset($outcome['result']['insertedIds']) ? $outcome['result']['insertedIds'] : [];
 if ($exception instanceof BulkWriteException) {
 return new BulkWriteResult($exception->getWriteResult(), $insertedIds);
 }
 break;
 case 'insertMany': $insertedIds = isset($outcome['result']['insertedIds']) ? $outcome['result']['insertedIds'] : [];
 if ($exception instanceof BulkWriteException) {
 return new InsertManyResult($exception->getWriteResult(), $insertedIds);
 }
 break;
 }
 return null;
 }
 private function executeAssertResult(array $operation, $expectedResult, $actualResult) {
 switch ($operation['name']) {
 case 'aggregate': if (! is_last_pipeline_operator_write($operation['arguments']['pipeline'])) {
 $this->assertSameDocuments($expectedResult, $actualResult);
 }
 break;
 case 'bulkWrite': $this->assertIsArray($expectedResult);
 $this->assertInstanceOf(BulkWriteResult::class, $actualResult);
 if (isset($expectedResult['deletedCount'])) {
 $this->assertSame($expectedResult['deletedCount'], $actualResult->getDeletedCount());
 }
 if (isset($expectedResult['insertedCount'])) {
 $this->assertSame($expectedResult['insertedCount'], $actualResult->getInsertedCount());
 }
 if (isset($expectedResult['insertedIds'])) {
 $this->assertSameDocument( ['insertedIds' => $expectedResult['insertedIds']], ['insertedIds' => $actualResult->getInsertedIds()] );
 }
 if (isset($expectedResult['matchedCount'])) {
 $this->assertSame($expectedResult['matchedCount'], $actualResult->getMatchedCount());
 }
 if (isset($expectedResult['modifiedCount'])) {
 $this->assertSame($expectedResult['modifiedCount'], $actualResult->getModifiedCount());
 }
 if (isset($expectedResult['upsertedCount'])) {
 $this->assertSame($expectedResult['upsertedCount'], $actualResult->getUpsertedCount());
 }
 if (isset($expectedResult['upsertedIds'])) {
 $this->assertSameDocument( ['upsertedIds' => $expectedResult['upsertedIds']], ['upsertedIds' => $actualResult->getUpsertedIds()] );
 }
 break;
 case 'count': case 'countDocuments': case 'estimatedDocumentCount': $this->assertSame($expectedResult, $actualResult);
 break;
 case 'distinct': $this->assertSameDocument( ['values' => $expectedResult], ['values' => $actualResult] );
 break;
 case 'find': $this->assertSameDocuments($expectedResult, $actualResult);
 break;
 case 'deleteMany': case 'deleteOne': $this->assertIsArray($expectedResult);
 $this->assertInstanceOf(DeleteResult::class, $actualResult);
 if (isset($expectedResult['deletedCount'])) {
 $this->assertSame($expectedResult['deletedCount'], $actualResult->getDeletedCount());
 }
 break;
 case 'findOneAndDelete': case 'findOneAndReplace': case 'findOneAndUpdate': $this->assertSameDocument( ['result' => $expectedResult], ['result' => $actualResult] );
 break;
 case 'insertMany': $this->assertIsArray($expectedResult);
 $this->assertInstanceOf(InsertManyResult::class, $actualResult);
 if (isset($expectedResult['insertedCount'])) {
 $this->assertSame($expectedResult['insertedCount'], $actualResult->getInsertedCount());
 }
 if (isset($expectedResult['insertedIds'])) {
 $this->assertSameDocument( ['insertedIds' => $expectedResult['insertedIds']], ['insertedIds' => $actualResult->getInsertedIds()] );
 }
 break;
 case 'insertOne': $this->assertIsArray($expectedResult);
 $this->assertInstanceOf(InsertOneResult::class, $actualResult);
 if (isset($expectedResult['insertedCount'])) {
 $this->assertSame($expectedResult['insertedCount'], $actualResult->getInsertedCount());
 }
 if (isset($expectedResult['insertedId'])) {
 $this->assertSameDocument( ['insertedId' => $expectedResult['insertedId']], ['insertedId' => $actualResult->getInsertedId()] );
 }
 break;
 case 'replaceOne': case 'updateMany': case 'updateOne': $this->assertIsArray($expectedResult);
 $this->assertInstanceOf(UpdateResult::class, $actualResult);
 if (isset($expectedResult['matchedCount'])) {
 $this->assertSame($expectedResult['matchedCount'], $actualResult->getMatchedCount());
 }
 if (isset($expectedResult['modifiedCount'])) {
 $this->assertSame($expectedResult['modifiedCount'], $actualResult->getModifiedCount());
 }
 if (isset($expectedResult['upsertedCount'])) {
 $this->assertSame($expectedResult['upsertedCount'], $actualResult->getUpsertedCount());
 }
 if (array_key_exists('upsertedId', $expectedResult)) {
 $this->assertSameDocument( ['upsertedId' => $expectedResult['upsertedId']], ['upsertedId' => $actualResult->getUpsertedId()] );
 }
 break;
 default: throw new LogicException('Unsupported operation: ' . $operation['name']);
 }
 }
 private function initializeData(array $initialData, array $expectedData = null) {
 if (! empty($initialData)) {
 $this->collection->insertMany($initialData);
 }
 if (! empty($expectedData)) {
 $this->expectedCollection->insertMany($expectedData);
 }
 }
 private function prepareBulkWriteRequest(array $request) {
 switch ($request['name']) {
 case 'deleteMany': case 'deleteOne': return [ $request['name'] => [ $request['arguments']['filter'], array_diff_key($request['arguments'], ['filter' => 1]), ], ];
 case 'insertOne': return [ 'insertOne' => [ $request['arguments']['document'] ]];
 case 'replaceOne': return [ 'replaceOne' => [ $request['arguments']['filter'], $request['arguments']['replacement'], array_diff_key($request['arguments'], ['filter' => 1, 'replacement' => 1]), ], ];
 case 'updateMany': case 'updateOne': return [ $request['name'] => [ $request['arguments']['filter'], $request['arguments']['update'], array_diff_key($request['arguments'], ['filter' => 1, 'update' => 1]), ], ];
 default: throw new LogicException('Unsupported bulk write request: ' . $request['name']);
 }
 }
 private function prepareFindAndModifyArguments(array $arguments) {
 if (isset($arguments['returnDocument'])) {
 $arguments['returnDocument'] = 'after' === strtolower($arguments['returnDocument']) ? FindOneAndReplace::RETURN_DOCUMENT_AFTER : FindOneAndReplace::RETURN_DOCUMENT_BEFORE;
 }
 return $arguments;
 }
 }
 