#!/usr/bin/python
# Copyright (C) 2018 Jelmer Vernooij
# This file is a part of debmutate.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

"""Tests for debmutate.control."""

import os

from debmutate.control import (
    ControlEditor,
    DefaultSortingOrder,
    MissingSourceParagraph,
    PkgRelation,
    WrapAndSortOrder,
    _cdbs_resolve_conflict,
    add_dependency,
    delete_from_list,
    drop_dependency,
    ensure_exact_version,
    ensure_minimum_version,
    ensure_relation,
    ensure_some_version,
    format_relations,
    get_relation,
    guess_template_type,
    is_dep_implied,
    is_relation_implied,
    iter_relations,
    parse_relations,
    parse_standards_version,
    relations_are_sorted,
    update_control,
)
from debmutate.reformatting import FormattingUnpreservable, GeneratedFile

from . import TestCase, TestCaseInTempDir


class UpdateControlTests(TestCaseInTempDir):
    def test_do_not_edit(self):
        self.build_tree_contents(
            [
                ("debian/",),
                (
                    "debian/control",
                    """\
# DO NOT EDIT
# This file was generated by blah

Source: blah
Testsuite: autopkgtest

""",
                ),
            ]
        )

        def source_cb(c):
            c["Source"] = "blah1"

        self.assertRaises(GeneratedFile, update_control, source_package_cb=source_cb)

    def test_add_binary(self):
        self.build_tree_contents(
            [
                ("debian/",),
                (
                    "debian/control",
                    """\
Source: blah
Testsuite: autopkgtest

Package: blah
Description: Some description
 And there are more lines
 And more lines
""",
                ),
            ]
        )
        with ControlEditor("debian/control") as editor:
            editor.add_binary({"Package": "foo", "Description": "A new package foo"})
            self.assertEqual([b["Package"] for b in editor.binaries], ["blah", "foo"])
        self.assertTrue(editor.changed)
        self.assertEqual(editor.changed_files, ["debian/control"])

    def test_list_binaries(self):
        self.build_tree_contents(
            [
                ("debian/",),
                (
                    "debian/control",
                    """\
Source: blah
Testsuite: autopkgtest

Package: blah
Description: Some description
 And there are more lines
 And more lines
""",
                ),
            ]
        )
        with ControlEditor("debian/control") as editor:
            self.assertEqual(list(editor.binaries)[0]["Package"], "blah")
        self.assertFalse(editor.changed)
        self.assertEqual(editor.changed_files, [])

    def test_no_source(self):
        self.build_tree_contents(
            [
                ("debian/",),
                (
                    "debian/control",
                    """\
Package: blah
Testsuite: autopkgtest

Package: bar
""",
                ),
            ]
        )
        with ControlEditor("debian/control") as editor:
            with self.assertRaises(MissingSourceParagraph):
                editor.source

    def test_create(self):
        self.build_tree_contents([("debian/",)])
        with ControlEditor.create("debian/control") as editor:
            editor.source["Source"] = "foo"
        self.assertFileEqual(
            """\
Source: foo
""",
            "debian/control",
        )

    def test_do_not_edit_no_change(self):
        self.build_tree_contents(
            [
                ("debian/",),
                (
                    "debian/control",
                    """\
# DO NOT EDIT
# This file was generated by blah

Source: blah
Testsuite: autopkgtest

""",
                ),
            ]
        )
        update_control()

    def test_unpreservable(self):
        self.build_tree_contents(
            [
                ("debian/",),
                (
                    "debian/control",
                    """\
Source: blah
# A comment
Testsuite: autopkgtest

""",
                ),
            ]
        )

        def update_source(control):
            control["NewField"] = "New Field"

        update_control(source_package_cb=update_source)

    def test_merge3(self):
        self.build_tree_contents(
            [
                ("debian/",),
                (
                    "debian/control",
                    """\
Source: blah
Testsuite: autopkgtest

Package: blah
Description: Some description
 And there are more lines
 And more lines
# A comment
Multi-Arch: foreign
""",
                ),
            ]
        )

        def update_source(control):
            control["NewField"] = "New Field"

        try:
            import merge3  # noqa: F401
        except ModuleNotFoundError:
            has_merge3 = False
        else:
            has_merge3 = merge3.__version__ >= (0, 0, 7)
        if has_merge3:
            update_control(source_package_cb=update_source)
            self.assertFileEqual(
                """\
Source: blah
Testsuite: autopkgtest
NewField: New Field

Package: blah
Description: Some description
 And there are more lines
 And more lines
# A comment
Multi-Arch: foreign
""",
                "debian/control",
            )
        else:
            self.assertRaises(
                FormattingUnpreservable, update_control, source_package_cb=update_source
            )

    def test_modify_source(self):
        self.build_tree_contents(
            [
                ("debian/",),
                (
                    "debian/control",
                    """\
Source: blah
Testsuite: autopkgtest
""",
                ),
            ]
        )

        def add_header(control):
            control["XS-Vcs-Git"] = "git://github.com/example/example"

        self.assertTrue(update_control(source_package_cb=add_header))
        self.assertFileEqual(
            """\
Source: blah
Testsuite: autopkgtest
XS-Vcs-Git: git://github.com/example/example
""",
            "debian/control",
        )

    def test_wrap_and_sort(self):
        self.build_tree_contents(
            [
                ("debian/",),
                (
                    "debian/control",
                    """\
Source: blah
Testsuite: autopkgtest
Depends: package3, package2

Package: libblah
Section: extra
""",
                ),
            ]
        )
        with ControlEditor() as editor:
            try:
                editor.wrap_and_sort(trailing_comma=True)
            except NotImplementedError as e:
                self.skipTest(str(e))

        self.assertFileEqual(
            """\
Source: blah
Testsuite: autopkgtest
Depends: package2, package3,

Package: libblah
Section: extra
""",
            "debian/control",
        )

    def test_sort_binaries(self):
        self.build_tree_contents(
            [
                ("debian/",),
                (
                    "debian/control",
                    """\
Source: blah
Testsuite: autopkgtest
Depends: package3, package2

Package: libfoo
Section: web

Package: libblah
Section: extra
""",
                ),
            ]
        )
        with ControlEditor() as editor:
            editor.sort_binary_packages()

        self.assertFileEqual(
            """\
Source: blah
Testsuite: autopkgtest
Depends: package3, package2

Package: libblah
Section: extra

Package: libfoo
Section: web
""",
            "debian/control",
        )

    def test_modify_binary(self):
        self.build_tree_contents(
            [
                ("debian/",),
                (
                    "debian/control",
                    """\
Source: blah
Testsuite: autopkgtest

Package: libblah
Section: extra
""",
                ),
            ]
        )

        def add_header(control):
            control["Arch"] = "all"

        self.assertTrue(update_control(binary_package_cb=add_header))
        self.assertFileEqual(
            """\
Source: blah
Testsuite: autopkgtest

Package: libblah
Section: extra
Arch: all
""",
            "debian/control",
        )

    def test_doesnt_strip_whitespace(self):
        self.build_tree_contents(
            [
                ("debian/",),
                (
                    "debian/control",
                    """\
Source: blah
Testsuite: autopkgtest

""",
                ),
            ]
        )
        self.assertFalse(update_control())
        self.assertFileEqual(
            """\
Source: blah
Testsuite: autopkgtest

""",
            "debian/control",
        )

    def test_update_template(self):
        self.build_tree_contents(
            [
                ("debian/",),
                (
                    "debian/control",
                    """\
# DO NOT EDIT
# This file was generated by blah

Source: blah
Testsuite: autopkgtest
Uploaders: Jelmer Vernooij <jelmer@jelmer.uk>

""",
                ),
                (
                    "debian/control.in",
                    """\
Source: blah
Testsuite: autopkgtest
Uploaders: @lintian-brush-test@

""",
                ),
                (
                    "debian/rules",
                    """\
#!/usr/bin/make -f

debian/control: debian/control.in
\tsed -e 's/@lintian-brush-test@/testvalue/' < $< > $@
""",
                ),
            ]
        )
        os.chmod("debian/rules", 0o755)

        with ControlEditor() as updater:
            updater.source["Testsuite"] = "autopkgtest8"
            updater.changes()
        self.assertFileEqual(
            """\
Source: blah
Testsuite: autopkgtest8
Uploaders: @lintian-brush-test@
""",
            "debian/control.in",
            strip_trailing_whitespace=True,
        )
        self.assertFileEqual(
            """\
Source: blah
Testsuite: autopkgtest8
Uploaders: testvalue
""",
            "debian/control",
            strip_trailing_whitespace=True,
        )
        self.assertTrue(updater.changed)
        self.assertEqual(["debian/control.in", "debian/control"], updater.changed_files)

    def test_update_template_only(self):
        self.build_tree_contents(
            [
                ("debian/",),
                (
                    "debian/control.in",
                    """\
Source: blah
Testsuite: autopkgtest
Uploaders: @lintian-brush-test@

""",
                ),
                (
                    "debian/rules",
                    """\
#!/usr/bin/make -f

debian/control: debian/control.in
\tsed -e 's/@lintian-brush-test@/testvalue/' < $< > $@
""",
                ),
            ]
        )
        os.chmod("debian/rules", 0o755)

        with ControlEditor() as updater:
            updater.source["Testsuite"] = "autopkgtest8"
            updater.changes()
        self.assertFileEqual(
            """\
Source: blah
Testsuite: autopkgtest8
Uploaders: @lintian-brush-test@
""",
            "debian/control.in",
            strip_trailing_whitespace=True,
        )
        self.assertFalse(os.path.exists("debian/control"))

    def test_update_template_invalid_tokens(self):
        self.build_tree_contents(
            [
                ("debian/",),
                (
                    "debian/control",
                    """\
# DO NOT EDIT
# This file was generated by blah

Source: blah
Testsuite: autopkgtest
Uploaders: Jelmer Vernooij <jelmer@jelmer.uk>
""",
                ),
                (
                    "debian/control.in",
                    """\
Source: blah
Testsuite: autopkgtest
@OTHERSTUFF@
""",
                ),
                (
                    "debian/rules",
                    """\
#!/usr/bin/make -f

debian/control: debian/control.in
\tsed -e 's/@OTHERSTUFF@/Vcs-Git: example.com/' < $< > $@
""",
                ),
            ]
        )
        os.chmod("debian/rules", 0o755)

        with ControlEditor() as updater:
            updater.source["Testsuite"] = "autopkgtest8"
            updater.changes()
        self.assertFileEqual(
            """\
Source: blah
Testsuite: autopkgtest8
@OTHERSTUFF@
""",
            "debian/control.in",
            strip_trailing_whitespace=True,
        )
        self.assertFileEqual(
            """\
Source: blah
Testsuite: autopkgtest8
Vcs-Git: example.com
""",
            "debian/control",
            strip_trailing_whitespace=True,
        )

    def test_update_cdbs_template(self):
        self.build_tree_contents(
            [
                ("debian/",),
                (
                    "debian/control",
                    """\
Source: blah
Testsuite: autopkgtest
Build-Depends: some-foo, libc6

""",
                ),
                (
                    "debian/control.in",
                    """\
Source: blah
Testsuite: autopkgtest
Build-Depends: @cdbs@, libc6

""",
                ),
            ]
        )

        with ControlEditor() as updater:
            updater.source["Build-Depends"] = "some-foo, libc6, some-bar"
        self.assertFileEqual(
            """\
Source: blah
Testsuite: autopkgtest
Build-Depends: some-foo, libc6, some-bar
""",
            "debian/control",
            strip_trailing_whitespace=True,
        )
        self.assertFileEqual(
            """\
Source: blah
Testsuite: autopkgtest
Build-Depends: @cdbs@, libc6, some-bar
""",
            "debian/control.in",
            strip_trailing_whitespace=True,
        )

    def test_description_stays_last(self):
        self.build_tree_contents(
            [
                ("debian/",),
                (
                    "debian/control",
                    """\
Source: blah
Testsuite: autopkgtest

Package: libblah
Section: extra
Description: foo
 bar

""",
                ),
            ]
        )

        def add_header(control):
            control["Arch"] = "all"

        self.assertTrue(update_control(binary_package_cb=add_header))
        self.assertFileEqual(
            """\
Source: blah
Testsuite: autopkgtest

Package: libblah
Section: extra
Arch: all
Description: foo
 bar
""",
            "debian/control",
            strip_trailing_whitespace=True,
        )

    def test_no_new_heading_whitespace(self):
        self.build_tree_contents(
            [
                ("debian/",),
                (
                    "debian/control",
                    """\
Source: blah
Build-Depends:
 debhelper-compat (= 11),
 uuid-dev
""",
                ),
            ]
        )

        with ControlEditor() as updater:
            updater.source["Build-Depends"] = "\n debhelper-compat (= 12),\n uuid-dev"
        self.assertFileEqual(
            """\
Source: blah
Build-Depends:
 debhelper-compat (= 12),
 uuid-dev
""",
            "debian/control",
            strip_trailing_whitespace=True,
        )


