# 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 datetime
import subprocess
import textwrap

from debian import changelog

from gitbuildrecipe.deb_version import (
    DebUpstreamBaseVariable,
    DebUpstreamVariable,
    DebVersionVariable,
    NotFullyExpanded,
    SubstitutionUnavailable,
    check_expanded_deb_version,
    version_extract_base,
    substitute_branch_vars,
    substitute_time,
    )
from gitbuildrecipe.recipe import (
    BaseRecipeBranch,
    build_tree,
    RecipeBranch,
    resolve_revisions,
    )
from gitbuildrecipe.tests import (
    GitRepository,
    GitTestCase,
    )


class ResolveRevisionsTests(GitTestCase):

    def setUp(self):
        super().setUp()
        self.use_temp_dir()

    def test_unchanged(self):
        source = GitRepository("source")
        commit = source.commit("one", date="2015-01-01 12:00:00 UTC")
        branch1 = BaseRecipeBranch("source", "{revdate}", 0.4, revspec="HEAD")
        branch2 = BaseRecipeBranch("source", "{revdate}", 0.4, revspec=commit)
        self.assertFalse(resolve_revisions(
            branch1, if_changed_from=branch2,
            substitute_branch_vars=substitute_branch_vars))
        self.assertEqual("source", branch1.url)
        self.assertEqual(commit, branch1.commit)
        self.assertEqual("HEAD", branch1.revspec)
        self.assertEqual("20150101", branch1.deb_version)

    def test_unchanged_not_explicit(self):
        source = GitRepository("source")
        commit = source.commit("one", date="2015-01-01 12:00:00 UTC")
        branch1 = BaseRecipeBranch("source", "{revdate}", 0.4)
        branch2 = BaseRecipeBranch("source", "{revdate}", 0.4, revspec=commit)
        self.assertFalse(resolve_revisions(
            branch1, if_changed_from=branch2,
            substitute_branch_vars=substitute_branch_vars))
        self.assertEqual("source", branch1.url)
        self.assertEqual(commit, branch1.commit)
        self.assertIsNone(branch1.revspec)
        self.assertEqual("20150101", branch1.deb_version)

    def test_unchanged_multilevel(self):
        source = GitRepository("source")
        commit = source.commit("one", date="2015-01-01 12:00:00 UTC")
        branch1 = BaseRecipeBranch("source", "{revdate}", 0.4)
        branch2 = RecipeBranch("nested1", "source")
        branch3 = RecipeBranch("nested2", "source")
        branch2.nest_branch("bar", branch3)
        branch1.nest_branch("foo", branch2)
        branch4 = BaseRecipeBranch("source", "{revdate}", 0.4, revspec=commit)
        branch5 = RecipeBranch("nested1", "source", revspec=commit)
        branch6 = RecipeBranch("nested2", "source", revspec=commit)
        branch5.nest_branch("bar", branch6)
        branch4.nest_branch("foo", branch5)
        self.assertFalse(resolve_revisions(
            branch1, if_changed_from=branch4,
            substitute_branch_vars=substitute_branch_vars))
        self.assertEqual("source", branch1.url)
        self.assertEqual(commit, branch1.commit)
        self.assertIsNone(branch1.revspec)
        self.assertEqual("20150101", branch1.deb_version)

    def test_changed(self):
        source = GitRepository("source")
        commit = source.commit("one", date="2015-01-01 12:00:00 UTC")
        branch1 = BaseRecipeBranch("source", "{revdate}", 0.4, revspec="HEAD")
        branch2 = BaseRecipeBranch("source", "{revdate}", 0.4, revspec="foo")
        self.assertTrue(resolve_revisions(
            branch1, if_changed_from=branch2,
            substitute_branch_vars=substitute_branch_vars))
        self.assertEqual("source", branch1.url)
        self.assertEqual(commit, branch1.commit)
        self.assertEqual("HEAD", branch1.revspec)
        self.assertEqual("20150101", branch1.deb_version)

    def test_changed_shape(self):
        source = GitRepository("source")
        commit = source.commit("one", date="2015-01-01 12:00:00 UTC")
        branch1 = BaseRecipeBranch("source", "{revdate}", 0.4, revspec="HEAD")
        branch2 = BaseRecipeBranch("source", "{revdate}", 0.4, revspec=commit)
        branch3 = RecipeBranch("nested", "source")
        branch1.nest_branch("foo", branch3)
        self.assertTrue(resolve_revisions(
            branch1, if_changed_from=branch2,
            substitute_branch_vars=substitute_branch_vars))
        self.assertEqual("source", branch1.url)
        self.assertEqual(commit, branch1.commit)
        self.assertEqual("HEAD", branch1.revspec)
        self.assertEqual("20150101", branch1.deb_version)

    def test_changed_command(self):
        source = GitRepository("source")
        source.commit("one")
        branch1 = BaseRecipeBranch("source", "{revdate}", 0.4)
        branch2 = BaseRecipeBranch("source", "{revdate}", 0.4)
        branch1.run_command("touch test1")
        branch2.run_command("touch test2")
        self.assertTrue(resolve_revisions(
            branch1, if_changed_from=branch2,
            substitute_branch_vars=substitute_branch_vars))
        self.assertEqual("source", branch1.url)

    def test_unchanged_command(self):
        source = GitRepository("source")
        source.commit("one")
        branch1 = BaseRecipeBranch("source", "{revdate}", 0.4)
        branch2 = BaseRecipeBranch("source", "{revdate}", 0.4)
        branch1.run_command("touch test1")
        branch2.run_command("touch test1")
        self.assertFalse(resolve_revisions(
            branch1, if_changed_from=branch2,
            substitute_branch_vars=substitute_branch_vars))
        self.assertEqual("source", branch1.url)

    def test_substitute(self):
        source = GitRepository("source")
        commit = source.commit("one", date="2015-01-01 12:00:00 UTC")
        source.commit("two", date="2015-02-01 12:00:00 UTC")
        branch1 = BaseRecipeBranch(
            "source", "{revdate}-{revdate:packaging}", 0.4, revspec=commit)
        branch2 = RecipeBranch("packaging", "source")
        branch1.nest_branch("debian", branch2)
        self.assertTrue(resolve_revisions(
            branch1, substitute_branch_vars=substitute_branch_vars))
        self.assertEqual("source", branch1.url)
        self.assertEqual(commit, branch1.commit)
        self.assertEqual(commit, branch1.revspec)
        self.assertEqual("20150101-20150201", branch1.deb_version)

    def test_substitute_supports_debupstream(self):
        # resolve_revisions should leave debupstream parameters alone and not
        # complain.
        source = GitRepository("source")
        source.commit("one", date="2015-01-01 12:00:00 UTC")
        source.commit("two", date="2015-01-02 12:00:00 UTC")
        branch1 = BaseRecipeBranch("source", "{debupstream}-{revdate}", 0.4)
        resolve_revisions(
            branch1, substitute_branch_vars=substitute_branch_vars)
        self.assertEqual("{debupstream}-20150102", branch1.deb_version)

    def test_substitute_not_fully_expanded(self):
        source = GitRepository("source")
        source.commit("one")
        source.commit("two")
        branch1 = BaseRecipeBranch("source", "{revdate:packaging}", 0.4)
        resolve_revisions(
            branch1, substitute_branch_vars=substitute_branch_vars)
        self.assertRaises(
            NotFullyExpanded, check_expanded_deb_version, branch1)

    def test_substitute_git(self):
        source = GitRepository("source")
        commit = source.commit("one")
        branch1 = BaseRecipeBranch("source", "foo-{git-commit}", 0.4)
        resolve_revisions(
            branch1, substitute_branch_vars=substitute_branch_vars)
        self.assertEqual("foo-%s" % commit[:7], branch1.deb_version)

    def test_latest_tag(self):
        source = GitRepository("source")
        commit = source.commit("one")
        source.tag("millbank", commit)
        source.commit("two")
        branch1 = BaseRecipeBranch("source", "foo-{latest-tag}", 0.4)
        resolve_revisions(branch1,
            substitute_branch_vars=substitute_branch_vars)
        self.assertEqual("foo-millbank", branch1.deb_version)

    def test_latest_tag_no_tag(self):
        source = GitRepository("source")
        source.commit("one")
        source.commit("two")
        branch1 = BaseRecipeBranch("source", "foo-{latest-tag}", 0.4)
        exc = self.assertRaises(
            subprocess.CalledProcessError, resolve_revisions,
            branch1, substitute_branch_vars=substitute_branch_vars)
        self.assertIn("No names found, cannot describe anything.", exc.output)

    def test_substitute_revdate(self):
        source = GitRepository("source")
        source.commit("one")
        source.commit("two", date="2011-06-10 12:23:48 UTC")
        branch1 = BaseRecipeBranch("source", "foo-{revdate}", 0.4)
        resolve_revisions(
            branch1, substitute_branch_vars=substitute_branch_vars)
        self.assertEqual("foo-20110610", branch1.deb_version)

    def test_substitute_revtime(self):
        source = GitRepository("source")
        source.commit("one")
        source.commit("two", date="2011-06-10 12:23:48 UTC")
        branch1 = BaseRecipeBranch("source", "foo-{revtime}", 0.4)
        resolve_revisions(
            branch1, substitute_branch_vars=substitute_branch_vars)
        self.assertEqual("foo-201106101223", branch1.deb_version)

    def test_substitute_revno(self):
        source = GitRepository("source")
        source.commit("one")
        source._git_call("checkout", "-q", "-b", "branch")
        source.build_tree(["a"])
        source.add(["a"])
        source.commit("two")
        source.commit("three")
        branch1 = BaseRecipeBranch("source", "foo-{revno}", 0.1)
        resolve_revisions(
            branch1, substitute_branch_vars=substitute_branch_vars)
        self.assertEqual("foo-3", branch1.deb_version)
        source._git_call("checkout", "-q", "master")
        source._git_call(
            "merge", "-q", "--no-ff", "--commit", "-m", "merge branch",
            "branch")
        branch2 = BaseRecipeBranch("source", "foo-{revno}", 0.1)
        resolve_revisions(
            branch2, substitute_branch_vars=substitute_branch_vars)
        self.assertEqual("foo-2", branch2.deb_version)

    def test_uses_source_commit(self):
        # The original commit is used for variable substitution, even if
        # the base branch was committed to by instructions such as merge.
        source = GitRepository("source")
        source.commit("one")
        source._git_call("checkout", "-q", "-b", "branch")
        source.build_tree(["a"])
        source.add(["a"])
        source.commit("two")
        source.commit("three")
        branch1 = BaseRecipeBranch(
            "source", "foo-{revno}+{revno:branch}", 0.1, revspec="master")
        branch2 = RecipeBranch("branch", "source", revspec="branch")
        branch1.merge_branch(branch2)
        build_tree(branch1, "target")
        resolve_revisions(
            branch1, substitute_branch_vars=substitute_branch_vars)
        self.assertEqual("foo-1+3", branch1.deb_version)


