#!/usr/bin/env python3
# Copyright: 2026, Maximiliano Curia <maxy@debian.org>
#
# License: ISC
#  Permission to use, copy, modify, and/or distribute this software for any
#  purpose with or without fee is hereby granted, provided that the above
#  copyright notice and this permission notice appear in all copies.
#  .
#  THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
#  REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
#  AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
#  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
#  LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
#  OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
#  PERFORMANCE OF THIS SOFTWARE.

"""Contract of the git changed-paths computation.

Builds throwaway git repositories under tmp_path (isolated from the developer's
global git config) and asserts changed_paths classifies working-tree changes and
untracked files against the baseline, and returns a typed Fallback with the right
reason when git or the baseline is unavailable.
"""

import os
import shutil
import subprocess

import pytest

from decopy.fstree import vcs
from decopy.fstree.vcs import (
    BAD_REF,
    COPYRIGHT_UNTRACKED,
    GIT_MISSING,
    NOT_A_REPO,
    ChangedPaths,
    Fallback,
)

pytestmark = pytest.mark.skipif(
    shutil.which("git") is None, reason="git is not installed"
)


def _run_git(repo, *args):
    env = dict(os.environ)
    env["GIT_CONFIG_GLOBAL"] = os.devnull
    env["GIT_CONFIG_SYSTEM"] = os.devnull
    env.update(
        GIT_AUTHOR_NAME="Test",
        GIT_AUTHOR_EMAIL="test@example.com",
        GIT_COMMITTER_NAME="Test",
        GIT_COMMITTER_EMAIL="test@example.com",
    )
    cmd = ["git", "-C", str(repo), *args]
    subprocess.run(  # noqa: S603
        cmd,
        check=True,
        capture_output=True,
        env=env,
    )


def _write(repo, relpath, content):
    path = repo / relpath
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content)


def _commit(repo, message):
    _run_git(repo, "add", "-A")
    _run_git(repo, "commit", "-m", message)


@pytest.fixture
def repo(tmp_path):
    """A repo whose only commit touches debian/copyright (the baseline)."""
    _run_git(tmp_path, "init")
    _write(tmp_path, "debian/copyright", "initial\n")
    _write(tmp_path, "src/a.c", "int a;\n")
    _write(tmp_path, "src/b.c", "int b;\n")
    _commit(tmp_path, "base: copyright and sources")
    return tmp_path


def test_no_changes_returns_empty_changedpaths(repo):
    result = vcs.changed_paths(str(repo), "debian/copyright")
    assert isinstance(result, ChangedPaths)
    assert result.added_or_modified == ()
    assert result.deleted == ()
    assert result.renamed == ()


def test_reports_change_committed_after_baseline(repo):
    # A later commit modifies a.c only, never touching copyright, so the working
    # tree equals HEAD. The change still shows because the baseline is the last
    # commit that touched copyright, not HEAD.
    _write(repo, "src/a.c", "int a2;\n")
    _commit(repo, "later: modify a.c")
    result = vcs.changed_paths(str(repo), "debian/copyright")
    assert isinstance(result, ChangedPaths)
    assert "src/a.c" in result.added_or_modified
    assert "src/b.c" not in result.added_or_modified


def test_untracked_file_is_reported(repo):
    _write(repo, "src/new.c", "int n;\n")
    result = vcs.changed_paths(str(repo), "debian/copyright")
    assert isinstance(result, ChangedPaths)
    assert "src/new.c" in result.added_or_modified


def test_deleted_file_lands_in_deleted(repo):
    (repo / "src" / "b.c").unlink()
    result = vcs.changed_paths(str(repo), "debian/copyright")
    assert isinstance(result, ChangedPaths)
    assert "src/b.c" in result.deleted
    assert "src/b.c" not in result.added_or_modified


def test_rename_lands_in_renamed(repo):
    _run_git(repo, "mv", "src/a.c", "src/a2.c")
    result = vcs.changed_paths(str(repo), "debian/copyright")
    assert isinstance(result, ChangedPaths)
    assert ("src/a.c", "src/a2.c") in result.renamed


def test_since_override_uses_given_ref(repo):
    _write(repo, "src/a.c", "int a2;\n")
    _commit(repo, "later: modify a.c")
    # Since the copyright baseline, a.c changed; since HEAD, nothing changed.
    since_head = vcs.changed_paths(str(repo), "debian/copyright", since="HEAD")
    assert isinstance(since_head, ChangedPaths)
    assert "src/a.c" not in since_head.added_or_modified
    default = vcs.changed_paths(str(repo), "debian/copyright")
    assert isinstance(default, ChangedPaths)
    assert "src/a.c" in default.added_or_modified


def test_not_a_git_repo_returns_fallback(tmp_path):
    result = vcs.changed_paths(str(tmp_path), "debian/copyright")
    assert isinstance(result, Fallback)
    assert result.reason == NOT_A_REPO


def test_copyright_untracked_returns_fallback(repo):
    result = vcs.changed_paths(str(repo), "debian/absent-copyright")
    assert isinstance(result, Fallback)
    assert result.reason == COPYRIGHT_UNTRACKED


def test_bad_since_ref_returns_fallback(repo):
    result = vcs.changed_paths(str(repo), "debian/copyright", since="no-such-ref")
    assert isinstance(result, Fallback)
    assert result.reason == BAD_REF


def test_git_missing_returns_fallback(repo, monkeypatch):
    monkeypatch.setattr(vcs.shutil, "which", lambda _cmd: None)
    result = vcs.changed_paths(str(repo), "debian/copyright")
    assert isinstance(result, Fallback)
    assert result.reason == GIT_MISSING
    assert "git" in result.message.lower()