class ParseRelationsTests(TestCase):
    def test_empty(self):
        self.assertEqual([], parse_relations(""))
        self.assertEqual([("\n", [], "")], parse_relations("\n"))

    def test_simple(self):
        self.assertEqual(
            [("", [PkgRelation("debhelper")], "")], parse_relations("debhelper")
        )
        self.assertEqual(
            [("  \n", [PkgRelation("debhelper")], "")], parse_relations("  \ndebhelper")
        )
        self.assertEqual(
            [("  \n", [PkgRelation("debhelper")], " \n")],
            parse_relations("  \ndebhelper \n"),
        )


class FormatRelationsTests(TestCase):
    def test_empty(self):
        self.assertEqual("", format_relations([("", [], "")]))
        self.assertEqual("", format_relations([("", [], "\n")]))
        self.assertEqual("", format_relations([("\n ", [], "")]))

    def test_simple(self):
        self.assertEqual(
            "debhelper", format_relations([("", [PkgRelation("debhelper")], "")])
        )
        self.assertEqual(
            format_relations([("  \n", [PkgRelation("debhelper")], "")]),
            "  \ndebhelper",
        )
        self.assertEqual(
            format_relations([("  \n", [PkgRelation("debhelper")], " \n")]),
            "  \ndebhelper ",
        )

    def test_multiple(self):
        self.assertEqual(
            "debhelper, blah",
            format_relations(
                [("", [PkgRelation("debhelper")], ""), (" ", [PkgRelation("blah")], "")]
            ),
        )


