#
#  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.exceptions import ErrorDomain
from .._utils import generate_junction
from .. import create_repo
from .. import cli  # pylint: disable=unused-import
from .utils import update_project_configuration
from .utils import kind  # pylint: disable=unused-import


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


def generate_element(repo, element_path, dep_name=None):
    element = {"kind": "import", "sources": [repo.source_config()]}
    if dep_name:
        element["depends"] = [dep_name]

    _yaml.roundtrip_dump(element, element_path)


@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.parametrize("ref_storage", ["inline", "project.refs"])
def test_track(cli, tmpdir, datafiles, ref_storage, kind):
    project = str(datafiles)
    dev_files_path = os.path.join(project, "files", "dev-files")
    element_path = os.path.join(project, "elements")
    element_name = "track-test-{}.bst".format(kind)

    update_project_configuration(project, {"ref-storage": ref_storage})

    # Create our repo object of the given source type with
    # the dev files, and then collect the initial ref.
    #
    repo = create_repo(kind, str(tmpdir))
    repo.create(dev_files_path)

    # Generate the element
    generate_element(repo, os.path.join(element_path, element_name))

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

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

    # And now fetch it: The Source has probably already cached the
    # latest ref locally, but it is not required to have cached
    # the associated content of the latest ref at track time, that
    # is the job of fetch.
    result = cli.run(project=project, args=["source", "fetch", element_name])
    result.assert_success()

    # Assert that we are now buildable because the source is
    # now cached.
    assert cli.get_element_state(project, element_name) == "buildable"

    # Assert there was a project.refs created, depending on the configuration
    if ref_storage == "project.refs":
        assert os.path.exists(os.path.join(project, "project.refs"))
    else:
        assert not os.path.exists(os.path.join(project, "project.refs"))


# NOTE:
#
#    This test checks that recursive tracking works by observing
#    element states after running a recursive tracking operation.
#
#    However, this test is ALSO valuable as it stresses the source
#    plugins in a situation where many source plugins are operating
#    at once on the same backing repository.
#
#    Do not change this test to use a separate 'Repo' per element
#    as that would defeat the purpose of the stress test, otherwise
#    please refactor that aspect into another test.
#
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.parametrize("amount", [1, 10])
def test_track_recurse(cli, tmpdir, datafiles, kind, amount):
    project = str(datafiles)
    dev_files_path = os.path.join(project, "files", "dev-files")
    element_path = os.path.join(project, "elements")

    # Try to actually launch as many fetch jobs as possible at the same time
    #
    # This stresses the Source plugins and helps to ensure that
    # they handle concurrent access to the store correctly.
    cli.configure(
        {
            "scheduler": {
                "fetchers": amount,
            }
        }
    )

    # Create our repo object of the given source type with
    # the dev files, and then collect the initial ref.
    #
    repo = create_repo(kind, str(tmpdir))
    repo.create(dev_files_path)

    # Write out our test targets
    element_names = []
    last_element_name = None
    for i in range(amount + 1):
        element_name = "track-test-{}-{}.bst".format(kind, i + 1)
        filename = os.path.join(element_path, element_name)

        element_names.append(element_name)

        generate_element(repo, filename, dep_name=last_element_name)
        last_element_name = element_name

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

    # Now first try to track it
    result = cli.run(project=project, args=["source", "track", "--deps", "all", last_element_name])
    result.assert_success()

    # And now fetch it: The Source has probably already cached the
    # latest ref locally, but it is not required to have cached
    # the associated content of the latest ref at track time, that
    # is the job of fetch.
    result = cli.run(project=project, args=["source", "fetch", "--deps", "all", last_element_name])
    result.assert_success()

    # Assert that the base is buildable and the rest are waiting
    states = cli.get_element_states(project, [last_element_name])
    for element_name in element_names:
        if element_name == element_names[0]:
            assert states[element_name] == "buildable"
        else:
            assert states[element_name] == "waiting"


