<commit_before><?php

namespace MongoDB\Tests;

use InvalidArgumentException;
use MongoDB\BSON\ObjectId;
use MongoDB\Client;
use MongoDB\Driver\Command;
use MongoDB\Driver\Exception\CommandException;
use MongoDB\Driver\Manager;
use MongoDB\Driver\Query;
use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\Server;
use MongoDB\Driver\ServerApi;
use MongoDB\Driver\WriteConcern;
use MongoDB\Operation\CreateCollection;
use MongoDB\Operation\DatabaseCommand;
use MongoDB\Operation\DropCollection;
use MongoDB\Operation\ListCollections;
use stdClass;
use UnexpectedValueException;

use function array_merge;
use function call_user_func;
use function count;
use function current;
use function explode;
use function filter_var;
use function getenv;
use function implode;
use function in_array;
use function is_array;
use function is_callable;
use function is_object;
use function is_string;
use function key;
use function ob_get_clean;
use function ob_start;
use function parse_url;
use function phpinfo;
use function preg_match;
use function preg_quote;
use function preg_replace;
use function sprintf;
use function version_compare;

use const FILTER_VALIDATE_BOOLEAN;
use const INFO_MODULES;

abstract class FunctionalTestCase extends TestCase
{
    /** @var Manager */
    protected $manager;

    /** @var array */
    private $configuredFailPoints = [];

    public function setUp(): void
    {
        parent::setUp();

        $this->manager = static::createTestManager();
        $this->configuredFailPoints = [];
    }

    public function tearDown(): void
    {
        $this->disableFailPoints();

        parent::tearDown();
    }

    public static function createTestClient(?string $uri = null, array $options = [], array $driverOptions = []): Client
    {
        return new Client(
            $uri ?? static::getUri(),
            static::appendAuthenticationOptions($options),
            static::appendServerApiOption($driverOptions)
        );
    }

    public static function createTestManager(?string $uri = null, array $options = [], array $driverOptions = []): Manager
    {
        return new Manager(
            $uri ?? static::getUri(),
            static::appendAuthenticationOptions($options),
            static::appendServerApiOption($driverOptions)
        );
    }