class EnsureMinimumVersionTests(TestCase):
    def test_added(self):
        self.assertEqual(
            "debhelper (>= 9)", ensure_minimum_version("", "debhelper", "9")
        )
        # debhelper is a build system, so it should come before "blah" (wrap-and-sort order)
        self.assertEqual(
            "debhelper (>= 9), blah", ensure_minimum_version("blah", "debhelper", "9")
        )

    def test_unchanged(self):
        self.assertEqual(
            "debhelper (>= 9)",
            ensure_minimum_version("debhelper (>= 9)", "debhelper", "9"),
        )
        self.assertEqual(
            "debhelper (= 9)",
            ensure_minimum_version("debhelper (= 9)", "debhelper", "9"),
        )
        self.assertEqual(
            "debhelper (>= 9)",
            ensure_minimum_version("debhelper (>= 9)", "debhelper", "9~"),
        )

    def test_updated(self):
        self.assertEqual(
            "debhelper (>= 9)", ensure_minimum_version("debhelper", "debhelper", "9")
        )
        self.assertEqual(
            "blah, debhelper (>= 9)",
            ensure_minimum_version("blah, debhelper", "debhelper", "9"),
        )
        self.assertEqual(
            "blah, debhelper (>= 9)",
            ensure_minimum_version("blah, debhelper (>= 8)", "debhelper", "9"),
        )
        self.assertEqual(
            "blah, debhelper (>= 9)",
            ensure_minimum_version(
                "blah, debhelper (>= 8), debhelper (>= 8.1) | dh-systemd",
                "debhelper",
                "9",
            ),
        )
        self.assertEqual(
            "blah, debhelper (>= 9), debhelper (>= 10) | dh-systemd",
            ensure_minimum_version(
                "blah, debhelper (>= 8), debhelper (>= 10) | dh-systemd",
                "debhelper",
                "9",
            ),
        )


