<?php

declare(strict_types=1);
namespace Tests\git2;
use git2\git;
use git2\git_delta_t;
use git2\git_diff;
use git2\git_diff_delta;
use git2\git_diff_find_options;
use git2\git_diff_flag_t;
use git2\git_diff_format_t;
use git2\git_diff_hunk;
use git2\git_diff_line;
use git2\git_diff_line_t;
use git2\git_diff_options;
use git2\git_diff_patchid_options;
use git2\git_diff_stats_format_t;
use git2\git_filemode_t;
use git2\git_oid;
use git2\git_submodule_ignore_t;

final class DiffTest extends GitTestCase
{

	public function testBlobToBuffer(): void
	{
		$dir = $this->mkdir('blob_to_buffer');
		git::repository_init($repo, $dir, false);
		git::blob_create_from_buffer($id, $repo, 'foo');
		$this->assertOK(git::blob_lookup($blob, $repo, $id));

		$lines = [];

		$this->assertOK(git::diff_blob_to_buffer(
			$blob,
			'foo.old',
			'bar',
			'foo.new',
			null,
			function (git_diff_delta $delta, float $progress): int {

				$this->assertSame(1., $progress);

				$this->assertSame(git_delta_t::MODIFIED, $delta->status);
				$this->assertSame(git_diff_flag_t::NOT_BINARY, $delta->flags);
				$this->assertSame(0, $delta->similarity);
				$this->assertSame(0, $delta->nfiles);

				$this->assertSame('19102815663d23f8b75a47e7a01965dcdc96468c', git::oid_tostr_s($delta->old_file->id));
				$this->assertSame('foo.old', $delta->old_file->path);
				$this->assertSame(3, $delta->old_file->size);
				$this->assertSame(git_diff_flag_t::NOT_BINARY | git_diff_flag_t::VALID_ID, $delta->old_file->flags);
				$this->assertSame(git_filemode_t::BLOB, $delta->old_file->mode);
				$this->assertSame(40, $delta->old_file->id_abbrev);

				$this->assertSame('ba0e162e1c47469e3fe4b393a8bf8c569f302116', git::oid_tostr_s($delta->new_file->id));
				$this->assertSame('foo.new', $delta->new_file->path);
				$this->assertSame(3, $delta->new_file->size);
				$this->assertSame(git_diff_flag_t::NOT_BINARY | git_diff_flag_t::VALID_ID, $delta->new_file->flags);
				$this->assertSame(git_filemode_t::BLOB, $delta->new_file->mode);
				$this->assertSame(40, $delta->new_file->id_abbrev);

				return 0;

			},
			null,
			null,
			function (git_diff_delta $delta, git_diff_hunk $hunk, git_diff_line $line) use(&$lines): int {

				$lines[] = [
					$line->num_lines,
					$line->new_lineno,
					$line->old_lineno,
					$line->content_len,
					$line->content_offset,
					$line->origin,
					$line->content,
				];

				return 0;

			},
		));

		$this->assertSame([
			[0, -1, 1, 3, 0, git_diff_line_t::DELETION, 'foo'],
			[2, -1, 1, 29, -1, git_diff_line_t::ADD_EOFNL, "\n\\ No newline at end of file\n"],
			[0, 1, -1, 3, 0, git_diff_line_t::ADDITION, 'bar'],
			[2, 1, -1, 29, -1, git_diff_line_t::DEL_EOFNL, "\n\\ No newline at end of file\n"],
		], $lines);
	}

