#
#  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 sys

import pytest

from buildstream import _yaml
from buildstream.exceptions import ErrorDomain, LoadErrorReason
from buildstream._testing.runcli import cli  # pylint: disable=unused-import

from tests.testutils.site import pip_sample_packages  # pylint: disable=unused-import
from tests.testutils.site import SAMPLE_PACKAGES_SKIP_REASON


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

# List of BuildStream protected variables
PROTECTED_VARIABLES = [("project-name"), ("element-name"), ("max-jobs")]


def print_warning(msg):
    RED, END = "\033[91m", "\033[0m"
    print(("\n{}{}{}").format(RED, msg, END), file=sys.stderr)


###############################################################
#  Test proper loading of some default commands from plugins  #
###############################################################
@pytest.mark.parametrize(
    "target,varname,expected",
    [("autotools.bst", "make-install", 'make -j1 DESTDIR="/buildstream-install" install')],
)
@pytest.mark.datafiles(os.path.join(DATA_DIR, "defaults"))
@pytest.mark.skipif("not pip_sample_packages()", reason=SAMPLE_PACKAGES_SKIP_REASON)
def test_defaults(cli, datafiles, target, varname, expected):
    project = str(datafiles)
    result = cli.run(project=project, silent=True, args=["show", "--deps", "none", "--format", "%{vars}", target])
    result.assert_success()
    result_vars = _yaml.load_data(result.output)
    assert result_vars.get_str(varname) == expected


################################################################
#  Test overriding of variables to produce different commands  #
################################################################
@pytest.mark.parametrize(
    "target,varname,expected",
    [("autotools.bst", "make-install", 'make -j1 DESTDIR="/custom/install/root" install')],
)
@pytest.mark.datafiles(os.path.join(DATA_DIR, "overrides"))
@pytest.mark.skipif("not pip_sample_packages()", reason=SAMPLE_PACKAGES_SKIP_REASON)
def test_overrides(cli, datafiles, target, varname, expected):
    project = str(datafiles)
    result = cli.run(project=project, silent=True, args=["show", "--deps", "none", "--format", "%{vars}", target])
    result.assert_success()
    result_vars = _yaml.load_data(result.output)
    assert result_vars.get_str(varname) == expected


@pytest.mark.parametrize(
    "element,provenance",
    [
        # This test makes a reference to an undefined variable in a build command
        ("manual.bst", "manual.bst [line 5 column 6]"),
        # This test makes a reference to an undefined variable by another variable,
        # ensuring that we validate variables even when they are unused
        ("manual2.bst", "manual2.bst [line 4 column 8]"),
        # This test uses a build command to refer to some variables which ultimately
        # refer to an undefined variable, testing a more complex case.
        ("manual3.bst", "manual3.bst [line 6 column 8]"),
    ],
    ids=["build-command", "variables", "complex"],
)
@pytest.mark.datafiles(os.path.join(DATA_DIR, "missing_variables"))
def test_undefined(cli, datafiles, element, provenance):
    project = str(datafiles)
    result = cli.run(project=project, silent=True, args=["show", "--deps", "none", "--format", "%{config}", element])
    result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.UNRESOLVED_VARIABLE)
    assert provenance in result.stderr


@pytest.mark.parametrize(
    "element,provenances",
    [
        # Test a simple a -> b and b -> a reference
        ("simple-cyclic.bst", ["simple-cyclic.bst [line 4 column 5]", "simple-cyclic.bst [line 5 column 5]"]),
        # Test a simple a -> b and b -> a reference with some text involved
        ("cyclic.bst", ["cyclic.bst [line 5 column 10]", "cyclic.bst [line 4 column 5]"]),
        # Test an indirect circular dependency
        (
            "indirect-cyclic.bst",
            [
                "indirect-cyclic.bst [line 5 column 5]",
                "indirect-cyclic.bst [line 6 column 5]",
                "indirect-cyclic.bst [line 7 column 5]",
                "indirect-cyclic.bst [line 8 column 5]",
            ],
        ),
        # Test an indirect circular dependency
        ("self-reference.bst", ["self-reference.bst [line 4 column 5]"]),
    ],
    ids=["simple", "simple-text", "indirect", "self-reference"],
)
@pytest.mark.timeout(30, method="signal")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "cyclic_variables"))
def test_circular_reference(cli, datafiles, element, provenances):
    print_warning("Performing cyclic test, if this test times out it will exit the test sequence")
    project = str(datafiles)
    result = cli.run(project=project, silent=True, args=["build", element])
    result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.CIRCULAR_REFERENCE_VARIABLE)
    for provenance in provenances:
        assert provenance in result.stderr


# Test that variables which refer to eachother very deeply are
# still resolved correctly, this ensures that we are not relying
# on a recursive algorithm limited by stack depth.
#
@pytest.mark.parametrize(
    "maxvars",
    [50, 500, 5000],
)
@pytest.mark.datafiles(os.path.join(DATA_DIR, "defaults"))
@pytest.mark.skipif("not pip_sample_packages()", reason=SAMPLE_PACKAGES_SKIP_REASON)
def test_deep_references(cli, datafiles, maxvars):
    project = str(datafiles)

    # Generate an element with very, very many variables to resolve,
    # each which expand to the value of the previous variable.
    #
    # The bottom variable defines a test value which we check for
    # in the top variable in `bst show` output.
    #
    topvar = "var{}".format(maxvars)
    bottomvar = "var0"
    testvalue = "testvalue {}".format(maxvars)

    # Generate
    variables = {"var{}".format(idx + 1): "%{var" + str(idx) + "}" for idx in range(maxvars)}
    variables[bottomvar] = testvalue
    element = {"kind": "manual", "variables": variables}
    _yaml.roundtrip_dump(element, os.path.join(project, "test.bst"))

    # Run `bst show`
    result = cli.run(project=project, args=["show", "--format", "%{vars}", "test.bst"])
    result.assert_success()

    # Test results
    result_vars = _yaml.load_data(result.output)
    assert result_vars.get_str(topvar) == testvalue


