# Copyright 2015 Canonical Ltd.

# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.

# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
# PURPOSE.  See the GNU General Public License for more details.

# You should have received a copy of the GNU General Public License along
# with this program.  If not, see <http://www.gnu.org/licenses/>.

import hashlib
import os.path
import re
import shlex
import signal
import subprocess
import sys
from textwrap import dedent
import unittest

try:
    from debian import changelog
except ImportError:
    from debian_bundle import changelog
from fixtures import EnvironmentVariable
from testtools.content import text_content
from testtools.matchers import (
    EndsWith,
    FileContains,
    MatchesRegex,
    Not,
    PathExists,
    StartsWith,
    )

from gitbuildrecipe.tests import (
    GitRepository,
    GitTestCase,
    )


def skipIfFakeroot():
    return unittest.skipIf(
        "FAKEROOTKEY" in os.environ, "running under fakeroot")


def requirePristineTar():
    path = "/usr/bin/pristine-tar"
    return unittest.skipUnless(os.path.exists(path), "requires %s" % path)


def sha1_file_by_name(name):
    s = hashlib.sha1()
    with open(name, "rb") as f:
        while True:
            buf = f.read(1 << 16)
            if not buf:
                break
            s.update(buf)
    return s.hexdigest()


def make_pristine_tar_delta(dest, tarball_path):
    """Create a pristine-tar delta for a tarball.

    :param dest: Directory to generate pristine tar delta for
    :param tarball_path: Path to the tarball
    :return: pristine-tarball
    """
    def subprocess_setup():
        signal.signal(signal.SIGPIPE, signal.SIG_DFL)
    # If tarball_path is relative, the cwd=dest parameter to Popen will make
    # pristine-tar faaaail. pristine-tar doesn't use the VFS either, so we
    # assume local paths.
    tarball_path = os.path.abspath(tarball_path)
    command = ["pristine-tar", "gendelta", tarball_path, "-"]
    proc = subprocess.Popen(command, stdout=subprocess.PIPE,
            cwd=dest, preexec_fn=subprocess_setup,
            stderr=subprocess.PIPE)
    stdout, stderr = proc.communicate()
    if proc.returncode != 0:
        raise Exception("Generating delta from tar failed: %s" % stderr)
    return stdout


