<?php
/**
 * WordPress queries based assertions.
 *
 * @package Codeception\Module
 */

namespace Codeception\Module;

use Codeception\Exception\ModuleException;
use Codeception\Lib\ModuleContainer;
use PHPUnit\Framework\Assert;
use tad\WPBrowser\Environment\Constants;
use tad\WPBrowser\Iterators\Filters\ActionsQueriesFilter;
use tad\WPBrowser\Iterators\Filters\ClassMethodQueriesFilter;
use tad\WPBrowser\Iterators\Filters\FactoryQueriesFilter;
use tad\WPBrowser\Iterators\Filters\FiltersQueriesFilter;
use tad\WPBrowser\Iterators\Filters\FunctionQueriesFilter;
use tad\WPBrowser\Iterators\Filters\MainStatementQueriesFilter;
use tad\WPBrowser\Iterators\Filters\SetupTearDownQueriesFilter;

/**
 * Class WPQueries
 *
 * @package Codeception\Module
 */
class WPQueries extends \Codeception\Module
{

    /**
     * A list of the filtered queries.
     *
     * @var array<string>
     */
    protected $filteredQueries = [];

    /**
     * @var callable[]
     */
    protected $assertions = [];

    /**
     * @var \wpdb
     */
    protected $wpdb;

    /**
     * @var Constants
     */
    private $constants;

    /**
     * WPQueries constructor.
     *
     * @param ModuleContainer $moduleContainer The current module container.
     * @param array<string,mixed>|null $config The current module configuration.
     * @param Constants|null $constants The constants adapter.
     * @param \wpdb|null $wpdbInstance The current wpdb instance.
     */
    public function __construct(
        ModuleContainer $moduleContainer,
        $config,
        Constants
        $constants = null,
        $wpdbInstance = null
    ) {
        /** @var \wpdb $wpdb */
        global $wpdb;
        $this->constants = $constants ? $constants : new Constants();
        $this->wpdb = $wpdbInstance ?: $wpdb;
        parent::__construct($moduleContainer, $config);
    }

    /**
     * Initializes the module.
     *
     * @throws ModuleException If the initialization fails.
     *
     * @return void
     */
    public function _initialize()
    {
        if (!$this->moduleContainer->hasModule('WPLoader')) {
            throw new ModuleException(
                __CLASS__,
                'The WPLoader module is required for WPQueries to work.'
            );
        }

        $this->constants->defineIfUndefined('SAVEQUERIES', true);

        if (!(bool)$this->constants->constant('SAVEQUERIES')) {
            $message = 'The "SAVEQUERIES" should either not be set or set to "true";'
                       . ' this module cannot work if "SAVEQUERIES" is not set to "true".';
            throw new ModuleException(__CLASS__, $message);
        }
    }

    /**
     * Runs before each test method.
     *
     * @return void
     */
    public function _cleanup()
    {
        $this->getWpdbInstance()->queries = [];
    }

    /**
     * Returns the corrent wpdb instance.
     *
     * @return \wpdb The current wpdb instance.
     */
    private function getWpdbInstance()
    {
        if (null === $this->wpdb) {
            /** @var \wpdb $wpdb */
            global $wpdb;
            return $wpdb;
        }

        return $this->wpdb;
    }

    /**
     * Asserts that at least one query was made during the test.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * wp_cache_delete('page-posts', 'acme');
     * $pagePosts = $plugin->getPagePosts();
     * $I->assertQueries('Queries should be made to set the cache.')
     * ```
     *
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertQueries($message = '')
    {
        $this->readQueries();
        $message = $message ? $message : 'Failed asserting that queries were made.';
        Assert::assertNotEmpty($this->filteredQueries, $message);
    }

    /**
     * Reads the current queries.
     *
     * @return void
     */
    private function readQueries()
    {
        $wpdb = $this->getWpdbInstance();

        if (empty($wpdb->queries)) {
            $this->filteredQueries = [];
        } else {
            $filteredQueriesIterator = $this->_getFilteredQueriesIterator();
            $this->filteredQueries = iterator_to_array($filteredQueriesIterator);
        }
    }

