#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
#

# Pylint doesn't play well with fixtures and dependency injection from pytest
# pylint: disable=redefined-outer-name

import os
import pytest
from buildstream._testing import cli  # pylint: disable=unused-import

# Project directory
DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "completions")

MAIN_COMMANDS = ["artifact ", "build ", "help ", "init ", "shell ", "show ", "source ", "workspace "]

MAIN_OPTIONS = [
    "--builders ",
    "-c ",
    "-C ",
    "--cache-buildtrees ",
    "--colors ",
    "--config ",
    "--debug ",
    "--default-mirror ",
    "--directory ",
    "--error-lines ",
    "--fetchers ",
    "--log-file ",
    "--max-jobs ",
    "--message-lines ",
    "--network-retries ",
    "--no-colors ",
    "--no-debug ",
    "--no-interactive ",
    "--no-strict ",
    "--no-verbose ",
    "-o ",
    "--option ",
    "--on-error ",
    "--pull-buildtrees ",
    "--pushers ",
    "--strict ",
    "--verbose ",
    "--version ",
]

SOURCE_COMMANDS = [
    "checkout ",
    "fetch ",
    "push ",
    "track ",
]

ARTIFACT_COMMANDS = [
    "checkout ",
    "delete ",
    "push ",
    "pull ",
    "log ",
    "list-contents ",
    "show ",
]

WORKSPACE_COMMANDS = ["close ", "list ", "open ", "reset "]

PROJECT_ELEMENTS = [
    "compose-all.bst",
    "compose-exclude-dev.bst",
    "compose-include-bin.bst",
    "import-bin.bst",
    "import-dev.bst",
    "target.bst",
]

INVALID_ELEMENTS = [
    "target.foo",
    "target.bst.bar",
]

MIXED_ELEMENTS = PROJECT_ELEMENTS + INVALID_ELEMENTS


def assert_completion(cli, cmd, word_idx, expected, cwd=None):
    result = cli.run(
        project=".", cwd=cwd, env={"_BST_COMPLETION": "complete", "COMP_WORDS": cmd, "COMP_CWORD": str(word_idx)}
    )
    words = []
    if result.output:
        words = result.output.splitlines()

    # The order is meaningless, bash will
    # take the results and order it by its
    # own little heuristics
    words = sorted(words)
    expected = sorted(expected)
    assert words == expected


def assert_completion_failed(cli, cmd, word_idx, expected, cwd=None):
    result = cli.run(cwd=cwd, env={"_BST_COMPLETION": "complete", "COMP_WORDS": cmd, "COMP_CWORD": str(word_idx)})
    words = []
    if result.output:
        words = result.output.splitlines()

    # The order is meaningless, bash will
    # take the results and order it by its
    # own little heuristics
    words = sorted(words)
    expected = sorted(expected)
    assert words != expected


@pytest.mark.parametrize(
    "cmd,word_idx,expected",
    [
        ("bst", 0, []),
        ("bst ", 1, MAIN_COMMANDS),
        ("bst artifact ", 2, ARTIFACT_COMMANDS),
        ("bst source ", 2, SOURCE_COMMANDS),
        ("bst w ", 1, ["workspace "]),
        ("bst workspace ", 2, WORKSPACE_COMMANDS),
    ],
)
def test_commands(cli, cmd, word_idx, expected):
    assert_completion(cli, cmd, word_idx, expected)


@pytest.mark.parametrize(
    "cmd,word_idx,expected",
    [
        ("bst -", 1, MAIN_OPTIONS),
        ("bst --l", 1, ["--log-file "]),
        # Test that options of subcommands also complete
        (
            "bst --no-colors build -",
            3,
            [
                "--deps ",
                "-d ",
                "--artifact-remote ",
                "--source-remote ",
                "--ignore-project-artifact-remotes ",
                "--ignore-project-source-remotes ",
                "--retry-failed ",
                "-r ",
            ],
        ),
        # Test the behavior of completing after an option that has a
        # parameter that cannot be completed, vs an option that has
        # no parameter
        ("bst --fetchers ", 2, []),
        ("bst --no-colors ", 2, MAIN_COMMANDS),
    ],
)
def test_options(cli, cmd, word_idx, expected):
    assert_completion(cli, cmd, word_idx, expected)


@pytest.mark.parametrize(
    "cmd,word_idx,expected",
    [
        ("bst --on-error ", 2, ["continue ", "quit ", "terminate "]),
        ("bst --cache-buildtrees ", 2, ["always ", "auto ", "never "]),
        ("bst show --deps ", 3, ["all ", "build ", "none ", "run "]),
        ("bst show --deps=", 2, ["all ", "build ", "none ", "run "]),
        ("bst show --deps b", 3, ["build "]),
        ("bst show --deps=b", 2, ["build "]),
        ("bst show --deps r", 3, ["run "]),
        ("bst source track --deps ", 4, ["all ", "build ", "none ", "run "]),
    ],
)
def test_option_choice(cli, cmd, word_idx, expected):
    assert_completion(cli, cmd, word_idx, expected)


