import json
import os
import platform
import textwrap
from collections import OrderedDict
from textwrap import dedent

import pytest

from conan.internal.paths import CONANFILE
from conan.test.assets.genconanfile import GenConanfile
from conan.test.utils.profiles import create_profile as _create_profile
from conan.test.utils.test_files import temp_folder
from conan.test.utils.tools import TestClient
from conan.internal.util.files import load, save

conanfile_scope_env = """
from conan import ConanFile

class AConan(ConanFile):
    name = "hello0"
    version = "0.1"
    settings = "os", "compiler", "arch"

    def build(self):
        # Print environment vars
        if self.settings.os == "Windows":
            self.run("SET")
        else:
            self.run("env")
"""


def create_profile(folder, name, settings=None, package_settings=None, options=None):
    _create_profile(folder, name, settings, package_settings, options)
    content = load(os.path.join(folder, name))
    content = "include(default)\n    \n" + content
    save(os.path.join(folder, name), content)


class TestProfile:

    @pytest.fixture(autouse=True)
    def setup(self):
        self.client = TestClient()

    def test_profile_relative_cwd(self):
        self.client.save({"conanfile.txt": "", "sub/sub/profile": ""})
        self.client.current_folder = os.path.join(self.client.current_folder, "sub")
        self.client.run("install .. -pr=sub/profile2", assert_error=True)
        assert "ERROR: Profile not found: sub/profile2" in self.client.out
        self.client.run("install .. -pr=sub/profile")
        assert "Installing packages" in self.client.out

    def test_bad_syntax(self):
        self.client.save({CONANFILE: conanfile_scope_env})
        self.client.run("export . --user=lasote --channel=stable")

        profile = '''
        [settings
        '''
        clang_profile_path = os.path.join(self.client.paths.profiles_path, "clang")
        save(clang_profile_path, profile)
        self.client.run("install --requires=hello0/0.1@lasote/stable --build missing -pr clang",
                        assert_error=True)
        assert "Error reading 'clang' profile" in self.client.out
        assert "Bad syntax" in self.client.out

        profile = '''
        [settings]
        [invented]
        '''
        save(clang_profile_path, profile)
        self.client.run("install --requires=hello0/0.1@lasote/stable --build missing -pr clang",
                        assert_error=True)
        assert "Unrecognized field 'invented'" in self.client.out
        assert "Error reading 'clang' profile" in self.client.out

        profile = '''
        [settings]
        as
        '''
        save(clang_profile_path, profile)
        self.client.run("install --requires=hello0/0.1@lasote/stable --build missing -pr clang",
                        assert_error=True)
        assert "Error reading 'clang' profile: Invalid setting line 'as'" in self.client.out

        profile = '''
        [settings]
        os =   a value
        '''
        save(clang_profile_path, profile)
        self.client.run("install --requires=hello0/0.1@lasote/stable --build missing -pr clang",
                        assert_error=True)
        # stripped "a value"
        assert "'a value' is not a valid 'settings.os'" in self.client.out

    @pytest.mark.parametrize("path", ["", "./local_profiles/", None])
    def test_install_with_missing_profile(self, path):
        if path is None:
            # Not good practice to introduce temp_folder() in the expand because it randomize
            # the test names causing issues to split them in N processes
            path = temp_folder() + "/"
        self.client.save({CONANFILE: conanfile_scope_env})
        self.client.run('install . -pr "%sscopes_env"' % path, assert_error=True)
        assert "ERROR: Profile not found:" in self.client.out
        assert "scopes_env" in self.client.out

    @pytest.mark.skipif(platform.system() != "Windows", reason="Windows profiles")
    def test_install_profile_settings(self):
        # Create a profile and use it
        profile_settings = OrderedDict([("compiler", "msvc"),
                                        ("compiler.version", "191"),
                                        ("compiler.runtime", "dynamic"),
                                        ("arch", "x86")])

        create_profile(self.client.paths.profiles_path, "vs_12_86",
                       settings=profile_settings, package_settings={})

        self.client.save({"conanfile.py": conanfile_scope_env})
        self.client.run("export . --user=lasote --channel=stable")
        self.client.run("install . --build missing -pr vs_12_86")
        info = self.client.out
        for setting, value in profile_settings.items():
            assert "%s=%s" % (setting, value) in info

        # Try to override some settings in install command
        self.client.run("install . --build missing -pr vs_12_86 -s compiler.version=191")
        info = self.client.out
        for setting, value in profile_settings.items():
            if setting != "compiler.version":
                assert "%s=%s" % (setting, value) in info
            else:
                assert "compiler.version=191" in info

        # Use package settings in profile
        tmp_settings = OrderedDict()
        tmp_settings["compiler"] = "gcc"
        tmp_settings["compiler.libcxx"] = "libstdc++11"
        tmp_settings["compiler.version"] = "4.8"
        package_settings = {"hello0/*": tmp_settings}
        create_profile(self.client.paths.profiles_path,
                       "vs_12_86_hello0_gcc", settings=profile_settings,
                       package_settings=package_settings)
        # Try to override some settings in install command
        self.client.run("install . --build missing -pr vs_12_86_hello0_gcc -s compiler.version=191")
        info = self.client.out
        assert "compiler=gcc" in info
        assert "compiler.libcxx=libstdc++11" in info

        # If other package is specified compiler is not modified
        package_settings = {"NoExistsRecipe": tmp_settings}
        create_profile(self.client.paths.profiles_path,
                       "vs_12_86_hello0_gcc", settings=profile_settings,
                       package_settings=package_settings)

        # Mix command line package settings with profile
        package_settings = {"hello0/*": tmp_settings}
        create_profile(self.client.paths.profiles_path, "vs_12_86_hello0_gcc",
                       settings=profile_settings, package_settings=package_settings)

        # Try to override some settings in install command
        self.client.run("install . --build missing -pr vs_12_86_hello0_gcc"
                        " -s compiler.version=191 -s hello0/*:compiler.libcxx=libstdc++")
        info = self.client.out
        assert "compiler=gcc" in info
        assert "compiler.libcxx=libstdc++11" not in info
        assert "compiler.libcxx=libstdc++" in info

    def test_install_profile_package_settings(self):

        conanfile = textwrap.dedent("""
            from conan import ConanFile
            class HelloConan(ConanFile):
                name = 'hello0'
                version = '0.1'
                settings = "os", "compiler", "arch", "build_type"
                def configure(self):
                    self.output.info(self.settings.compiler)
                    self.output.info(self.settings.compiler.version)
            """)
        self.client.save({"conanfile.py": conanfile})

        # Create a profile and use it
        profile_settings = OrderedDict([("os", "Windows"),
                                        ("compiler", "msvc"),
                                        ("compiler.version", "191"),
                                        ("compiler.runtime", "dynamic"),
                                        ("arch", "x86")])

        # Use package settings in profile
        tmp_settings = OrderedDict()
        tmp_settings["compiler"] = "gcc"
        tmp_settings["compiler.libcxx"] = "libstdc++11"
        tmp_settings["compiler.version"] = "4.8"
        package_settings = {"*@lasote/*": tmp_settings}
        _create_profile(self.client.paths.profiles_path,
                        "myprofile", settings=profile_settings,
                        package_settings=package_settings)
        # Try to override some settings in install command
        self.client.run("install . --user=lasote --channel=testing -pr myprofile")
        info = self.client.out
        assert "(hello0/0.1@lasote/testing): gcc" in info
        assert "(hello0/0.1@lasote/testing): 4.8" in info

        package_settings = {"*@other/*": tmp_settings}
        _create_profile(self.client.paths.profiles_path,
                        "myprofile", settings=profile_settings,
                        package_settings=package_settings)
        # Try to override some settings in install command
        self.client.run("install . --user=lasote --channel=testing -pr myprofile")
        info = self.client.out
        assert "(hello0/0.1@lasote/testing): msvc" in info
        assert "(hello0/0.1@lasote/testing): 191" in info
        assert "(hello0/0.1@lasote/testing): gcc" not in info
        assert "(hello0/0.1@lasote/testing): 4.8" not in info

    def test_package_settings_no_user_channel(self):
        conanfile = textwrap.dedent("""
            from conan import ConanFile
            class Pkg(ConanFile):
                settings = "os"
                def build(self):
                    self.output.info("SETTINGS! os={}!!".format(self.settings.os))
                """)
        profile = textwrap.dedent("""
            [settings]
            os=Windows
            # THIS FAILED BEFORE WITH NO MATCH
            mypkg/0.1:os=Linux
            mypkg/0.1@user/channel:os=FreeBSD
            """)
        client = TestClient()
        client.save({"conanfile.py": conanfile,
                     "profile": profile})

        client.run("create . --name=mypkg --version=0.1 --user=user --channel=channel -pr=profile")
        assert "mypkg/0.1@user/channel: SETTINGS! os=FreeBSD!!" in client.out
        client.run("create . --name=mypkg --version=0.1 -pr=profile")
        assert "mypkg/0.1: SETTINGS! os=Linux!!" in client.out

    def test_install_profile_options(self):
        create_profile(self.client.paths.profiles_path, "vs_12_86",
                       options={"hello0*:language": 1,
                                "hello0*:static": False})

        self.client.save({"conanfile.py": GenConanfile("hello0", "1").with_option("language", [1, 2])
                          .with_option("static", [True, False])})
        self.client.run("install . --build missing -pr vs_12_86")
        info = self.client.out
        assert "language=1" in info
        assert "static=False" in info

    def test_scopes_env(self):
        # Create a profile and use it
        c = TestClient()
        c.save({"profile": "include(default)\n[buildenv]\nCXX=/path/tomy/g++",
                "conanfile.py": conanfile_scope_env})

        c.run("build . -pr=profile")
        assert "CXX=/path/tomy/g++" in c.out

        # The env variable shouldn't persist after install command
        assert os.environ.get("CC", None) != "/path/tomy/gcc"
        assert os.environ.get("CXX", None) != "/path/tomy/g++"

    def test_info_with_profiles(self):

        self.client.run("remove '*' -c")
        # Create a simple recipe to require
        winreq_conanfile = '''
from conan import ConanFile

class winrequireDefaultNameConan(ConanFile):
    name = "winrequire"
    version = "0.1"
    settings = "os", "compiler", "arch", "build_type"

'''

        files = {"conanfile.py": winreq_conanfile}
        self.client.save(files)
        self.client.run("export . --user=lasote --channel=stable")

        # Now require the first recipe depending on OS=windows
        conanfile = '''from conan import ConanFile
import os

class DefaultNameConan(ConanFile):
    name = "hello"
    version = "0.1"
    settings = "os", "compiler", "arch", "build_type"

    def requirements(self):
        if self.settings.os == "Windows":
            self.requires("winrequire/0.1@lasote/stable")

'''
        files = {"conanfile.py": conanfile}
        self.client.save(files)
        self.client.run("export . --user=lasote --channel=stable")

        # Create a profile that doesn't activate the require
        create_profile(self.client.paths.profiles_path, "scopes_env",
                       settings={"os": "Linux"})

        # Install with the previous profile
        self.client.run("graph info --requires=hello/0.1@lasote/stable --profile scopes_env")
        assert '''Requires:
                winrequire/0.1@lasote/stable''' not in self.client.out

        # Create a profile that activate the require
        create_profile(self.client.paths.profiles_path, "scopes_env",
                       settings={"os": "Windows"})

        # Install with the previous profile
        self.client.run("graph info --requires=hello/0.1@lasote/stable --profile scopes_env")
        assert ' winrequire/0.1@lasote/stable' in self.client.out


