# Copyright (C) 2010 by Canonical Ltd
#
# 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

import os
import subprocess
import sys

import breezy
from breezy import commands, osutils, tests
from breezy.plugins.bash_completion.bashcomp import *
from breezy.tests import features


class BashCompletionMixin:
    """Component for testing execution of a bash completion script."""

    _test_needs_features = [features.bash_feature]
    script = None

    def complete(self, words, cword=-1):
        """Perform a bash completion.

        :param words: a list of words representing the current command.
        :param cword: the current word to complete, defaults to the last one.
        """
        if self.script is None:
            self.script = self.get_script()
        env = dict(os.environ)
        env["PYTHONPATH"] = ":".join(sys.path)
        proc = subprocess.Popen(
            [features.bash_feature.path, "--noprofile"],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            env=env,
        )
        if cword < 0:
            cword = len(words) + cword
        encoding = osutils.get_user_encoding()
        input = b"%s\n" % self.script.encode(encoding)
        input += b"COMP_WORDS=( %s )\n" % b" ".join(
            [b"'" + w.replace("'", "'\\''").encode(encoding) + b"'" for w in words]
        )
        input += b"COMP_CWORD=%d\n" % cword
        input += b"%s\n" % getattr(self, "script_name", "_brz").encode(encoding)
        input += b"echo ${#COMPREPLY[*]}\n"
        input += b"IFS=$'\\n'\n"
        input += b'echo "${COMPREPLY[*]}"\n'
        (out, err) = proc.communicate(input)
        errlines = [
            line for line in err.splitlines() if not line.startswith(b"brz: warning: ")
        ]
        if [] != errlines:
            raise AssertionError("Unexpected error message:\n%s" % err)
        self.assertEqual(b"", b"".join(errlines), "No messages to standard error")
        # import sys
        # print >>sys.stdout, '---\n%s\n---\n%s\n---\n' % (input, out)
        lines = out.split(b"\n")
        nlines = int(lines[0])
        del lines[0]
        self.assertEqual(b"", lines[-1], "Newline at end")
        del lines[-1]
        if nlines == 0 and len(lines) == 1 and lines[0] == b"":
            del lines[0]
        self.assertEqual(nlines, len(lines), "No newlines in generated words")
        self.completion_result = {l.decode(encoding) for l in lines}
        return self.completion_result

    def assertCompletionEquals(self, *words):
        self.assertEqual(set(words), self.completion_result)

    def assertCompletionContains(self, *words):
        missing = set(words) - self.completion_result
        if missing:
            raise AssertionError(
                "Completion should contain %r but it has %r"
                % (missing, self.completion_result)
            )

    def assertCompletionOmits(self, *words):
        surplus = set(words) & self.completion_result
        if surplus:
            raise AssertionError(
                "Completion should omit %r but it has %r"
                % (surplus, self.completion_result)
            )

    def get_script(self):
        commands.install_bzr_command_hooks()
        dc = DataCollector()
        data = dc.collect()
        cg = BashCodeGen(data)
        res = cg.function()
        return res