class EnsureRelationTests(TestCase):
    def test_added(self):
        self.assertEqual("debhelper (>= 9)", ensure_relation("", "debhelper (>= 9)"))
        # debhelper is a build system, so it should come before "blah" (wrap-and-sort order)
        self.assertEqual(
            "debhelper (>= 9), blah", ensure_relation("blah", "debhelper (>= 9)")
        )

    def test_unchanged(self):
        self.assertEqual(
            "debhelper (>= 9)", ensure_relation("debhelper (>= 9)", "debhelper (>= 9)")
        )
        self.assertEqual(
            "debhelper (= 9)", ensure_relation("debhelper (= 9)", "debhelper (>= 9)")
        )
        self.assertEqual(
            "debhelper (>= 9)", ensure_relation("debhelper (>= 9)", "debhelper (>= 9~)")
        )

    def test_updated(self):
        self.assertEqual(
            "debhelper (>= 9)", ensure_relation("debhelper", "debhelper (>= 9)")
        )
        self.assertEqual(
            "blah, debhelper (>= 9)",
            ensure_relation("blah, debhelper", "debhelper (>= 9)"),
        )
        self.assertEqual(
            "blah, debhelper (>= 9)",
            ensure_relation("blah, debhelper (>= 8)", "debhelper (>= 9)"),
        )
        self.assertEqual(
            "blah, debhelper (>= 9)",
            ensure_relation(
                "blah, debhelper (>= 8), debhelper (>= 8.1) | dh-systemd",
                "debhelper (>= 9)",
            ),
        )
        self.assertEqual(
            "blah, debhelper (>= 8), debhelper (>= 10) | dh-systemd",
            ensure_relation(
                "blah, debhelper (>= 8), debhelper (>= 10) | dh-systemd",
                "debhelper (>= 9)",
            ),
        )

    def test_bug(self):
        self.assertEqual(
            "python3-pytest,python3-setuptools (>= 46.4),,",
            ensure_relation("python3-setuptools (>= 46.4),,\n", "python3-pytest"),
        )


class EnsureSomeVersionTests(TestCase):
    def test_added(self):
        self.assertEqual("debhelper", ensure_some_version("", "debhelper"))
        # debhelper is a build system, so it should come before "blah" (wrap-and-sort order)
        self.assertEqual("debhelper, blah", ensure_some_version("blah", "debhelper"))

    def test_unchanged(self):
        self.assertEqual(
            "debhelper (>= 9)", ensure_some_version("debhelper (>= 9)", "debhelper")
        )
        self.assertEqual(
            "debhelper (= 9)", ensure_some_version("debhelper (= 9)", "debhelper")
        )
        self.assertEqual(
            "debhelper (>= 9)", ensure_some_version("debhelper (>= 9)", "debhelper")
        )
        self.assertEqual("debhelper", ensure_some_version("debhelper", "debhelper"))


class EnsureExactVersionTests(TestCase):
    def test_added(self):
        self.assertEqual("debhelper (= 9)", ensure_exact_version("", "debhelper", "9"))
        # debhelper is a build system, so it should come before "blah" (wrap-and-sort order)
        self.assertEqual(
            "debhelper (= 9), blah", ensure_exact_version("blah", "debhelper", "9")
        )

    def test_unchanged(self):
        self.assertEqual(
            "debhelper (= 9)", ensure_exact_version("debhelper (= 9)", "debhelper", "9")
        )

    def test_updated(self):
        self.assertEqual(
            "debhelper (= 9)",
            ensure_exact_version("debhelper (>= 9)", "debhelper", "9"),
        )
        self.assertEqual(
            "debhelper (= 9)", ensure_exact_version("debhelper", "debhelper", "9")
        )
        self.assertEqual(
            "blah, debhelper (= 9)",
            ensure_exact_version("blah, debhelper", "debhelper", "9"),
        )
        self.assertEqual(
            "blah, debhelper (= 9)",
            ensure_exact_version("blah, debhelper (= 8)", "debhelper", "9"),
        )
        self.assertEqual(
            "blah, debhelper (= 9)",
            ensure_exact_version("blah, debhelper (= 10)", "debhelper", "9"),
        )
        self.assertEqual(
            """
 debhelper-compat (= 12),
 pkg-config,
 uuid-dev""",
            ensure_exact_version(
                """
 debhelper-compat (= 11),
 pkg-config,
 uuid-dev""",
                "debhelper-compat",
                "12",
            ),
        )