class TestProfileAggregation:

    profile1 = dedent("""
    [settings]
    os=Windows
    arch=x86_64
    """)

    profile2 = dedent("""
    [settings]
    arch=x86
    build_type=Debug
    compiler=msvc
    compiler.version=191
    compiler.runtime=dynamic
    """)

    conanfile = dedent("""
    from conan import ConanFile
    import os

    class DefaultNameConan(ConanFile):
        settings = "os", "compiler", "arch", "build_type"

        def build(self):
            self.output.warning("ENV1:%s" % os.getenv("ENV1"))
            self.output.warning("ENV2:%s" % os.getenv("ENV2"))
            self.output.warning("ENV3:%s" % os.getenv("ENV3"))
    """)

    consumer = dedent("""
    from conan import ConanFile
    import os

    class DefaultNameConan(ConanFile):
        settings = "os", "compiler", "arch", "build_type"
        requires = "lib/1.0@user/channel"
    """)

    @pytest.fixture(autouse=True)
    def setup(self):
        self.client = TestClient()
        self.client.save({CONANFILE: self.conanfile,
                          "profile1": self.profile1, "profile2": self.profile2})
        self._pkg_lib_10_id = "9b7f1e80c96289e8d9a3e7ded02830525090d5d4"

    def test_info(self):
        # The latest declared profile has priority
        self.client.run("create . --name=lib --version=1.0 --user=user --channel=channel --profile profile1 -pr profile2")

        self.client.save({CONANFILE: self.consumer})
        self.client.run("graph info . --profile profile1 --profile profile2")
        assert self._pkg_lib_10_id in self.client.out

    def test_export_pkg(self):
        self.client.run("export-pkg . --name=lib --version=1.0 --user=user --channel=channel -pr profile1 -pr profile2")
        # ID for the expected settings applied: x86, Visual Studio 15,...
        assert self._pkg_lib_10_id in self.client.out

    def test_profile_crazy_inheritance(self):
        profile1 = dedent("""
            [settings]
            os=Windows
            arch=x86_64
            compiler=msvc
            compiler.version=191
            compiler.runtime=dynamic
            """)

        profile2 = dedent("""
            include(profile1)
            [settings]
            os=Linux
            """)

        self.client.save({"profile1": profile1, "profile2": profile2})
        self.client.run("create . --name=lib --version=1.0 --profile profile2 -pr profile1")
        assert dedent("""\
                             [settings]
                             arch=x86_64
                             compiler=msvc
                             compiler.runtime=dynamic
                             compiler.runtime_type=Release
                             compiler.version=191
                             os=Windows""") in self.client.out


