<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/

use PHPUnit\DbUnit\TestCase;

abstract class phpbb_database_test_case extends TestCase
{
	static private $already_connected;

	private $db_connections;

	protected $test_case_helpers;

	protected $fixture_xml_data;

	static protected $schema_file;

	static protected $phpbb_schema_copy;

	static protected $install_schema_file;

	static protected $phpunit_version;

	public function __construct($name = NULL, array $data = [], $dataName = '')
	{
		parent::__construct($name, $data, $dataName);

		self::$phpunit_version = PHPUnit\Runner\Version::id();

		$backupStaticAttributesBlacklist = [
			'SebastianBergmann\CodeCoverage\CodeCoverage' => ['instance'],
			'SebastianBergmann\CodeCoverage\Filter' => ['instance'],
			'SebastianBergmann\CodeCoverage\Util' => ['ignoredLines', 'templateMethods'],
			'SebastianBergmann\Timer\Timer' => ['startTimes'],
			'PHP_Token_Stream' => ['customTokens'],
			'PHP_Token_Stream_CachingFactory' => ['cache'],

			'phpbb_database_test_case' => ['already_connected'],
		];

		if (version_compare(self::$phpunit_version, '9.0', '>='))
		{
			$this->backupStaticAttributesExcludeList += $backupStaticAttributesBlacklist;
		}
		else
		{
			$this->backupStaticAttributesBlacklist += $backupStaticAttributesBlacklist;
		}

		$this->db_connections = [];
	}

	/**
	* @return array List of extensions that should be set up
	*/
	static protected function setup_extensions()
	{
		return array();
	}

	static public function setUpBeforeClass(): void
	{
		global $phpbb_root_path, $phpEx;

		$setup_extensions = static::setup_extensions();

		$finder = new \phpbb\finder(new \phpbb\filesystem\filesystem(), $phpbb_root_path, null, $phpEx);
		$finder->core_path('phpbb/db/migration/data/');
		if (!empty($setup_extensions))
		{
			$finder->set_extensions($setup_extensions)
				->extension_directory('/migrations');
		}
		$classes = $finder->get_classes();

		$schema_sha1 = sha1(serialize($classes));
		self::$schema_file = __DIR__ . '/../tmp/' . $schema_sha1 . '.json';
		self::$install_schema_file = __DIR__ . '/../../phpBB/install/schemas/schema.json';

		if (!file_exists(self::$schema_file))
		{

			global $table_prefix;

			$db = new \phpbb\db\driver\sqlite3();
			$factory = new \phpbb\db\tools\factory();
			$db_tools = $factory->get($db, true);

			$schema_generator = new \phpbb\db\migration\schema_generator($classes, new \phpbb\config\config(array()), $db, $db_tools, $phpbb_root_path, $phpEx, $table_prefix);
			file_put_contents(self::$schema_file, json_encode($schema_generator->get_schema()));
		}

		copy(self::$schema_file, self::$install_schema_file);

		parent::setUpBeforeClass();
	}

	static public function tearDownAfterClass(): void
	{
		if (file_exists(self::$install_schema_file))
		{
			unlink(self::$install_schema_file);
		}

		parent::tearDownAfterClass();
	}

	protected function tearDown(): void
	{
		parent::tearDown();

		// Close all database connections from this test
		if (!empty($this->db_connections))
		{
			foreach ($this->db_connections as $db)
			{
				$db->sql_close();
			}
		}
	}

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