	public function testBlobs(): void
	{
		$dir = $this->mkdir('blob_to_buffer');
		git::repository_init($repo, $dir, false);
		git::blob_create_from_buffer($oid, $repo, 'old');
		$this->assertOK(git::blob_lookup($oldBlob, $repo, $oid));
		git::blob_create_from_buffer($nid, $repo, 'new');
		$this->assertOK(git::blob_lookup($newBlob, $repo, $nid));

		$this->assertSame(1, git::diff_blobs(
			$oldBlob,
			null,
			$newBlob,
			null,
			null,
			function (git_diff_delta $delta, float $progress): int {

				$this->assertSame(1., $progress);

				$this->assertSame(git_delta_t::MODIFIED, $delta->status);
				$this->assertSame(git_diff_flag_t::NOT_BINARY, $delta->flags);
				$this->assertSame(0, $delta->similarity);
				$this->assertSame(0, $delta->nfiles);

				$this->assertSame('489ce0f857e7634a0eb9f328265a3e91fad49f61', git::oid_tostr_s($delta->old_file->id));
				$this->assertSame('file', $delta->old_file->path);
				$this->assertSame(3, $delta->old_file->size);
				$this->assertSame(git_diff_flag_t::NOT_BINARY | git_diff_flag_t::VALID_ID, $delta->old_file->flags);
				$this->assertSame(git_filemode_t::BLOB, $delta->old_file->mode);
				$this->assertSame(40, $delta->old_file->id_abbrev);

				$this->assertSame('3e5126c4e761fd09582fc517918a1601b218dff0', git::oid_tostr_s($delta->new_file->id));
				$this->assertSame('file', $delta->new_file->path);
				$this->assertSame(3, $delta->new_file->size);
				$this->assertSame(git_diff_flag_t::NOT_BINARY | git_diff_flag_t::VALID_ID, $delta->new_file->flags);
				$this->assertSame(git_filemode_t::BLOB, $delta->new_file->mode);
				$this->assertSame(40, $delta->new_file->id_abbrev);

				return 1;

			},
			null,
			null,
			null,
		));
	}

	public function testBuffers(): void
	{
		$this->assertSame(1, git::diff_buffers(
			null,
			null,
			'foo',
			null,
			null,
			function (git_diff_delta $delta, float $progress): int {

				$this->assertSame(1., $progress);

				$this->assertSame(git_delta_t::ADDED, $delta->status);
				$this->assertSame(git_diff_flag_t::NOT_BINARY, $delta->flags);
				$this->assertSame(0, $delta->similarity);
				$this->assertSame(0, $delta->nfiles);

				$this->assertSame('0000000000000000000000000000000000000000', git::oid_tostr_s($delta->old_file->id));
				$this->assertSame('file', $delta->old_file->path);
				$this->assertSame(0, $delta->old_file->size);
				$this->assertSame(git_diff_flag_t::NOT_BINARY, $delta->old_file->flags);
				$this->assertSame(git_filemode_t::UNREADABLE, $delta->old_file->mode);
				$this->assertSame(0, $delta->old_file->id_abbrev);

				$this->assertSame('19102815663d23f8b75a47e7a01965dcdc96468c', git::oid_tostr_s($delta->new_file->id));
				$this->assertSame('file', $delta->new_file->path);
				$this->assertSame(3, $delta->new_file->size);
				$this->assertSame(git_diff_flag_t::NOT_BINARY | git_diff_flag_t::VALID_ID, $delta->new_file->flags);
				$this->assertSame(git_filemode_t::BLOB, $delta->new_file->mode);
				$this->assertSame(40, $delta->new_file->id_abbrev);

				return 1;

			},
			null,
			null,
			null,
		));
	}

	public function testFindOptionsInit(): void
	{
		$this->assertOK(git::diff_find_options_init($opts, git_diff_find_options::VERSION));
		$this->assertInstanceOf(git_diff_find_options::class, $opts);
		$this->assertSame(git_diff_find_options::VERSION, $opts->version);
		$this->assertSame(0, $opts->flags);
		$this->assertSame(0, $opts->rename_threshold);
		$this->assertSame(0, $opts->rename_from_rewrite_threshold);
		$this->assertSame(0, $opts->copy_threshold);
		$this->assertSame(0, $opts->break_rewrite_threshold);
		$this->assertSame(0, $opts->rename_limit);
	}

	public function testFromBuffer(): void
	{
		$this->assertOK(git::diff_from_buffer($diff, file_get_contents(__DIR__ . '/fixtures/diff.foo.patch')));
		$this->assertInstanceOf(git_diff::class, $diff);
	}

	public function testFindSimilar(): void
	{
		$this->assertOK(git::diff_from_buffer($diff, file_get_contents(__DIR__ . '/fixtures/diff.foo.patch')));
		$this->assertOK(git::diff_find_similar($diff));
	}

