#
#  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 import _yaml
from buildstream._testing import cli_integration as cli  # pylint: disable=unused-import
from buildstream._testing._utils.site import HAVE_SANDBOX
from buildstream.exceptions import ErrorDomain
from buildstream.utils import BST_ARBITRARY_TIMESTAMP

from tests.testutils import wait_for_cache_granularity


pytestmark = pytest.mark.integration


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


@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_workspace_stages_once(cli, datafiles):
    project = str(datafiles)
    workspace = os.path.join(cli.directory, "workspace")
    element_name = "workspace/workspace-mount.bst"

    res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
    assert res.exit_code == 0
    assert cli.get_element_key(project, element_name) != "{:?<64}".format("")
    res = cli.run(project=project, args=["build", element_name])
    assert res.exit_code == 0


@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_workspace_mount(cli, datafiles):
    project = str(datafiles)
    workspace = os.path.join(cli.directory, "workspace")
    element_name = "workspace/workspace-mount.bst"

    res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
    assert res.exit_code == 0

    res = cli.run(project=project, args=["build", element_name])
    assert res.exit_code == 0

    assert os.path.exists(os.path.join(cli.directory, "workspace"))


@pytest.mark.datafiles(DATA_DIR)
def test_workspace_mount_on_read_only_directory(cli, datafiles):
    project = str(datafiles)
    workspace = os.path.join(cli.directory, "workspace")
    os.makedirs(workspace)
    element_name = "workspace/workspace-mount.bst"

    # make directory RO
    os.chmod(workspace, 0o555)

    res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
    assert res.exit_code == 0


@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_workspace_commanddir(cli, datafiles):
    project = str(datafiles)
    workspace = os.path.join(cli.directory, "workspace")
    element_name = "workspace/workspace-commanddir.bst"

    res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
    assert res.exit_code == 0

    res = cli.run(project=project, args=["build", element_name])
    assert res.exit_code == 0

    # Check that the object file was created in the command-subdir `build`
    # using the cached buildtree.
    res = cli.run(
        project=project,
        args=[
            "shell",
            "--build",
            element_name,
            "--use-buildtree",
            "--",
            "find",
            "..",
            "-mindepth",
            "1",
        ],
    )
    res.assert_success()

    files = res.output.splitlines()
    assert "../build/hello.o" in files


@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_workspace_updated_dependency(cli, datafiles):
    project = str(datafiles)
    workspace = os.path.join(cli.directory, "workspace")
    element_path = os.path.join(project, "elements")
    element_name = "workspace/workspace-updated-dependency.bst"
    dep_name = "workspace/dependency.bst"

    dependency = {
        "kind": "manual",
        "depends": [{"filename": "base.bst", "type": "build"}],
        "config": {
            "build-commands": [
                "mkdir -p %{install-root}/etc/test/",
                'echo "Hello world!" > %{install-root}/etc/test/hello.txt',
            ]
        },
    }
    os.makedirs(os.path.dirname(os.path.join(element_path, dep_name)), exist_ok=True)
    _yaml.roundtrip_dump(dependency, os.path.join(element_path, dep_name))

    # First open the workspace
    res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
    assert res.exit_code == 0

    # We build the workspaced element, so that we have an artifact
    # with specific built dependencies
    res = cli.run(project=project, args=["build", element_name])
    assert res.exit_code == 0

    # Now we update a dependency of our element.
    dependency["config"]["build-commands"] = [
        "mkdir -p %{install-root}/etc/test/",
        'echo "Hello china!" > %{install-root}/etc/test/hello.txt',
    ]
    _yaml.roundtrip_dump(dependency, os.path.join(element_path, dep_name))

    # `Make` would look at timestamps and normally not realize that
    # our dependency's header files changed. BuildStream must
    # therefore ensure that we change the mtimes of any files touched
    # since the last successful build of this element, otherwise this
    # build will fail.
    res = cli.run(project=project, args=["build", element_name])
    assert res.exit_code == 0

    res = cli.run(project=project, args=["shell", element_name, "/usr/bin/test.sh"])
    assert res.exit_code == 0
    assert res.output == "Hello china!\n\n"


