<?php
declare(strict_types=1);

namespace zesk;

use zesk\CacheItemPool\FileCacheItemPool;
use zesk\Exception\ClassNotFound;
use zesk\Exception\ConfigurationException;
use zesk\Exception\DirectoryCreate;
use zesk\Exception\DirectoryNotFound;
use zesk\Exception\DirectoryPermission;
use zesk\Exception\NotFoundException;
use zesk\Exception\ParameterException;
use zesk\Exception\ParseException;
use zesk\Exception\SemanticsException;
use zesk\Exception\UnsupportedException;
use zesk\PHPUnit\TestCase;

class TestApplicationUnitTest extends TestCase {
	/**
	 * @var array
	 */
	protected array $testApplicationOptions = [];

	/**
	 * @var TestApplication
	 */
	protected TestApplication $testApplication;

	/**
	 * @throws ConfigurationException
	 * @throws DirectoryPermission
	 * @throws UnsupportedException
	 * @throws NotFoundException
	 * @throws DirectoryCreate
	 * @throws ClassNotFound
	 * @throws DirectoryNotFound
	 * @throws ParseException
	 * @throws SemanticsException
	 */
	public function setUp(): void {
		parent::setUp(); // TODO: Change the autogenerated stub
		$_SERVER['DOCUMENT_ROOT'] = $this->test_sandbox();
		$this->testApplication = $this->newApplicationFactory($this->testApplicationOptions);
	}

	/**
	 * @return TestApplication
	 * @throws SemanticsException
	 */
	public static function testApplication(): TestApplication {
		$app = Kernel::singleton()->applicationByClass(TestApplication::class);
		if (!$app) {
			throw new SemanticsException('No TestApplication');
		}
		assert($app instanceof TestApplication);
		return $app;
	}

	/**
	 * @return void
	 */
	public function tearDown(): void {
		$this->testApplication->shutdown();
		unset($_SERVER['DOCUMENT_ROOT']);
		parent::tearDown(); // TODO: Change the autogenerated stub
	}

	/**
	 * @param array $options
	 * @return TestApplication
	 * @throws ClassNotFound
	 * @throws ConfigurationException
	 * @throws DirectoryCreate
	 * @throws DirectoryNotFound
	 * @throws DirectoryPermission
	 * @throws NotFoundException
	 * @throws ParseException
	 * @throws UnsupportedException
	 */
	private function newApplicationFactory(array $options = []): TestApplication {
		$cacheDir = $this->application->cachePath('testApp/fileCache');
		Directory::depend($this->application->cachePath('testApp/fileCache'));
		$fileCache = new FileCacheItemPool($cacheDir);
		$newApplication = Kernel::createApplication([
			Application::OPTION_CACHE_POOL => $fileCache,
			Application::OPTION_APPLICATION_CLASS => TestApplication::class,
			Application::OPTION_PATH => $this->application->cachePath('testApp'),
			Application::OPTION_DEVELOPMENT => true, 'parentRoot' => $this->application->path(),
			'parentClass' => get_class($this->application),
		] + $options + [
			'isSecondary' => true, Application::OPTION_VERSION => '1.0.0',
		]);
		$newApplication->configureInclude([
			$this->application->path('test/etc/test.conf'),
			$this->application->path('test/etc/bad.json'),
			$this->application->path('test/etc/test.json'),
			$this->application->path('test/etc/nope.json'),
		]);
		$newApplication->modules->load('Diff');
		$newApplication->modules->load('CSV');
		$this->assertInstanceOf(TestApplication::class, $newApplication);
		return $newApplication;
	}

	/**
	 * @param string $path
	 * @return string
	 */
	public static function applicationPath(string $path = ''): string {
		$appRoot = dirname(__DIR__, 2);
		return Directory::path($appRoot, $path);
	}

	/**
	 * @param string $class
	 * @param array $testArguments
	 * @param int $expectedStatus
	 * @param string $expectedOutputOrPattern
	 * @return void
	 * @throws ClassNotFound
	 * @throws ConfigurationException
	 * @throws ParseException
	 * @throws SemanticsException
	 * @throws NotFoundException
	 * @throws ParameterException
	 * @throws UnsupportedException
	 */
	public function assertCommandClass(string $class, array $testArguments, int $expectedStatus, string $expectedOutputOrPattern): void {
		$options = ['exit' => false, 'no-ansi' => true];

		$command = $this->testApplication->factory($class, $this->testApplication, $options);
		$this->assertInstanceOf(Command::class, $command);

		$this->assertIsArray($command->shortcuts());

		$hasTest = count($testArguments) !== 0;
		array_unshift($testArguments, $command::class);
		$command->parseArguments($testArguments);
		$this->assertTrue($command->optionBool('no-ansi'), $class);
		if ($hasTest) {
			$foundQuote = '';
			StringTools::unquote($expectedOutputOrPattern, '##//', $foundQuote);
			$this->streamCapture(STDOUT);
			$this->streamCapture(STDERR);
			if ($foundQuote) {
				$this->expectOutputRegex($expectedOutputOrPattern);
			} else {
				$this->expectOutputString($expectedOutputOrPattern);
			}
			$exitStatus = $command->go();
			$this->assertEquals($expectedStatus, $exitStatus, "Command $class exited with incorrect code: " .
				JSON::encode($testArguments));
		}
	}
}