class FunctionalBuilderTests(GitTestCase):

    def setUp(self):
        super().setUp()
        self.use_temp_dir()
        # Replace DEBEMAIL and DEBFULLNAME so that they are known values
        # for the changelog checks.
        self.useFixture(EnvironmentVariable("DEBEMAIL", "maint@maint.org"))
        self.useFixture(EnvironmentVariable("DEBFULLNAME", "M. Maintainer"))

    def _get_file_contents(self, filename, mode="r"):
        """Helper to read contents of a file

        Use FileContains instead to just assert the contents match."""
        self.assertThat(filename, PathExists())
        with open(filename, mode) as f:
            return f.read()

    def run_recipe(self, args, retcode=0):
        cmd = [sys.executable, "-m", "gitbuildrecipe.main"]
        if isinstance(args, str):
            cmd += shlex.split(args)
        else:
            cmd += list(args)
        process = subprocess.Popen(
            cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
            universal_newlines=True)
        stdout, stderr = process.communicate()
        if stdout:
            self.addDetail("stdout", text_content(stdout))
        if stderr:
            self.addDetail("stderr", text_content(stderr))
        self.assertEqual(retcode, process.returncode)
        return stdout, stderr

    @skipIfFakeroot()
    def test_cmd_dailydeb(self):
        #TODO: define a test feature for debuild and require it here.
        source = GitRepository("source")
        source.build_tree(["a", "debian/"])
        source.build_tree_contents([
            ("debian/rules", "#!/usr/bin/make -f\nclean:\n"),
            ("debian/control",
                "Source: foo\nMaintainer: maint maint@maint.org\n\n"
                "Package: foo\nArchitecture: all\n")])
        source.add(["a", "debian/rules", "debian/control"])
        commit = source.commit("one")
        with open("test.recipe", "w") as recipe:
            recipe.write(
                "# git-build-recipe format 0.1 deb-version 1\nsource\n")
        out, err = self.run_recipe("--manifest manifest test.recipe working")
        self.assertThat("working/a", Not(PathExists()))
        package_root = "working/test/"
        self.assertThat(os.path.join(package_root, "a"), PathExists())
        self.assertThat(
            os.path.join(package_root, "debian/git-build-recipe.manifest"),
            PathExists())
        self.assertThat("manifest", PathExists())
        self.assertThat("manifest", FileContains(
            "# git-build-recipe format 0.1 deb-version 1\n"
            "source git-commit:%s\n" % commit))
        self.assertThat(
            os.path.join(package_root, "debian/git-build-recipe.manifest"),
            FileContains(
                "# git-build-recipe format 0.1 deb-version 1\n"
                "source git-commit:%s\n" % commit))
        self.assertThat(
            os.path.join(package_root, "debian/changelog"),
            FileContains(matcher=StartsWith(
                "foo (1) unstable; urgency=low\n")))

    @skipIfFakeroot()
    def test_cmd_dailydeb_no_work_dir(self):
        #TODO: define a test feature for debuild and require it here.
        source = GitRepository("source")
        source.build_tree(["a", "debian/"])
        source.build_tree_contents([
            ("debian/rules", "#!/usr/bin/make -f\nclean:\n"),
            ("debian/control",
                "Source: foo\nMaintainer: maint maint@maint.org\n\n"
                "Package: foo\nArchitecture: all\n")])
        source.add(["a", "debian/rules", "debian/control"])
        source.commit("one")
        with open("test.recipe", "w") as recipe:
            recipe.write(
                "# git-build-recipe format 0.1 deb-version 1\nsource\n")
        out, err = self.run_recipe("--manifest manifest test.recipe")

    @skipIfFakeroot()
    def test_cmd_dailydeb_if_changed_from_non_existent(self):
        #TODO: define a test feature for debuild and require it here.
        source = GitRepository("source")
        source.build_tree(["a", "debian/"])
        source.build_tree_contents([
            ("debian/rules", "#!/usr/bin/make -f\nclean:\n"),
            ("debian/control",
                "Source: foo\nMaintainer: maint maint@maint.org\n"
                "\nPackage: foo\nArchitecture: all\n")])
        source.add(["a", "debian/rules", "debian/control"])
        source.commit("one")
        with open("test.recipe", "w") as recipe:
            recipe.write(
                "# git-build-recipe format 0.1 deb-version 1\nsource\n")
        out, err = self.run_recipe(
            "--manifest manifest --if-changed-from bar test.recipe")

    def make_upstream_version(self, source, package_name, version, contents,
                              pristine_tar_format=None):
        source._git_call("checkout", "-q", "--orphan", "upstream")
        source._git_call("rm", "-qrf", ".")
        source.build_tree_contents(contents)
        source.add(["."])
        commit = source.commit("import upstream %s" % version)
        cmd = []
        if pristine_tar_format in (None, "gz"):
            tarfile_path = "%s_%s.orig.tar.gz" % (package_name, version)
        elif pristine_tar_format == "bz2":
            tarfile_path = "%s_%s.orig.tar.bz2" % (package_name, version)
            cmd.extend(["-c", "tar.tar.bz2.command=bzip2 -c"])
        else:
            raise AssertionError(
                "unknown pristine tar format %s" % pristine_tar_format)
        cmd.extend([
            "archive",
            "--prefix=upstream/",
            "-o", os.path.abspath(tarfile_path),
            "upstream",
            ])
        source._git_call(*cmd)
        subprocess.check_call(
            ["pristine-tar", "commit", os.path.abspath(tarfile_path)],
            stderr=subprocess.DEVNULL, cwd=source.path)
        tarfile_sha1 = sha1_file_by_name(tarfile_path)
        source.tag("upstream/%s" % version, commit)
        source._git_call("checkout", "-q", "master")
        return tarfile_sha1

    def make_simple_package(self, path):
        source = GitRepository(path)
        source.build_tree(["a", "debian/"])
        cl_contents = ("package (0.1-1) unstable; urgency=low\n  * foo\n"
                    " -- maint <maint@maint.org>  Tue, 04 Aug 2009 "
                    "10:03:10 +0100\n")
        source.build_tree_contents([
            ("debian/rules", "#!/usr/bin/make -f\nclean:\n"),
            ("debian/control",
                "Source: package\nMaintainer: maint maint@maint.org\n\n"
                "Package: package\nArchitecture: all\n"),
            ("debian/changelog", cl_contents)
            ])
        source.add(["a", "debian/rules", "debian/control", "debian/changelog"])
        source.commit("one")
        return source

    def test_cmd_dailydeb_no_build(self):
        self.make_simple_package("source")
        with open("test.recipe", "w") as recipe:
            recipe.write(
                "# git-build-recipe format 0.1 deb-version 1\nsource\n")
        out, err = self.run_recipe(
            "--manifest manifest --no-build test.recipe working")
        new_cl_contents = (
            "package (1) unstable; urgency=low\n\n"
            "  * Auto build.\n\n -- M. Maintainer <maint@maint.org>  ")
        self.assertThat(
            "working/test/debian/changelog",
            FileContains(matcher=StartsWith(new_cl_contents)))
        for fn in os.listdir("working"):
            self.assertThat(fn, Not(EndsWith(".changes")))

    @skipIfFakeroot()
    def test_cmd_dailydeb_with_package_from_changelog(self):
        #TODO: define a test feature for debuild and require it here.
        self.make_simple_package("source")
        with open("test.recipe", "w") as recipe:
            recipe.write(
                "# git-build-recipe format 0.1 deb-version 1\nsource\n")
        out, err = self.run_recipe(
            "--manifest manifest --if-changed-from bar test.recipe working")
        new_cl_contents = (
            "package (1) unstable; urgency=low\n\n"
            "  * Auto build.\n\n -- M. Maintainer <maint@maint.org>  ")
        self.assertThat(
            "working/test/debian/changelog",
            FileContains(matcher=StartsWith(new_cl_contents)))

    @skipIfFakeroot()
    def test_cmd_dailydeb_with_version_from_changelog(self):
        self.make_simple_package("source")
        with open("test.recipe", "w") as recipe:
            recipe.write(
                "# git-build-recipe format 0.3 deb-version {debversion}-2\n"
                "source\n")
        out, err = self.run_recipe(
            "--allow-fallback-to-native test.recipe working")
        cl = changelog.Changelog(self._get_file_contents(
            "working/test/debian/changelog"))
        self.assertEqual("0.1-1-2", str(cl._blocks[0].version))

    @skipIfFakeroot()
    def test_cmd_dailydeb_with_version_from_other_branch_changelog(self):
        self.make_simple_package("source")
        other = self.make_simple_package("other")
        cl_contents = ("package (0.4-1) unstable; urgency=low\n  * foo\n"
                    " -- maint <maint@maint.org>  Tue, 04 Aug 2009 "
                    "10:03:10 +0100\n")
        other.build_tree_contents([("debian/changelog", cl_contents)])
        other.add(["debian/changelog"])
        other.commit("new changelog entry")
        with open("test.recipe", "w") as recipe:
            recipe.write(
                "# git-build-recipe format 0.4 "
                "deb-version {debversion:other}.2\n"
                "source\n"
                "nest other other other\n")
        out, err = self.run_recipe(
            "--allow-fallback-to-native test.recipe working")
        cl = changelog.Changelog(self._get_file_contents(
            "working/test/debian/changelog"))
        self.assertEqual("0.4-1.2", str(cl._blocks[0].version))

    @skipIfFakeroot()
    def test_cmd_dailydeb_with_upstream_version_from_changelog(self):
        self.make_simple_package("source")
        with open("test.recipe", "w") as recipe:
            recipe.write(
                "# git-build-recipe format 0.1 deb-version {debupstream}-2\n"
                "source\n")
        out, err = self.run_recipe(
            "--allow-fallback-to-native test.recipe working")
        new_cl_contents = (
            "package (0.1-2) unstable; urgency=low\n\n"
            "  * Auto build.\n\n -- M. Maintainer <maint@maint.org>  ")
        self.assertThat(
            "working/test/debian/changelog",
            FileContains(matcher=StartsWith(new_cl_contents)))

    @skipIfFakeroot()
    def test_cmd_dailydeb_with_append_version(self):
        self.make_simple_package("source")
        with open("test.recipe", "w") as recipe:
            recipe.write(
                "# git-build-recipe format 0.1 deb-version 1\nsource\n")
        out, err = self.run_recipe(
            "--append-version ~ppa1 test.recipe working")
        new_cl_contents = (
            "package (1~ppa1) unstable; urgency=low\n\n"
            "  * Auto build.\n\n -- M. Maintainer <maint@maint.org>  ")
        self.assertThat(
            "working/test/debian/changelog",
            FileContains(matcher=StartsWith(new_cl_contents)))

    def test_cmd_dailydeb_with_nonascii_maintainer_in_changelog(self):
        self.useFixture(EnvironmentVariable(
            "DEBFULLNAME", u"Micha\u25c8 Sawicz"))
        self.make_simple_package("source")
        with open("test.recipe", "w") as recipe:
            recipe.write(
                "# git-build-recipe format 0.1 deb-version 1\nsource\n")
        out, err = self.run_recipe("test.recipe working")
        new_cl_contents = (
            "package (1) unstable; urgency=low\n\n"
            "  * Auto build.\n\n"
            u" -- Micha\u25c8 Sawicz <maint@maint.org>  ")
        self.assertThat(
            "working/test/debian/changelog",
            FileContains(matcher=StartsWith(new_cl_contents)))

    def test_cmd_dailydeb_with_invalid_version(self):
        source = GitRepository("source")
        source.build_tree(["a"])
        source.build_tree_contents([
            ("debian/", None),
            ("debian/control",
             "Source: foo\nMaintainer: maint maint@maint.org\n")
            ])
        source.add(["a", "debian/control"])
        source.commit("one")
        with open("test.recipe", "w") as recipe:
            recipe.write(
                "# git-build-recipe format 0.1 deb-version $\nsource\n")
        err = self.run_recipe("test.recipe working", retcode=1)[1]
        self.assertThat(err, MatchesRegex(
            r".*Invalid deb-version: \$: "
            r"(Could not parse version: \$|Invalid version string '\$')\n",
            flags=re.S))

    def test_cmd_dailydeb_with_safe(self):
        self.make_simple_package("source")
        with open("test.recipe", "w") as recipe:
            recipe.write(
                "# git-build-recipe format 0.3 deb-version 1\n"
                "source\nrun something bad")
        out, err = self.run_recipe("--safe test.recipe working", retcode=1)
        self.assertIn("The 'run' instruction is forbidden.\n", err)

    def make_simple_quilt_package(self):
        source = self.make_simple_package("source")
        source.build_tree(["debian/source/"])
        source.build_tree_contents([
            ("debian/source/format", "3.0 (quilt)\n"),
            ("debian/source/options", 'compression = "gzip"\n')])
        source.add(["debian/source/format", "debian/source/options"])
        source.commit("set source format")
        return source

    def test_cmd_dailydeb_missing_orig_tarball(self):
        self.make_simple_quilt_package()
        with open("test.recipe", "w") as recipe:
            recipe.write(
                "# git-build-recipe format 0.3 deb-version 1-1\nsource\n")
        out, err = self.run_recipe("test.recipe working", retcode=2)
        self.assertIn(
            'error: Unable to find the upstream source.  '
            'Import it as tag upstream/1 or build with '
            '--allow-fallback-to-native.\n', err)

    @skipIfFakeroot()
    def test_cmd_dailydeb_with_orig_tarball(self):
        source = self.make_simple_package("source")
        self.make_upstream_version(
            source, "package", "0.1", [("file", "content\n")])
        with open("test.recipe", "w") as recipe:
            recipe.write(
                "# git-build-recipe format 0.3 deb-version 0.1-1\nsource\n")
        out, err = self.run_recipe("test.recipe working")
        self.assertThat("working/package_0.1.orig.tar.gz", PathExists())
        self.assertThat("working/package_0.1-1.diff.gz", PathExists())

    @skipIfFakeroot()
    @requirePristineTar()
    def test_cmd_dailydeb_with_pristine_orig_gz_tarball(self):
        source = self.make_simple_package("source")
        pristine_tar_sha1 = self.make_upstream_version(
            source, "package", "0.1", [("file", "content\n")],
            pristine_tar_format="gz")
        with open("test.recipe", "w") as recipe:
            recipe.write(
                "# git-build-recipe format 0.3 deb-version 0.1-1\nsource\n")
        out, err = self.run_recipe("test.recipe working")
        self.assertThat("working/package_0.1.orig.tar.gz", PathExists())
        self.assertThat("working/package_0.1-1.diff.gz", PathExists())
        self.assertEqual(
            sha1_file_by_name("working/package_0.1.orig.tar.gz"),
            pristine_tar_sha1)

    @skipIfFakeroot()
    @requirePristineTar()
    def test_cmd_dailydeb_with_pristine_orig_bz2_tarball(self):
        source = self.make_simple_quilt_package()
        pristine_tar_sha1 = self.make_upstream_version(
            source, "package", "0.1",
            [("file", "content\n"), ("a", "contents of a\n")],
            pristine_tar_format="bz2")
        with open("test.recipe", "w") as recipe:
            recipe.write(
                "# git-build-recipe format 0.3 deb-version 0.1-1\nsource\n")
        source.build_tree([("upstream/")])
        source.build_tree_contents([
            ("upstream/file", "content\n"),
            ("upstream/a", "contents of a\n")])
        source.add(["upstream/file", "upstream/a"])
        out, err = self.run_recipe("test.recipe working")
        self.assertThat("working/package_0.1.orig.tar.bz2", PathExists())
        self.assertThat("working/package_0.1-1.debian.tar.gz", PathExists())
        self.assertEqual(
            sha1_file_by_name("working/package_0.1.orig.tar.bz2"),
            pristine_tar_sha1)

    @requirePristineTar()
    def test_cmd_dailydeb_allow_fallback_to_native_with_orig_tarball(self):
        source = self.make_simple_quilt_package()
        pristine_tar_sha1 = self.make_upstream_version(
            source, "package", "0.1",
            [("file", "content\n"), ("a", "contents of a\n")],
            pristine_tar_format="bz2")
        with open("test.recipe", "w") as recipe:
            recipe.write(
                "# git-build-recipe format 0.3 deb-version 0.1-1\nsource\n")
        source.build_tree([("upstream/")])
        source.build_tree_contents([
            ("upstream/file", "content\n"),
            ("upstream/a", "contents of a\n")])
        source.add(["upstream/file", "upstream/a"])
        out, err = self.run_recipe(
            "--allow-fallback-to-native test.recipe working")
        self.assertThat("working/package_0.1.orig.tar.bz2", PathExists())
        self.assertThat("working/package_0.1-1.debian.tar.gz", PathExists())
        self.assertThat(
            "source/debian/source/format", FileContains("3.0 (quilt)\n"))
        self.assertEqual(
            sha1_file_by_name("working/package_0.1.orig.tar.bz2"),
            pristine_tar_sha1)

    @skipIfFakeroot()
    def test_cmd_dailydeb_force_native(self):
        self.make_simple_quilt_package()
        with open("test.recipe", "w") as recipe:
            recipe.write(
                "# git-build-recipe format 0.3 deb-version 1\nsource\n")
        out, err = self.run_recipe(
            "--allow-fallback-to-native test.recipe working")
        self.assertThat(
            "working/test/debian/source/format",
            FileContains("3.0 (native)\n"))

    @skipIfFakeroot()
    def test_cmd_dailydeb_force_native_empty_series(self):
        source = self.make_simple_quilt_package()
        source.build_tree(['debian/patches/'])
        source.build_tree_contents([("debian/patches/series", "\n")])
        source.add(["debian/patches/series"])
        source.commit("add patches")
        with open("test.recipe", "w") as recipe:
            recipe.write(
                "# git-build-recipe format 0.3 deb-version 1\nsource\n")
        out, err = self.run_recipe(
            "--allow-fallback-to-native test.recipe working")
        self.assertThat(
            "working/test/debian/source/format",
            FileContains("3.0 (native)\n"))
        self.assertThat("working/test/debian/patches", Not(PathExists()))

    @skipIfFakeroot()
    def test_cmd_dailydeb_force_native_apply_quilt(self):
        source = self.make_simple_quilt_package()
        source.build_tree(["debian/patches/"])
        patch = dedent("""\
            diff -ur a/thefile b/thefile
            --- a/thefile	2010-12-05 20:14:22.000000000 +0100
            +++ b/thefile	2010-12-05 20:14:26.000000000 +0100
            @@ -1 +1 @@
            -old-contents
            +new-contents
            """)
        source.build_tree_contents([
            ("thefile", "old-contents\n"),
            ("debian/patches/series", "01_foo.patch"),
            ("debian/patches/01_foo.patch", patch)])
        source.add([
            "thefile", "debian/patches/series",
            "debian/patches/01_foo.patch"])
        source.commit("add patch")
        with open("test.recipe", "w") as recipe:
            recipe.write(
                "# git-build-recipe format 0.3 deb-version 1\nsource\n")
        out, err = self.run_recipe(
            "--allow-fallback-to-native test.recipe working")
        self.assertThat(
            "working/test/debian/source/format",
            FileContains("3.0 (native)\n"))
        self.assertThat("working/test/thefile", FileContains("new-contents\n"))
        self.assertThat("working/test/debian/patches", Not(PathExists()))

    def test_cmd_dailydeb_force_native_apply_quilt_failure(self):
        source = self.make_simple_quilt_package()
        source.build_tree(["debian/patches/"])
        patch = dedent("""\
            diff -ur a/thefile b/thefile
            --- a/thefile	2010-12-05 20:14:22.000000000 +0100
            +++ b/thefile	2010-12-05 20:14:26.000000000 +0100
            @@ -1 +1 @@
            -old-contents
            +new-contents
            """)
        source.build_tree_contents([
            ("thefile", "contents\n"),
            ("debian/patches/series", "01_foo.patch"),
            ("debian/patches/01_foo.patch", patch)])
        source.add([
            "thefile", "debian/patches/series",
            "debian/patches/01_foo.patch"])
        source.commit("add patch")
        with open("test.recipe", "w") as recipe:
            recipe.write(
                "# git-build-recipe format 0.3 deb-version 1-1\nsource\n")
        out, err = self.run_recipe(
            "--allow-fallback-to-native test.recipe working", retcode=1)
        self.assertIn("Failed to apply quilt patches\n", err)

    def test_unknown_source_format(self):
        source = self.make_simple_package("source")
        source.build_tree(["debian/source/"])
        source.build_tree_contents([("debian/source/format", "2.0\n")])
        source.add(["debian/source/format"])
        source.commit("set source format")
        with open("test.recipe", "w") as recipe:
            recipe.write(
                "# git-build-recipe format 0.3 deb-version 1-1\nsource\n")
        out, err = self.run_recipe(
            "--allow-fallback-to-native test.recipe working", retcode=1)
        self.assertIn("Unknown source format 2.0\n", err)