@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_workspace_update_dependency_failed(cli, datafiles):
    project = str(datafiles)
    workspace = os.path.join(cli.directory, "workspace")
    element_path = os.path.join(project, "elements")
    element_name = "workspace/workspace-updated-dependency-failed.bst"
    dep_name = "workspace/dependency.bst"

    dependency = {
        "kind": "manual",
        "depends": [{"filename": "base.bst", "type": "build"}],
        "config": {
            "build-commands": [
                "mkdir -p %{install-root}/etc/test/",
                'echo "Hello world!" > %{install-root}/etc/test/hello.txt',
                'echo "Hello brazil!" > %{install-root}/etc/test/brazil.txt',
            ]
        },
    }
    os.makedirs(os.path.dirname(os.path.join(element_path, dep_name)), exist_ok=True)
    _yaml.roundtrip_dump(dependency, os.path.join(element_path, dep_name))

    # First open the workspace
    res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
    assert res.exit_code == 0

    # We build the workspaced element, so that we have an artifact
    # with specific built dependencies
    res = cli.run(project=project, args=["build", element_name])
    assert res.exit_code == 0

    # Now we update a dependency of our element.
    dependency["config"]["build-commands"] = [
        "mkdir -p %{install-root}/etc/test/",
        'echo "Hello china!" > %{install-root}/etc/test/hello.txt',
        'echo "Hello brazil!" > %{install-root}/etc/test/brazil.txt',
    ]
    _yaml.roundtrip_dump(dependency, os.path.join(element_path, dep_name))

    # And our build fails!
    with open(os.path.join(workspace, "Makefile"), "a", encoding="utf-8") as f:
        f.write("\texit 1")

    res = cli.run(project=project, args=["build", element_name])
    assert res.exit_code != 0

    # We update our dependency again...
    dependency["config"]["build-commands"] = [
        "mkdir -p %{install-root}/etc/test/",
        'echo "Hello world!" > %{install-root}/etc/test/hello.txt',
        'echo "Hello spain!" > %{install-root}/etc/test/brazil.txt',
    ]
    _yaml.roundtrip_dump(dependency, os.path.join(element_path, dep_name))

    # And fix the source
    with open(os.path.join(workspace, "Makefile"), "r", encoding="utf-8") as f:
        makefile = f.readlines()
    with open(os.path.join(workspace, "Makefile"), "w", encoding="utf-8") as f:
        f.write("\n".join(makefile[:-1]))

    # Since buildstream thinks hello.txt did not change, we could end
    # up not rebuilding a file! We need to make sure that a case like
    # this can't blind-side us.
    res = cli.run(project=project, args=["build", element_name])
    assert res.exit_code == 0

    res = cli.run(project=project, args=["shell", element_name, "/usr/bin/test.sh"])
    assert res.exit_code == 0
    assert res.output == "Hello world!\nHello spain!\n\n"


@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_updated_dependency_nested(cli, datafiles):
    project = str(datafiles)
    workspace = os.path.join(cli.directory, "workspace")
    element_path = os.path.join(project, "elements")
    element_name = "workspace/workspace-updated-dependency-nested.bst"
    dep_name = "workspace/dependency.bst"

    dependency = {
        "kind": "manual",
        "depends": [{"filename": "base.bst", "type": "build"}],
        "config": {
            "build-commands": [
                "mkdir -p %{install-root}/etc/test/tests/",
                'echo "Hello world!" > %{install-root}/etc/test/hello.txt',
                'echo "Hello brazil!" > %{install-root}/etc/test/tests/brazil.txt',
            ]
        },
    }
    os.makedirs(os.path.dirname(os.path.join(element_path, dep_name)), exist_ok=True)
    _yaml.roundtrip_dump(dependency, os.path.join(element_path, dep_name))

    # First open the workspace
    res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
    assert res.exit_code == 0

    # We build the workspaced element, so that we have an artifact
    # with specific built dependencies
    res = cli.run(project=project, args=["build", element_name])
    assert res.exit_code == 0

    # Now we update a dependency of our element.
    dependency["config"]["build-commands"] = [
        "mkdir -p %{install-root}/etc/test/tests/",
        'echo "Hello world!" > %{install-root}/etc/test/hello.txt',
        'echo "Hello test!" > %{install-root}/etc/test/tests/tests.txt',
    ]
    _yaml.roundtrip_dump(dependency, os.path.join(element_path, dep_name))

    res = cli.run(project=project, args=["build", element_name])
    assert res.exit_code == 0

    # Buildstream should pick up the newly added element, and pick up
    # the lack of the newly removed element
    res = cli.run(project=project, args=["shell", element_name, "/usr/bin/test.sh"])
    assert res.exit_code == 0
    assert res.output == "Hello world!\nHello test!\n\n"