class DropDependencyTests(TestCase):
    def test_deleted(self):
        self.assertEqual(
            "debhelper (>= 9)",
            drop_dependency("debhelper (>= 9), dh-autoreconf", "dh-autoreconf"),
        )
        self.assertEqual(
            "dh-autoreconf",
            drop_dependency("debhelper (>= 9), dh-autoreconf", "debhelper"),
        )
        self.assertEqual("", drop_dependency("debhelper (>= 9)", "debhelper"))
        self.assertEqual(
            "debhelper-compat (= 12)",
            drop_dependency("debhelper (>= 9), debhelper-compat (= 12)", "debhelper"),
        )


class AddDependencyTests(TestCase):
    def test_added(self):
        self.assertEqual(
            "debhelper (>= 9), dh-autoreconf",
            add_dependency("debhelper (>= 9)", "dh-autoreconf"),
        )
        self.assertEqual(
            "debhelper (>= 9), ${misc:Depends}",
            add_dependency("debhelper (>= 9)", "${misc:Depends}"),
        )
        # debhelper is a build system, so it should come before "blah" (wrap-and-sort order)
        self.assertEqual(
            "debhelper (>= 9), blah,", add_dependency("debhelper (>= 9),", "blah")
        )

    def test_indentation(self):
        self.assertEqual(
            """foo,
    bar,
    blah""",
            add_dependency(
                """foo,
    bar""",
                "blah",
            ),
        )
        self.assertEqual(
            """foo,
 bar,
 blah""",
            add_dependency(
                """foo,
 bar""",
                "blah",
            ),
        )
        self.assertEqual(
            """foo,
 bar,
 blah""",
            add_dependency(
                """foo,
 bar
""",
                "blah",
            ),
        )

    def test_insert(self):
        self.assertEqual(
            """blah,
    foo,
    bar""",
            add_dependency(
                """foo,
    bar""",
                "blah",
                position=0,
            ),
        )
        self.assertEqual(
            """foo,
    blah,
    bar""",
            add_dependency(
                """foo,
    bar""",
                "blah",
                position=1,
            ),
        )

    def test_odd_syntax(self):
        self.assertEqual(
            """
 foo
 , bar
 , blah""",
            add_dependency(
                """
 foo
 , bar
""",
                "blah",
            ),
        )
        self.assertEqual(
            """
 foo
 , blah
 , bar""",
            add_dependency(
                """
 foo
 , bar
""",
                "blah",
                position=1,
            ),
        )


class GetRelationTests(TestCase):
    def test_missing(self):
        self.assertRaises(KeyError, get_relation, "", "debhelper")
        self.assertRaises(KeyError, get_relation, "blah", "debhelper")

    def test_simple(self):
        self.assertEqual(
            (0, [PkgRelation("debhelper", (">=", "9"))]),
            get_relation("debhelper (>= 9)", "debhelper"),
        )
        self.assertEqual(
            (1, [PkgRelation("debhelper", ("=", "9"))]),
            get_relation("blah, debhelper (= 9)", "debhelper"),
        )

    def test_complex(self):
        self.assertRaises(
            ValueError, get_relation, "blah | debhelper (= 9)", "debhelper"
        )
        self.assertRaises(
            ValueError,
            get_relation,
            "blah, debhelper (= 9) | debhelper (<< 10)",
            "debhelper",
        )


class IterRelationsTests(TestCase):
    def test_missing(self):
        self.assertEqual([], list(iter_relations("", "debhelper")))
        self.assertEqual([], list(iter_relations("blah", "debhelper")))

    def test_simple(self):
        self.assertEqual(
            [(0, [PkgRelation("debhelper", (">=", "9"))])],
            list(iter_relations("debhelper (>= 9)", "debhelper")),
        )
        self.assertEqual(
            [(1, [PkgRelation("debhelper", ("=", "9"))])],
            list(iter_relations("blah, debhelper (= 9)", "debhelper")),
        )

    def test_complex(self):
        self.assertEqual(
            [(0, [PkgRelation("blah"), PkgRelation("debhelper", ("=", "9"))])],
            list(iter_relations("blah | debhelper (= 9)", "debhelper")),
        )
        self.assertEqual(
            [
                (
                    1,
                    [
                        PkgRelation("debhelper", ("=", "9")),
                        PkgRelation("debhelper", ("<<", "10")),
                    ],
                )
            ],
            list(
                iter_relations("blah, debhelper (= 9) | debhelper (<< 10)", "debhelper")
            ),
        )


class DeleteFromListTests(TestCase):
    def test_intermediate(self):
        self.assertEqual("a, c", delete_from_list("a, b, c", "b"))
        self.assertEqual("a, c", delete_from_list("a, b, c", "b "))

    def test_head(self):
        self.assertEqual("b, c", delete_from_list("a, b, c", "a"))
        self.assertEqual(" b, c", delete_from_list(" a, b, c", "a"))

    def test_tail(self):
        self.assertEqual("a, b", delete_from_list("a, b, c", "c"))
        self.assertEqual("a, b", delete_from_list("a, b , c", "c"))

    def test_only(self):
        self.assertEqual("a", delete_from_list("a", "c"))
        self.assertEqual("", delete_from_list("a", "a"))