def test_profile_from_cache_path():
    """ When passing relative folder/profile as profile file, it MUST be used
        conan install . -pr=profiles/default
        /tmp/profiles/default MUST be consumed as target profile
        https://github.com/conan-io/conan/pull/8685
    """
    client = TestClient()
    path = os.path.join(client.paths.profiles_path, "android", "profile1")
    save(path, "[settings]\nos=Android")
    client.save({"conanfile.txt": ""})
    client.run("install . -pr=android/profile1")
    assert "os=Android" in client.out


def test_profile_from_relative_pardir():
    """ When passing relative ../path as profile file, it MUST be used
        conan install . -pr=../profiles/default
        /tmp/profiles/default MUST be consumed as target profile
    """
    client = TestClient()
    client.save({"profiles/default": "[settings]\nos=AIX",
                 "current/conanfile.txt": ""})
    with client.chdir("current"):
        client.run("install . -pr=../profiles/default")
    assert "os=AIX" in client.out


def test_profile_from_relative_dotdir():
    """ When passing relative ./path as profile file, it MUST be used
        conan install . -pr=./profiles/default
        /tmp/profiles/default MUST be consumed as target profile
    """
    client = TestClient()
    client.save({os.path.join("profiles", "default"): "[settings]\nos=AIX",
                 os.path.join("current", "conanfile.txt"): ""})
    client.run("install ./current -pr=./profiles/default")
    assert "os=AIX" in client.out


