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