<?php

declare(strict_types=1);
namespace Tests\git2;
use FFI;
use FilesystemIterator;
use git2\git;
use git2\git_error_code;
use git2\Internal\Handle\Map;
use PHPUnit\Framework\ExpectationFailedException;
use PHPUnit\Framework\TestCase;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;

/**
 * Base class for libgit2's test cases.
 */
abstract class GitTestCase extends TestCase
{

	private string $temp;

	/**
	 * Initializes libgit2, creates root temporary folder and purges it.
	 */
	protected function setUp(): void
	{
		git::libgit2_init();

		$this->temp = sprintf('%s/git2/%s.%s', sys_get_temp_dir(), basename(str_replace('\\', DIRECTORY_SEPARATOR, static::class)), $this->getName());
		$this->rmdir($this->temp);
	}

	/**
	 * Shuts down libgit2, purges temporary folder and checks that all the handles have been released.
	 */
	protected function tearDown(): void
	{
		git::libgit2_shutdown();

		if (!$this->hasFailed()) {
			$this->rmdir($this->temp);
		}

		if (Map::count() > 0) {

			$list = '';

			foreach (Map::$handles as $id => $weakReference) {
				$list .= sprintf(
					"\n%08x %s",
					$id,
					$weakReference->get() !== null ? $weakReference->get()::class : 'released'
				);
			}

			Map::$handles = []; // To clear it out for the consecutive test.
			$this->fail('Detected possible memory leak in ' . static::class . '::' . $this->getName() . '()' . $list);
		}
	}

	/**
	 * Creates a temporary directory.
	 */
	protected function mkdir(string $name): string
	{
		$path = $this->temp . '/' . $name;
		mkdir($path, 0777, true);
		return $path;
	}

	/**
	 * Purges a directory recursively.
	 */
	private function rmdir(string $dir): void
	{
		if (!file_exists($dir)) {
			return;
		}

		$files = new RecursiveIteratorIterator(
			new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
			RecursiveIteratorIterator::CHILD_FIRST
		);

		$kernel32 = null;

		foreach ($files as $file) {

			if ($file->isDir()) {
				rmdir($file->getRealPath());
			} else {

				$result = @unlink($file->getRealPath());

				if ($result === false) {

					// This removes the 'read-only' attribute on Windows.
					if (stripos(PHP_OS, 'win') === 0) {

						if ($kernel32 === null) {

							$kernel32 = FFI::cdef(
								<<<'C'
									int GetFileAttributesA(char* lpFileName);
									int SetFileAttributesA(char* lpFileName, int dwFileAttributes);
								C,
								'kernel32',
							);

						}

						$attributes = $kernel32->{'GetFileAttributesA'}($file->getRealPath());

						// FILE_ATTRIBUTE_READONLY(0x1)
						if ($attributes & 0x1) {
							$kernel32->{'SetFileAttributesA'}($file->getRealPath(), $attributes & ~0x1);
							unlink($file->getRealPath());
							continue;
						}

					}

					$error = error_get_last();
					trigger_error($error['message'], $error['type'] << 8);

				}

			}

		}

		rmdir($dir);
	}

	/**
	 * Asserts that the $code is equal to git_error_code::OK.
	 */
	public function assertOK(git_error_code $code): void
	{
		try {
			self::assertSame(git_error_code::OK, $code);
		} catch (ExpectationFailedException $exception) {

			if (isset($this) && ($error = git::error_last()) !== null) {
				$this->fail($error->message);
			}

			throw $exception;

		}
	}

}