def test_profile_from_temp_absolute_path():
    """ When passing absolute path as profile file, it MUST be used
        conan install . -pr=/tmp/profiles/default
        /tmp/profiles/default MUST be consumed as target profile
    """
    client = TestClient()
    client.save({"profiles/default": "[settings]\nos=AIX",
                 "current/conanfile.txt": ""})
    profile_path = os.path.join(client.current_folder, "profiles", "default")
    recipe_path = os.path.join(client.current_folder, "current", "conanfile.txt")
    client.run('install "{}" -pr="{}"'.format(recipe_path, profile_path))
    assert "os=AIX" in client.out


def test_consumer_specific_settings():
    client = TestClient()
    dep = str(GenConanfile().with_settings("build_type").with_option("shared", [True, False])
              .with_default_option("shared", False))
    configure = """
    def configure(self):
        self.output.warning("I'm {} and my build type is {}".format(self.name,
                                                                 self.settings.build_type))
        self.output.warning("I'm {} and my shared is {}".format(self.name, self.options.shared))
    """
    dep += configure
    client.save({"conanfile.py": dep})
    client.run("create . --name=dep --version=1.0")
    client.run("create . --name=dep --version=1.0 -s build_type=Debug -o dep*:shared=True")

    consumer = str(GenConanfile().with_settings("build_type").with_requires("dep/1.0")
                   .with_option("shared", [True, False]).with_default_option("shared", False))
    consumer += configure
    client.save({"conanfile.py": consumer})

    # Regular install with release
    client.run("install . -s build_type=Release")
    assert "I'm dep and my build type is Release" in client.out
    assert "I'm None and my build type is Release" in client.out
    assert "I'm dep and my shared is False" in client.out
    assert "I'm None and my shared is False" in client.out

    # Now the dependency by name
    client.run("install . -s dep/*:build_type=Debug -o dep/*:shared=True")
    assert "I'm dep and my build type is Debug" in client.out
    assert "I'm None and my build type is Release" in client.out
    assert "I'm dep and my shared is True" in client.out
    assert "I'm None and my shared is False" in client.out

    # Now the consumer using &
    client.run("install . -s &:build_type=Debug -o shared=True")
    assert "I'm dep and my build type is Release" in client.out
    assert "I'm None and my build type is Debug" in client.out
    assert "I'm dep and my shared is False" in client.out
    assert "I'm None and my shared is True" in client.out

    # Now use a conanfile.txt
    client.save({"conanfile.txt": textwrap.dedent("""
            [requires]
            dep/1.0
    """)}, clean_first=True)

    # Regular install with release
    client.run("install . -s build_type=Release")
    assert "I'm dep and my build type is Release" in client.out

    # Now the dependency by name
    client.run("install . -s dep*:build_type=Debug -o dep*:shared=True")
    assert "I'm dep and my build type is Debug" in client.out
    assert "I'm dep and my shared is True" in client.out

    # Test that the generators take the setting
    if platform.system() != "Windows":  # Toolchain in windows is multiconfig
        # Now the consumer using &
        client.run("install . -s &:build_type=Debug -g CMakeToolchain")
        assert "I'm dep and my build type is Release" in client.out
        # Verify the cmake toolchain takes Debug
        assert "I'm dep and my shared is False" in client.out
        presets = json.loads(client.load("CMakePresets.json"))
        assert presets["configurePresets"][0]["cacheVariables"]['CMAKE_BUILD_TYPE'] == "Debug"