@pytest.mark.datafiles(os.path.join(DATA_DIR, "project"))
@pytest.mark.parametrize(
    "cmd,word_idx,expected,subdir",
    [
        # Note that elements/ and files/ are partial completions and
        # as such do not come with trailing whitespace
        ("bst --config ", 2, ["cache/", "elements/", "files/", "project.conf "], None),
        ("bst --log-file ", 2, ["cache/", "elements/", "files/", "project.conf "], None),
        ("bst --config f", 2, ["files/"], None),
        ("bst --log-file f", 2, ["files/"], None),
        ("bst --config files", 2, ["files/bin-files/", "files/dev-files/"], None),
        ("bst --log-file files", 2, ["files/bin-files/", "files/dev-files/"], None),
        ("bst --config files/", 2, ["files/bin-files/", "files/dev-files/"], None),
        ("bst --log-file elements/", 2, [os.path.join("elements", e) + " " for e in PROJECT_ELEMENTS], None),
        ("bst --config ../", 2, ["../cache/", "../elements/", "../files/", "../project.conf "], "files"),
        ("bst --config ../elements/", 2, [os.path.join("..", "elements", e) + " " for e in PROJECT_ELEMENTS], "files"),
        ("bst --config ../nofile", 2, [], "files"),
        ("bst --config /pony/rainbow/nobodyhas/this/file", 2, [], "files"),
    ],
)
def test_option_file(datafiles, cli, cmd, word_idx, expected, subdir):
    cwd = str(datafiles)
    if subdir:
        cwd = os.path.join(cwd, subdir)
    assert_completion(cli, cmd, word_idx, expected, cwd=cwd)


@pytest.mark.datafiles(os.path.join(DATA_DIR, "project"))
@pytest.mark.parametrize(
    "cmd,word_idx,expected,subdir",
    [
        # Note that regular files like project.conf are not returned when
        # completing for a directory
        ("bst --directory ", 2, ["cache/", "elements/", "files/"], None),
        ("bst --directory elements/", 2, [], None),
        ("bst --directory ", 2, ["dev-files/", "bin-files/"], "files"),
        ("bst --directory ../", 2, ["../cache/", "../elements/", "../files/"], "files"),
    ],
)
def test_option_directory(datafiles, cli, cmd, word_idx, expected, subdir):
    cwd = str(datafiles)
    if subdir:
        cwd = os.path.join(cwd, subdir)
    assert_completion(cli, cmd, word_idx, expected, cwd=cwd)


@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.parametrize(
    "project,cmd,word_idx,expected,subdir",
    [
        # When running in the project directory
        ("project", "bst show ", 2, [e + " " for e in PROJECT_ELEMENTS], None),
        (
            "project",
            "bst build com",
            2,
            ["compose-all.bst ", "compose-include-bin.bst ", "compose-exclude-dev.bst "],
            None,
        ),
        # When running from the files subdir
        ("project", "bst show ", 2, [e + " " for e in PROJECT_ELEMENTS], "files"),
        (
            "project",
            "bst build com",
            2,
            ["compose-all.bst ", "compose-include-bin.bst ", "compose-exclude-dev.bst "],
            "files",
        ),
        # When passing the project directory
        ("project", "bst --directory ../ show ", 4, [e + " " for e in PROJECT_ELEMENTS], "files"),
        (
            "project",
            "bst --directory ../ build com",
            4,
            ["compose-all.bst ", "compose-include-bin.bst ", "compose-exclude-dev.bst "],
            "files",
        ),
        # Also try multi arguments together
        ("project", "bst --directory ../ artifact checkout t ", 5, ["target.bst "], "files"),
        ("project", "bst --directory ../ artifact checkout --directory ", 6, ["bin-files/", "dev-files/"], "files"),
        # When running in the project directory
        ("no-element-path", "bst show ", 2, [e + " " for e in PROJECT_ELEMENTS] + ["files/"], None),
        (
            "no-element-path",
            "bst build com",
            2,
            ["compose-all.bst ", "compose-include-bin.bst ", "compose-exclude-dev.bst "],
            None,
        ),
        # When running from the files subdir
        ("no-element-path", "bst show ", 2, [e + " " for e in PROJECT_ELEMENTS] + ["files/"], "files"),
        (
            "no-element-path",
            "bst build com",
            2,
            ["compose-all.bst ", "compose-include-bin.bst ", "compose-exclude-dev.bst "],
            "files",
        ),
        # When passing the project directory
        ("no-element-path", "bst --directory ../ show ", 4, [e + " " for e in PROJECT_ELEMENTS] + ["files/"], "files"),
        ("no-element-path", "bst --directory ../ show f", 4, ["files/"], "files"),
        ("no-element-path", "bst --directory ../ show files/", 4, ["files/bin-files/", "files/dev-files/"], "files"),
        (
            "no-element-path",
            "bst --directory ../ build com",
            4,
            ["compose-all.bst ", "compose-include-bin.bst ", "compose-exclude-dev.bst "],
            "files",
        ),
        # Also try multi arguments together
        ("no-element-path", "bst --directory ../ artifact checkout t ", 5, ["target.bst "], "files"),
        (
            "no-element-path",
            "bst --directory ../ artifact checkout --directory ",
            6,
            ["bin-files/", "dev-files/"],
            "files",
        ),
        # When element-path have sub-folders
        ("sub-folders", "bst show base", 2, ["base/wanted.bst "], None),
        ("sub-folders", "bst show base/", 2, ["base/wanted.bst "], None),
    ],
)
def test_argument_element(datafiles, cli, project, cmd, word_idx, expected, subdir):
    cwd = os.path.join(str(datafiles), project)
    if subdir:
        cwd = os.path.join(cwd, subdir)
    assert_completion(cli, cmd, word_idx, expected, cwd=cwd)