class IsDepImpliedTests(TestCase):
    def parse(self, p):
        [dep] = PkgRelation.parse(p)
        return dep

    def test_no_version(self):
        self.assertTrue(is_dep_implied(self.parse("bzr"), self.parse("bzr")))
        self.assertTrue(is_dep_implied(self.parse("bzr"), self.parse("bzr (>= 3)")))
        self.assertTrue(is_dep_implied(self.parse("bzr"), self.parse("bzr (<< 3)")))

    def test_wrong_package(self):
        self.assertFalse(is_dep_implied(self.parse("bzr"), self.parse("foo (<< 3)")))

    def test_version(self):
        self.assertFalse(
            is_dep_implied(self.parse("bzr (>= 3)"), self.parse("bzr (<< 3)"))
        )
        self.assertTrue(
            is_dep_implied(self.parse("bzr (>= 3)"), self.parse("bzr (= 3)"))
        )
        self.assertFalse(
            is_dep_implied(self.parse("bzr (= 3)"), self.parse("bzr (>= 3)"))
        )
        self.assertFalse(
            is_dep_implied(self.parse("bzr (>= 3)"), self.parse("bzr (>> 3)"))
        )
        self.assertFalse(
            is_dep_implied(self.parse("bzr (= 3)"), self.parse("bzr (= 4)"))
        )
        self.assertFalse(
            is_dep_implied(self.parse("bzr (>= 3)"), self.parse("bzr (>= 2)"))
        )
        self.assertTrue(
            is_dep_implied(self.parse("bzr (>= 3)"), self.parse("bzr (>= 3)"))
        )
        self.assertTrue(is_dep_implied(self.parse("bzr"), self.parse("bzr (<< 3)")))
        self.assertTrue(
            is_dep_implied(self.parse("bzr (<< 3)"), self.parse("bzr (<< 3)"))
        )
        self.assertTrue(
            is_dep_implied(self.parse("bzr (<= 3)"), self.parse("bzr (<< 3)"))
        )
        self.assertFalse(
            is_dep_implied(self.parse("bzr (>= 2)"), self.parse("bzr (<< 3)"))
        )
        self.assertFalse(
            is_dep_implied(self.parse("bzr (<< 2)"), self.parse("bzr (<< 3)"))
        )
        self.assertFalse(
            is_dep_implied(self.parse("bzr (<= 2)"), self.parse("bzr (<< 3)"))
        )
        self.assertTrue(
            is_dep_implied(self.parse("bzr (<= 5)"), self.parse("bzr (<< 3)"))
        )
        self.assertTrue(
            is_dep_implied(self.parse("bzr (<= 5)"), self.parse("bzr (= 3)"))
        )
        self.assertFalse(
            is_dep_implied(self.parse("bzr (<= 5)"), self.parse("bzr (>= 3)"))
        )
        self.assertTrue(
            is_dep_implied(self.parse("bzr (>> 5)"), self.parse("bzr (>> 6)"))
        )
        self.assertTrue(
            is_dep_implied(self.parse("bzr (>> 5)"), self.parse("bzr (>> 5)"))
        )
        self.assertFalse(
            is_dep_implied(self.parse("bzr (>> 5)"), self.parse("bzr (>> 4)"))
        )
        self.assertTrue(
            is_dep_implied(self.parse("bzr (>> 5)"), self.parse("bzr (= 6)"))
        )
        self.assertFalse(
            is_dep_implied(self.parse("bzr (>> 5)"), self.parse("bzr (= 5)"))
        )
        self.assertTrue(
            is_dep_implied(self.parse("bzr:any (>> 5)"), self.parse("bzr:any (= 6)"))
        )


class IsRelationImpliedTests(TestCase):
    def test_unrelated(self):
        self.assertFalse(is_relation_implied("bzr", "bar"))
        self.assertFalse(is_relation_implied("bzr (= 3)", "bar"))
        self.assertFalse(is_relation_implied("bzr (= 3) | foo", "bar"))
        self.assertFalse(is_relation_implied("bzr (= 3)", ""))

    def test_too_old(self):
        self.assertFalse(is_relation_implied("bzr (= 3)", "bzr"))
        self.assertFalse(is_relation_implied("bzr (= 3)", "bzr (= 2)"))
        self.assertFalse(is_relation_implied("bzr (= 3)", "bzr (>= 2)"))

    def test_ors(self):
        self.assertFalse(is_relation_implied("bzr (= 3)", "bzr | foo"))
        self.assertTrue(is_relation_implied("bzr", "bzr | foo"))
        self.assertTrue(is_relation_implied("bzr | foo", "bzr | foo"))

    def test_implied(self):
        self.assertTrue(is_relation_implied("bzr (= 3)", "bzr (= 3)"))
        self.assertTrue(is_relation_implied("bzr (>= 3)", "bzr (>= 4)"))
        self.assertTrue(is_relation_implied("bzr (>= 4)", "bzr (>= 4)"))
        self.assertTrue(is_relation_implied("bzr", "bzr"))
        self.assertTrue(is_relation_implied("bzr | foo", "bzr"))
        self.assertFalse(is_relation_implied("bzr (= 3)", "bzr (>= 3)"))
        self.assertTrue(
            is_relation_implied("python3:any | dh-sequence-python3", "python3:any")
        )
        self.assertTrue(
            is_relation_implied(
                "python3:any | python3-dev:any | dh-sequence-python3",
                "python3:any | python3-dev:any",
            )
        )


