<?php
/**
 * Copyright (c) 2014 Vincent Petry <pvince81@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 OC\Files\Cache\Cache;
use OC\Files\Cache\Storage;
use OCP\Migration\IOutput;
use PHPUnit_Framework_MockObject_MockObject;
use Test\TestCase;

/**
 * Tests for the converting of legacy storages to home storages.
 *
 * @group DB
 *
 * @see \OC\Repair\RepairLegacyStorages
 */
class RepairLegacyStoragesTest extends TestCase {
	/** @var \OCP\IDBConnection */
	private $connection;
	/** @var \OCP\IConfig */
	private $config;
	private $user;
	/** @var \OC\Repair\RepairLegacyStorages */
	private $repair;

	private $dataDir;
	private $oldDataDir;

	private $legacyStorageId;
	private $newStorageId;

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

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

		$this->config = \OC::$server->getConfig();
		$this->connection = \OC::$server->getDatabaseConnection();
		$this->oldDataDir = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data/');

		$this->repair = new \OC\Repair\RepairLegacyStorages($this->config, $this->connection);

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

	protected function tearDown() {
		$user = \OC::$server->getUserManager()->get($this->user);
		if ($user) {
			$user->delete();
		}

		$sql = 'DELETE FROM `*PREFIX*storages`';
		$this->connection->executeQuery($sql);
		$sql = 'DELETE FROM `*PREFIX*filecache`';
		$this->connection->executeQuery($sql);
		$this->config->setSystemValue('datadirectory', $this->oldDataDir);
		$this->config->setAppValue('core', 'repairlegacystoragesdone', 'no');

		parent::tearDown();
	}

	/**
	 * @param string $dataDir
	 * @param string $userId
	 * @throws \Exception
	 */
	function prepareSettings($dataDir, $userId) {
		// hard-coded string as we want a predictable fixed length
		// no data will be written there
		$this->dataDir = $dataDir;
		$this->config->setSystemValue('datadirectory', $this->dataDir);

		$this->user = $userId;
		$this->legacyStorageId = 'local::' . $this->dataDir . $this->user . '/';
		$this->newStorageId = 'home::' . $this->user;
		\OC::$server->getUserManager()->createUser($this->user, $this->user);
	}

	/**
	 * Create a storage entry
	 *
	 * @param string $storageId
	 * @return int
	 */
	private function createStorage($storageId) {
		$sql = 'INSERT INTO `*PREFIX*storages` (`id`)'
			. ' VALUES (?)';

		$storageId = Storage::adjustStorageId($storageId);
		$numRows = $this->connection->executeUpdate($sql, array($storageId));
		$this->assertSame(1, $numRows);

		return (int)\OC::$server->getDatabaseConnection()->lastInsertId('*PREFIX*storages');
	}

	/**
	 * Create dummy data in the filecache for the given storage numeric id
	 *
	 * @param string $storageId storage id
	 */
	private function createData($storageId) {
		$cache = new Cache($storageId);
		$cache->put(
			'dummyfile.txt',
			array('size' => 5, 'mtime' => 12, 'mimetype' => 'text/plain')
		);
	}

	/**
	 * Test that existing home storages are left alone when valid.
	 *
	 * @dataProvider settingsProvider
	 *
	 * @param string $dataDir
	 * @param string $userId
	 */
	public function testNoopWithExistingHomeStorage($dataDir, $userId) {
		$this->prepareSettings($dataDir, $userId);
		$newStorageNumId = $this->createStorage($this->newStorageId);

		$this->repair->run($this->outputMock);

		$this->assertNull(Storage::getNumericStorageId($this->legacyStorageId));
		$this->assertSame($newStorageNumId, Storage::getNumericStorageId($this->newStorageId));
	}

	/**
	 * Test that legacy storages are converted to home storages when
	 * the latter does not exist.
	 *
	 * @dataProvider settingsProvider
	 *
	 * @param string $dataDir
	 * @param string $userId
	 */
	public function testConvertLegacyToHomeStorage($dataDir, $userId) {
		$this->prepareSettings($dataDir, $userId);
		$legacyStorageNumId = $this->createStorage($this->legacyStorageId);

		$this->repair->run($this->outputMock);

		$this->assertNull(Storage::getNumericStorageId($this->legacyStorageId));
		$this->assertSame($legacyStorageNumId, Storage::getNumericStorageId($this->newStorageId));
	}

	/**
	 * Test that legacy storages are converted to home storages
	 * when home storage already exists but has no data.
	 *
	 * @dataProvider settingsProvider
	 *
	 * @param string $dataDir
	 * @param string $userId
	 */
	public function testConvertLegacyToExistingEmptyHomeStorage($dataDir, $userId) {
		$this->prepareSettings($dataDir, $userId);
		$legacyStorageNumId = $this->createStorage($this->legacyStorageId);
		$this->createStorage($this->newStorageId);

		$this->createData($this->legacyStorageId);

		$this->repair->run($this->outputMock);

		$this->assertNull(Storage::getNumericStorageId($this->legacyStorageId));
		$this->assertSame($legacyStorageNumId, Storage::getNumericStorageId($this->newStorageId));
	}