@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_incremental_configure_commands_run_only_once(cli, datafiles):
    project = str(datafiles)
    workspace = os.path.join(cli.directory, "workspace")
    element_path = os.path.join(project, "elements")
    element_name = "workspace/incremental.bst"

    element = {
        "kind": "manual",
        "depends": [{"filename": "base.bst", "type": "build"}],
        "sources": [{"kind": "local", "path": "files/workspace-configure-only-once"}],
        "config": {"configure-commands": ["$SHELL configure"]},
    }
    _yaml.roundtrip_dump(element, os.path.join(element_path, element_name))

    # We open a workspace on the above element
    res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
    res.assert_success()

    # Then we build, and check whether the configure step succeeded
    res = cli.run(project=project, args=["--cache-buildtrees", "always", "build", element_name])
    res.assert_success()
    # check that the workspace was not configured outside the sandbox
    assert not os.path.exists(os.path.join(workspace, "prepared"))

    # the configure should have been run in the sandbox, so check the buildtree
    res = cli.run(
        project=project,
        args=[
            "shell",
            "--build",
            element_name,
            "--use-buildtree",
            "--",
            "find",
            ".",
            "-mindepth",
            "1",
        ],
    )
    res.assert_success()

    files = res.output.splitlines()
    assert "./prepared" in files
    assert not "./prepared-again" in files

    # Add file to workspace to trigger an (incremental) build
    with open(os.path.join(workspace, "newfile"), "w", encoding="utf-8"):
        pass

    # When we build again, the configure commands should not be
    # called, and we should therefore exit cleanly (the configure
    # commands are set to always fail after the first run)
    res = cli.run(project=project, args=["--cache-buildtrees", "always", "build", element_name])
    res.assert_success()

    assert not os.path.exists(os.path.join(workspace, "prepared-again"))
    res = cli.run(
        project=project,
        args=[
            "shell",
            "--build",
            element_name,
            "--use-buildtree",
            "--",
            "find",
            ".",
            "-mindepth",
            "1",
        ],
    )
    res.assert_success()

    files = res.output.splitlines()
    assert "./prepared" in files
    assert not "./prepared-again" in files


# Test that rebuilding an already built workspaced element does
# not crash after the last successfully built artifact is removed
# from the cache
#
# A user can remove their artifact cache, or manually remove the
# artifact with `bst artifact delete`, or BuildStream can delete
# the last successfully built artifact for this workspace as a
# part of a cleanup job.
#
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_workspace_missing_last_successful(cli, datafiles):
    project = str(datafiles)
    workspace = os.path.join(cli.directory, "workspace")
    element_name = "workspace/workspace-commanddir.bst"

    # Open workspace
    res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
    assert res.exit_code == 0

    # Build first, this will record the last successful build in local state
    res = cli.run(project=project, args=["build", element_name])
    assert res.exit_code == 0

    # Remove the artifact from the cache, invalidating the last successful build
    res = cli.run(project=project, args=["artifact", "delete", element_name])
    assert res.exit_code == 0

    # Build again, ensure we dont crash just because the artifact went missing
    res = cli.run(project=project, args=["build", element_name])
    assert res.exit_code == 0


# Check that we can still read failed workspace logs
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_workspace_failed_logs(cli, datafiles):
    project = str(datafiles)
    workspace = os.path.join(cli.directory, "failing_amhello")
    element_name = "autotools/amhello-failure.bst"

    # Open workspace
    res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
    res.assert_success()

    # Try to build and ensure the build fails
    res = cli.run(project=project, args=["build", element_name])
    res.assert_main_error(ErrorDomain.STREAM, None)
    assert cli.get_element_state(project, element_name) == "failed"

    res = cli.run(project=project, args=["artifact", "log", element_name])
    res.assert_success()

    log = res.output
    # Assert that we can get the log
    assert log != ""
    fail_str = "FAILURE {}: Running build-commands".format(element_name)
    batch_fail_str = "FAILURE {}: Running commands".format(element_name)
    assert fail_str in log or batch_fail_str in log


def get_buildtree_file_contents(cli, project, element_name, filename):
    res = cli.run(
        project=project,
        args=[
            "shell",
            "--build",
            element_name,
            "--use-buildtree",
            "--",
            "cat",
            filename,
        ],
    )
    res.assert_success()
    return res.output