@pytest.mark.datafiles(DATA_DIR)
def test_track_recurse_except(cli, tmpdir, datafiles, kind):
    project = str(datafiles)
    dev_files_path = os.path.join(project, "files", "dev-files")
    element_path = os.path.join(project, "elements")
    element_dep_name = "track-test-dep-{}.bst".format(kind)
    element_target_name = "track-test-target-{}.bst".format(kind)

    # Create our repo object of the given source type with
    # the dev files, and then collect the initial ref.
    #
    repo = create_repo(kind, str(tmpdir))
    repo.create(dev_files_path)

    # Write out our test targets
    generate_element(repo, os.path.join(element_path, element_dep_name))
    generate_element(repo, os.path.join(element_path, element_target_name), dep_name=element_dep_name)

    # Assert that a fetch is needed
    states = cli.get_element_states(project, [element_target_name])
    assert states[element_dep_name] == "no reference"
    assert states[element_target_name] == "no reference"

    # Now first try to track it
    result = cli.run(
        project=project, args=["source", "track", "--deps", "all", "--except", element_dep_name, element_target_name]
    )
    result.assert_success()

    # And now fetch it: The Source has probably already cached the
    # latest ref locally, but it is not required to have cached
    # the associated content of the latest ref at track time, that
    # is the job of fetch.
    result = cli.run(project=project, args=["source", "fetch", "--deps", "none", element_target_name])
    result.assert_success()

    # Assert that the dependency is buildable and the target is waiting
    states = cli.get_element_states(project, [element_target_name])
    assert states[element_dep_name] == "no reference"
    assert states[element_target_name] == "waiting"


@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.parametrize("ref_storage", ["inline", "project.refs"])
def test_cross_junction(cli, tmpdir, datafiles, ref_storage, kind):
    project = str(datafiles)
    subproject_path = os.path.join(project, "files", "sub-project")
    junction_path = os.path.join(project, "elements", "junction.bst")
    etc_files = os.path.join(subproject_path, "files", "etc-files")
    repo_element_path = os.path.join(subproject_path, "elements", "import-etc-repo.bst")

    update_project_configuration(project, {"ref-storage": ref_storage})

    repo = create_repo(kind, str(tmpdir.join("element_repo")))
    repo.create(etc_files)

    generate_element(repo, repo_element_path)

    generate_junction(str(tmpdir.join("junction_repo")), subproject_path, junction_path, store_ref=False)

    # Track the junction itself first.
    result = cli.run(project=project, args=["source", "track", "junction.bst"])
    result.assert_success()

    assert cli.get_element_state(project, "junction.bst:import-etc-repo.bst") == "no reference"

    # Track the cross junction element. -J is not given, it is implied.
    result = cli.run(project=project, args=["source", "track", "junction.bst:import-etc-repo.bst"])

    if ref_storage == "inline":
        # This is not allowed to track cross junction without project.refs.
        result.assert_main_error(ErrorDomain.STREAM, "untrackable-sources")
    else:
        result.assert_success()

        assert cli.get_element_state(project, "junction.bst:import-etc-repo.bst") == "buildable"

        assert os.path.exists(os.path.join(project, "project.refs"))


