<?php
 namespace MongoDB\Tests\SpecTests;
 use ArrayObject;
 use InvalidArgumentException;
 use MongoDB\BSON\BinaryInterface;
 use MongoDB\BSON\DBPointer;
 use MongoDB\BSON\Decimal128;
 use MongoDB\BSON\Int64;
 use MongoDB\BSON\Javascript;
 use MongoDB\BSON\MaxKey;
 use MongoDB\BSON\MinKey;
 use MongoDB\BSON\ObjectId;
 use MongoDB\BSON\Regex;
 use MongoDB\BSON\Symbol;
 use MongoDB\BSON\Timestamp;
 use MongoDB\BSON\Undefined;
 use MongoDB\BSON\UTCDateTime;
 use MongoDB\Model\BSONArray;
 use MongoDB\Model\BSONDocument;
 use PHPUnit\Framework\Constraint\Constraint;
 use PHPUnit\Framework\Constraint\IsInstanceOf;
 use PHPUnit\Framework\Constraint\IsNull;
 use PHPUnit\Framework\Constraint\IsType;
 use PHPUnit\Framework\Constraint\LogicalAnd;
 use PHPUnit\Framework\Constraint\LogicalNot;
 use PHPUnit\Framework\Constraint\LogicalOr;
 use RuntimeException;
 use SebastianBergmann\Comparator\ComparisonFailure;
 use SebastianBergmann\Comparator\Factory;
 use stdClass;
 use Symfony\Bridge\PhpUnit\ConstraintTrait;
 use function array_values;
 use function get_class;
 use function gettype;
 use function in_array;
 use function is_array;
 use function is_object;
 use function is_scalar;
 use function method_exists;
 use function sprintf;
 use const PHP_INT_SIZE;
 class DocumentsMatchConstraint extends Constraint {
 use ConstraintTrait;
 private $ignoreExtraKeysInRoot = false;
 private $ignoreExtraKeysInEmbedded = false;
 private $placeholders = [];
 private $sortKeys = false;
 private $value;
 private $lastFailure;
 private $comparatorFactory;
 public function __construct($value, $ignoreExtraKeysInRoot = false, $ignoreExtraKeysInEmbedded = false, array $placeholders = []) {
 $this->value = $this->prepareBSON($value, true, $this->sortKeys);
 $this->ignoreExtraKeysInRoot = $ignoreExtraKeysInRoot;
 $this->ignoreExtraKeysInEmbedded = $ignoreExtraKeysInEmbedded;
 $this->placeholders = $placeholders;
 $this->comparatorFactory = Factory::getInstance();
 }
 public function evaluate($other, $description = '', $returnResult = false) {
 $other = $this->prepareBSON($other, true, $this->sortKeys);
 $success = false;
 $this->lastFailure = null;
 try {
 $this->assertEquals($this->value, $other, $this->ignoreExtraKeysInRoot);
 $success = true;
 }
 catch (RuntimeException $e) {
 $this->lastFailure = new ComparisonFailure( $this->value, $other, $this->exporter()->export($this->value), $this->exporter()->export($other), false, $e->getMessage() );
 }
 if ($returnResult) {
 return $success;
 }
 if (! $success) {
 $this->fail($other, $description, $this->lastFailure);
 }
 }
 private function assertBSONType($expectedType, $actualValue) {
 switch ($expectedType) {
 case 'double': (new IsType('float'))->evaluate($actualValue);
 return;
 case 'string': (new IsType('string'))->evaluate($actualValue);
 return;
 case 'object': $constraints = [ new IsType('object'), new LogicalNot(new IsInstanceOf(BSONArray::class)), ];
 if (method_exists(LogicalAnd::class, 'fromConstraints')) {
 $constraint = LogicalAnd::fromConstraints(...$constraints);
 }
 else {
 $constraint = new LogicalAnd();
 $constraint->setConstraints($constraints);
 }
 $constraint->evaluate($actualValue);
 return;
 case 'array': $constraints = [ new IsType('array'), new IsInstanceOf(BSONArray::class), ];
 if (method_exists(LogicalOr::class, 'fromConstraints')) {
 $constraint = LogicalOr::fromConstraints(...$constraints);
 }
 else {
 $constraint = new LogicalOr();
 $constraint->setConstraints($constraints);
 }
 $constraint->evaluate($actualValue);
 return;
 case 'binData': (new IsInstanceOf(BinaryInterface::class))->evaluate($actualValue);
 return;
 case 'undefined': (new IsInstanceOf(Undefined::class))->evaluate($actualValue);
 return;
 case 'objectId': (new IsInstanceOf(ObjectId::class))->evaluate($actualValue);
 return;
 case 'boolean': (new IsType('bool'))->evaluate($actualValue);
 return;
 case 'date': (new IsInstanceOf(UTCDateTime::class))->evaluate($actualValue);
 return;
 case 'null': (new IsNull())->evaluate($actualValue);
 return;
 case 'regex': (new IsInstanceOf(Regex::class))->evaluate($actualValue);
 return;
 case 'dbPointer': (new IsInstanceOf(DBPointer::class))->evaluate($actualValue);
 return;
 case 'javascript': (new IsInstanceOf(Javascript::class))->evaluate($actualValue);
 return;
 case 'symbol': (new IsInstanceOf(Symbol::class))->evaluate($actualValue);
 return;
 case 'int': (new IsType('int'))->evaluate($actualValue);
 return;
 case 'timestamp': (new IsInstanceOf(Timestamp::class))->evaluate($actualValue);
 return;
 case 'long': if (PHP_INT_SIZE == 4) {
 (new IsInstanceOf(Int64::class))->evaluate($actualValue);
 }
 else {
 (new IsType('int'))->evaluate($actualValue);
 }
 return;
 case 'decimal': (new IsInstanceOf(Decimal128::class))->evaluate($actualValue);
 return;
 case 'minKey': (new IsInstanceOf(MinKey::class))->evaluate($actualValue);
 return;
 case 'maxKey': (new IsInstanceOf(MaxKey::class))->evaluate($actualValue);
 return;
 }
 }
 private function assertEquals(ArrayObject $expected, ArrayObject $actual, $ignoreExtraKeys, $keyPrefix = '') {
 if (get_class($expected) !== get_class($actual)) {
 throw new RuntimeException(sprintf( '%s is not instance of expected class "%s"', $this->exporter()->shortenedExport($actual), get_class($expected) ));
 }
 foreach ($expected as $key => $expectedValue) {
 $actualHasKey = $actual->offsetExists($key);
 if (! $actualHasKey) {
 throw new RuntimeException(sprintf('$actual is missing key: "%s"', $keyPrefix . $key));
 }
 if (in_array($expectedValue, $this->placeholders, true)) {
 continue;
 }
 $actualValue = $actual[$key];
 if ($expectedValue instanceof BSONDocument && isset($expectedValue['$$type'])) {
 $this->assertBSONType($expectedValue['$$type'], $actualValue);
 continue;
 }
 if (($expectedValue instanceof BSONArray && $actualValue instanceof BSONArray) || ($expectedValue instanceof BSONDocument && $actualValue instanceof BSONDocument)) {
 $this->assertEquals($expectedValue, $actualValue, $this->ignoreExtraKeysInEmbedded, $keyPrefix . $key . '.');
 continue;
 }
 if (is_scalar($expectedValue) && is_scalar($actualValue)) {
 if ($expectedValue !== $actualValue) {
 throw new ComparisonFailure( $expectedValue, $actualValue, '', '', false, sprintf('Field path "%s": %s', $keyPrefix . $key, 'Failed asserting that two values are equal.') );
 }
 continue;
 }
 $expectedType = is_object($expectedValue) ? get_class($expectedValue) : gettype($expectedValue);
 $actualType = is_object($expectedValue) ? get_class($actualValue) : gettype($actualValue);
 if ($expectedType !== $actualType) {
 throw new ComparisonFailure( $expectedValue, $actualValue, '', '', false, sprintf( 'Field path "%s": %s is not instance of expected type "%s".', $keyPrefix . $key, $this->exporter()->shortenedExport($actualValue), $expectedType ) );
 }
 try {
 $this->comparatorFactory->getComparatorFor($expectedValue, $actualValue)->assertEquals($expectedValue, $actualValue);
 }
 catch (ComparisonFailure $failure) {
 throw new ComparisonFailure( $expectedValue, $actualValue, '', '', false, sprintf('Field path "%s": %s', $keyPrefix . $key, $failure->getMessage()) );
 }
 }
 if ($ignoreExtraKeys) {
 return;
 }
 foreach ($actual as $key => $value) {
 if (! $expected->offsetExists($key)) {
 throw new RuntimeException(sprintf('$actual has extra key: "%s"', $keyPrefix . $key));
 }
 }
 }
 private function doAdditionalFailureDescription($other) {
 if ($this->lastFailure === null) {
 return '';
 }
 return $this->lastFailure->getMessage();
 }
 private function doFailureDescription($other) {
 return 'two BSON objects are equal';
 }
 private function doMatches($other) {
 $other = $this->prepareBSON($other, true, $this->sortKeys);
 try {
 $this->assertEquals($this->value, $other, $this->ignoreExtraKeysInRoot);
 }
 catch (RuntimeException $e) {
 return false;
 }
 return true;
 }
 private function doToString() {
 return 'matches ' . $this->exporter()->export($this->value);
 }
 private function prepareBSON($bson, $isRoot, $sortKeys = false) {
 if (! is_array($bson) && ! is_object($bson)) {
 throw new InvalidArgumentException('$bson is not an array or object');
 }
 if ($isRoot && is_array($bson)) {
 $bson = (object) $bson;
 }
 if ($bson instanceof BSONArray || (is_array($bson) && $bson === array_values($bson))) {
 if (! $bson instanceof BSONArray) {
 $bson = new BSONArray($bson);
 }
 }
 else {
 if (! $bson instanceof BSONDocument) {
 $bson = new BSONDocument((array) $bson);
 }
 if ($sortKeys) {
 $bson->ksort();
 }
 }
 foreach ($bson as $key => $value) {
 if ($value instanceof BSONArray || (is_array($value) && $value === array_values($value))) {
 $bson[$key] = $this->prepareBSON($value, false, $sortKeys);
 continue;
 }
 if ($value instanceof BSONDocument || $value instanceof stdClass || is_array($value)) {
 $bson[$key] = $this->prepareBSON($value, false, $sortKeys);
 continue;
 }
 if ($value instanceof Int64 && PHP_INT_SIZE != 4) {
 $bson[$key] = (int) ((string) $value);
 }
 }
 return $bson;
 }
 }
 