@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_incremental(cli, datafiles):
    project = str(datafiles)
    workspace = os.path.join(cli.directory, "workspace")
    element_path = os.path.join(project, "elements")
    element_name = "workspace/incremental.bst"

    element = {
        "kind": "manual",
        "depends": [{"filename": "base.bst", "type": "build"}],
        "sources": [{"kind": "local", "path": "files/workspace-incremental"}],
        "config": {"build-commands": ["make"]},
    }
    _yaml.roundtrip_dump(element, os.path.join(element_path, element_name))

    # We open a workspace on the above element
    res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
    res.assert_success()

    # Initial (non-incremental) build of the workspace
    res = cli.run(project=project, args=["build", element_name])
    res.assert_success()

    # Save the random hash
    random_hash = get_buildtree_file_contents(cli, project, element_name, "random")

    # Verify the expected output file of the initial build
    assert get_buildtree_file_contents(cli, project, element_name, "copy") == "1"

    wait_for_cache_granularity()

    # Replace source file contents with '2'
    with open(os.path.join(workspace, "source"), "w", encoding="utf-8") as f:
        f.write("2")

    # Perform incremental build of the workspace
    res = cli.run(project=project, args=["build", element_name])
    res.assert_success()

    # Verify that this was an incremental build by comparing the random hash
    assert get_buildtree_file_contents(cli, project, element_name, "random") == random_hash

    # Verify that the output file matches the new source file
    assert get_buildtree_file_contents(cli, project, element_name, "copy") == "2"

    wait_for_cache_granularity()

    # Replace source file contents with '3', however, set an old mtime such
    # that `make` will not pick up the change
    with open(os.path.join(workspace, "source"), "w", encoding="utf-8") as f:
        f.write("3")
    os.utime(os.path.join(workspace, "source"), (BST_ARBITRARY_TIMESTAMP, BST_ARBITRARY_TIMESTAMP))

    # Perform incremental build of the workspace
    res = cli.run(project=project, args=["build", element_name])
    res.assert_success()

    # Verify that this was an incremental build by comparing the random hash
    assert get_buildtree_file_contents(cli, project, element_name, "random") == random_hash

    # Verify that the output file still matches the previous content '2'
    assert get_buildtree_file_contents(cli, project, element_name, "copy") == "2"


# Test incremental build after partial build / build failure
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_incremental_partial(cli, datafiles):
    project = str(datafiles)
    workspace = os.path.join(cli.directory, "workspace")
    element_path = os.path.join(project, "elements")
    element_name = "workspace/incremental.bst"

    element = {
        "kind": "manual",
        "depends": [{"filename": "base.bst", "type": "build"}],
        "sources": [{"kind": "local", "path": "files/workspace-partial"}],
        "config": {"build-commands": ["make random", "make copy1", "make copy2"]},
    }
    _yaml.roundtrip_dump(element, os.path.join(element_path, element_name))

    # We open a workspace on the above element
    res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
    res.assert_success()

    # Initial (non-incremental) build of the workspace
    res = cli.run(project=project, args=["build", element_name])
    res.assert_success()

    # Save the random hash
    random_hash = get_buildtree_file_contents(cli, project, element_name, "random")

    # Verify the expected output files of the initial build
    assert get_buildtree_file_contents(cli, project, element_name, "copy1") == "1"
    assert get_buildtree_file_contents(cli, project, element_name, "copy2") == "1"

    wait_for_cache_granularity()

    # Delete source1 and replace source2 file contents with '2'
    os.unlink(os.path.join(workspace, "source1"))
    with open(os.path.join(workspace, "source2"), "w", encoding="utf-8") as f:
        f.write("2")

    # Perform incremental build of the workspace
    # This should fail because of the missing source1 file.
    res = cli.run(project=project, args=["build", element_name])
    res.assert_main_error(ErrorDomain.STREAM, None)

    wait_for_cache_granularity()

    # Recreate source1 file
    with open(os.path.join(workspace, "source1"), "w", encoding="utf-8") as f:
        f.write("2")

    # Perform incremental build of the workspace
    res = cli.run(project=project, args=["build", element_name])
    res.assert_success()

    # Verify that this was an incremental build by comparing the random hash
    assert get_buildtree_file_contents(cli, project, element_name, "random") == random_hash

    # Verify that both files got rebuilt
    assert get_buildtree_file_contents(cli, project, element_name, "copy1") == "2"
    assert get_buildtree_file_contents(cli, project, element_name, "copy2") == "2"