class TestBashCompletion(tests.TestCase, BashCompletionMixin):
    """Test bash completions that don't execute brz."""

    def test_simple_scipt(self):
        """Ensure that the test harness works as expected"""
        self.script = """
_brz() {
    COMPREPLY=()
    # add all words in reverse order, with some markup around them
    for ((i = ${#COMP_WORDS[@]}; i > 0; --i)); do
        COMPREPLY+=( "-${COMP_WORDS[i-1]}+" )
    done
    # and append the current word
    COMPREPLY+=( "+${COMP_WORDS[COMP_CWORD]}-" )
}
"""
        self.complete(["foo", '"bar', "'baz"], cword=1)
        self.assertCompletionEquals("-'baz+", '-"bar+', "-foo+", '+"bar-')

    def test_cmd_ini(self):
        self.complete(["brz", "ini"])
        self.assertCompletionContains(
            "init", "init-shared-repo", "init-shared-repository"
        )
        self.assertCompletionOmits("commit")

    def test_init_opts(self):
        self.complete(["brz", "init", "-"])
        self.assertCompletionContains("-h", "--format=2a")

    def test_global_opts(self):
        self.complete(["brz", "-", "init"], cword=1)
        self.assertCompletionContains("--no-plugins", "--builtin")

    def test_commit_dashm(self):
        self.complete(["brz", "commit", "-m"])
        self.assertCompletionEquals("-m")

    def test_status_negated(self):
        self.complete(["brz", "status", "--n"])
        self.assertCompletionContains("--no-versioned", "--no-verbose")

    def test_init_format_any(self):
        self.complete(["brz", "init", "--format", "=", "directory"], cword=3)
        self.assertCompletionContains("1.9", "2a")

    def test_init_format_2(self):
        self.complete(["brz", "init", "--format", "=", "2", "directory"], cword=4)
        self.assertCompletionContains("2a")
        self.assertCompletionOmits("1.9")


class TestBashCompletionInvoking(tests.TestCaseWithTransport, BashCompletionMixin):
    """Test bash completions that might execute brz.

    Only the syntax ``$(brz ...`` is supported so far. The brz command
    will be replaced by the brz instance running this selftest.
    """

    def setUp(self):
        super().setUp()
        if sys.platform == "win32":
            raise tests.KnownFailure("see bug #709104, completion is broken on windows")

    def get_script(self):
        s = super().get_script()
        s = s.replace("$(brz ", "$(%s " % " ".join(self.get_brz_command()))
        s = s.replace("2>/dev/null", "")
        return s

    def test_revspec_tag_all(self):
        self.requireFeature(features.sed_feature)
        wt = self.make_branch_and_tree(".")
        wt.branch.tags.set_tag("tag1", b"null:")
        wt.branch.tags.set_tag("tag2", b"null:")
        wt.branch.tags.set_tag("3tag", b"null:")
        self.complete(["brz", "log", "-r", "tag", ":"])
        self.assertCompletionEquals("tag1", "tag2", "3tag")

    def test_revspec_tag_prefix(self):
        self.requireFeature(features.sed_feature)
        wt = self.make_branch_and_tree(".")
        wt.branch.tags.set_tag("tag1", b"null:")
        wt.branch.tags.set_tag("tag2", b"null:")
        wt.branch.tags.set_tag("3tag", b"null:")
        self.complete(["brz", "log", "-r", "tag", ":", "t"])
        self.assertCompletionEquals("tag1", "tag2")

    def test_revspec_tag_spaces(self):
        self.requireFeature(features.sed_feature)
        wt = self.make_branch_and_tree(".")
        wt.branch.tags.set_tag("tag with spaces", b"null:")
        self.complete(["brz", "log", "-r", "tag", ":", "t"])
        self.assertCompletionEquals(r"tag\ with\ spaces")
        self.complete(["brz", "log", "-r", '"tag:t'])
        self.assertCompletionEquals("tag:tag with spaces")
        self.complete(["brz", "log", "-r", "'tag:t"])
        self.assertCompletionEquals("tag:tag with spaces")

    def test_revspec_tag_endrange(self):
        self.requireFeature(features.sed_feature)
        wt = self.make_branch_and_tree(".")
        wt.branch.tags.set_tag("tag1", b"null:")
        wt.branch.tags.set_tag("tag2", b"null:")
        self.complete(["brz", "log", "-r", "3..tag", ":", "t"])
        self.assertCompletionEquals("tag1", "tag2")
        self.complete(["brz", "log", "-r", '"3..tag:t'])
        self.assertCompletionEquals("3..tag:tag1", "3..tag:tag2")
        self.complete(["brz", "log", "-r", "'3..tag:t"])
        self.assertCompletionEquals("3..tag:tag1", "3..tag:tag2")


