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

import pytest

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

DATA_DIR = os.path.join(
    os.path.dirname(os.path.realpath(__file__)),
    "filter",
)


@pytest.mark.datafiles(os.path.join(DATA_DIR, "basic"))
def test_filter_include(datafiles, cli, tmpdir):
    project = str(datafiles)
    result = cli.run(project=project, args=["build", "output-include.bst"])
    result.assert_success()

    checkout = os.path.join(tmpdir.dirname, tmpdir.basename, "checkout")
    result = cli.run(project=project, args=["artifact", "checkout", "output-include.bst", "--directory", checkout])
    result.assert_success()
    assert os.path.exists(os.path.join(checkout, "foo"))
    assert not os.path.exists(os.path.join(checkout, "bar"))


@pytest.mark.datafiles(os.path.join(DATA_DIR, "basic"))
def test_filter_include_dynamic(datafiles, cli, tmpdir):
    project = str(datafiles)
    result = cli.run(project=project, args=["build", "output-dynamic-include.bst"])
    result.assert_success()

    checkout = os.path.join(tmpdir.dirname, tmpdir.basename, "checkout")
    result = cli.run(
        project=project, args=["artifact", "checkout", "output-dynamic-include.bst", "--directory", checkout]
    )
    result.assert_success()
    assert os.path.exists(os.path.join(checkout, "foo"))
    assert not os.path.exists(os.path.join(checkout, "bar"))


@pytest.mark.datafiles(os.path.join(DATA_DIR, "basic"))
def test_filter_exclude(datafiles, cli, tmpdir):
    project = str(datafiles)
    result = cli.run(project=project, args=["build", "output-exclude.bst"])
    result.assert_success()

    checkout = os.path.join(tmpdir.dirname, tmpdir.basename, "checkout")
    result = cli.run(project=project, args=["artifact", "checkout", "output-exclude.bst", "--directory", checkout])
    result.assert_success()
    assert not os.path.exists(os.path.join(checkout, "foo"))
    assert os.path.exists(os.path.join(checkout, "bar"))


@pytest.mark.datafiles(os.path.join(DATA_DIR, "basic"))
def test_filter_orphans(datafiles, cli, tmpdir):
    project = str(datafiles)
    result = cli.run(project=project, args=["build", "output-orphans.bst"])
    result.assert_success()

    checkout = os.path.join(tmpdir.dirname, tmpdir.basename, "checkout")
    result = cli.run(project=project, args=["artifact", "checkout", "output-orphans.bst", "--directory", checkout])
    result.assert_success()
    assert os.path.exists(os.path.join(checkout, "baz"))


@pytest.mark.datafiles(os.path.join(DATA_DIR, "basic"))
def test_filter_deps_ok(datafiles, cli):
    project = str(datafiles)
    result = cli.run(project=project, args=["build", "deps-permitted.bst"])
    result.assert_success()

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

    assert "output-exclude.bst" in result.output
    assert "output-orphans.bst" in result.output


@pytest.mark.datafiles(os.path.join(DATA_DIR, "basic"))
def test_filter_forbid_sources(datafiles, cli):
    project = str(datafiles)
    result = cli.run(project=project, args=["build", "forbidden-source.bst"])
    result.assert_main_error(ErrorDomain.ELEMENT, "element-forbidden-sources")


@pytest.mark.datafiles(os.path.join(DATA_DIR, "basic"))
def test_filter_forbid_multi_bdep(datafiles, cli):
    project = str(datafiles)
    result = cli.run(project=project, args=["build", "forbidden-multi-bdep.bst"])
    result.assert_main_error(ErrorDomain.ELEMENT, "filter-bdepend-wrong-count")


@pytest.mark.datafiles(os.path.join(DATA_DIR, "basic"))
def test_filter_forbid_no_bdep(datafiles, cli):
    project = str(datafiles)
    result = cli.run(project=project, args=["build", "forbidden-no-bdep.bst"])
    result.assert_main_error(ErrorDomain.ELEMENT, "filter-bdepend-wrong-count")


