# Copyright Spack Project Developers. See COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import os
import pathlib

import pytest

import spack.builder
import spack.concretize
import spack.paths
import spack.repo
from spack.llnl.util.filesystem import touch


@pytest.fixture()
def builder_test_repository(config):
    builder_test_path = os.path.join(spack.paths.test_repos_path, "spack_repo", "builder_test")
    with spack.repo.use_repositories(builder_test_path) as mock_repo:
        yield mock_repo


@pytest.mark.parametrize(
    "spec_str,expected_values",
    [
        (
            "callbacks@2.0",
            [
                ("BEFORE_INSTALL_1_CALLED", "1"),
                ("BEFORE_INSTALL_2_CALLED", "1"),
                ("CALLBACKS_INSTALL_CALLED", "1"),
                ("AFTER_INSTALL_1_CALLED", "1"),
                ("TEST_VALUE", "3"),
                ("INSTALL_VALUE", "CALLBACKS"),
            ],
        ),
        # The last callback is conditional on "@1.0", check it's being executed
        (
            "callbacks@1.0",
            [
                ("BEFORE_INSTALL_1_CALLED", "1"),
                ("BEFORE_INSTALL_2_CALLED", "1"),
                ("CALLBACKS_INSTALL_CALLED", "1"),
                ("AFTER_INSTALL_1_CALLED", "1"),
                ("AFTER_INSTALL_2_CALLED", "1"),
                ("TEST_VALUE", "4"),
                ("INSTALL_VALUE", "CALLBACKS"),
            ],
        ),
        # The package below adds to "callbacks" using inheritance, test that using super()
        # works with builder hierarchies
        (
            "inheritance@1.0",
            [
                ("DERIVED_BEFORE_INSTALL_CALLED", "1"),
                ("BEFORE_INSTALL_1_CALLED", "1"),
                ("BEFORE_INSTALL_2_CALLED", "1"),
                ("CALLBACKS_INSTALL_CALLED", "1"),
                ("INHERITANCE_INSTALL_CALLED", "1"),
                ("AFTER_INSTALL_1_CALLED", "1"),
                ("AFTER_INSTALL_2_CALLED", "1"),
                ("TEST_VALUE", "4"),
                ("INSTALL_VALUE", "INHERITANCE"),
            ],
        ),
        # Generate custom phases using a GenericBuilder
        (
            "custom-phases",
            [("CONFIGURE_CALLED", "1"), ("INSTALL_CALLED", "1"), ("LAST_PHASE", "INSTALL")],
        ),
        # Old-style package, with phase defined in base builder
        ("old-style-autotools@1.0", [("AFTER_AUTORECONF_1_CALLED", "1")]),
        ("old-style-autotools@2.0", [("AFTER_AUTORECONF_2_CALLED", "1")]),
        ("old-style-custom-phases", [("AFTER_CONFIGURE_CALLED", "1"), ("TEST_VALUE", "0")]),
    ],
)
@pytest.mark.usefixtures("builder_test_repository", "config")
@pytest.mark.disable_clean_stage_check
def test_callbacks_and_installation_procedure(spec_str, expected_values, working_env):
    """Test the correct execution of callbacks and installation procedures for packages."""
    s = spack.concretize.concretize_one(spec_str)
    builder = spack.builder.create(s.package)
    for phase_fn in builder:
        phase_fn.execute()

    # Check calls have produced the expected side effects
    for var_name, expected in expected_values:
        assert os.environ[var_name] == expected, os.environ


@pytest.mark.usefixtures("builder_test_repository", "config")
@pytest.mark.parametrize(
    "spec_str,method_name,expected",
    [
        # Call a function defined on the package, which calls the same function defined
        # on the super(builder)
        ("old-style-autotools", "configure_args", ["--with-foo"]),
        # Call a function defined on the package, which calls the same function defined on the
        # super(pkg), which calls the same function defined in the super(builder)
        ("old-style-derived", "configure_args", ["--with-bar", "--with-foo"]),
    ],
)
def test_old_style_compatibility_with_super(spec_str, method_name, expected):
    s = spack.concretize.concretize_one(spec_str)
    builder = spack.builder.create(s.package)
    value = getattr(builder, method_name)()
    assert value == expected