		// Resynchronise tables if a fixture was loaded
		if (isset($this->fixture_xml_data))
		{
			$config = $this->get_database_config();
			$manager = $this->create_connection_manager($config);
			$manager->connect();
			$manager->post_setup_synchronisation($this->fixture_xml_data);
		}
	}

	/**
	* Performs synchronisations for a given table/column set on the database
	*
	* @param	array	$table_column_map		Information about the tables/columns to synchronise
	*
	* @return null
	*/
	protected function database_synchronisation($table_column_map)
	{
		$config = $this->get_database_config();
		$manager = $this->create_connection_manager($config);
		$manager->connect();
		$manager->database_synchronisation($table_column_map);
	}

	/**
	 * Create xml data set for insertion into database
	 *
	 * @param string $path Path to fixture XML
	 * @return PHPUnit\DbUnit\DataSet\DefaultDataSet|PHPUnit\DbUnit\DataSet\XmlDataSet
	 */
	public function createXMLDataSet($path)
	{
		$this->fixture_xml_data = parent::createXMLDataSet($path);

		// Extend XML data set on MSSQL
		if (strpos($this->get_database_config()['dbms'], 'mssql') !== false)
		{
			$newXmlData = new PHPUnit\DbUnit\DataSet\DefaultDataSet([]);
			$db = $this->new_dbal();
			foreach ($this->fixture_xml_data as $key => $value)
			{
				/** @var PHPUnit\DbUnit\DataSet\DefaultTableMetaData $tableMetaData */
				$tableMetaData = $value->getTableMetaData();
				$columns = $tableMetaData->getColumns();
				$primaryKeys = $tableMetaData->getPrimaryKeys();

				$sql = "SELECT COLUMN_NAME AS identity_column
					FROM INFORMATION_SCHEMA.COLUMNS
					WHERE COLUMNPROPERTY(object_id(TABLE_SCHEMA + '.' + TABLE_NAME), COLUMN_NAME, 'IsIdentity') = 1
						AND TABLE_NAME = '$key'
					ORDER BY TABLE_NAME";
				$result = $db->sql_query($sql);
				$identity_columns = $db->sql_fetchrowset($result);
				$has_default_identity = false;
				$add_primary_keys = false;

				// Iterate over identity columns to check for missing primary
				// keys in data set and special identity column 'mssqlindex'
				// that might have been added when no default identity column
				// exists in the current table.
				foreach ($identity_columns as $column)
				{
					if (in_array($column['identity_column'], $columns) && !in_array($column['identity_column'], $primaryKeys))
					{
						$primaryKeys[] = $column['identity_column'];
						$add_primary_keys = true;
					}

					if ($column['identity_column'] === 'mssqlindex')
					{
						$has_default_identity = true;
						break;
					}
				}

				if ($has_default_identity || $add_primary_keys)
				{
					// Add default identity column to columns list
					if ($has_default_identity)
					{
						$columns[] = 'mssqlindex';
					}

					$newMetaData = new PHPUnit\DbUnit\DataSet\DefaultTableMetaData($key, $columns, $primaryKeys);
					$newTable = new PHPUnit\DbUnit\DataSet\DefaultTable($newMetaData);
					for ($i = 0; $i < $value->getRowCount(); $i++)
					{
						$dataRow = $value->getRow($i);
						if ($has_default_identity)
						{
							$dataRow['mssqlindex'] = $i + 1;
						}
						$newTable->addRow($dataRow);
					}
					$newXmlData->addTable($newTable);
				}
				else
				{
					$newXmlData->addTable($value);
				}
			}

			$this->fixture_xml_data = $newXmlData;
		}
		return $this->fixture_xml_data;
	}

	public function get_test_case_helpers()
	{
		if (!$this->test_case_helpers)
		{
			$this->test_case_helpers = new phpbb_test_case_helpers($this);
		}

		return $this->test_case_helpers;
	}

	public function get_database_config()
	{
		$config = phpbb_test_case_helpers::get_test_config();

		if (!isset($config['dbms']))
		{
			$this->markTestSkipped('Missing test_config.php: See first error.');
		}

		return $config;
	}

	public function getConnection()
	{
		$config = $this->get_database_config();

		$manager = $this->create_connection_manager($config);

		if (!self::$already_connected)
		{
			$manager->recreate_db();
		}

		$manager->connect();

		if (!self::$already_connected)
		{
			$manager->load_schema($this->new_dbal());
			self::$already_connected = true;
		}

		return $this->createDefaultDBConnection($manager->get_pdo(), 'testdb');
	}

	public function new_dbal() : \phpbb\db\driver\driver_interface
	{
		$config = $this->get_database_config();

		$db = new $config['dbms']();
		$db->sql_connect($config['dbhost'], $config['dbuser'], $config['dbpasswd'], $config['dbname'], $config['dbport']);

		$this->db_connections[] = $db;

		return $db;
	}

	public function assertSqlResultEquals($expected, $sql, $message = '')
	{
		$db = $this->new_dbal();

		$result = $db->sql_query($sql);
		$rows = $db->sql_fetchrowset($result);
		$db->sql_freeresult($result);

		$this->assertEquals($expected, $rows, $message);
	}

	public function setExpectedTriggerError($errno, $message = '')
	{
		$this->get_test_case_helpers()->setExpectedTriggerError($errno, $message);
	}

	protected function create_connection_manager($config)
	{
		return new phpbb_database_test_connection_manager($config);
	}

	/** array_diff() does not corretly compare multidimensionsl arrays
	 *  This solution used for that https://www.codeproject.com/Questions/780780/PHP-Finding-differences-in-two-multidimensional-ar
	 */
	function array_diff_assoc_recursive($array1, $array2)
	{
		$difference = array();
		foreach ($array1 as $key => $value)
		{
			if (is_array($value))
			{
				if (!isset($array2[$key]))
				{
					$difference[$key] = $value;
				}
				else if (!is_array($array2[$key]))
				{
					$difference[$key] = $value;
				}
				else
				{
					$new_diff = $this->array_diff_assoc_recursive($value, $array2[$key]);
					if (!empty($new_diff))
					{
						$difference[$key] = $new_diff;
					}
				}
			}
			else if (!isset($array2[$key]) || $array2[$key] != $value)
			{
				$difference[$key] = $value;
			}
		}
		return $difference;
	}

	public function assert_array_content_equals($one, $two)
	{
		// one-way comparison is not enough!
		if (count($this->array_diff_assoc_recursive($one, $two)) || count($this->array_diff_assoc_recursive($two, $one)))
		{
			// get a nice error message
			$this->assertEquals($one, $two);
		}
		else
		{
			// increase assertion count
			$this->assertTrue(true);
		}
	}

	/**
	 * PHPUnit deprecates several methods and properties in its recent versions
	 * Provide BC layer to be able to test in multiple environment settings
	 */
	public function expectException(string $exception): void
	{
		if (version_compare(self::$phpunit_version, '9.0', '>='))
		{
			switch ($exception) {
				case PHPUnit\Framework\Error\Deprecated::class:
					parent::expectDeprecation();
				break;

				case PHPUnit\Framework\Error\Error::class:
					parent::expectError();
				break;

				case PHPUnit\Framework\Error\Notice::class:
					parent::expectNotice();
				break;

				case PHPUnit\Framework\Error\Warning::class:
					parent::expectWarning();
				break;

				default:
					parent::expectException($exception);
				break;
			}
		}
		else
		{
			parent::expectException($exception);
		}
	}

	/**
	 * PHPUnit deprecates several methods and properties in its recent versions
	 * Provide BC layer to be able to test in multiple environment settings
	 */
	public static function assertFileNotExists(string $filename, string $message = ''): void
	{
		if (version_compare(self::$phpunit_version, '9.0', '>='))
		{
			parent::assertFileDoesNotExist($filename, $message);
		}
		else
		{
			parent::assertFileNotExists($filename, $message);
		}
	}
}