class DebUpstreamVariableTests(GitTestCase):

    def write_changelog(self, version):
        contents = textwrap.dedent("""
            package (%s) experimental; urgency=low

              * Initial release. (Closes: #XXXXXX)

             -- Jelmer Vernooij <jelmer@debian.org>  Thu, 19 May 2011 10:07:41 +0100
            """ % version)[1:]
        return changelog.Changelog(file=contents)

    def test_empty_changelog(self):
        var = DebUpstreamVariable.from_changelog(None, changelog.Changelog())
        self.assertRaises(SubstitutionUnavailable, var.get)

    def test_version(self):
        var = DebUpstreamVariable.from_changelog(
            None, self.write_changelog("2.3"))
        self.assertEqual("2.3", var.get())

    def test_epoch(self):
        # The epoch is (currently) ignored by {debupstream}.
        var = DebUpstreamVariable.from_changelog(
            None, self.write_changelog("2:2.3"))
        self.assertEqual("2.3", var.get())

    def test_base_without_snapshot(self):
        var = DebUpstreamBaseVariable.from_changelog(
            None, self.write_changelog("2.4"))
        self.assertEqual("2.4+", var.get())

    def test_base_with_svn_snapshot(self):
        var = DebUpstreamBaseVariable.from_changelog(
            None, self.write_changelog("2.4~svn4"))
        self.assertEqual("2.4~", var.get())

    def test_base_with_bzr_snapshot(self):
        var = DebUpstreamBaseVariable.from_changelog(
            None, self.write_changelog("2.4+bzr343"))
        self.assertEqual("2.4+", var.get())