@pytest.mark.not_on_windows("log_ouput cannot currently be used outside of subprocess on Windows")
@pytest.mark.regression("33928")
@pytest.mark.usefixtures("builder_test_repository", "config", "working_env")
@pytest.mark.disable_clean_stage_check
def test_build_time_tests_are_executed_from_default_builder():
    s = spack.concretize.concretize_one("old-style-autotools")
    builder = spack.builder.create(s.package)
    builder.pkg.run_tests = True
    for phase_fn in builder:
        phase_fn.execute()

    assert os.environ.get("CHECK_CALLED") == "1", "Build time tests not executed"
    assert os.environ.get("INSTALLCHECK_CALLED") == "1", "Install time tests not executed"


@pytest.mark.regression("34518")
@pytest.mark.usefixtures("builder_test_repository", "config", "working_env")
def test_monkey_patching_wrapped_pkg():
    """Confirm 'run_tests' is accessible through wrappers."""
    s = spack.concretize.concretize_one("old-style-autotools")
    builder = spack.builder.create(s.package)
    assert s.package.run_tests is False
    assert builder.pkg.run_tests is False
    assert builder.pkg_with_dispatcher.run_tests is False

    s.package.run_tests = True
    assert builder.pkg.run_tests is True
    assert builder.pkg_with_dispatcher.run_tests is True


@pytest.mark.regression("34440")
@pytest.mark.usefixtures("builder_test_repository", "config", "working_env")
def test_monkey_patching_test_log_file():
    """Confirm 'test_log_file' is accessible through wrappers."""
    s = spack.concretize.concretize_one("old-style-autotools")
    builder = spack.builder.create(s.package)

    s.package.tester.test_log_file = "/some/file"
    assert builder.pkg.tester.test_log_file == "/some/file"
    assert builder.pkg_with_dispatcher.tester.test_log_file == "/some/file"


# Windows context manager's __exit__ fails with ValueError ("I/O operation
# on closed file").
@pytest.mark.not_on_windows("Does not run on windows")
def test_install_time_test_callback(tmp_path: pathlib.Path, config, mock_packages, mock_stage):
    """Confirm able to run stand-alone test as a post-install callback."""
    s = spack.concretize.concretize_one("py-test-callback")
    builder = spack.builder.create(s.package)
    builder.pkg.run_tests = True
    s.package.tester.test_log_file = str(tmp_path / "install_test.log")
    touch(s.package.tester.test_log_file)

    for phase_fn in builder:
        phase_fn.execute()

    with open(s.package.tester.test_log_file, "r", encoding="utf-8") as f:
        results = f.read().replace("\n", " ")
        assert "PyTestCallback test" in results


@pytest.mark.regression("43097")
@pytest.mark.usefixtures("builder_test_repository", "config")
def test_mixins_with_builders(working_env):
    """Tests that run_after and run_before callbacks are accumulated correctly,
    when mixins are used with builders.
    """
    s = spack.concretize.concretize_one("builder-and-mixins")
    builder = spack.builder.create(s.package)

    # Check that callbacks added by the mixin are in the list
    assert any(fn.__name__ == "before_install" for _, fn in builder._run_before_callbacks)
    assert any(fn.__name__ == "after_install" for _, fn in builder._run_after_callbacks)

    # Check that callback from the GenericBuilder are in the list too
    assert any(fn.__name__ == "sanity_check_prefix" for _, fn in builder._run_after_callbacks)


def test_reading_api_v20_attributes():
    """Tests that we can read attributes from API v2.0 builders."""

    class TestBuilder(spack.builder.Builder):
        legacy_methods = ("configure", "install")
        legacy_attributes = ("foo", "bar")
        legacy_long_methods = ("baz", "fee")

    methods = spack.builder.package_methods(TestBuilder)
    assert methods == ("configure", "install")
    attributes = spack.builder.package_attributes(TestBuilder)
    assert attributes == ("foo", "bar")
    long_methods = spack.builder.package_long_methods(TestBuilder)
    assert long_methods == ("baz", "fee")


def test_reading_api_v22_attributes():
    """Tests that we can read attributes from API v2.2 builders."""

    class TestBuilder(spack.builder.Builder):
        package_methods = ("configure", "install")
        package_attributes = ("foo", "bar")
        package_long_methods = ("baz", "fee")

    methods = spack.builder.package_methods(TestBuilder)
    assert methods == ("configure", "install")
    attributes = spack.builder.package_attributes(TestBuilder)
    assert attributes == ("foo", "bar")
    long_methods = spack.builder.package_long_methods(TestBuilder)
    assert long_methods == ("baz", "fee")