	public function testForeach(): void
	{
		$this->assertOK(git::diff_from_buffer($diff, file_get_contents(__DIR__ . '/fixtures/diff.foo.patch')));
		$this->assertSame(1, git::diff_foreach(
			$diff,
			function (git_diff_delta $delta, float $progress): int {

				$this->assertSame(0., $progress);

				$this->assertSame(git_delta_t::ADDED, $delta->status);
				$this->assertSame(git_diff_flag_t::NOT_BINARY, $delta->flags);
				$this->assertSame(0, $delta->similarity);
				$this->assertSame(1, $delta->nfiles);

				$this->assertSame('0000000000000000000000000000000000000000', git::oid_tostr_s($delta->old_file->id));
				$this->assertSame('foo', $delta->old_file->path);
				$this->assertSame(0, $delta->old_file->size);
				$this->assertSame(0, $delta->old_file->flags);
				$this->assertSame(git_filemode_t::UNREADABLE, $delta->old_file->mode);
				$this->assertSame(0, $delta->old_file->id_abbrev);

				$this->assertSame('1910281000000000000000000000000000000000', git::oid_tostr_s($delta->new_file->id));
				$this->assertSame('foo', $delta->new_file->path);
				$this->assertSame(0, $delta->new_file->size);
				$this->assertSame(0, $delta->new_file->flags);
				$this->assertSame(git_filemode_t::BLOB, $delta->new_file->mode);
				$this->assertSame(7, $delta->new_file->id_abbrev);

				return 1;

			},
			null,
			null,
			null,
		));
	}

	public function testGetDelta(): void
	{
		git::diff_from_buffer($diff, file_get_contents(__DIR__ . '/fixtures/diff.foo.patch'));
		$this->assertNull(git::diff_get_delta($diff, 1));
		$delta = git::diff_get_delta($diff, 0);
		$this->assertSame('1910281000000000000000000000000000000000', git::oid_tostr_s($delta->new_file->id));
		$this->assertSame('foo', $delta->new_file->path);
		$this->assertSame(0, $delta->new_file->size);
		$this->assertSame(0, $delta->new_file->flags);
		$this->assertSame(git_filemode_t::BLOB, $delta->new_file->mode);
		$this->assertSame(7, $delta->new_file->id_abbrev);
	}

	public function testGetStats(): void
	{
		git::diff_from_buffer($diff, file_get_contents(__DIR__ . '/fixtures/diff.foo.patch'));
		$this->assertOK(git::diff_get_stats($stats, $diff));
		$this->assertSame(1, git::diff_stats_files_changed($stats));
		$this->assertSame(1, git::diff_stats_insertions($stats));
		$this->assertSame(0, git::diff_stats_deletions($stats));
		$this->assertOK(git::diff_stats_to_buf($buf, $stats, git_diff_stats_format_t::FULL, 80));
		$this->assertSame(" foo | 1 +\n 1 file changed, 1 insertion(+)\n", (string) $buf);
	}

	public function testStatusChar(): void
	{
		$this->assertSame('A', git::diff_status_char(git_delta_t::ADDED));
	}

	public function testToBuf(): void
	{
		git::diff_from_buffer($diff, file_get_contents(__DIR__ . '/fixtures/diff.foo.patch'));
		$this->assertOK(git::diff_to_buf($buf, $diff, git_diff_format_t::NAME_ONLY));
		$this->assertSame("foo\n", (string) $buf);
	}

	public function testIndexToIndex(): void
	{
		$dir = $this->mkdir('index_to_index');
		git::repository_init($repo, $dir, false);
		git::index_new($oIndex);
		git::index_new($nIndex);
		$this->assertOK(git::diff_index_to_index($diff, $repo, $oIndex, $nIndex));
		$this->assertInstanceOf(git_diff::class, $diff);
		$this->assertOK(git::diff_get_stats($stats, $diff));
		$this->assertSame(0, git::diff_stats_files_changed($stats));
	}

	public function testIndexToWorkdir(): void
	{
		$dir = $this->mkdir('index_to_workdir');
		git::repository_init($repo, $dir, false);
		file_put_contents($dir . '/foo', 'foo');
		$this->assertOK(git::repository_index($index, $repo));
		$this->assertOK(git::index_add_bypath($index, 'foo'));
		unlink($dir . '/foo');
		$this->assertOK(git::diff_index_to_workdir($diff, $repo, $index));
		$this->assertInstanceOf(git_diff::class, $diff);
		$this->assertOK(git::diff_get_stats($stats, $diff));
		$this->assertSame(1, git::diff_stats_files_changed($stats));
		$this->assertSame(stripos(PHP_OS, 'win') === 0, git::diff_is_sorted_icase($diff));
		$this->assertSame(1, git::diff_num_deltas($diff));
		$this->assertSame(1, git::diff_num_deltas_of_type($diff, git_delta_t::DELETED));
	}