@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.parametrize(
    "project,cmd,word_idx,expected,subdir",
    [
        # When element has invalid suffix
        ("project", "bst --directory ../ show ", 4, [e + " " for e in MIXED_ELEMENTS], "files")
    ],
)
def test_argument_element_invalid(datafiles, cli, project, cmd, word_idx, expected, subdir):
    cwd = os.path.join(str(datafiles), project)
    if subdir:
        cwd = os.path.join(cwd, subdir)
    assert_completion_failed(cli, cmd, word_idx, expected, cwd=cwd)


@pytest.mark.parametrize(
    "cmd,word_idx,expected",
    [
        ("bst he", 1, ["help "]),
        ("bst help ", 2, MAIN_COMMANDS),
        ("bst help artifact ", 3, ARTIFACT_COMMANDS),
        ("bst help in", 2, ["init "]),
        ("bst help source ", 3, SOURCE_COMMANDS),
        ("bst help artifact ", 3, ARTIFACT_COMMANDS),
        ("bst help w", 2, ["workspace "]),
        ("bst help workspace ", 3, WORKSPACE_COMMANDS),
    ],
)
def test_help_commands(cli, cmd, word_idx, expected):
    assert_completion(cli, cmd, word_idx, expected)


@pytest.mark.datafiles(os.path.join(DATA_DIR, "project"))
def test_argument_artifact(cli, datafiles):
    project = str(datafiles)

    # Build an import element with no dependencies (this will generate one artifact with 2 keys)
    result = cli.run(project=project, args=["build", "import-bin.bst"])  # Has no dependencies
    result.assert_success()

    # Use hard coded artifact names, cache keys should be stable now
    artifacts = [
        "test/import-bin/4066f11d4e3f681bc7b9f738af8093473478907946801be2f28033e0f4c904a1",
        "test/import-bin/b9f5aa9c50349a109431e2fbc23d3384a3a0bf490158b0352e896b82033c5137",
    ]

    # Test autocompletion of the artifact
    cmds = ["bst artifact log ", "bst artifact log t", "bst artifact log test/"]

    for i, cmd in enumerate(cmds):
        word_idx = 3
        result = cli.run(
            project=project,
            cwd=project,
            env={"_BST_COMPLETION": "complete", "COMP_WORDS": cmd, "COMP_CWORD": str(word_idx)},
        )

        if result.output:
            words = result.output.splitlines()  # This leaves an extra space on each e.g. ['foo.bst ']
            words = [word.strip() for word in words]

            # We should now be able to see the artifacts, but the order in which artifacts
            # are displayed in the completion list is not guaranteed to be ordered, so we
            # test for both orders.
            if i == 0:
                expected1 = PROJECT_ELEMENTS + artifacts
                expected2 = PROJECT_ELEMENTS + list(reversed(artifacts))
            elif i == 1:
                expected1 = ["target.bst"] + artifacts
                expected2 = ["target.bst"] + list(reversed(artifacts))
            elif i == 2:
                expected1 = artifacts
                expected2 = list(reversed(artifacts))
            else:
                assert False, "unreachable"

            assert words in (expected1, expected2)