class CdbsResolverConflictTests(TestCase):
    def test_build_depends(self):
        val = _cdbs_resolve_conflict(
            ("Source", "libnetsds-perl"),
            "Build-Depends",
            "debhelper (>= 6), foo",
            "@cdbs@",
            "debhelper (>= 10), foo",
        )
        # debhelper is a build system, so it comes before @cdbs@ template var (wrap-and-sort order)
        self.assertEqual(val, "debhelper (>= 10), @cdbs@")
        val = _cdbs_resolve_conflict(
            ("Source", "libnetsds-perl"),
            "Build-Depends",
            "debhelper (>= 6), foo",
            "@cdbs@, foo",
            "debhelper (>= 10), foo",
        )
        self.assertEqual(val, "@cdbs@, debhelper (>= 10), foo")
        val = _cdbs_resolve_conflict(
            ("Source", "libnetsds-perl"),
            "Build-Depends",
            "debhelper (>= 6), foo",
            "@cdbs@, debhelper (>= 9)",
            "debhelper (>= 10), foo",
        )
        self.assertEqual(val, "@cdbs@, debhelper (>= 10)")


class ParseStandardsVersionTests(TestCase):
    def test_parse(self):
        self.assertEqual((4, 5, 0), parse_standards_version("4.5.0"))


class GuessTemplateTypeTests(TestCaseInTempDir):
    def setUp(self):
        super().setUp()
        os.mkdir("debian")

    def test_rules_generates_control(self):
        with open("debian/rules", "w") as f:
            f.write(
                """\
%:
    dh $@

debian/control: debian/control.in
    cp $@ $<
"""
            )
        self.assertEqual("rules", guess_template_type("debian/control.in", "debian"))

        with open("debian/rules", "w") as f:
            f.write(
                """\
%:
    dh $@

debian/%: debian/%.in
    cp $@ $<
"""
            )
        self.assertEqual("rules", guess_template_type("debian/control.in", "debian"))

    def test_blends(self):
        with open("debian/rules", "w") as f:
            f.write(
                """\
%:
    dh $@

include /usr/share/blends-dev/rules
"""
            )
        self.assertEqual("rules", guess_template_type("debian/control.stub", "debian"))

    def test_no_build_depends(self):
        with open("debian/control.in", "w") as f:
            # No paragraph
            f.write("")

        self.assertIs(None, guess_template_type("debian/control.in", "debian"))

        with open("debian/control.in", "w") as f:
            f.write(
                """\
Source: blah
Build-Depends: cdbs
Vcs-Git: file://

Package: bar
"""
            )
        self.assertEqual("cdbs", guess_template_type("debian/control.in", "debian"))

        with open("debian/control.in", "w") as f:
            f.write(
                """\
Source: blah
Vcs-Git: file://

Package: bar
"""
            )
        self.assertIs(None, guess_template_type("debian/control.in", "debian"))

    def test_cdbs(self):
        with open("debian/control.in", "w") as f:
            f.write(
                """\
Source: blah
Build-Depends: debhelper, cdbs
"""
            )
        self.assertEqual("cdbs", guess_template_type("debian/control.in"))

    def test_multiple_paragraphs(self):
        with open("debian/control.in", "w") as f:
            f.write(
                """\
Source: blah
Build-Depends: debhelper, cdbs

Package: foo
"""
            )
        self.assertEqual("cdbs", guess_template_type("debian/control.in"))

    def test_directory(self):
        os.mkdir("debian/control.in")
        self.assertEqual("directory", guess_template_type("debian/control.in"))

    def test_debcargo(self):
        with open("debian/control.in", "w") as f:
            f.write(
                """\
Source: blah
Build-Depends: bar
"""
            )
        with open("debian/debcargo.toml", "w") as f:
            f.write("maintainer = Joe Example <joe@example.com>\n")
        self.assertEqual("debcargo", guess_template_type("debian/control.in", "debian"))


class RelationsAreSortedTests(TestCase):
    def test_sorted(self):
        self.assertTrue(relations_are_sorted("a, b, c", DefaultSortingOrder()))
        self.assertTrue(
            relations_are_sorted("a (>= 1), b (>= 2), c (>= 3)", DefaultSortingOrder())
        )
        self.assertTrue(
            relations_are_sorted("a, a (>= 1), b, b (>= 2), c", DefaultSortingOrder())
        )

    def test_not_sorted(self):
        self.assertFalse(relations_are_sorted("b, a, c", DefaultSortingOrder()))
        self.assertFalse(
            relations_are_sorted("a (>= 2), a (>= 1), b, c", DefaultSortingOrder())
        )
        self.assertFalse(relations_are_sorted("a, c, b", DefaultSortingOrder()))