	public function testPrint(): void
	{
		git::diff_from_buffer($diff, file_get_contents(__DIR__ . '/fixtures/diff.foo.patch'));
		$this->assertSame(1, git::diff_print(
			$diff,
			git_diff_format_t::NAME_ONLY,
			function(git_diff_delta $delta, ?git_diff_hunk $hunk, git_diff_line $line): int {
				$this->assertSame(1, $delta->nfiles);
				$this->assertNull($hunk);
				$this->assertSame("foo\n", $line->content);
				return 1;
			},
		));
	}

	public function testMerge(): void
	{
		$dir = $this->mkdir('merge');
		git::repository_init($repo, $dir, false);
		git::repository_index($index, $repo);
		git::index_write_tree($treeId, $index);
		git::tree_lookup($tree, $repo, $treeId);
		git::signature_now($aut, 'John Doe', 'john.doe@example.com');
		git::signature_now($com, 'Jane Doe', 'jane.doe@example.com');
		git::commit_create_v($id, $repo, 'HEAD', $aut, $com, null, 'Hello world!', $tree);
		git::diff_tree_to_index($diff, $repo, $tree, null);
		git::diff_index_to_workdir($diff2, $repo, null);
		$this->assertOK(git::diff_merge($diff, $diff2));
		$this->assertOK(git::diff_get_stats($stats, $diff));
		$this->assertSame(0, git::diff_stats_files_changed($stats));
	}

	public function testOptions(): void
	{
		$this->assertOK(git::diff_options_init($opts, git_diff_options::VERSION));
		$this->assertInstanceOf(git_diff_options::class, $opts);
		$this->assertSame(git_diff_options::VERSION, $opts->version);
		$this->assertSame(0, $opts->flags);
		$this->assertSame(3, $opts->context_lines);
		$this->assertSame(0, $opts->interhunk_lines);
		$this->assertSame(0, $opts->id_abbrev);
		$this->assertSame(0, $opts->max_size);
		$this->assertSame(git_submodule_ignore_t::UNSPECIFIED, $opts->ignore_submodules);
		$this->assertNull($opts->old_prefix);
		$this->assertNull($opts->new_prefix);
		$this->assertSame(0, $opts->pathspec->count);
	}

	public function testPatchid(): void
	{
		$this->assertOK(git::diff_patchid_options_init($opts, git_diff_patchid_options::VERSION));
		$this->assertInstanceOf(git_diff_patchid_options::class, $opts);
		git::diff_from_buffer($diff, file_get_contents(__DIR__ . '/fixtures/diff.foo.patch'));
		$this->assertOK(git::diff_patchid($id, $diff, $opts));
		$this->assertInstanceOf(git_oid::class, $id);
		$this->assertSame('079ff8583aea85d1178a48d5b6b6dd863a7e98c1', git::oid_tostr_s($id));
	}

	public function testTreeToIndex(): void
	{
		$dir = $this->mkdir('tree_to_index');
		git::repository_init($repo, $dir, false);
		git::repository_index($index, $repo);
		git::index_write_tree($id, $index);
		git::tree_lookup($tree, $repo, $id);
		$this->assertOK(git::diff_tree_to_index($diff, $repo, $tree, null));
		$this->assertInstanceOf(git_diff::class, $diff);
		git::diff_get_stats($stats, $diff);
		$this->assertSame(0, git::diff_stats_files_changed($stats));
	}

	public function testTreeToTree(): void
	{
		$dir = $this->mkdir('tree_to_tree');
		git::repository_init($repo, $dir, false);
		$this->assertOK(git::diff_tree_to_tree($diff, $repo, null, null));
		$this->assertInstanceOf(git_diff::class, $diff);
		git::diff_get_stats($stats, $diff);
		$this->assertSame(0, git::diff_stats_files_changed($stats));
	}

	public function testTreeToWorkdir(): void
	{
		$dir = $this->mkdir('tree_to_workdir');
		git::repository_init($repo, $dir, false);
		$this->assertOK(git::diff_tree_to_workdir($diff, $repo, null));
		$this->assertInstanceOf(git_diff::class, $diff);
		git::diff_get_stats($stats, $diff);
		$this->assertSame(0, git::diff_stats_files_changed($stats));
	}

	public function testTreeToWorkdirWithIndex(): void
	{
		$dir = $this->mkdir('tree_to_workdir_with_index');
		git::repository_init($repo, $dir, false);
		$this->assertOK(git::diff_tree_to_workdir_with_index($diff, $repo, null));
		$this->assertInstanceOf(git_diff::class, $diff);
		git::diff_get_stats($stats, $diff);
		$this->assertSame(0, git::diff_stats_files_changed($stats));
	}

}