    public static function getUri($allowMultipleMongoses = false): string
    {
        $uri = parent::getUri();

        if ($allowMultipleMongoses) {
            return $uri;
        }

        $urlParts = parse_url($uri);
        if ($urlParts === false) {
            return $uri;
        }

        // Only modify URIs using the mongodb scheme
        if ($urlParts['scheme'] !== 'mongodb') {
            return $uri;
        }

        $hosts = explode(',', $urlParts['host']);
        $numHosts = count($hosts);
        if ($numHosts === 1) {
            return $uri;
        }

        $manager = static::createTestManager($uri);
        if ($manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY))->getType() !== Server::TYPE_MONGOS) {
            return $uri;
        }

        // Re-append port to last host
        if (isset($urlParts['port'])) {
            $hosts[$numHosts - 1] .= ':' . $urlParts['port'];
        }

        $parts = ['mongodb://'];

        if (isset($urlParts['user'], $urlParts['pass'])) {
            $parts += [
                $urlParts['user'],
                ':',
                $urlParts['pass'],
                '@',
            ];
        }

        $parts[] = $hosts[0];

        if (isset($urlParts['path'])) {
            $parts[] = $urlParts['path'];
        }

        if (isset($urlParts['query'])) {
            $parts = array_merge($parts, [
                '?',
                $urlParts['query'],
            ]);
        }

        return implode('', $parts);
    }

    protected function assertCollectionCount($namespace, $count): void
    {
        [$databaseName, $collectionName] = explode('.', $namespace, 2);

        $cursor = $this->manager->executeCommand($databaseName, new Command(['count' => $collectionName]));
        $cursor->setTypeMap(['root' => 'array', 'document' => 'array']);
        $document = current($cursor->toArray());

        $this->assertArrayHasKey('n', $document);
        $this->assertEquals($count, $document['n']);
    }

    /**
     * Asserts that a collection with the given name does not exist on the
     * server.
     *
     * $databaseName defaults to TestCase::getDatabaseName() if unspecified.
     */
    protected function assertCollectionDoesNotExist(string $collectionName, ?string $databaseName = null): void
    {
        if (! isset($databaseName)) {
            $databaseName = $this->getDatabaseName();
        }

        $operation = new ListCollections($this->getDatabaseName());
        $collections = $operation->execute($this->getPrimaryServer());

        $foundCollection = null;

        foreach ($collections as $collection) {
            if ($collection->getName() === $collectionName) {
                $foundCollection = $collection;
                break;
            }
        }

        $this->assertNull($foundCollection, sprintf('Collection %s exists', $collectionName));
    }

    /**
     * Asserts that a collection with the given name exists on the server.
     *
     * $databaseName defaults to TestCase::getDatabaseName() if unspecified.
     * An optional $callback may be provided, which should take a CollectionInfo
     * argument as its first and only parameter. If a CollectionInfo matching
     * the given name is found, it will be passed to the callback, which may
     * perform additional assertions.
     */
    protected function assertCollectionExists(string $collectionName, ?string $databaseName = null, ?callable $callback = null): void
    {
        if (! isset($databaseName)) {
            $databaseName = $this->getDatabaseName();
        }

        if ($callback !== null && ! is_callable($callback)) {
            throw new InvalidArgumentException('$callback is not a callable');
        }

        $operation = new ListCollections($databaseName);
        $collections = $operation->execute($this->getPrimaryServer());

        $foundCollection = null;

        foreach ($collections as $collection) {
            if ($collection->getName() === $collectionName) {
                $foundCollection = $collection;
                break;
            }
        }

        $this->assertNotNull($foundCollection, sprintf('Found %s collection in the database', $collectionName));

        if ($callback !== null) {
            call_user_func($callback, $foundCollection);
        }
    }

    protected function assertCommandSucceeded($document): void
    {
        $document = is_object($document) ? (array) $document : $document;

        $this->assertArrayHasKey('ok', $document);
        $this->assertEquals(1, $document['ok']);
    }

    protected function assertSameObjectId($expectedObjectId, $actualObjectId): void
    {
        $this->assertInstanceOf(ObjectId::class, $expectedObjectId);
        $this->assertInstanceOf(ObjectId::class, $actualObjectId);
        $this->assertEquals((string) $expectedObjectId, (string) $actualObjectId);
    }

    /**
     * Configure a fail point for the test.
     *
     * The fail point will automatically be disabled during tearDown() to avoid
     * affecting a subsequent test.
     *
     * @param array|stdClass $command configureFailPoint command document
     * @throws InvalidArgumentException if $command is not a configureFailPoint command
     */
    public function configureFailPoint($command, ?Server $server = null): void
    {
        if (! $this->isFailCommandSupported()) {
            $this->markTestSkipped('failCommand is only supported on mongod >= 4.0.0 and mongos >= 4.1.5.');
        }

        if (! $this->isFailCommandEnabled()) {
            $this->markTestSkipped('The enableTestCommands parameter is not enabled.');
        }

        if (is_array($command)) {
            $command = (object) $command;
        }

        if (! $command instanceof stdClass) {
            throw new InvalidArgumentException('$command is not an array or stdClass instance');
        }

        if (key((array) $command) !== 'configureFailPoint') {
            throw new InvalidArgumentException('$command is not a configureFailPoint command');
        }

        $failPointServer = $server ?: $this->getPrimaryServer();

        $operation = new DatabaseCommand('admin', $command);
        $cursor = $operation->execute($failPointServer);
        $result = $cursor->toArray()[0];

        $this->assertCommandSucceeded($result);

        // Record the fail point so it can be disabled during tearDown()
        $this->configuredFailPoints[] = [$command->configureFailPoint, $failPointServer];
    }

    public static function getModuleInfo(string $row): ?string
    {
        ob_start();
        phpinfo(INFO_MODULES);
        $info = ob_get_clean();

        $pattern = sprintf('/^%s([\w ]+)$/m', preg_quote($row . ' => '));

        if (preg_match($pattern, $info, $matches) !== 1) {
            return null;
        }

        return $matches[1];
    }

    /**
     * Creates the test collection with the specified options.
     *
     * If the "writeConcern" option is not specified but is supported by the
     * server, a majority write concern will be used. This is helpful for tests
     * using transactions or secondary reads.
     *
     * @param array $options<commit_msg>PHPLIB-942: Fix pattern for matching phpinfo() values (#962)<commit_after><?php

namespace MongoDB\Tests;

use InvalidArgumentException;
use MongoDB\BSON\ObjectId;
use MongoDB\Client;
use MongoDB\Driver\Command;
use MongoDB\Driver\Exception\CommandException;
use MongoDB\Driver\Manager;
use MongoDB\Driver\Query;
use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\Server;
use MongoDB\Driver\ServerApi;
use MongoDB\Driver\WriteConcern;
use MongoDB\Operation\CreateCollection;
use MongoDB\Operation\DatabaseCommand;
use MongoDB\Operation\DropCollection;
use MongoDB\Operation\ListCollections;
use stdClass;
use UnexpectedValueException;

use function array_merge;
use function call_user_func;
use function count;
use function current;
use function explode;
use function filter_var;
use function getenv;
use function implode;
use function in_array;
use function is_array;
use function is_callable;
use function is_object;
use function is_string;
use function key;
use function ob_get_clean;
use function ob_start;
use function parse_url;
use function phpinfo;
use function preg_match;
use function preg_quote;
use function preg_replace;
use function sprintf;
use function version_compare;

use const FILTER_VALIDATE_BOOLEAN;
use const INFO_MODULES;

abstract class FunctionalTestCase extends TestCase
{
    /** @var Manager */
    protected $manager;

    /** @var array */
    private $configuredFailPoints = [];

    public function setUp(): void
    {
        parent::setUp();

        $this->manager = static::createTestManager();
        $this->configuredFailPoints = [];
    }

    public function tearDown(): void
    {
        $this->disableFailPoints();

        parent::tearDown();
    }

    public static function createTestClient(?string $uri = null, array $options = [], array $driverOptions = []): Client
    {
        return new Client(
            $uri ?? static::getUri(),
            static::appendAuthenticationOptions($options),
            static::appendServerApiOption($driverOptions)
        );
    }

    public static function createTestManager(?string $uri = null, array $options = [], array $driverOptions = []): Manager
    {
        return new Manager(
            $uri ?? static::getUri(),
            static::appendAuthenticationOptions($options),
            static::appendServerApiOption($driverOptions)
        );
    }

    public static function getUri($allowMultipleMongoses = false): string
    {
        $uri = parent::getUri();

        if ($allowMultipleMongoses) {
            return $uri;
        }

        $urlParts = parse_url($uri);
        if ($urlParts === false) {
            return $uri;
        }

        // Only modify URIs using the mongodb scheme
        if ($urlParts['scheme'] !== 'mongodb') {
            return $uri;
        }

        $hosts = explode(',', $urlParts['host']);
        $numHosts = count($hosts);
        if ($numHosts === 1) {
            return $uri;
        }

        $manager = static::createTestManager($uri);
        if ($manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY))->getType() !== Server::TYPE_MONGOS) {
            return $uri;
        }

        // Re-append port to last host
        if (isset($urlParts['port'])) {
            $hosts[$numHosts - 1] .= ':' . $urlParts['port'];
        }

        $parts = ['mongodb://'];

        if (isset($urlParts['user'], $urlParts['pass'])) {
            $parts += [
                $urlParts['user'],
                ':',
                $urlParts['pass'],
                '@',
            ];
        }

        $parts[] = $hosts[0];

        if (isset($urlParts['path'])) {
            $parts[] = $urlParts['path'];
        }

        if (isset($urlParts['query'])) {
            $parts = array_merge($parts, [
                '?',
                $urlParts['query'],
            ]);
        }

        return implode('', $parts);
    }

    protected function assertCollectionCount($namespace, $count): void
    {
        [$databaseName, $collectionName] = explode('.', $namespace, 2);

        $cursor = $this->manager->executeCommand($databaseName, new Command(['count' => $collectionName]));
        $cursor->setTypeMap(['root' => 'array', 'document' => 'array']);
        $document = current($cursor->toArray());

        $this->assertArrayHasKey('n', $document);
        $this->assertEquals($count, $document['n']);
    }

    /**
     * Asserts that a collection with the given name does not exist on the
     * server.
     *
     * $databaseName defaults to TestCase::getDatabaseName() if unspecified.
     */
    protected function assertCollectionDoesNotExist(string $collectionName, ?string $databaseName = null): void
    {
        if (! isset($databaseName)) {
            $databaseName = $this->getDatabaseName();
        }

        $operation = new ListCollections($this->getDatabaseName());
        $collections = $operation->execute($this->getPrimaryServer());

        $foundCollection = null;

        foreach ($collections as $collection) {
            if ($collection->getName() === $collectionName) {
                $foundCollection = $collection;
                break;
            }
        }

        $this->assertNull($foundCollection, sprintf('Collection %s exists', $collectionName));
    }

    /**
     * Asserts that a collection with the given name exists on the server.
     *
     * $databaseName defaults to TestCase::getDatabaseName() if unspecified.
     * An optional $callback may be provided, which should take a CollectionInfo
     * argument as its first and only parameter. If a CollectionInfo matching
     * the given name is found, it will be passed to the callback, which may
     * perform additional assertions.
     */
    protected function assertCollectionExists(string $collectionName, ?string $databaseName = null, ?callable $callback = null): void
    {
        if (! isset($databaseName)) {
            $databaseName = $this->getDatabaseName();
        }

        if ($callback !== null && ! is_callable($callback)) {
            throw new InvalidArgumentException('$callback is not a callable');
        }

        $operation = new ListCollections($databaseName);
        $collections = $operation->execute($this->getPrimaryServer());

        $foundCollection = null;

        foreach ($collections as $collection) {
            if ($collection->getName() === $collectionName) {
                $foundCollection = $collection;
                break;
            }
        }

        $this->assertNotNull($foundCollection, sprintf('Found %s collection in the database', $collectionName));

        if ($callback !== null) {
            call_user_func($callback, $foundCollection);
        }
    }

    protected function assertCommandSucceeded($document): void
    {
        $document = is_object($document) ? (array) $document : $document;

        $this->assertArrayHasKey('ok', $document);
        $this->assertEquals(1, $document['ok']);
    }

    protected function assertSameObjectId($expectedObjectId, $actualObjectId): void
    {
        $this->assertInstanceOf(ObjectId::class, $expectedObjectId);
        $this->assertInstanceOf(ObjectId::class, $actualObjectId);
        $this->assertEquals((string) $expectedObjectId, (string) $actualObjectId);
    }

    /**
     * Configure a fail point for the test.
     *
     * The fail point will automatically be disabled during tearDown() to avoid
     * affecting a subsequent test.
     *
     * @param array|stdClass $command configureFailPoint command document
     * @throws InvalidArgumentException if $command is not a configureFailPoint command
     */
    public function configureFailPoint($command, ?Server $server = null): void
    {
        if (! $this->isFailCommandSupported()) {
            $this->markTestSkipped('failCommand is only supported on mongod >= 4.0.0 and mongos >= 4.1.5.');
        }

        if (! $this->isFailCommandEnabled()) {
            $this->markTestSkipped('The enableTestCommands parameter is not enabled.');
        }

        if (is_array($command)) {
            $command = (object) $command;
        }

        if (! $command instanceof stdClass) {
            throw new InvalidArgumentException('$command is not an array or stdClass instance');
        }

        if (key((array) $command) !== 'configureFailPoint') {
            throw new InvalidArgumentException('$command is not a configureFailPoint command');
        }

        $failPointServer = $server ?: $this->getPrimaryServer();

        $operation = new DatabaseCommand('admin', $command);
        $cursor = $operation->execute($failPointServer);
        $result = $cursor->toArray()[0];

        $this->assertCommandSucceeded($result);

        // Record the fail point so it can be disabled during tearDown()
        $this->configuredFailPoints[] = [$command->configureFailPoint, $failPointServer];
    }

    public static function getModuleInfo(string $row): ?string
    {
        ob_start();
        phpinfo(INFO_MODULES);
        $info = ob_get_clean();

        $pattern = sprintf('/^%s(.*)$/m', preg_quote($row . ' => '));

        if (preg_match($pattern, $info, $matches) !== 1) {
            return null;
        }

        return $matches[1];
    }

    /**
     * Creates the test collection with the specified options.
     *
     * If the "writeConcern" option is not specified but is supported by the
     * server, a majority write concern will be used. This is helpful for tests
     * using transactions or secondary reads.
     *
     * @param array $options
{'fraction_non_alphanumeric': 0.12507210656038598, 'fraction_numerical': 0.0017829985840893596, 'mean_word_length': 2.628234398782344, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 89, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '492380', 'n_tokens_mistral': 5154, 'n_tokens_neox': 4948, 'n_words': 1323}