class WrapAndSortOrderTests(TestCase):
    def test_build_systems_sorted_first(self):
        """Test that build system packages are sorted before regular packages."""
        order = WrapAndSortOrder()
        self.assertTrue(order.lt("debhelper-compat", "python3"))
        self.assertTrue(order.lt("cdbs", "zlib1g-dev"))
        self.assertTrue(order.lt("dpkg-dev", "apt"))

    def test_substvars_sorted_last(self):
        """Test that substvars are sorted after regular packages."""
        order = WrapAndSortOrder()
        self.assertTrue(order.lt("python3", "${misc:Depends}"))
        self.assertTrue(order.lt("zlib1g-dev", "${shlibs:Depends}"))
        self.assertFalse(order.lt("${misc:Depends}", "python3"))

    def test_regular_packages_sorted_alphabetically(self):
        """Test that regular packages are sorted alphabetically."""
        order = WrapAndSortOrder()
        self.assertTrue(order.lt("aaa", "bbb"))
        self.assertTrue(order.lt("python3", "zlib1g-dev"))
        self.assertFalse(order.lt("zlib1g-dev", "python3"))

    def test_build_systems_sorted_alphabetically_within_group(self):
        """Test that build system packages are sorted alphabetically within their group."""
        order = WrapAndSortOrder()
        self.assertTrue(order.lt("cdbs", "debhelper-compat"))
        self.assertTrue(order.lt("debhelper", "dpkg-dev"))

    def test_substvars_sorted_alphabetically_within_group(self):
        """Test that substvars are sorted alphabetically within their group."""
        order = WrapAndSortOrder()
        self.assertTrue(order.lt("${misc:Depends}", "${shlibs:Depends}"))
        self.assertFalse(order.lt("${shlibs:Depends}", "${misc:Depends}"))

    def test_three_group_sorting(self):
        """Test the complete three-group sorting order."""
        order = WrapAndSortOrder()
        # Build system < Regular < Substvar
        self.assertTrue(order.lt("debhelper-compat", "python3"))
        self.assertTrue(order.lt("python3", "${misc:Depends}"))
        self.assertTrue(order.lt("debhelper-compat", "${misc:Depends}"))

    def test_relations_are_sorted_with_wrap_and_sort_order(self):
        """Test relations_are_sorted with WrapAndSortOrder."""
        order = WrapAndSortOrder()
        # Properly sorted: build systems, then regular packages, then substvars
        self.assertTrue(
            relations_are_sorted(
                "debhelper-compat (= 13), python3, zlib1g-dev, ${misc:Depends}",
                order,
            )
        )
        self.assertTrue(
            relations_are_sorted(
                "cdbs, debhelper, apt, python3, ${shlibs:Depends}", order
            )
        )

    def test_relations_not_sorted_with_wrap_and_sort_order(self):
        """Test detection of unsorted relations with WrapAndSortOrder."""
        order = WrapAndSortOrder()
        # Regular package before build system - not sorted
        self.assertFalse(relations_are_sorted("python3, debhelper-compat", order))
        # Substvar before regular package - not sorted
        self.assertFalse(relations_are_sorted("${misc:Depends}, python3", order))
        # Wrong alphabetical order within regular packages
        self.assertFalse(relations_are_sorted("zlib1g-dev, python3", order))

    def test_no_packages_ignored(self):
        """Test that WrapAndSortOrder doesn't ignore any packages."""
        order = WrapAndSortOrder()
        self.assertFalse(order.ignore("python3"))
        self.assertFalse(order.ignore("${misc:Depends}"))
        self.assertFalse(order.ignore("debhelper-compat"))
        self.assertFalse(order.ignore("@cdbs@"))


class AddDependencyWithWrapAndSortOrderTests(TestCase):
    def test_add_build_system_to_wrap_and_sort_sorted_list(self):
        """Test adding a build system package maintains wrap-and-sort order."""
        # List sorted with wrap-and-sort order: build systems first
        result = add_dependency(
            "debhelper-compat (= 13), python3-dev, zlib1g-dev",
            "cdbs",
        )
        # cdbs should be inserted before debhelper-compat (alphabetically within build systems)
        self.assertEqual(
            "cdbs, debhelper-compat (= 13), python3-dev, zlib1g-dev", result
        )

    def test_add_regular_package_to_wrap_and_sort_sorted_list(self):
        """Test adding a regular package maintains wrap-and-sort order."""
        # List sorted with wrap-and-sort order
        result = add_dependency(
            "debhelper-compat (= 13), apt, zlib1g-dev, ${misc:Depends}",
            "python3-dev",
        )
        # python3-dev should be inserted in alphabetical order after apt but before zlib1g-dev
        self.assertEqual(
            "debhelper-compat (= 13), apt, python3-dev, zlib1g-dev, ${misc:Depends}",
            result,
        )

    def test_add_substvar_to_wrap_and_sort_sorted_list(self):
        """Test adding a substvar maintains wrap-and-sort order."""
        result = add_dependency(
            "debhelper-compat (= 13), python3-dev, ${misc:Depends}",
            "${shlibs:Depends}",
        )
        # ${shlibs:Depends} should be added at the end (after ${misc:Depends})
        self.assertEqual(
            "debhelper-compat (= 13), python3-dev, ${misc:Depends}, ${shlibs:Depends}",
            result,
        )

    def test_add_to_default_sorted_list_still_works(self):
        """Test that DefaultSortingOrder still works for simple alphabetical lists."""
        # Simple alphabetical list (no build systems or substvars)
        result = add_dependency("aaa, ccc", "bbb")
        # bbb should be inserted between aaa and ccc
        self.assertEqual("aaa, bbb, ccc", result)

    def test_add_to_unsorted_list_appends(self):
        """Test that adding to an unsorted list just appends."""
        result = add_dependency("zlib1g-dev, python3-dev", "apt")
        # Unsorted list, so just append
        self.assertEqual("zlib1g-dev, python3-dev, apt", result)

    def test_add_build_system_before_regular_packages(self):
        """Test adding build system to list with only regular packages."""
        # Regular packages only
        result = add_dependency("python3-dev, zlib1g-dev", "debhelper-compat")
        # debhelper-compat should go first (build system before regular)
        self.assertEqual("debhelper-compat, python3-dev, zlib1g-dev", result)