class VersionExtractBaseTests(GitTestCase):

    def test_simple_extract(self):
        self.assertEqual("2.4", version_extract_base("2.4"))
        self.assertEqual("2.4+foobar", version_extract_base("2.4+foobar"))

    def test_with_bzr(self):
        self.assertEqual("2.4+", version_extract_base("2.4+bzr32"))
        self.assertEqual("2.4~", version_extract_base("2.4~bzr32"))

    def test_with_git(self):
        self.assertEqual("2.4+", version_extract_base("2.4+git20101010"))
        self.assertEqual("2.4~", version_extract_base("2.4~gitaabbccdd"))

    def test_with_svn(self):
        self.assertEqual("2.4+", version_extract_base("2.4+svn45"))
        self.assertEqual("2.4~", version_extract_base("2.4~svn45"))

    def test_with_dfsg(self):
        self.assertEqual("2.4+", version_extract_base("2.4+bzr32+dfsg1"))
        self.assertEqual("2.4~", version_extract_base("2.4~bzr32+dfsg.1"))
        self.assertEqual("2.4~", version_extract_base("2.4~bzr32.dfsg.1"))
        self.assertEqual("2.4~", version_extract_base("2.4~bzr32dfsg.1"))
        self.assertEqual("1.6~", version_extract_base("1.6~git20120320.dfsg.1"))


