<?php
/**
 * Copyright (c) 2015 Joas Schilling <nickvergessen@owncloud.com>
 * This file is licensed under the Affero General Public License version 3 or
 * later.
 * See the COPYING-README file.
 */

namespace Test\Repair;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Migration\IOutput;

/**
 * Tests for the cleaning the tags tables
 *
 * @group DB
 *
 * @see \OC\Repair\CleanTags
 */
class CleanTagsTest extends \Test\TestCase {
	/** @var \OC\Repair\CleanTags */
	protected $repair;

	/** @var \OCP\IDBConnection */
	protected $connection;

	/** @var \OCP\IUserManager|\PHPUnit\Framework\MockObject\MockObject */
	protected $userManager;

	/** @var int */
	protected $createdFile;

	/** @var IOutput */
	private $outputMock;

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

		$this->outputMock = $this->getMockBuilder('\OCP\Migration\IOutput')
			->disableOriginalConstructor()
			->getMock();

		$this->userManager = $this->getMockBuilder('\OCP\IUserManager')
			->disableOriginalConstructor()
			->getMock();

		$this->connection = \OC::$server->getDatabaseConnection();
		$this->repair = new \OC\Repair\CleanTags($this->connection, $this->userManager);
		$this->cleanUpTables();
	}

	protected function tearDown(): void {
		$this->cleanUpTables();

		parent::tearDown();
	}

	protected function cleanUpTables() {
		$qb = $this->connection->getQueryBuilder();
		$qb->delete('vcategory')
			->execute();

		$qb->delete('vcategory_to_object')
			->execute();

		$qb->delete('filecache')
			->execute();
	}

	public function testRun() {
		$cat1 = $this->addTagCategory('TestRepairCleanTags', 'files'); // Retained
		$cat2 = $this->addTagCategory('TestRepairCleanTags2', 'files'); // Deleted: Category will be empty
		$this->addTagCategory('TestRepairCleanTags3', 'files'); // Deleted: Category is empty
		$cat3 = $this->addTagCategory('TestRepairCleanTags', 'contacts'); // Retained

		$this->addTagEntry($this->getFileID(), $cat2, 'files'); // Retained
		$this->addTagEntry($this->getFileID() + 1, $cat1, 'files'); // Deleted: File is NULL
		$this->addTagEntry(9999999, $cat3, 'contacts'); // Retained
		$this->addTagEntry($this->getFileID(), $cat3 + 1, 'files'); // Deleted: Category is NULL

		$this->assertEntryCount('vcategory_to_object', 4, 'Assert tag entries count before repair step');
		$this->assertEntryCount('vcategory', 4, 'Assert tag categories count before repair step');

		self::invokePrivate($this->repair, 'deleteOrphanFileEntries', [$this->outputMock]);
		$this->assertEntryCount('vcategory_to_object', 3, 'Assert tag entries count after cleaning file entries');
		$this->assertEntryCount('vcategory', 4, 'Assert tag categories count after cleaning file entries');

		self::invokePrivate($this->repair, 'deleteOrphanTagEntries', [$this->outputMock]);
		$this->assertEntryCount('vcategory_to_object', 2, 'Assert tag entries count after cleaning tag entries');
		$this->assertEntryCount('vcategory', 4, 'Assert tag categories count after cleaning tag entries');

		self::invokePrivate($this->repair, 'deleteOrphanCategoryEntries', [$this->outputMock]);
		$this->assertEntryCount('vcategory_to_object', 2, 'Assert tag entries count after cleaning category entries');
		$this->assertEntryCount('vcategory', 2, 'Assert tag categories count after cleaning category entries');

		$this->addTagCategory('TestRepairCleanTags', 'contacts', 'userExists'); // Retained
		$this->assertEntryCount('vcategory', 3, 'Assert tag categories count before cleaning categories by users');

		$this->userManager->expects($this->exactly(2))
			->method('userExists')
			->willReturnMap([
				['userExists', true],
				['TestRepairCleanTags', false],
			]);

		self::invokePrivate($this->repair, 'deleteOrphanTags', [$this->outputMock]);
		$this->assertEntryCount('vcategory', 1, 'Assert tag categories count after cleaning categories by users');
	}

	/**
	 * @param string $tableName
	 * @param int $expected
	 * @param string $message
	 */
	protected function assertEntryCount($tableName, $expected, $message = '') {
		$qb = $this->connection->getQueryBuilder();
		$result = $qb->select($qb->createFunction('COUNT(*)'))
			->from($tableName)
			->execute();

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

	/**
	 * Adds a new tag category to the database
	 *
	 * @param string $category
	 * @param string $type
	 * @param string $user
	 * @return int
	 */
	protected function addTagCategory($category, $type, $user = 'TestRepairCleanTags') {
		$qb = $this->connection->getQueryBuilder();
		$qb->insert('vcategory')
			->values([
				'uid'		=> $qb->createNamedParameter($user),
				'category'	=> $qb->createNamedParameter($category),
				'type'		=> $qb->createNamedParameter($type),
			])
			->execute();

		return (int) $this->getLastInsertID('vcategory', 'id');
	}

	/**
	 * Adds a new tag entry to the database
	 * @param int $objectId
	 * @param int $category
	 * @param string $type
	 */
	protected function addTagEntry($objectId, $category, $type) {
		$qb = $this->connection->getQueryBuilder();
		$qb->insert('vcategory_to_object')
			->values([
				'objid'			=> $qb->createNamedParameter($objectId, IQueryBuilder::PARAM_INT),
				'categoryid'	=> $qb->createNamedParameter($category, IQueryBuilder::PARAM_INT),
				'type'			=> $qb->createNamedParameter($type),
			])
			->execute();
	}

	/**
	 * Gets the last fileid from the file cache
	 * @return int
	 */
	protected function getFileID() {
		if ($this->createdFile) {
			return $this->createdFile;
		}

		$qb = $this->connection->getQueryBuilder();

		// We create a new file entry and delete it after the test again
		$fileName = self::getUniqueID('TestRepairCleanTags', 12);
		$qb->insert('filecache')
			->values([
				'path'			=> $qb->createNamedParameter($fileName),
				'path_hash'		=> $qb->createNamedParameter(\md5($fileName)),
			])
			->execute();
		$fileName = self::getUniqueID('TestRepairCleanTags', 12);
		$qb->insert('filecache')
			->values([
				'path'			=> $qb->createNamedParameter($fileName),
				'path_hash'		=> $qb->createNamedParameter(\md5($fileName)),
			])
			->execute();

		$this->createdFile = (int) $this->getLastInsertID('filecache', 'fileid');
		return $this->createdFile;
	}

	/**
	 * @param $tableName
	 * @param $idName
	 * @return int
	 */
	protected function getLastInsertID($tableName, $idName) {
		return $this->connection->lastInsertId("*PREFIX*$tableName");
	}
}