@pytest.mark.parametrize("protected_var", PROTECTED_VARIABLES)
@pytest.mark.datafiles(os.path.join(DATA_DIR, "protected-vars"))
def test_use_of_protected_var_project_conf(cli, datafiles, protected_var):
    project = str(datafiles)
    conf = {"name": "test", "min-version": "2.0", "variables": {protected_var: "some-value"}}
    _yaml.roundtrip_dump(conf, os.path.join(project, "project.conf"))

    element = {
        "kind": "import",
        "sources": [{"kind": "local", "path": "foo.txt"}],
    }
    _yaml.roundtrip_dump(element, os.path.join(project, "target.bst"))

    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.PROTECTED_VARIABLE_REDEFINED)


@pytest.mark.parametrize("protected_var", PROTECTED_VARIABLES)
@pytest.mark.datafiles(os.path.join(DATA_DIR, "protected-vars"))
def test_use_of_protected_var_element_overrides(cli, datafiles, protected_var):
    project = str(datafiles)
    conf = {"name": "test", "min-version": "2.0", "elements": {"manual": {"variables": {protected_var: "some-value"}}}}
    _yaml.roundtrip_dump(conf, os.path.join(project, "project.conf"))

    element = {
        "kind": "manual",
        "sources": [{"kind": "local", "path": "foo.txt"}],
    }
    _yaml.roundtrip_dump(element, os.path.join(project, "target.bst"))

    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.PROTECTED_VARIABLE_REDEFINED)


@pytest.mark.parametrize("protected_var", PROTECTED_VARIABLES)
@pytest.mark.datafiles(os.path.join(DATA_DIR, "protected-vars"))
def test_use_of_protected_var_in_element(cli, datafiles, protected_var):
    project = str(datafiles)
    element = {
        "kind": "import",
        "sources": [{"kind": "local", "path": "foo.txt"}],
        "variables": {protected_var: "some-value"},
    }
    _yaml.roundtrip_dump(element, os.path.join(project, "target.bst"))

    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.PROTECTED_VARIABLE_REDEFINED)


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

    result = cli.run(project=project, args=["build"])
    result.assert_success()

    checkout_dir = os.path.join(project, "checkout")
    for elem in ["one", "two"]:
        result = cli.run(
            project=project,
            args=["artifact", "checkout", "--directory", os.path.join(checkout_dir, elem), "{}.bst".format(elem)],
        )
        result.assert_success()

    assert (os.listdir(os.path.join(checkout_dir, "one")), os.listdir(os.path.join(checkout_dir, "two"))) == (
        ["one.bst"],
        ["two.bst"],
    )


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

    result = cli.run(project=project, args=["show", "--format", "%{public}", "public.bst"])
    result.assert_success()

    output = _yaml.load_data(result.output).strip_node_info()
    expected = {"integration-commands": ["echo expanded"], "test": "expanded"}

    assert {k: v for k, v in output.items() if k in expected} == expected


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

    result = cli.run(project=project, args=["show", "--format", "%{public}", "public_unresolved.bst"])
    result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.UNRESOLVED_VARIABLE)


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

    result = cli.run(project=project, args=["show", "--format", "%{vars}", "test.bst"])
    result.assert_success()
    result_vars = _yaml.load_data(result.output)
    assert result_vars.get_str("eltvar") == "/bar/foo/baz"


# The notparallel tests use a custom plugin which recreates a situation where
# a plugin substitutes an environment variable with the protected %{max-jobs}
# variable, which is set depending on whether the plugin declared notparallel.
#
# These are a regression test against issue #1360, where we found variable
# substitution at the plugin default YAML was buggy when multiple instances
# were not getting the correct results.
#
@pytest.mark.datafiles(os.path.join(DATA_DIR, "notparallel"))
def test_notparallel(cli, datafiles):
    project = str(datafiles)

    # Test the vars
    result = cli.run(project=project, args=["show", "--format", "%{vars}%{env}", "notparallel.bst"])
    result.assert_success()
    result_vars = _yaml.load_data(result.output)
    assert result_vars.get_str("element-name") == "notparallel.bst"
    assert result_vars.get_str("max-jobs") == "1"
    assert result_vars.get_str("MAKEFLAGS") == "-j1"


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

    #
    # Explicitly configure default max-jobs using user configuration
    #
    cli.configure({"build": {"max-jobs": 2}})

    # Fetch the variables and environment of both elements, where parallel.bst depends on notparallel.bst
    result = cli.run(project=project, args=["show", "--format", "%{vars}%{env}", "parallel.bst"])
    result.assert_success()

    # Split on the empty line, which separates elements in bst show output
    groups = result.output.split("\n\n")
    assert len(groups) >= 2
    notparallel_vars = _yaml.load_data(groups[0])
    parallel_vars = _yaml.load_data(groups[1])

    # Test the first group for the expected notparallel state
    assert notparallel_vars.get_str("element-name") == "notparallel.bst"
    assert notparallel_vars.get_str("max-jobs") == "1"
    assert notparallel_vars.get_str("MAKEFLAGS") == "-j1"

    # Test the second group for the expected !notparallel state
    assert parallel_vars.get_str("element-name") == "parallel.bst"
    assert parallel_vars.get_str("max-jobs") == "2"
    assert parallel_vars.get_str("MAKEFLAGS") == "-j2"