class DebVersionVariableTests(GitTestCase):

    def write_changelog(self, version):
        contents = textwrap.dedent("""
            package (%s) experimental; urgency=low

              * Initial release. (Closes: #XXXXXX)

             -- Jelmer Vernooij <jelmer@debian.org>  Thu, 19 May 2011 10:07:41 +0100
            """ % version)[1:]
        return changelog.Changelog(file=contents)

    def test_empty_changelog(self):
        var = DebVersionVariable.from_changelog(None, changelog.Changelog())
        self.assertRaises(SubstitutionUnavailable, var.get)

    def test_simple(self):
        var = DebVersionVariable.from_changelog(
            None, self.write_changelog("2.3-1"))
        self.assertEqual("2.3-1", var.get())

    def test_epoch(self):
        var = DebVersionVariable.from_changelog(
            None, self.write_changelog("4:2.3-1"))
        self.assertEqual("4:2.3-1", var.get())


class RecipeBranchTests(GitTestCase):

    def setUp(self):
        super().setUp()
        self.use_temp_dir()

    def test_substitute_time(self):
        time = datetime.datetime.utcfromtimestamp(1)
        base_branch = BaseRecipeBranch("base_url", "1-{time}", 0.2)
        substitute_time(base_branch, time)
        self.assertEqual("1-197001010000", base_branch.deb_version)
        substitute_time(base_branch, time)
        self.assertEqual("1-197001010000", base_branch.deb_version)

    def test_substitute_date(self):
        time = datetime.datetime.utcfromtimestamp(1)
        base_branch = BaseRecipeBranch("base_url", "1-{date}", 0.2)
        substitute_time(base_branch, time)
        self.assertEqual("1-19700101", base_branch.deb_version)
        substitute_time(base_branch, time)
        self.assertEqual("1-19700101", base_branch.deb_version)

    def test_substitute_branch_vars(self):
        wt = GitRepository("base_url")
        commit1 = wt.commit("acommit", date="2015-01-01 12:00:00 UTC")
        commit2 = wt.commit("bcommit", date="2015-01-02 12:00:00 UTC")
        base_branch = BaseRecipeBranch("base_url", "1", 0.2, revspec=commit1)
        substitute_branch_vars(base_branch, base_branch)
        self.assertEqual("1", base_branch.deb_version)
        substitute_branch_vars(base_branch, base_branch)
        self.assertEqual("1", base_branch.deb_version)
        base_branch = BaseRecipeBranch(
            "base_url", "{revdate}", 0.4, revspec=commit1)
        base_branch.resolve_commit()
        substitute_branch_vars(base_branch, base_branch)
        self.assertEqual("20150101", base_branch.deb_version)
        base_branch = BaseRecipeBranch(
            "base_url", "{revdate}", 0.4, revspec=commit1)
        base_branch.resolve_commit()
        child_branch = RecipeBranch("foo", "base_url", revspec=commit2)
        child_branch.resolve_commit()
        substitute_branch_vars(base_branch, child_branch)
        self.assertEqual("{revdate}", base_branch.deb_version)
        substitute_branch_vars(base_branch, child_branch)
        self.assertEqual("{revdate}", base_branch.deb_version)
        base_branch = BaseRecipeBranch(
            "base_url", "{revdate:foo}", 0.4, revspec=commit1)
        base_branch.resolve_commit()
        substitute_branch_vars(base_branch, child_branch)
        self.assertEqual("20150102", base_branch.deb_version)

    def test_substitute_branch_vars_debupstream(self):
        wt = GitRepository("base_url")
        commit1 = wt.commit("acommit")
        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")
        wt.build_tree_contents(
            [("debian/", ), ('debian/changelog', cl_contents)])
        wt.add(['debian/changelog'])
        commit2 = wt.commit("with changelog")
        base_branch = BaseRecipeBranch(
            "base_url", "{debupstream}", 0.4, revspec=commit1)
        # No changelog file, so no substitution
        substitute_branch_vars(base_branch, base_branch)
        self.assertEqual("{debupstream}", base_branch.deb_version)
        base_branch = BaseRecipeBranch(
            "base_url", "{debupstream}", 0.4, revspec=commit2)
        substitute_branch_vars(base_branch, base_branch)
        self.assertEqual("0.1", base_branch.deb_version)
        base_branch = BaseRecipeBranch(
            "base_url", "{debupstream:tehname}", 0.4)
        child_branch = RecipeBranch("tehname", "base_url", revspec=commit2)
        substitute_branch_vars(base_branch, child_branch)
        self.assertEqual("0.1", base_branch.deb_version)

    def test_substitute_branch_vars_debupstream_pre_0_4(self):
        wt = GitRepository("base_url")
        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")
        wt.build_tree_contents(
            [("debian/", ), ('debian/changelog', cl_contents)])
        wt.add(['debian/changelog'])
        commit = wt.commit("with changelog")
        # In recipe format < 0.4 {debupstream} gets replaced from the
        # resulting tree, not from the branch vars.
        base_branch = BaseRecipeBranch(
            "base_url", "{debupstream}", 0.2, revspec=commit)
        substitute_branch_vars(base_branch, base_branch)
        self.assertEqual("{debupstream}", base_branch.deb_version)