@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.parametrize("ref_storage", ["inline", "project.refs"])
def test_track_include(cli, tmpdir, datafiles, ref_storage, kind):
    project = str(datafiles)
    dev_files_path = os.path.join(project, "files", "dev-files")
    element_path = os.path.join(project, "elements")
    element_name = "track-test-{}.bst".format(kind)

    update_project_configuration(project, {"ref-storage": ref_storage})

    # Create our repo object of the given source type with
    # the dev files, and then collect the initial ref.
    #
    repo = create_repo(kind, str(tmpdir))
    ref = repo.create(dev_files_path)

    # Generate the element
    element = {"kind": "import", "(@)": ["elements/sources.yml"]}
    sources = {"sources": [repo.source_config()]}

    _yaml.roundtrip_dump(element, os.path.join(element_path, element_name))
    _yaml.roundtrip_dump(sources, os.path.join(element_path, "sources.yml"))

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

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

    # And now fetch it: The Source has probably already cached the
    # latest ref locally, but it is not required to have cached
    # the associated content of the latest ref at track time, that
    # is the job of fetch.
    result = cli.run(project=project, args=["source", "fetch", element_name])
    result.assert_success()

    # Assert that we are now buildable because the source is
    # now cached.
    assert cli.get_element_state(project, element_name) == "buildable"

    # Assert there was a project.refs created, depending on the configuration
    if ref_storage == "project.refs":
        assert os.path.exists(os.path.join(project, "project.refs"))
    else:
        assert not os.path.exists(os.path.join(project, "project.refs"))

        new_sources = _yaml.load(os.path.join(element_path, "sources.yml"), shortname="sources.yml")

        # Get all of the sources
        assert "sources" in new_sources
        sources_list = new_sources.get_sequence("sources")
        assert len(sources_list) == 1

        # Get the first source from the sources list
        new_source = sources_list.mapping_at(0)
        assert "ref" in new_source
        assert ref == new_source.get_str("ref")


@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.parametrize("ref_storage", ["inline", "project.refs"])
def test_track_include_junction(cli, tmpdir, datafiles, ref_storage, kind):
    project = str(datafiles)
    dev_files_path = os.path.join(project, "files", "dev-files")
    element_path = os.path.join(project, "elements")
    element_name = "track-test-{}.bst".format(kind)
    subproject_path = os.path.join(project, "files", "sub-project")
    sub_element_path = os.path.join(subproject_path, "elements")
    junction_path = os.path.join(element_path, "junction.bst")

    update_project_configuration(project, {"ref-storage": ref_storage})

    # Create our repo object of the given source type with
    # the dev files, and then collect the initial ref.
    #
    repo = create_repo(kind, str(tmpdir.join("element_repo")))
    repo.create(dev_files_path)

    # Generate the element
    element = {"kind": "import", "(@)": ["junction.bst:elements/sources.yml"]}
    sources = {"sources": [repo.source_config()]}

    _yaml.roundtrip_dump(element, os.path.join(element_path, element_name))
    _yaml.roundtrip_dump(sources, os.path.join(sub_element_path, "sources.yml"))

    generate_junction(str(tmpdir.join("junction_repo")), subproject_path, junction_path, store_ref=True)

    result = cli.run(project=project, args=["source", "track", "junction.bst"])
    result.assert_success()

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

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

    # Assert there was a project.refs created, depending on the configuration
    if ref_storage == "inline":
        # FIXME: We should expect an error. But only a warning is emitted
        # result.assert_main_error(ErrorDomain.SOURCE, 'tracking-junction-fragment')

        assert "junction.bst:elements/sources.yml: Cannot track source in a fragment from a junction" in result.stderr
    else:
        assert os.path.exists(os.path.join(project, "project.refs"))

        # And now fetch it: The Source has probably already cached the
        # latest ref locally, but it is not required to have cached
        # the associated content of the latest ref at track time, that
        # is the job of fetch.
        result = cli.run(project=project, args=["source", "fetch", element_name])
        result.assert_success()

        # Assert that we are now buildable because the source is
        # now cached.
        assert cli.get_element_state(project, element_name) == "buildable"


@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.parametrize("ref_storage", ["inline", "project.refs"])
def test_track_junction_included(cli, tmpdir, datafiles, ref_storage, kind):
    project = str(datafiles)
    element_path = os.path.join(project, "elements")
    subproject_path = os.path.join(project, "files", "sub-project")
    junction_path = os.path.join(element_path, "junction.bst")

    update_project_configuration(project, {"ref-storage": ref_storage, "(@)": ["junction.bst:test.yml"]})

    generate_junction(str(tmpdir.join("junction_repo")), subproject_path, junction_path, store_ref=False)

    result = cli.run(project=project, args=["source", "track", "junction.bst"])
    result.assert_success()