@pytest.mark.datafiles(os.path.join(DATA_DIR, "basic"))
def test_filter_forbid_also_rdep(datafiles, cli):
    project = str(datafiles)
    result = cli.run(project=project, args=["build", "forbidden-also-rdep.bst"])
    result.assert_main_error(ErrorDomain.ELEMENT, "filter-bdepend-also-rdepend")


@pytest.mark.datafiles(os.path.join(DATA_DIR, "basic"))
def test_filter_workspace_open(datafiles, cli, tmpdir):
    project = str(datafiles)
    workspace_dir = os.path.join(tmpdir.dirname, tmpdir.basename, "workspace")
    result = cli.run(project=project, args=["workspace", "open", "--directory", workspace_dir, "deps-permitted.bst"])
    result.assert_success()
    assert os.path.exists(os.path.join(workspace_dir, "foo"))
    assert os.path.exists(os.path.join(workspace_dir, "bar"))
    assert os.path.exists(os.path.join(workspace_dir, "baz"))


@pytest.mark.datafiles(os.path.join(DATA_DIR, "basic"))
def test_filter_workspace_open_multi(datafiles, cli):
    project = str(datafiles)
    result = cli.run(
        cwd=project, project=project, args=["workspace", "open", "deps-permitted.bst", "output-orphans.bst"]
    )
    result.assert_success()
    assert os.path.exists(os.path.join(project, "input"))


@pytest.mark.datafiles(os.path.join(DATA_DIR, "basic"))
def test_filter_workspace_build(datafiles, cli, tmpdir):
    project = str(datafiles)
    tempdir = os.path.join(tmpdir.dirname, tmpdir.basename)
    workspace_dir = os.path.join(tempdir, "workspace")
    result = cli.run(project=project, args=["workspace", "open", "--directory", workspace_dir, "output-orphans.bst"])
    result.assert_success()
    src = os.path.join(workspace_dir, "foo")
    dst = os.path.join(workspace_dir, "quux")
    shutil.copyfile(src, dst)
    result = cli.run(project=project, args=["build", "output-orphans.bst"])
    result.assert_success()
    checkout_dir = os.path.join(tempdir, "checkout")
    result = cli.run(project=project, args=["artifact", "checkout", "output-orphans.bst", "--directory", checkout_dir])
    result.assert_success()
    assert os.path.exists(os.path.join(checkout_dir, "quux"))