def test_create_and_priority_of_consumer_specific_setting():
    client = TestClient()
    conanfile = str(GenConanfile().with_settings("build_type").with_name("foo").with_version("1.0"))
    configure = """
    def configure(self):
        self.output.warning("I'm {} and my build type is {}".format(self.name,
                                                                 self.settings.build_type))
    """
    conanfile += configure
    client.save({"conanfile.py": conanfile})
    client.run("create . -s foo*:build_type=Debug")
    assert "I'm foo and my build type is Debug" in client.out

    client.run("create . -s foo*:build_type=Debug -s &:build_type=Release")
    assert "I'm foo and my build type is Release" in client.out

    # The order DOES matter
    client.run("create . -s &:build_type=Release -s foo*:build_type=Debug ")
    assert "I'm foo and my build type is Debug" in client.out

    # With test_package also works
    test = str(GenConanfile().with_test("pass").with_setting("build_type"))
    test += configure
    client.save({"test_package/conanfile.py": test})
    client.run("create . -s &:build_type=Debug -s build_type=Release")
    assert "I'm foo and my build type is Debug" in client.out
    # the test package recipe has debug too
    assert "I'm None and my build type is Debug" in client.out


def test_package_consumer_is_only_the_tested_one():
    """
    the &:xxx pattern applies only to the package being tested, not other requires
    """
    c = TestClient(light=True)
    test = textwrap.dedent("""
        from conan import ConanFile
        class Tool(ConanFile):
            def requirements(self):
                self.requires(self.tested_reference_str)
                self.requires("dep/1.0")

            def test(self):
                pass
        """)
    c.save({"dep/conanfile.py": GenConanfile("dep", "1.0"),
            "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_option("myoption", [1, 2]),
            "pkg/test_package/conanfile.py": test})
    c.run("create dep")
    c.run("create pkg -o &:myoption=1")
    # This would crash if myoption is applied to dep
    assert 'pkg/0.1 (test package): Running test()' in c.out