	/**
	 * Test that legacy storages are converted to home storages
	 * when home storage already exists and the legacy storage
	 * has no data.
	 *
	 * @dataProvider settingsProvider
	 *
	 * @param string $dataDir
	 * @param string $userId
	 */
	public function testConvertEmptyLegacyToHomeStorage($dataDir, $userId) {
		$this->prepareSettings($dataDir, $userId);
		$this->createStorage($this->legacyStorageId);
		$newStorageNumId = $this->createStorage($this->newStorageId);

		$this->createData($this->newStorageId);

		$this->repair->run($this->outputMock);

		$this->assertNull(Storage::getNumericStorageId($this->legacyStorageId));
		$this->assertSame($newStorageNumId, Storage::getNumericStorageId($this->newStorageId));
	}

	/**
	 * Test that nothing is done when both conflicting legacy
	 * and home storage have data.
	 *
	 * @dataProvider settingsProvider
	 *
	 * @param string $dataDir
	 * @param string $userId
	 */
	public function testConflictNoop($dataDir, $userId) {
		$this->prepareSettings($dataDir, $userId);
		$legacyStorageNumId = $this->createStorage($this->legacyStorageId);
		$newStorageNumId = $this->createStorage($this->newStorageId);

		$this->createData($this->legacyStorageId);
		$this->createData($this->newStorageId);

		$this->outputMock->expects($this->exactly(2))->method('warning');
		$this->repair->run($this->outputMock);

		// storages left alone
		$this->assertSame($legacyStorageNumId, Storage::getNumericStorageId($this->legacyStorageId));
		$this->assertSame($newStorageNumId, Storage::getNumericStorageId($this->newStorageId));

		// do not set the done flag
		$this->assertNotEquals('yes', $this->config->getAppValue('core', 'repairlegacystoragesdone'));
	}

	/**
	 * Test that the data dir local entry is left alone
	 *
	 * @dataProvider settingsProvider
	 *
	 * @param string $dataDir
	 * @param string $userId
	 */
	public function testDataDirEntryNoop($dataDir, $userId) {
		$this->prepareSettings($dataDir, $userId);
		$storageId = 'local::' . $this->dataDir;
		$numId = $this->createStorage($storageId);

		$this->repair->run($this->outputMock);

		$this->assertSame($numId, Storage::getNumericStorageId($storageId));
	}

	/**
	 * Test that external local storages are left alone
	 *
	 * @dataProvider settingsProvider
	 *
	 * @param string $dataDir
	 * @param string $userId
	 */
	public function testLocalExtStorageNoop($dataDir, $userId) {
		$this->prepareSettings($dataDir, $userId);
		$storageId = 'local::/tmp/somedir/' . $this->user;
		$numId = $this->createStorage($storageId);

		$this->repair->run($this->outputMock);

		$this->assertSame($numId, Storage::getNumericStorageId($storageId));
	}

	/**
	 * Test that other external storages are left alone
	 *
	 * @dataProvider settingsProvider
	 *
	 * @param string $dataDir
	 * @param string $userId
	 */
	public function testExtStorageNoop($dataDir, $userId) {
		$this->prepareSettings($dataDir, $userId);
		$storageId = 'smb::user@password/tmp/somedir/' . $this->user;
		$numId = $this->createStorage($storageId);

		$this->repair->run($this->outputMock);

		$this->assertSame($numId, Storage::getNumericStorageId($storageId));
	}

	/**
	 * Provides data dir and user name
	 */
	function settingsProvider() {
		return array(
			// regular data dir
			array(
				'/tmp/oc-autotest/datadir/',
				$this->getUniqueID('user_'),
			),
			// long datadir / short user
			array(
				'/tmp/oc-autotest/datadir01234567890123456789012345678901234567890123456789END/',
				$this->getUniqueID('user_'),
			),
			// short datadir / long user
			array(
				'/tmp/oc-autotest/datadir/',
				'u123456789012345678901234567890123456789012345678901234567890END', // 64 chars
			),
		);
	}

	/**
	 * Only run the repair once
	 */
	public function testOnlyRunOnce() {
		$this->outputMock->expects($this->exactly(1))->method('info');

		$this->prepareSettings('/tmp/oc-autotest/datadir', $this->getUniqueID('user_'));
		$this->assertNotEquals('yes', $this->config->getAppValue('core', 'repairlegacystoragesdone'));
		$this->repair->run($this->outputMock);
		$this->assertEquals('yes', $this->config->getAppValue('core', 'repairlegacystoragesdone'));

		$this->outputMock->expects($this->never())->method('info');
		$this->repair->run($this->outputMock);
		$this->assertEquals('yes', $this->config->getAppValue('core', 'repairlegacystoragesdone'));
	}
}