@pytest.mark.datafiles(os.path.join(DATA_DIR, "basic"))
def test_filter_workspace_close(datafiles, cli, tmpdir):
    project = str(datafiles)
    tempdir = os.path.join(tmpdir.dirname, tmpdir.basename)
    workspace_dir = os.path.join(tempdir, "workspace")
    result = cli.run(project=project, args=["workspace", "open", "--directory", workspace_dir, "output-orphans.bst"])
    result.assert_success()
    src = os.path.join(workspace_dir, "foo")
    dst = os.path.join(workspace_dir, "quux")
    shutil.copyfile(src, dst)
    result = cli.run(project=project, args=["workspace", "close", "deps-permitted.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["build", "output-orphans.bst"])
    result.assert_success()
    checkout_dir = os.path.join(tempdir, "checkout")
    result = cli.run(project=project, args=["artifact", "checkout", "output-orphans.bst", "--directory", checkout_dir])
    result.assert_success()
    assert not os.path.exists(os.path.join(checkout_dir, "quux"))


@pytest.mark.datafiles(os.path.join(DATA_DIR, "basic"))
def test_filter_workspace_reset(datafiles, cli, tmpdir):
    project = str(datafiles)
    tempdir = os.path.join(tmpdir.dirname, tmpdir.basename)
    workspace_dir = os.path.join(tempdir, "workspace")
    result = cli.run(project=project, args=["workspace", "open", "--directory", workspace_dir, "output-orphans.bst"])
    result.assert_success()
    src = os.path.join(workspace_dir, "foo")
    dst = os.path.join(workspace_dir, "quux")
    shutil.copyfile(src, dst)
    result = cli.run(project=project, args=["workspace", "reset", "deps-permitted.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["build", "output-orphans.bst"])
    result.assert_success()
    checkout_dir = os.path.join(tempdir, "checkout")
    result = cli.run(project=project, args=["artifact", "checkout", "output-orphans.bst", "--directory", checkout_dir])
    result.assert_success()
    assert not os.path.exists(os.path.join(checkout_dir, "quux"))


@pytest.mark.datafiles(os.path.join(DATA_DIR, "basic"))
def test_filter_track(datafiles, cli, tmpdir):
    repo = create_repo("tar", str(tmpdir))
    ref = repo.create(os.path.join(str(datafiles), "files"))
    elements_dir = os.path.join(str(tmpdir), "elements")
    project = str(tmpdir)
    input_name = "input.bst"

    project_config = {
        "name": "filter-track-test",
        "min-version": "2.0",
        "element-path": "elements",
    }
    project_file = os.path.join(str(tmpdir), "project.conf")
    _yaml.roundtrip_dump(project_config, project_file)

    input_config = {
        "kind": "import",
        "sources": [repo.source_config()],
    }

    input_file = os.path.join(elements_dir, input_name)
    _yaml.roundtrip_dump(input_config, input_file)

    filter1_config = {"kind": "filter", "depends": [{"filename": input_name, "type": "build"}]}
    filter1_file = os.path.join(elements_dir, "filter1.bst")
    _yaml.roundtrip_dump(filter1_config, filter1_file)

    filter2_config = {"kind": "filter", "depends": [{"filename": "filter1.bst", "type": "build"}]}
    filter2_file = os.path.join(elements_dir, "filter2.bst")
    _yaml.roundtrip_dump(filter2_config, filter2_file)

    # Assert that a fetch is needed
    assert cli.get_element_state(project, input_name) == "no reference"

    # Now try to track it
    result = cli.run(project=project, args=["source", "track", "filter2.bst"])
    result.assert_success()

    # Now check that a ref field exists
    new_input = _yaml.load(input_file, shortname=None)
    source_node = new_input.get_sequence("sources").mapping_at(0)
    new_input_ref = source_node.get_str("ref")
    assert new_input_ref == ref


@pytest.mark.datafiles(os.path.join(DATA_DIR, "basic"))
def test_filter_track_excepted(datafiles, cli, tmpdir):
    repo = create_repo("tar", str(tmpdir))
    repo.create(os.path.join(str(datafiles), "files"))
    elements_dir = os.path.join(str(tmpdir), "elements")
    project = str(tmpdir)
    input_name = "input.bst"

    project_config = {
        "name": "filter-track-test",
        "min-version": "2.0",
        "element-path": "elements",
    }
    project_file = os.path.join(str(tmpdir), "project.conf")
    _yaml.roundtrip_dump(project_config, project_file)

    input_config = {
        "kind": "import",
        "sources": [repo.source_config()],
    }

    input_file = os.path.join(elements_dir, input_name)
    _yaml.roundtrip_dump(input_config, input_file)

    filter1_config = {"kind": "filter", "depends": [{"filename": input_name, "type": "build"}]}
    filter1_file = os.path.join(elements_dir, "filter1.bst")
    _yaml.roundtrip_dump(filter1_config, filter1_file)

    filter2_config = {"kind": "filter", "depends": [{"filename": "filter1.bst", "type": "build"}]}
    filter2_file = os.path.join(elements_dir, "filter2.bst")
    _yaml.roundtrip_dump(filter2_config, filter2_file)

    # Assert that a fetch is needed
    assert cli.get_element_state(project, input_name) == "no reference"

    # Now try to track it
    result = cli.run(project=project, args=["source", "track", "filter2.bst", "--except", "input.bst"])
    result.assert_success()

    # Now check that a ref field exists
    new_input = _yaml.load(input_file, shortname=None)
    source_node = new_input.get_sequence("sources").mapping_at(0)
    assert "ref" not in source_node


@pytest.mark.datafiles(os.path.join(DATA_DIR, "basic"))
def test_filter_track_multi_to_one(datafiles, cli, tmpdir):
    repo = create_repo("tar", str(tmpdir))
    ref = repo.create(os.path.join(str(datafiles), "files"))
    elements_dir = os.path.join(str(tmpdir), "elements")
    project = str(tmpdir)
    input_name = "input.bst"

    project_config = {
        "name": "filter-track-test",
        "min-version": "2.0",
        "element-path": "elements",
    }
    project_file = os.path.join(str(tmpdir), "project.conf")
    _yaml.roundtrip_dump(project_config, project_file)

    input_config = {
        "kind": "import",
        "sources": [repo.source_config()],
    }

    input_file = os.path.join(elements_dir, input_name)
    _yaml.roundtrip_dump(input_config, input_file)

    filter1_config = {"kind": "filter", "depends": [{"filename": input_name, "type": "build"}]}
    filter1_file = os.path.join(elements_dir, "filter1.bst")
    _yaml.roundtrip_dump(filter1_config, filter1_file)

    filter2_config = {"kind": "filter", "depends": [{"filename": input_name, "type": "build"}]}
    filter2_file = os.path.join(elements_dir, "filter2.bst")
    _yaml.roundtrip_dump(filter2_config, filter2_file)

    # Assert that a fetch is needed
    assert cli.get_element_state(project, input_name) == "no reference"

    # Now try to track it
    result = cli.run(project=project, args=["source", "track", "filter1.bst", "filter2.bst"])
    result.assert_success()

    # Now check that a ref field exists
    new_input = _yaml.load(input_file, shortname=None)
    source_node = new_input.get_sequence("sources").mapping_at(0)
    new_ref = source_node.get_str("ref")
    assert new_ref == ref


@pytest.mark.datafiles(os.path.join(DATA_DIR, "basic"))
def test_filter_track_multi(datafiles, cli, tmpdir):
    repo = create_repo("tar", str(tmpdir))
    ref = repo.create(os.path.join(str(datafiles), "files"))
    elements_dir = os.path.join(str(tmpdir), "elements")
    project = str(tmpdir)

    project_config = {
        "name": "filter-track-test",
        "min-version": "2.0",
        "element-path": "elements",
    }
    project_file = os.path.join(str(tmpdir), "project.conf")
    _yaml.roundtrip_dump(project_config, project_file)

    input_config = {
        "kind": "import",
        "sources": [repo.source_config()],
    }

    #
    # Lets do 10 of them at the same time, this may help regression
    # test against https://github.com/apache/buildstream/issues/1831
    #
    filter_elements = []
    input_elements = []
    for num in range(0, 10):
        input_elt = f"input{num}.bst"
        input_file = os.path.join(elements_dir, input_elt)
        _yaml.roundtrip_dump(input_config, input_file)

        filter_config = {"kind": "filter", "depends": [{"filename": input_elt, "type": "build"}]}
        filter_elt = f"filter{num}.bst"
        filter_file = os.path.join(elements_dir, filter_elt)
        _yaml.roundtrip_dump(filter_config, filter_file)

        input_elements.append(input_elt)
        filter_elements.append(filter_elt)

    # Assert that a fetch is needed
    states = cli.get_element_states(project, input_elements)
    for key in states:
        assert states[key] == "no reference"

    # Now try to track it
    result = cli.run(project=project, args=["source", "track"] + filter_elements)
    result.assert_success()

    # Now check that a ref field exists
    for elt in input_elements:
        input_file = os.path.join(elements_dir, elt)
        new_input = _yaml.load(input_file, shortname=None)
        source_node = new_input.get_sequence("sources").mapping_at(0)
        new_ref = source_node.get_str("ref")
        assert new_ref == ref


@pytest.mark.datafiles(os.path.join(DATA_DIR, "basic"))
def test_filter_track_multi_exclude(datafiles, cli, tmpdir):
    repo = create_repo("tar", str(tmpdir))
    ref = repo.create(os.path.join(str(datafiles), "files"))
    elements_dir = os.path.join(str(tmpdir), "elements")
    project = str(tmpdir)
    input_name = "input.bst"
    input2_name = "input2.bst"

    project_config = {
        "name": "filter-track-test",
        "min-version": "2.0",
        "element-path": "elements",
    }
    project_file = os.path.join(str(tmpdir), "project.conf")
    _yaml.roundtrip_dump(project_config, project_file)

    input_config = {
        "kind": "import",
        "sources": [repo.source_config()],
    }

    input_file = os.path.join(elements_dir, input_name)
    _yaml.roundtrip_dump(input_config, input_file)

    input2_config = dict(input_config)
    input2_file = os.path.join(elements_dir, input2_name)
    _yaml.roundtrip_dump(input2_config, input2_file)

    filter1_config = {"kind": "filter", "depends": [{"filename": input_name, "type": "build"}]}
    filter1_file = os.path.join(elements_dir, "filter1.bst")
    _yaml.roundtrip_dump(filter1_config, filter1_file)

    filter2_config = {"kind": "filter", "depends": [{"filename": input2_name, "type": "build"}]}
    filter2_file = os.path.join(elements_dir, "filter2.bst")
    _yaml.roundtrip_dump(filter2_config, filter2_file)

    # Assert that a fetch is needed
    states = cli.get_element_states(project, [input_name, input2_name])
    assert states == {
        input_name: "no reference",
        input2_name: "no reference",
    }

    # Now try to track it
    result = cli.run(project=project, args=["source", "track", "filter1.bst", "filter2.bst", "--except", input_name])
    result.assert_success()

    # Now check that a ref field exists
    new_input = _yaml.load(input_file, shortname=None)
    source_node = new_input.get_sequence("sources").mapping_at(0)
    assert "ref" not in source_node

    new_input2 = _yaml.load(input2_file, shortname=None)
    source_node2 = new_input2.get_sequence("sources").mapping_at(0)
    new_ref2 = source_node2.get_str("ref")
    assert new_ref2 == ref


@pytest.mark.datafiles(os.path.join(DATA_DIR, "basic"))
def test_filter_include_with_indirect_deps(datafiles, cli, tmpdir):
    project = str(datafiles)
    result = cli.run(project=project, args=["build", "output-include-with-indirect-deps.bst"])
    result.assert_success()

    checkout = os.path.join(tmpdir.dirname, tmpdir.basename, "checkout")
    result = cli.run(
        project=project,
        args=["artifact", "checkout", "output-include-with-indirect-deps.bst", "--directory", checkout],
    )
    result.assert_success()

    # direct dependencies should be staged and filtered
    assert os.path.exists(os.path.join(checkout, "baz"))

    # indirect dependencies shouldn't be staged and filtered
    assert not os.path.exists(os.path.join(checkout, "foo"))
    assert not os.path.exists(os.path.join(checkout, "bar"))


@pytest.mark.datafiles(os.path.join(DATA_DIR, "basic"))
def test_filter_fails_for_nonexisting_domain(datafiles, cli):
    project = str(datafiles)
    result = cli.run(project=project, args=["build", "output-include-nonexistent-domain.bst"])
    result.assert_main_error(ErrorDomain.STREAM, None)

    error = "Unknown domains were used in output-include-nonexistent-domain.bst [line 7 column 2]"
    assert error in result.stderr
    assert "- unknown_file" in result.stderr


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

    # Explicitly not passing integration commands should be fine
    result = cli.run(project=project, args=["build", "no-pass-integration.bst"])
    result.assert_success()

    # Passing integration commands should build nicely
    result = cli.run(project=project, args=["build", "pass-integration.bst"])
    result.assert_success()

    # Checking out elements which don't pass integration commands should still work
    checkout_dir = os.path.join(project, "no-pass")
    result = cli.run(
        project=project,
        args=["artifact", "checkout", "--integrate", "--directory", checkout_dir, "no-pass-integration.bst"],
    )
    result.assert_success()

    # Checking out the artifact should fail if we run integration commands, as
    # the staged artifacts don't have a shell
    checkout_dir = os.path.join(project, "pass")
    result = cli.run(
        project=project,
        args=["artifact", "checkout", "--integrate", "--directory", checkout_dir, "pass-integration.bst"],
    )
    result.assert_main_error(ErrorDomain.STREAM, "missing-command")


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

    result = cli.run(project=project, args=["build", "forbidden-stack-dep.bst"])
    result.assert_main_error(ErrorDomain.ELEMENT, "filter-bdepend-no-artifact")