    /**
     * Returns the saved queries after filtering.
     *
     * @param \wpdb $wpdb
     * @return \FilterIterator
     */
    public function _getFilteredQueriesIterator($wpdb = null)
    {
        if (null === $wpdb) {
            $wpdb = $this->getWpdbInstance();
        }

        $queriesArrayIterator = new \ArrayIterator($wpdb->queries);
        $filteredQueriesIterator = new SetupTearDownQueriesFilter(new FactoryQueriesFilter($queriesArrayIterator));

        return $filteredQueriesIterator;
    }

    /**
     * Asserts that no queries were made.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * $posts = $this->factory()->post->create_many(3);
     * wp_cache_set('page-posts', $posts, 'acme');
     * $pagePosts = $plugin->getPagePosts();
     * $I->assertNotQueries('Queries should not be made if the cache is set.')
     * ```
     *
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertNotQueries($message = '')
    {
        $this->readQueries();
        $message = $message ? $message : 'Failed asserting that no queries were made.';
        Assert::assertEmpty($this->filteredQueries, $message);
    }

    /**
     * Asserts that n queries have been made.
     *
     * @example
     * ```php
     * $posts = $this->factory()->post->create_many(3);
     * $cachedUsers = $this->factory()->user->create_many(2);
     * $nonCachedUsers = $this->factory()->user->create_many(2);
     * foreach($cachedUsers as $userId){
     *      wp_cache_set('page-posts-for-user-' . $userId, $posts, 'acme');
     * }
     * // Run the same query as different users
     * foreach(array_merge($cachedUsers, $nonCachedUsers) as $userId){
     *      $pagePosts = $plugin->getPagePostsForUser($userId);
     * }
     * $I->assertCountQueries(2, 'A query should be made for each user missing cached posts.')
     * ```
     *
     * @param int $n The expected number of queries.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertCountQueries($n, $message = '')
    {
        $this->readQueries();
        $message = $message ? $message : 'Failed asserting that ' . $n . ' queries were made.';
        Assert::assertCount($n, $this->filteredQueries, $message);
    }

    /**
     * Asserts that at least a query starting with the specified statement was made.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * wp_cache_flush();
     * cached_get_posts($args);
     * $I->assertQueriesByStatement('SELECT');
     * ```
     *
     * @param string $statement A simple string the statement should start with or a valid regular expression.
     *                           Regular expressions must contain delimiters.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertQueriesByStatement($statement, $message = '')
    {
        $this->readQueries();
        $message = $message ?: 'Failed asserting that queries beginning with statement [' . $statement . '] were made.';
        $statementIterator = new MainStatementQueriesFilter(new \ArrayIterator($this->filteredQueries), $statement);
        Assert::assertNotEmpty(iterator_to_array($statementIterator), $message);
    }

    /**
     * Asserts that at least one query has been made by the specified class method.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * $options = new Acme\Options();
     * $options->update('showAds', false);
     * $I->assertQueriesByMethod('Acme\Options', 'update');
     * ```
     *
     * @param string $class The fully qualified name of the class to check.
     * @param string $method The name of the method to check.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertQueriesByMethod($class, $method, $message = '')
    {
        $this->readQueries();
        $class = ltrim($class, '\\');
        $message = $message ?: 'Failed asserting that queries were made by method [' . $class . '::' . $method . ']';
        $statementIterator = new ClassMethodQueriesFilter(new \ArrayIterator($this->filteredQueries), $class, $method);
        Assert::assertNotEmpty(iterator_to_array($statementIterator), $message);
    }

    /**
     * Asserts that no queries have been made by the specified class method.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * $bookRepository = new Acme\BookRepository();
     * $repository->where('ID', 23)->set('title', 'Peter Pan', $deferred = true);
     * $this->assertNotQueriesByStatement('INSERT', 'Deferred write should happen on __destruct');
     * ```
     *
     * @param string $statement A simple string the statement should start with or a valid regular expression.
     *                           Regular expressions must contain delimiters.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertNotQueriesByStatement($statement, $message = '')
    {
        $message = $message ?: 'Failed asserting that no queries beginning with statement ['
                               . $statement . '] were made.';
        $this->assertQueriesCountByStatement(0, $statement, $message);
    }

    /**
     * Asserts that n queries starting with the specified statement were made.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * $bookRepository = new Acme\BookRepository();
     * $repository->where('ID', 23)->set('title', 'Peter Pan', $deferred = true);
     * $repository->where('ID', 89)->set('title', 'Moby-dick', $deferred = true);
     * $repository->where('ID', 2389)->set('title', 'The call of the wild', $deferred = false);
     * $this->assertQueriesCountByStatement(1, 'INSERT', 'Deferred write should happen on __destruct');
     * ```
     *
     * @param int $n The expected number of queries.
     * @param string $statement A simple string the statement should start with or a valid regular expression.
     *                           Regular expressions must contain delimiters.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertQueriesCountByStatement($n, $statement, $message = '')
    {
        $this->readQueries();
        $message = $message ?: 'Failed asserting that ' . $n . ' queries beginning with statement ['
                               . $statement . '] were made.';
        $statementIterator = new MainStatementQueriesFilter(new \ArrayIterator($this->filteredQueries), $statement);
        Assert::assertCount($n, iterator_to_array($statementIterator), $message);
    }

    /**
     * Asserts that no queries have been made by the specified class method.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * $options = new Acme\Options();
     * $options->update('adsSource', 'not-a-real-url.org');
     * $I->assertNotQueriesByMethod('Acme\Options', 'update');
     * ```
     *
     * @param string $class The fully qualified name of the class to check.
     * @param string $method The name of the method to check.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertNotQueriesByMethod($class, $method, $message = '')
    {
        $message = $message ?: 'Failed asserting that no queries were made by method [' . $class . '::' . $method . ']';
        $this->assertQueriesCountByMethod(0, $class, $method, $message);
    }

    /**
     * Asserts that n queries have been made by the specified class method.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * $bookRepository = new Acme\BookRepository();
     * $repository->where('ID', 23)->commit('title', 'Peter Pan');
     * $repository->where('ID', 89)->commit('title', 'Moby-dick');
     * $repository->where('ID', 2389)->commit('title', 'The call of the wild');
     * $this->assertQueriesCountByMethod(3, 'Acme\BookRepository', 'commit');
     * ```
     * @param int $n The expected number of queries.
     * @param string $class The fully qualified name of the class to check.
     * @param string $method The name of the method to check.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertQueriesCountByMethod($n, $class, $method, $message = '')
    {
        $this->readQueries();
        $class = ltrim($class, '\\');
        $message = $message ?: 'Failed asserting that ' . $n . ' queries were made by method ['
                               . $class . '::' . $method . ']';
        $statementIterator = new ClassMethodQueriesFilter(new \ArrayIterator($this->filteredQueries), $class, $method);
        Assert::assertCount($n, iterator_to_array($statementIterator), $message);
    }

    /**
     * Asserts that queries were made by the specified function.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * acme_clean_queue();
     * $this->assertQueriesByFunction('acme_clean_queue');
     * ```
     *
     * @param string $function The fully qualified name of the function to check.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertQueriesByFunction($function, $message = '')
    {
        $this->readQueries();
        $message = $message ? $message : 'Failed asserting that queries were made by function [' . $function . ']';
        $statementIterator = new FunctionQueriesFilter(new \ArrayIterator($this->filteredQueries), $function);
        Assert::assertNotEmpty(iterator_to_array($statementIterator), $message);
    }

    /**
     * Asserts that no queries were made by the specified function.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * $this->assertEmpty(Acme\get_orphaned_posts());
     * Acme\delete_orphaned_posts();
     * $this->assertNotQueriesByFunction('Acme\delete_orphaned_posts');
     * ```
     *
     * @param string $function The fully qualified name of the function to check.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertNotQueriesByFunction($function, $message = '')
    {
        $message = $message ? $message : 'Failed asserting that no queries were made by function [' . $function . ']';
        $this->assertQueriesCountByFunction(0, $function, $message);
    }

    /**
     * Asserts that n queries were made by the specified function.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * $this->assertCount(3, Acme\get_orphaned_posts());
     * Acme\delete_orphaned_posts();
     * $this->assertQueriesCountByFunction(3, 'Acme\delete_orphaned_posts');
     * ```
     *
     * @param int $n The expected number of queries.
     * @param string $function The function to check the queries for.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertQueriesCountByFunction($n, $function, $message = '')
    {
        $this->readQueries();
        $message = $message ?: 'Failed asserting that ' . $n . ' queries were made by function [' . $function . ']';
        $statementIterator = new FunctionQueriesFilter(new \ArrayIterator($this->filteredQueries), $function);
        Assert::assertCount($n, iterator_to_array($statementIterator), $message);
    }

    /**
     * Asserts that queries were made by the specified class method starting with the specified SQL statement.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * Acme\BookRepository::new(['title' => 'Alice in Wonderland'])->commit();
     * $this->assertQueriesByStatementAndMethod('UPDATE', Acme\BookRepository::class, 'commit');
     * ```
     *
     * @param string $statement A simple string the statement should start with or a valid regular expression.
     *                           Regular expressions must contain delimiters.
     * @param string $class The fully qualified name of the class to check.
     * @param string $method The name of the method to check.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertQueriesByStatementAndMethod($statement, $class, $method, $message = '')
    {
        $this->readQueries();
        $message = $message ?: 'Failed asserting that queries were made by method ['
                               . $class . '::' . $method . '] containing statement [' . $statement . ']';
        $statementIterator = new MainStatementQueriesFilter(new ClassMethodQueriesFilter(
            new \ArrayIterator($this->filteredQueries),
            $class,
            $method
        ), $statement);
        Assert::assertNotEmpty(iterator_to_array($statementIterator), $message);
    }

    /**
     * Asserts that no queries were made by the specified class method starting with the specified SQL statement.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * Acme\BookRepository::new(['title' => 'Alice in Wonderland'])->commit();
     * $this->assertQueriesByStatementAndMethod('INSERT', Acme\BookRepository::class, 'commit');
     * ```
     *
     * @param string $statement A simple string the statement should start with or a valid regular expression.
     *                           Regular expressions must contain delimiters.
     * @param string $class The fully qualified name of the class to check.
     * @param string $method The name of the method to check.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertNotQueriesByStatementAndMethod($statement, $class, $method, $message = '')
    {
        $message = $message ?: 'Failed asserting that no queries were made by method [' . $class . '::'
                               . $method . '] containing statement [' . $statement . ']';
        $this->assertQueriesCountByStatementAndMethod(0, $statement, $class, $method, $message);
    }

    /**
     * Asserts that n queries were made by the specified class method starting with the specified SQL statement.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * Acme\BookRepository::new(['title' => 'Alice in Wonderland'])->commit();
     * Acme\BookRepository::new(['title' => 'Moby-Dick'])->commit();
     * Acme\BookRepository::new(['title' => 'The Call of the Wild'])->commit();
     * $this->assertQueriesCountByStatementAndMethod(3, 'INSERT', Acme\BookRepository::class, 'commit');
     * ```
     *
     * @param int $n The expected number of queries.
     * @param string $statement A simple string the statement should start with or a valid regular expression.
     *                           Regular expressions must contain delimiters.
     * @param string $class The fully qualified name of the class to check.
     * @param string $method The name of the method to check.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertQueriesCountByStatementAndMethod($n, $statement, $class, $method, $message = '')
    {
        $this->readQueries();
        $message = $message ?: 'Failed asserting that ' . $n . ' queries were made by method ['
                               . $class . '::' . $method . '] containing statement [' . $statement . ']';
        $statementIterator = new MainStatementQueriesFilter(new ClassMethodQueriesFilter(
            new \ArrayIterator($this->filteredQueries),
            $class,
            $method
        ), $statement);
        Assert::assertCount($n, iterator_to_array($statementIterator), $message);
    }

    /**
     * Asserts that queries were made by the specified function starting with the specified SQL statement.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * wp_insert_post(['post_type' => 'book', 'post_title' => 'Alice in Wonderland']);
     * $this->assertQueriesByStatementAndFunction('INSERT', 'wp_insert_post');
     * ```
     *
     * @param string $statement A simple string the statement should start with or a valid regular expression.
     *                           Regular expressions must contain delimiters.
     * @param string $function The fully qualified function name.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertQueriesByStatementAndFunction($statement, $function, $message = '')
    {
        $this->readQueries();
        $message = $message ?: 'Failed asserting that queries were made by function ['
                               . $function . '] containing statement [' . $statement . ']';
        $statementIterator = new MainStatementQueriesFilter(new FunctionQueriesFilter(
            new \ArrayIterator($this->filteredQueries),
            $function
        ), $statement);
        Assert::assertNotEmpty(iterator_to_array($statementIterator), $message);
    }

    /**
     * Asserts that no queries were made by the specified function starting with the specified SQL statement.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * wp_insert_post(['ID' => $bookId, 'post_title' => 'The Call of the Wild']);
     * $this->assertNotQueriesByStatementAndFunction('INSERT', 'wp_insert_post');
     * $this->assertQueriesByStatementAndFunction('UPDATE', 'wp_insert_post');
     * ```
     *
     * @param string $statement A simple string the statement should start with or a valid regular expression.
     *                           Regular expressions must contain delimiters.
     * @param string $function The name of the function to check the assertions for.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertNotQueriesByStatementAndFunction($statement, $function, $message = '')
    {
        $message = $message ?: 'Failed asserting that no queries were made by function ['
                               . $function . '] containing statement [' . $statement . ']';
        $this->assertQueriesCountByStatementAndFunction(0, $statement, $function, $message);
    }

    /**
     * Asserts that n queries were made by the specified function starting with the specified SQL statement.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * wp_insert_post(['post_type' => 'book', 'post_title' => 'The Call of the Wild']);
     * wp_insert_post(['post_type' => 'book', 'post_title' => 'Alice in Wonderland']);
     * wp_insert_post(['post_type' => 'book', 'post_title' => 'The Chocolate Factory']);
     * $this->assertQueriesCountByStatementAndFunction(3, 'INSERT', 'wp_insert_post');
     * ```
     *
     * @param int $n The expected number of queries.
     * @param string $statement A simple string the statement should start with or a valid regular expression.
     *                           Regular expressions must contain delimiters.
     * @param string $function The fully-qualified function name.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertQueriesCountByStatementAndFunction($n, $statement, $function, $message = '')
    {
        $this->readQueries();
        $message = $message ?: 'Failed asserting that ' . $n . ' queries were made by method ['
                               . $function . '] containing statement [' . $statement . ']';
        $statementIterator = new MainStatementQueriesFilter(new FunctionQueriesFilter(
            new \ArrayIterator($this->filteredQueries),
            $function
        ), $statement);
        Assert::assertCount($n, iterator_to_array($statementIterator), $message);
    }

    /**
     * Asserts that at least one query was made as a consequence of the specified action.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * add_action( 'edit_post', function($postId){
     *         $count = get_option('acme_title_updates_count');
     *         update_option('acme_title_updates_count', ++$count);
     * } );
     * wp_update_post(['ID' => $bookId, 'post_title' => 'New Title']);
     * $this->assertQueriesByAction('edit_post');
     * ```
     *
     * @param string $action The action name, e.g. 'init'.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertQueriesByAction($action, $message = '')
    {
        $this->readQueries();
        $message = $message ? $message : 'Failed asserting that queries were triggered by action [' . $action . ']';
        $iterator = new ActionsQueriesFilter(new \ArrayIterator($this->filteredQueries), $action);
        Assert::assertNotEmpty(iterator_to_array($iterator, false), $message);
    }

    /**
     * Asserts that no queries were made as a consequence of the specified action.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * add_action( 'edit_post', function($postId){
     *         $count = get_option('acme_title_updates_count');
     *         update_option('acme_title_updates_count', ++$count);
     * } );
     * wp_delete_post($bookId);
     * $this->assertNotQueriesByAction('edit_post');
     * ```
     *
     * @param string $action The action name, e.g. 'init'.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertNotQueriesByAction($action, $message = '')
    {
        $message = $message ? $message : 'Failed asserting that no queries were triggered by action [' . $action . ']';
        $this->assertQueriesCountByAction(0, $action, $message);
    }

    /**
     * Asserts that n queries were made as a consequence of the specified action.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * add_action( 'edit_post', function($postId){
     *         $count = get_option('acme_title_updates_count');
     *         update_option('acme_title_updates_count', ++$count);
     * } );
     * wp_update_post(['ID' => $bookOneId, 'post_title' => 'One']);
     * wp_update_post(['ID' => $bookTwoId, 'post_title' => 'Two']);
     * wp_update_post(['ID' => $bookThreeId, 'post_title' => 'Three']);
     * $this->assertQueriesCountByAction(3, 'edit_post');
     * ```
     *
     * @param int $n The expected number of queries.
     * @param string $action The action name, e.g. 'init'.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertQueriesCountByAction($n, $action, $message = '')
    {
        $this->readQueries();
        $message = $message ?: 'Failed asserting that ' . $n . ' queries were triggered by action [' . $action . ']';
        $iterator = new ActionsQueriesFilter(new \ArrayIterator($this->filteredQueries), $action);
        Assert::assertCount($n, iterator_to_array($iterator), $message);
    }

    /**
     * Asserts that at least one query was made as a consequence of the specified action containing the SQL query.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * add_action( 'edit_post', function($postId){
     *         $count = get_option('acme_title_updates_count');
     *         update_option('acme_title_updates_count', ++$count);
     * } );
     * wp_update_post(['ID' => $bookId, 'post_title' => 'New']);
     * $this->assertQueriesByStatementAndAction('UPDATE', 'edit_post');
     * ```
     *
     * @param string $statement A simple string the statement should start with or a valid regular expression.
     *                           Regular expressions must contain delimiters.
     * @param string $action The action name, e.g. 'init'.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertQueriesByStatementAndAction($statement, $action, $message = '')
    {
        $this->readQueries();
        $message = $message ?: 'Failed asserting that queries were triggered by action  ['
                               . $action . '] containing statement [' . $statement . ']';
        $iterator = new MainStatementQueriesFilter(new ActionsQueriesFilter(
            new \ArrayIterator($this->filteredQueries),
            $action
        ), $statement);
        Assert::assertNotEmpty(iterator_to_array($iterator), $message);
    }

    /**
     * Asserts that no queries were made as a consequence of the specified action containing the SQL query.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * add_action( 'edit_post', function($postId){
     *         $count = get_option('acme_title_updates_count');
     *         update_option('acme_title_updates_count', ++$count);
     * } );
     * wp_delete_post($bookId);
     * $this->assertNotQueriesByStatementAndAction('DELETE', 'delete_post');
     * ```
     *
     * @param string $statement A simple string the statement should start with or a valid regular expression.
     *                           Regular expressions must contain delimiters.
     * @param string $action The action name, e.g. 'init'.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertNotQueriesByStatementAndAction($statement, $action, $message = '')
    {
        $message = $message ?: 'Failed asserting that no queries were triggered by action  ['
                               . $action . '] containing statement [' . $statement . ']';
        $this->assertQueriesCountByStatementAndAction(0, $statement, $action, $message);
    }

    /**
     * Asserts that n queries were made as a consequence of the specified action containing the specified SQL statement.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * add_action( 'edit_post', function($postId){
     *         $count = get_option('acme_title_updates_count');
     *         update_option('acme_title_updates_count', ++$count);
     * } );
     * wp_delete_post($bookOneId);
     * wp_delete_post($bookTwoId);
     * wp_update_post(['ID' => $bookThreeId, 'post_title' => 'New']);
     * $this->assertQueriesCountByStatementAndAction(2, 'DELETE', 'delete_post');
     * $this->assertQueriesCountByStatementAndAction(1, 'INSERT', 'edit_post');
     * ```
     *
     * @param int $n The expected number of queries.
     * @param string $statement A simple string the statement should start with or a valid regular expression.
     *                           Regular expressions must contain delimiters.
     * @param string $action The action name, e.g. 'init'.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertQueriesCountByStatementAndAction($n, $statement, $action, $message = '')
    {
        $this->readQueries();
        $message = $message ?: 'Failed asserting that ' . $n . ' queries were triggered by action  ['
                               . $action . '] containing statement [' . $statement . ']';
        $iterator = new MainStatementQueriesFilter(new ActionsQueriesFilter(
            new \ArrayIterator($this->filteredQueries),
            $action
        ), $statement);
        Assert::assertCount($n, iterator_to_array($iterator), $message);
    }

    /**
     * Asserts that at least one query was made as a consequence of the specified filter.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * add_filter('the_title', function($title, $postId){
     *      $post = get_post($postId);
     *      if($post->post_type !== 'book'){
     *          return $title;
     *      }
     *      $new = get_option('acme_new_prefix');
     *      return "{$new} - " . $title;
     * });
     * $title = apply_filters('the_title', get_post($bookId)->post_title, $bookId);
     * $this->assertQueriesByFilter('the_title');
     * ```
     *
     * @param string $filter The filter name, e.g. 'posts_where'.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertQueriesByFilter($filter, $message = '')
    {
        $this->readQueries();
        $message = $message ? $message : 'Failed asserting that queries were triggered by filter [' . $filter . ']';
        $iterator = new FiltersQueriesFilter(new \ArrayIterator($this->filteredQueries), $filter);
        Assert::assertNotEmpty(iterator_to_array($iterator), $message);
    }

    /**
     * Asserts that no queries were made as a consequence of the specified filter.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * add_filter('the_title', function($title, $postId){
     *      $post = get_post($postId);
     *      if($post->post_type !== 'book'){
     *          return $title;
     *      }
     *      $new = get_option('acme_new_prefix');
     *      return "{$new} - " . $title;
     * });
     * $title = apply_filters('the_title', get_post($notABookId)->post_title, $notABookId);
     * $this->assertNotQueriesByFilter('the_title');
     * ```
     *
     * @param string $filter The filter name, e.g. 'posts_where'.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertNotQueriesByFilter($filter, $message = '')
    {
        $message = $message ? $message : 'Failed asserting that no queries were triggered by filter [' . $filter . ']';
        $this->assertQueriesCountByFilter(0, $filter, $message);
    }

    /**
     * Asserts that n queries were made as a consequence of the specified filter.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * add_filter('the_title', function($title, $postId){
     *      $post = get_post($postId);
     *      if($post->post_type !== 'book'){
     *          return $title;
     *      }
     *      $new = get_option('acme_new_prefix');
     *      return "{$new} - " . $title;
     * });
     * $title = apply_filters('the_title', get_post($bookOneId)->post_title, $bookOneId);
     * $title = apply_filters('the_title', get_post($notABookId)->post_title, $notABookId);
     * $title = apply_filters('the_title', get_post($bookTwoId)->post_title, $bookTwoId);
     * $this->assertQueriesCountByFilter(2, 'the_title');
     * ```
     *
     * @param int $n The expected number of queries.
     * @param string $filter The filter name, e.g. 'posts_where'.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertQueriesCountByFilter($n, $filter, $message = '')
    {
        $this->readQueries();
        $message = $message ?: 'Failed asserting that ' . $n . ' queries were triggered by filter [' . $filter . ']';
        $iterator = new FiltersQueriesFilter(new \ArrayIterator($this->filteredQueries), $filter);
        Assert::assertCount($n, iterator_to_array($iterator), $message);
    }

    /**
     * Asserts that at least one query was made as a consequence of the specified filter containing the SQL query.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * add_filter('the_title', function($title, $postId){
     *      $post = get_post($postId);
     *      if($post->post_type !== 'book'){
     *          return $title;
     *      }
     *      $new = get_option('acme_new_prefix');
     *      return "{$new} - " . $title;
     * });
     * $title = apply_filters('the_title', get_post($bookId)->post_title, $bookId);
     * $this->assertQueriesByStatementAndFilter('SELECT', 'the_title');
     * ```
     *
     * @param string $statement A simple string the statement should start with or a valid regular expression.
     *                          Regular expressions must contain delimiters.
     * @param string $filter The filter name, e.g. 'posts_where'.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertQueriesByStatementAndFilter($statement, $filter, $message = '')
    {
        $this->readQueries();
        $message = $message ?: 'Failed asserting that queries were triggered by filter  ['
                               . $filter . '] containing statement [' . $statement . ']';
        $iterator = new MainStatementQueriesFilter(new FiltersQueriesFilter(
            new \ArrayIterator($this->filteredQueries),
            $filter
        ), $statement);
        Assert::assertNotEmpty(iterator_to_array($iterator), $message);
    }

    /**
     * Asserts that no queries were made as a consequence of the specified filter containing the specified SQL query.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * add_filter('the_title', function($title, $postId){
     *      $post = get_post($postId);
     *      if($post->post_type !== 'book'){
     *          return $title;
     *      }
     *      $new = get_option('acme_new_prefix');
     *      return "{$new} - " . $title;
     * });
     * $title = apply_filters('the_title', get_post($notABookId)->post_title, $notABookId);
     * $this->assertNotQueriesByStatementAndFilter('SELECT', 'the_title');
     * ```
     *
     * @param string $statement A simple string the statement should start with or a valid regular expression.
     *                           Regular expressions must contain delimiters.
     * @param string $filter The filter name, e.g. 'posts_where'.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertNotQueriesByStatementAndFilter($statement, $filter, $message = '')
    {
        $message = $message ?: 'Failed asserting that no queries were triggered by filter  ['
                               . $filter . '] containing statement [' . $statement . ']';
        $this->assertQueriesCountByStatementAndFilter(0, $statement, $filter, $message);
    }

    /**
     * Asserts that n queries were made as a consequence of the specified filter containing the specified SQL statement.
     *
     * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default.
     *
     * @example
     * ```php
     * add_filter('the_title', function($title, $postId){
     *      $post = get_post($postId);
     *      if($post->post_type !== 'book'){
     *          return $title;
     *      }
     *      $new = get_option('acme_new_prefix');
     *      return "{$new} - " . $title;
     * });
     * // Warm up the cache.
     * $title = apply_filters('the_title', get_post($bookOneId)->post_title, $bookOneId);
     * // Cache is warmed up now.
     * $title = apply_filters('the_title', get_post($bookTwoId)->post_title, $bookTwoId);
     * $title = apply_filters('the_title', get_post($bookThreeId)->post_title, $bookThreeId);
     * $this->assertQueriesCountByStatementAndFilter(1, 'SELECT', 'the_title');
     * ```
     *
     * @param int $n The expected number of queries.
     * @param string $statement A simple string the statement should start with or a valid regular expression.
     *                           Regular expressions must contain delimiters.
     * @param string $filter The filter name, e.g. 'posts_where'.
     * @param string $message An optional message to override the default one.
     *
     * @return void
     */
    public function assertQueriesCountByStatementAndFilter($n, $statement, $filter, $message = '')
    {
        $this->readQueries();
        $message = $message ?: 'Failed asserting that ' . $n . ' queries were triggered by filter  ['
                               . $filter . '] containing statement [' . $statement . ']';
        $iterator = new MainStatementQueriesFilter(new FiltersQueriesFilter(
            new \ArrayIterator($this->filteredQueries),
            $filter
        ), $statement);
        Assert::assertCount($n, iterator_to_array($iterator), $message);
    }