class TestBashCodeGen(tests.TestCase):
    def test_command_names(self):
        data = CompletionData()
        bar = CommandData("bar")
        bar.aliases.append("baz")
        data.commands.append(bar)
        data.commands.append(CommandData("foo"))
        cg = BashCodeGen(data)
        self.assertEqual("bar baz foo", cg.command_names())

    def test_debug_output(self):
        data = CompletionData()
        self.assertEqual("", BashCodeGen(data, debug=False).debug_output())
        self.assertTrue(BashCodeGen(data, debug=True).debug_output())

    def test_brz_version(self):
        data = CompletionData()
        cg = BashCodeGen(data)
        self.assertEqual("%s." % breezy.version_string, cg.brz_version())
        data.plugins["foo"] = PluginData("foo", "1.0")
        data.plugins["bar"] = PluginData("bar", "2.0")
        cg = BashCodeGen(data)
        self.assertEqual(
            """\
%s and the following plugins:
# bar 2.0
# foo 1.0"""
            % breezy.version_string,
            cg.brz_version(),
        )

    def test_global_options(self):
        data = CompletionData()
        data.global_options.add("--foo")
        data.global_options.add("--bar")
        cg = BashCodeGen(data)
        self.assertEqual("--bar --foo", cg.global_options())

    def test_command_cases(self):
        data = CompletionData()
        bar = CommandData("bar")
        bar.aliases.append("baz")
        bar.options.append(OptionData("--opt"))
        data.commands.append(bar)
        data.commands.append(CommandData("foo"))
        cg = BashCodeGen(data)
        self.assertEqualDiff(
            """\
\tbar|baz)
\t\tcmdOpts=( --opt )
\t\t;;
\tfoo)
\t\tcmdOpts=(  )
\t\t;;
""",
            cg.command_cases(),
        )

    def test_command_case(self):
        cmd = CommandData("cmd")
        cmd.plugin = PluginData("plugger", "1.0")
        bar = OptionData("--bar")
        bar.registry_keys = ["that", "this"]
        bar.error_messages.append("Some error message")
        cmd.options.append(bar)
        cmd.options.append(OptionData("--foo"))
        data = CompletionData()
        data.commands.append(cmd)
        cg = BashCodeGen(data)
        self.assertEqualDiff(
            """\
\tcmd)
\t\t# plugin "plugger 1.0"
\t\t# Some error message
\t\tcmdOpts=( --bar=that --bar=this --foo )
\t\tcase $curOpt in
\t\t\t--bar) optEnums=( that this ) ;;
\t\tesac
\t\t;;
""",
            cg.command_case(cmd),
        )


class TestDataCollector(tests.TestCase):
    def setUp(self):
        super().setUp()
        commands.install_bzr_command_hooks()

    def test_global_options(self):
        dc = DataCollector()
        dc.global_options()
        self.assertSubset(["--no-plugins", "--builtin"], dc.data.global_options)

    def test_commands(self):
        dc = DataCollector()
        dc.commands()
        self.assertSubset(
            ["init", "init-shared-repo", "init-shared-repository"],
            dc.data.all_command_aliases(),
        )

    def test_commands_from_plugins(self):
        dc = DataCollector()
        dc.commands()
        self.assertSubset(["bash-completion"], dc.data.all_command_aliases())

    def test_commit_dashm(self):
        dc = DataCollector()
        cmd = dc.command("commit")
        self.assertSubset(["-m"], [str(o) for o in cmd.options])

    def test_status_negated(self):
        dc = DataCollector()
        cmd = dc.command("status")
        self.assertSubset(
            ["--no-versioned", "--no-verbose"], [str(o) for o in cmd.options]
        )

    def test_init_format(self):
        dc = DataCollector()
        cmd = dc.command("init")
        for opt in cmd.options:
            if opt.name == "--format":
                self.assertSubset(["2a"], opt.registry_keys)
                return
        raise AssertionError("Option --format not found")


class BlackboxTests(tests.TestCaseWithMemoryTransport):
    def test_bash_completion(self):
        self.run_bzr("bash-completion", encoding="utf-8")
