from __future__ import annotations

from pathlib import Path

import pytest
from pygit2 import Oid, Repository, Signature, init_repository

from pygittools.hooks import HookResult
from pygittools.hooks_patterns import NULL_OID, PreReceiveProtectedPattern

MSG_PATTERN: str = r"^(\S+): (.+)"
PROTECTED_MAIN: str = r"^refs/heads/main$"
SIG = Signature("test", "test@example.com")


@pytest.fixture
def repo(tmp_path: Path) -> Repository:
	repo_path = tmp_path / "repo.git"
	repo_path.mkdir()
	return init_repository(str(repo_path), bare=True)


def _commit(repo: Repository, message: str, parents: list[Oid] | None = None) -> Oid:
	tb = repo.TreeBuilder()
	tree = tb.write()
	return repo.create_commit(None, SIG, SIG, message, tree, parents or [])


def _run(
	repo: Repository,
	ref: str,
	old_oid: str,
	new_oid: str,
	*,
	reject_merge_commits: bool = False,
) -> HookResult:
	hook = PreReceiveProtectedPattern(
		repo,
		PROTECTED_MAIN,
		MSG_PATTERN,
		reject_merge_commits=reject_merge_commits,
	)
	return hook.run([(ref, old_oid, new_oid)])


def test_ignores_unprotected_ref(repo: Repository) -> None:
	bad = _commit(repo, "not conventional")
	assert _run(repo, "refs/heads/feature", NULL_OID, str(bad)) == HookResult.SUCCESS


def test_accepts_valid_commit_on_protected_ref(repo: Repository) -> None:
	good = _commit(repo, "feat: add thing")
	assert _run(repo, "refs/heads/main", NULL_OID, str(good)) == HookResult.SUCCESS


def test_rejects_invalid_commit_on_protected_ref(repo: Repository) -> None:
	bad = _commit(repo, "not conventional")
	assert _run(repo, "refs/heads/main", NULL_OID, str(bad)) == HookResult.FAILURE


def test_walks_every_commit_in_fast_forward_push(repo: Repository) -> None:
	good = _commit(repo, "feat: first")
	bad = _commit(repo, "not conventional", [good])
	assert _run(repo, "refs/heads/main", str(good), str(bad)) == HookResult.FAILURE


def test_skips_merge_commit_message_by_default(repo: Repository) -> None:
	base = _commit(repo, "feat: base")
	feature = _commit(repo, "feat: side", [base])
	merge = _commit(repo, "Merge branch 'feature'", [base, feature])
	assert _run(repo, "refs/heads/main", str(base), str(merge)) == HookResult.SUCCESS


def test_rejects_merge_commit_when_configured(repo: Repository) -> None:
	base = _commit(repo, "feat: base")
	feature = _commit(repo, "feat: side", [base])
	merge = _commit(repo, "Merge branch 'feature'", [base, feature])
	result = _run(repo, "refs/heads/main", str(base), str(merge), reject_merge_commits=True)
	assert result == HookResult.FAILURE


def test_validates_non_merge_commits_introduced_by_merge(repo: Repository) -> None:
	base = _commit(repo, "feat: base")
	bad = _commit(repo, "bad message on feature", [base])
	merge = _commit(repo, "Merge branch 'feature'", [base, bad])
	assert _run(repo, "refs/heads/main", str(base), str(merge)) == HookResult.FAILURE


def test_allows_ref_deletion(repo: Repository) -> None:
	base = _commit(repo, "feat: base")
	assert _run(repo, "refs/heads/main", str(base), NULL_OID) == HookResult.SUCCESS