    /**
     * Returns the current number of queries.
     * Set-up and tear-down queries performed by the test case are filtered out.
     *
     * @example
     * ```php
     * // In a WPTestCase, using the global $wpdb object.
     * $queriesCount = $this->queries()->countQueries();
     * // In a WPTestCase, using a custom $wpdb object.
     * $queriesCount = $this->queries()->countQueries($customWdbb);
     * ```
     *
     * @param \wpdb|null $wpdb A specific instance of the `wpdb` class or `null` to use the global one.
     *
     * @return int The current count of performed queries.
     */
    public function countQueries($wpdb = null)
    {
        return count($this->getQueries($wpdb));
    }

    /**
     * Returns the queries currently performed by the global database object or the specified one.
     * Set-up and tear-down queries performed by the test case are filtered out.
     *
     * @example
     * ```php
     * // In a WPTestCase, using the global $wpdb object.
     * $queries = $this->queries()->getQueries();
     * // In a WPTestCase, using a custom $wpdb object.
     * $queries = $this->queries()->getQueries($customWdbb);
     * ```
     *
     * @param null|\wpdb $wpdb A specific instance of the `wpdb` class or `null` to use the global one.
     *
     * @return array<string> An array of queries.
     */
    public function getQueries($wpdb = null)
    {
        return iterator_to_array($this->_getFilteredQueriesIterator($wpdb), false);
    }
}