def test_consumer_specific_settings_from_profile():
    client = TestClient()
    conanfile = str(GenConanfile().with_settings("build_type").with_name("hello"))
    configure = """
    def configure(self):
        self.output.warning("I'm {} and my build type is {}".format(self.name,
                                                                 self.settings.build_type))
    """
    conanfile += configure
    profile = textwrap.dedent("""
        include(default)
        [settings]
        &:build_type=Debug
    """)
    client.save({"conanfile.py": conanfile, "my_profile.txt": profile})
    client.run("install . --profile my_profile.txt")
    assert "I'm hello and my build type is Debug" in client.out


def test_consumer_invalid_profile_multiple_groups():
    """
    Issue related: https://github.com/conan-io/conan/issues/16448
    """
    client = TestClient()
    conanfile = GenConanfile(name="hello", version="1.0").with_settings("os", "arch",
                                                                        "build_type", "compiler")
    prof1 = textwrap.dedent("""\
    [settings]
    arch=x86_64
    os=Linux
    build_type=Release
    compiler=clang
    compiler.libcxx=libstdc++11
    compiler.version=18
    compiler.cppstd=20

    [conf]
    tools.build:compiler_executables={'c': '/usr/bin/clang-18', 'cpp': '/usr/bin/clang++-18'}

    [settings]
    # Empty and duplicated section

    [options]
    package/*:option=Whatever

    [conf]
    # Another one
    """)
    client.save({
        "conanfile.py": conanfile,
        "myprofs/myprofile": prof1
    })
    client.run("build . --profile:host myprofs/myprofile -g CMakeToolchain",
               assert_error=True)
    assert ("ERROR: Error reading 'myprofs/myprofile' profile: ConfigParser: "
            "Duplicated section: [settings]") in client.out

def test_compose_numeric_values_scoped_pkg():
    tc = TestClient(light=True)
    tc.save({"conanfile.py": GenConanfile("hello", "0.1")
                .with_package("self.output.info('user.var.value: ' + str(self.conf.get('user.var:value')))"),
             "profile": """[conf]\nuser.var:value=8.1\nhello/*:user.var:value=10\n"""})

    tc.run("create . -pr=profile")
    assert "user.var.value: 10" in tc.out


def test_nullify_settings():
    c = TestClient()
    conanfile = textwrap.dedent("""
        from conan import ConanFile
        class Pkg(ConanFile):
            settings = "build_type"
            def configure(self):
                if not self.settings.build_type:
                    self.output.info("BUILD TYPE NOT DEFINED!!!")
                else:
                    self.output.info(f"I am a {self.settings.build_type} pkg!!!")
        """)
    c.save({"conanfile.py": conanfile,
            "profile": "[settings]\nbuild_type=Release\npkg1/*:build_type=~"})
    c.run("create . --name=pkg1 --version=0.1")
    c.run("create . --name=pkg2 --version=0.1")
    c.run("create . --name=pkg3 --version=0.1")
    c.save({"conanfile.py": GenConanfile().with_requires("pkg1/0.1", "pkg2/0.1", "pkg3/0.1")})

    c.run("graph info . -s build_type=Release -s pkg1/*:build_type=~")
    assert "pkg1/0.1: BUILD TYPE NOT DEFINED!!!" in c.out
    assert "pkg2/0.1: I am a Release pkg!!!" in c.out
    assert "pkg3/0.1: I am a Release pkg!!!" in c.out

    c.run("graph info . -pr=profile")
    assert "pkg1/0.1: BUILD TYPE NOT DEFINED!!!" in c.out
    assert "pkg2/0.1: I am a Release pkg!!!" in c.out
    assert "pkg3/0.1: I am a Release pkg!!!" in c.out
