# test_am.py -- test am compatibility with CGit
# Copyright (C) 2025 Jelmer Vernooij <jelmer@jelmer.uk>
#
# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
# General Public License as published by the Free Software Foundation; version 2.0
# or (at your option) any later version. You can redistribute it and/or
# modify it under the terms of either of these two licenses.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# You should have received a copy of the licenses; if not, see
# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
# License, Version 2.0.
#

"""Tests for git am compatibility with CGit."""

import os
import shutil
import tempfile

from dulwich import porcelain
from dulwich.repo import Repo

from .utils import CompatTestCase, rmtree_ro, run_git_or_fail


class CompatAmTestCase(CompatTestCase):
    def setUp(self) -> None:
        super().setUp()
        self.test_dir = tempfile.mkdtemp()
        self.addCleanup(rmtree_ro, self.test_dir)
        self.repo_path = os.path.join(self.test_dir, "repo")
        self.repo = Repo.init(self.repo_path, mkdir=True)
        self.addCleanup(self.repo.close)
        # Configure git identity for C git operations
        run_git_or_fail(
            ["-C", self.repo_path, "config", "user.email", "test@example.com"]
        )
        run_git_or_fail(["-C", self.repo_path, "config", "user.name", "Test User"])
        run_git_or_fail(["-C", self.repo_path, "config", "core.autocrlf", "false"])

    def test_dulwich_am_cgit_format_patch(self) -> None:
        """Apply a patch generated by C git format-patch using dulwich am."""
        # Create initial commit with C git
        file_path = os.path.join(self.repo_path, "test.txt")
        with open(file_path, "wb") as f:
            f.write(b"line 1\nline 2\nline 3\n")
        run_git_or_fail(["-C", self.repo_path, "add", "test.txt"])
        run_git_or_fail(["-C", self.repo_path, "commit", "-m", "initial"])

        # Make a change and commit with C git
        with open(file_path, "wb") as f:
            f.write(b"line 1\nline two\nline 3\n")
        run_git_or_fail(["-C", self.repo_path, "add", "test.txt"])
        run_git_or_fail(["-C", self.repo_path, "commit", "-m", "Change line 2"])

        # Generate patch with C git format-patch
        patch_dir = os.path.join(self.test_dir, "patches")
        os.makedirs(patch_dir)
        run_git_or_fail(["-C", self.repo_path, "format-patch", "-1", "-o", patch_dir])
        patch_files = sorted(os.listdir(patch_dir))
        self.assertEqual(len(patch_files), 1)
        patch_path = os.path.join(patch_dir, patch_files[0])

        # Make a copy of the repo at the initial commit
        copy_path = os.path.join(self.test_dir, "copy")
        shutil.copytree(self.repo_path, copy_path)
        run_git_or_fail(["-C", copy_path, "reset", "--hard", "HEAD~1"])

        # Apply with dulwich am
        shas = porcelain.am(copy_path, patches=patch_path)
        self.assertEqual(len(shas), 1)

        # Verify file content matches
        with open(os.path.join(copy_path, "test.txt"), "rb") as f:
            self.assertEqual(f.read(), b"line 1\nline two\nline 3\n")

        # Verify commit message
        copy_repo = Repo(copy_path)
        self.addCleanup(copy_repo.close)
        commit = copy_repo[shas[0]]
        self.assertIn(b"Change line 2", commit.message)

    def test_cgit_am_dulwich_format_patch(self) -> None:
        """Apply a patch generated by dulwich format-patch using C git am."""
        # Create initial commit
        file_path = os.path.join(self.repo_path, "test.txt")
        with open(file_path, "wb") as f:
            f.write(b"line 1\nline 2\nline 3\n")
        porcelain.add(self.repo_path, paths=["test.txt"])
        porcelain.commit(
            self.repo_path,
            message=b"initial",
            author=b"Test User <test@example.com>",
            committer=b"Test User <test@example.com>",
        )

        # Make a change and commit
        with open(file_path, "wb") as f:
            f.write(b"line 1\nline two\nline 3\n")
        porcelain.add(self.repo_path, paths=["test.txt"])
        porcelain.commit(
            self.repo_path,
            message=b"Change line 2",
            author=b"Test User <test@example.com>",
            committer=b"Test User <test@example.com>",
        )

        # Generate patch with dulwich format-patch
        patch_dir = os.path.join(self.test_dir, "patches")
        os.makedirs(patch_dir)
        patch_files = porcelain.format_patch(self.repo_path, n=1, outdir=patch_dir)
        self.assertEqual(len(patch_files), 1)

        # Make a copy at the initial commit
        copy_path = os.path.join(self.test_dir, "copy")
        shutil.copytree(self.repo_path, copy_path)
        run_git_or_fail(["-C", copy_path, "reset", "--hard", "HEAD~1"])

        # Apply with C git am
        run_git_or_fail(["-C", copy_path, "am", patch_files[0]])

        # Verify file content
        with open(os.path.join(copy_path, "test.txt"), "rb") as f:
            self.assertEqual(f.read(), b"line 1\nline two\nline 3\n")

        # Verify commit message
        output = run_git_or_fail(["-C", copy_path, "log", "-1", "--format=%s"])
        self.assertEqual(output.strip(), b"Change line 2")

    def test_dulwich_am_multiple_cgit_patches(self) -> None:
        """Apply multiple patches from C git format-patch using dulwich am."""
        # Create initial commit
        file_path = os.path.join(self.repo_path, "test.txt")
        with open(file_path, "wb") as f:
            f.write(b"line 1\nline 2\nline 3\n")
        run_git_or_fail(["-C", self.repo_path, "add", "test.txt"])
        run_git_or_fail(["-C", self.repo_path, "commit", "-m", "initial"])

        # Two changes
        with open(file_path, "wb") as f:
            f.write(b"line 1\nline two\nline 3\n")
        run_git_or_fail(["-C", self.repo_path, "add", "test.txt"])
        run_git_or_fail(["-C", self.repo_path, "commit", "-m", "Change line 2"])

        with open(file_path, "wb") as f:
            f.write(b"line 1\nline two\nline three\n")
        run_git_or_fail(["-C", self.repo_path, "add", "test.txt"])
        run_git_or_fail(["-C", self.repo_path, "commit", "-m", "Change line 3"])

        # Generate patches with C git
        patch_dir = os.path.join(self.test_dir, "patches")
        os.makedirs(patch_dir)
        run_git_or_fail(["-C", self.repo_path, "format-patch", "-2", "-o", patch_dir])
        patch_files = sorted(os.path.join(patch_dir, f) for f in os.listdir(patch_dir))
        self.assertEqual(len(patch_files), 2)

        # Copy repo at initial commit
        copy_path = os.path.join(self.test_dir, "copy")
        shutil.copytree(self.repo_path, copy_path)
        run_git_or_fail(["-C", copy_path, "reset", "--hard", "HEAD~2"])

        # Apply with dulwich am
        shas = porcelain.am(copy_path, patches=patch_files)
        self.assertEqual(len(shas), 2)

        # Verify final content
        with open(os.path.join(copy_path, "test.txt"), "rb") as f:
            self.assertEqual(f.read(), b"line 1\nline two\nline three\n")

    def test_dulwich_am_preserves_author(self) -> None:
        """Verify dulwich am preserves author info from C git patches."""
        # Create commit with specific author
        file_path = os.path.join(self.repo_path, "test.txt")
        with open(file_path, "wb") as f:
            f.write(b"hello\n")
        run_git_or_fail(["-C", self.repo_path, "add", "test.txt"])
        run_git_or_fail(["-C", self.repo_path, "commit", "-m", "initial"])

        with open(file_path, "wb") as f:
            f.write(b"hello world\n")
        run_git_or_fail(["-C", self.repo_path, "add", "test.txt"])
        run_git_or_fail(
            [
                "-C",
                self.repo_path,
                "commit",
                "--author=Patch Author <author@example.org>",
                "-m",
                "Add world",
            ]
        )

        # Generate patch
        patch_dir = os.path.join(self.test_dir, "patches")
        os.makedirs(patch_dir)
        run_git_or_fail(["-C", self.repo_path, "format-patch", "-1", "-o", patch_dir])
        patch_files = sorted(os.listdir(patch_dir))
        patch_path = os.path.join(patch_dir, patch_files[0])

        # Copy at initial commit
        copy_path = os.path.join(self.test_dir, "copy")
        shutil.copytree(self.repo_path, copy_path)
        run_git_or_fail(["-C", copy_path, "reset", "--hard", "HEAD~1"])

        # Apply with dulwich am
        shas = porcelain.am(copy_path, patches=patch_path)
        self.assertEqual(len(shas), 1)

        # Verify author is preserved
        copy_repo = Repo(copy_path)
        self.addCleanup(copy_repo.close)
        commit = copy_repo[shas[0]]
        self.assertEqual(commit.author, b"Patch Author <author@example.org>")
