class PackageIDTest(unittest.TestCase):
    def setUp(self):
        self.client = TestClient()

    def cross_build_settings_test(self):
        client = TestClient()
        conanfile = """from conans import ConanFile
class Pkg(ConanFile):
    settings = "os", "arch", "compiler", "os_build", "arch_build"
        """
        client.save({"conanfile.py": conanfile})
        client.run(
            'install . -s os=Windows -s compiler="Visual Studio" '
            '-s compiler.version=15 -s compiler.runtime=MD '
            '-s os_build=Windows -s arch_build=x86 -s compiler.toolset=v141')
        conaninfo = client.load("conaninfo.txt")
        self.assertNotIn("compiler.toolset=None", conaninfo)
        self.assertNotIn("os_build=None", conaninfo)
        self.assertNotIn("arch_build=None", conaninfo)

    def _export(self,
                name,
                version,
                package_id_text=None,
                requires=None,
                channel=None,
                default_option_value='"off"',
                settings=None):
        conanfile = GenConanfile().with_name(name).with_version(version)\
                                  .with_option(option_name="an_option", values=["on", "off"])\
                                  .with_default_option("an_option", default_option_value)\
                                  .with_package_id(package_id_text)
        if settings:
            for setting in settings:
                conanfile = conanfile.with_setting(setting)

        if requires:
            for require in requires:
                conanfile = conanfile.with_require(
                    ConanFileReference.loads(require))

        self.client.save({"conanfile.py": str(conanfile)}, clean_first=True)
        revisions_enabled = self.client.cache.config.revisions_enabled
        self.client.disable_revisions()
        # Trick to allow export a new recipe without removing old binary packages
        self.client.run("export . %s" % (channel or "lasote/stable"))
        if revisions_enabled:
            self.client.enable_revisions()

    @property
    def conaninfo(self):
        return load(os.path.join(self.client.current_folder, CONANINFO))

    def test_version_semver_schema(self):
        self._export("Hello", "1.2.0")
        self._export("Hello2",
                     "2.3.8",
                     package_id_text='self.info.requires["Hello"].semver()',
                     requires=["Hello/1.2.0@lasote/stable"])

        # Build the dependencies with --build missing
        self.client.save(
            {"conanfile.txt": "[requires]\nHello2/2.3.8@lasote/stable"},
            clean_first=True)
        self.client.run("install . --build missing")
        self.assertIn("Hello2/2.Y.Z",
                      [line.strip() for line in self.conaninfo.splitlines()])

        # Now change the Hello version and build it, if we install out requires should not be
        # needed the --build needed because Hello2 don't need to be rebuilt
        self._export("Hello", "1.5.0", package_id_text=None, requires=None)
        self.client.run("install Hello/1.5.0@lasote/stable --build missing")
        self._export("Hello2",
                     "2.3.8",
                     package_id_text='self.info.requires["Hello"].semver()',
                     requires=["Hello/1.5.0@lasote/stable"])

        self.client.save(
            {"conanfile.txt": "[requires]\nHello2/2.3.8@lasote/stable"},
            clean_first=True)

        if not self.client.cache.config.revisions_enabled:
            self.client.run("install .")
            self.assertIn(
                "Hello2/2.3.8@lasote/stable:e0d17b497b58c730aac949f374cf0bdb533549ab",
                self.client.out)
            self.assertIn(
                "Hello2/2.Y.Z",
                [line.strip() for line in self.conaninfo.splitlines()])
        else:
            # As we have changed Hello2, the binary is not valid anymore so it won't find it
            # but will look for the same package_id
            self.client.run("install .", assert_error=True)
            self.assertIn(
                "WARN: The package Hello2/2.3.8@lasote/stable:"
                "e0d17b497b58c730aac949f374cf0bdb533549ab doesn't belong to the "
                "installed recipe revision, removing folder", self.client.out)
            self.assertIn(
                "- Package ID: e0d17b497b58c730aac949f374cf0bdb533549ab",
                self.client.out)

        # Try to change user and channel too, should be the same, not rebuilt needed
        self._export("Hello",
                     "1.5.0",
                     package_id_text=None,
                     requires=None,
                     channel="memsharded/testing")
        self.client.run(
            "install Hello/1.5.0@memsharded/testing --build missing")
        self._export("Hello2",
                     "2.3.8",
                     package_id_text='self.info.requires["Hello"].semver()',
                     requires=["Hello/1.5.0@memsharded/testing"])

        self.client.save(
            {"conanfile.txt": "[requires]\nHello2/2.3.8@lasote/stable"},
            clean_first=True)

        if not self.client.cache.config.revisions_enabled:
            self.client.run("install .")
            self.assertIn(
                "Hello2/2.3.8@lasote/stable:e0d17b497b58c730aac949f374cf0bdb533549ab",
                self.client.out)
            self.assertIn(
                "Hello2/2.Y.Z",
                [line.strip() for line in self.conaninfo.splitlines()])
        else:
            self.client.run("install .", assert_error=True)
            self.assertIn(
                "Hello2/2.3.8@lasote/stable:e0d17b497b58c730aac949f374cf0bdb533549ab",
                self.client.out)

    def test_version_full_version_schema(self):
        self._export("Hello", "1.2.0", package_id_text=None, requires=None)
        self._export(
            "Hello2",
            "2.3.8",
            package_id_text='self.info.requires["Hello"].full_version_mode()',
            requires=["Hello/1.2.0@lasote/stable"])

        # Build the dependencies with --build missing
        self.client.save(
            {"conanfile.txt": "[requires]\nHello2/2.3.8@lasote/stable"},
            clean_first=True)
        self.client.run("install . --build missing")
        self.assertIn("Hello2/2.3.8", self.conaninfo)

        # If we change the user and channel should not be needed to rebuild
        self._export("Hello",
                     "1.2.0",
                     package_id_text=None,
                     requires=None,
                     channel="memsharded/testing")
        self.client.run(
            "install Hello/1.2.0@memsharded/testing --build missing")
        self._export(
            "Hello2",
            "2.3.8",
            package_id_text='self.info.requires["Hello"].full_version_mode()',
            requires=["Hello/1.2.0@memsharded/testing"])

        self.client.save(
            {"conanfile.txt": "[requires]\nHello2/2.3.8@lasote/stable"},
            clean_first=True)
        if not self.client.cache.config.revisions_enabled:
            self.client.run("install .")
            self.assertIn(
                "Hello2/2.3.8@lasote/stable:3ec60bb399a8bcb937b7af196f6685ba878aab02",
                self.client.out)
        else:
            # As we have changed Hello2, the binary is not valid anymore so it won't find it
            # but will look for the same package_id
            self.client.run("install .", assert_error=True)
            self.assertIn(
                "- Package ID: 3ec60bb399a8bcb937b7af196f6685ba878aab02",
                self.client.out)

        # Now change the Hello version and build it, if we install out requires is
        # needed the --build needed because Hello2 needs to be build
        self._export("Hello", "1.5.0", package_id_text=None, requires=None)
        self.client.run("install Hello/1.5.0@lasote/stable --build missing")
        self._export(
            "Hello2",
            "2.3.8",
            package_id_text='self.info.requires["Hello"].full_version_mode()',
            requires=["Hello/1.5.0@lasote/stable"])

        self.client.save(
            {"conanfile.txt": "[requires]\nHello2/2.3.8@lasote/stable"},
            clean_first=True)
        with self.assertRaises(Exception):
            self.client.run("install .")
        self.assertIn("Can't find a 'Hello2/2.3.8@lasote/stable' package",
                      self.client.out)

    def test_version_full_recipe_schema(self):
        self._export("Hello", "1.2.0", package_id_text=None, requires=None)
        self._export(
            "Hello2",
            "2.3.8",
            package_id_text='self.info.requires["Hello"].full_recipe_mode()',
            requires=["Hello/1.2.0@lasote/stable"])

        # Build the dependencies with --build missing
        self.client.save(
            {"conanfile.txt": "[requires]\nHello2/2.3.8@lasote/stable"},
            clean_first=True)
        self.client.run("install . --build missing")
        self.assertIn("Hello2/2.3.8", self.conaninfo)

        # If we change the user and channel should be needed to rebuild
        self._export("Hello",
                     "1.2.0",
                     package_id_text=None,
                     requires=None,
                     channel="memsharded/testing")
        self.client.run(
            "install Hello/1.2.0@memsharded/testing --build missing")
        self._export(
            "Hello2",
            "2.3.8",
            package_id_text='self.info.requires["Hello"].full_recipe_mode()',
            requires=["Hello/1.2.0@memsharded/testing"])

        self.client.save(
            {"conanfile.txt": "[requires]\nHello2/2.3.8@lasote/stable"},
            clean_first=True)
        with self.assertRaises(Exception):
            self.client.run("install .")
        self.assertIn("Can't find a 'Hello2/2.3.8@lasote/stable' package",
                      self.client.out)

        # If we change only the package ID from hello (one more defaulted option
        #  to True) should not affect
        self._export("Hello",
                     "1.2.0",
                     package_id_text=None,
                     requires=None,
                     default_option_value='"on"')
        self.client.run("install Hello/1.2.0@lasote/stable --build missing")
        self._export(
            "Hello2",
            "2.3.8",
            package_id_text='self.info.requires["Hello"].full_recipe_mode()',
            requires=["Hello/1.2.0@lasote/stable"])

        self.client.save(
            {"conanfile.txt": "[requires]\nHello2/2.3.8@lasote/stable"},
            clean_first=True)

        self.client.run("install .")

    def test_version_full_package_schema(self):
        self._export("Hello", "1.2.0", package_id_text=None, requires=None)
        self._export(
            "Hello2",
            "2.3.8",
            package_id_text='self.info.requires["Hello"].full_package_mode()',
            requires=["Hello/1.2.0@lasote/stable"])

        # Build the dependencies with --build missing
        self.client.save(
            {"conanfile.txt": "[requires]\nHello2/2.3.8@lasote/stable"},
            clean_first=True)
        self.client.run("install . --build missing")
        self.assertIn("Hello2/2.3.8", self.conaninfo)

        # If we change only the package ID from hello (one more defaulted option
        #  to True) should affect
        self._export("Hello",
                     "1.2.0",
                     package_id_text=None,
                     requires=None,
                     default_option_value='"on"')
        self.client.run("install Hello/1.2.0@lasote/stable --build missing")
        self.client.save(
            {"conanfile.txt": "[requires]\nHello2/2.3.8@lasote/stable"},
            clean_first=True)
        with self.assertRaises(Exception):
            self.client.run("install .")
        self.assertIn("Can't find a 'Hello2/2.3.8@lasote/stable' package",
                      self.client.out)
        self.assertIn("Package ID:", self.client.out)

    def test_nameless_mode(self):
        self._export("Hello", "1.2.0", package_id_text=None, requires=None)
        self._export(
            "Hello2",
            "2.3.8",
            package_id_text='self.info.requires["Hello"].unrelated_mode()',
            requires=["Hello/1.2.0@lasote/stable"])

        # Build the dependencies with --build missing
        self.client.save(
            {"conanfile.txt": "[requires]\nHello2/2.3.8@lasote/stable"},
            clean_first=True)
        self.client.run("install . --build missing")
        self.assertIn("Hello2/2.3.8", self.conaninfo)

        # If we change even the require, should not affect
        self._export("HelloNew", "1.2.0")
        self.client.run("install HelloNew/1.2.0@lasote/stable --build missing")
        self._export(
            "Hello2",
            "2.3.8",
            package_id_text='self.info.requires["HelloNew"].unrelated_mode()',
            requires=["HelloNew/1.2.0@lasote/stable"])

        self.client.save(
            {"conanfile.txt": "[requires]\nHello2/2.3.8@lasote/stable"},
            clean_first=True)
        # Not needed to rebuild Hello2, it doesn't matter its requires
        if not self.client.cache.config.revisions_enabled:
            self.client.run("install .")
        else:  # We have changed hello2, so a new binary is required, but same id
            self.client.run("install .", assert_error=True)
            self.assertIn(
                "The package "
                "Hello2/2.3.8@lasote/stable:0c8b5ebf2790dd989f84360c366965b731a9bfc8 "
                "doesn't belong to the installed recipe revision, removing folder",
                self.client.out)
            self.assertIn(
                "Hello2/2.3.8@lasote/stable:0c8b5ebf2790dd989f84360c366965b731a9bfc8 -"
                " Missing", self.client.out)

    def test_toolset_visual_compatibility(self):
        # By default is the same to build with native visual or the toolchain
        for package_id in [None, "self.info.vs_toolset_compatible()"]:
            self._export("Hello",
                         "1.2.0",
                         package_id_text=package_id,
                         channel="user/testing",
                         settings=[
                             "compiler",
                         ])
            self.client.run('install Hello/1.2.0@user/testing '
                            ' -s compiler="Visual Studio" '
                            ' -s compiler.version=14 --build')

            # Should have binary available
            self.client.run('install Hello/1.2.0@user/testing'
                            ' -s compiler="Visual Studio" '
                            ' -s compiler.version=15 -s compiler.toolset=v140')

            # Should NOT have binary available
            self.client.run(
                'install Hello/1.2.0@user/testing '
                '-s compiler="Visual Studio" '
                '-s compiler.version=15 -s compiler.toolset=v120',
                assert_error=True)

            self.assertIn(
                "Missing prebuilt package for 'Hello/1.2.0@user/testing'",
                self.client.out)

            # Specify a toolset not involved with the visual version is ok, needed to build:
            self.client.run(
                'install Hello/1.2.0@user/testing'
                ' -s compiler="Visual Studio" '
                ' -s compiler.version=15 -s compiler.toolset=v141_clang_c2 '
                '--build missing')

    def test_toolset_visual_incompatibility(self):
        # By default is the same to build with native visual or the toolchain
        self._export(
            "Hello",
            "1.2.0",
            package_id_text="self.info.vs_toolset_incompatible()",
            channel="user/testing",
            settings=[
                "compiler",
            ],
        )
        self.client.run('install Hello/1.2.0@user/testing '
                        ' -s compiler="Visual Studio" '
                        ' -s compiler.version=14 --build')

        # Should NOT have binary available
        self.client.run(
            'install Hello/1.2.0@user/testing'
            ' -s compiler="Visual Studio" '
            ' -s compiler.version=15 -s compiler.toolset=v140',
            assert_error=True)
        self.assertIn(
            "Missing prebuilt package for 'Hello/1.2.0@user/testing'",
            self.client.out)

    def test_build_settings(self):
        def install_and_get_info(package_id_text):
            self.client.run("remove * -f")
            self._export("Hello",
                         "1.2.0",
                         package_id_text=package_id_text,
                         channel="user/testing",
                         settings=["os", "os_build", "arch", "arch_build"])
            self.client.run('install Hello/1.2.0@user/testing '
                            ' -s os="Windows" '
                            ' -s os_build="Linux"'
                            ' -s arch="x86_64"'
                            ' -s arch_build="x86"'
                            ' --build missing')

            ref = ConanFileReference.loads("Hello/1.2.0@user/testing")
            pkg = os.listdir(self.client.cache.package_layout(ref).packages())
            pref = PackageReference(ref, pkg[0])
            pkg_folder = self.client.cache.package_layout(
                pref.ref).package(pref)
            return ConanInfo.loads(load(os.path.join(pkg_folder, CONANINFO)))

        info = install_and_get_info(None)  # Default

        self.assertEqual(str(info.settings.os_build), "None")
        self.assertEqual(str(info.settings.arch_build), "None")

        # Package has to be present with only os and arch settings
        self.client.run('install Hello/1.2.0@user/testing '
                        ' -s os="Windows" '
                        ' -s arch="x86_64"')

        # Even with wrong build settings
        self.client.run('install Hello/1.2.0@user/testing '
                        ' -s os="Windows" '
                        ' -s arch="x86_64"'
                        ' -s os_build="Macos"'
                        ' -s arch_build="x86_64"')

        # take into account build
        info = install_and_get_info("self.info.include_build_settings()")
        self.assertEqual(str(info.settings.os_build), "Linux")
        self.assertEqual(str(info.settings.arch_build), "x86")

        # Now the build settings matter
        err = self.client.run(
            'install Hello/1.2.0@user/testing '
            ' -s os="Windows" '
            ' -s arch="x86_64"'
            ' -s os_build="Macos"'
            ' -s arch_build="x86_64"',
            assert_error=True)
        self.assertTrue(err)
        self.assertIn("Can't find", self.client.out)

        self.client.run('install Hello/1.2.0@user/testing '
                        ' -s os="Windows" '
                        ' -s arch="x86_64"'
                        ' -s os_build="Linux"'
                        ' -s arch_build="x86"')

        # Now only settings for build
        self.client.run("remove * -f")
        self._export("Hello",
                     "1.2.0",
                     channel="user/testing",
                     settings=["os_build", "arch_build"])
        self.client.run('install Hello/1.2.0@user/testing '
                        ' -s os_build="Linux"'
                        ' -s arch_build="x86"'
                        ' --build missing')
        ref = ConanFileReference.loads("Hello/1.2.0@user/testing")
        pkg = os.listdir(self.client.cache.package_layout(ref).packages())
        pref = PackageReference(ref, pkg[0])
        pkg_folder = self.client.cache.package_layout(pref.ref).package(pref)
        info = ConanInfo.loads(load(os.path.join(pkg_folder, CONANINFO)))
        self.assertEqual(str(info.settings.os_build), "Linux")
        self.assertEqual(str(info.settings.arch_build), "x86")

    @unittest.skipIf(get_env("TESTING_REVISIONS_ENABLED", False),
                     "No sense with revs")
    def test_standard_version_default_matching(self):
        self._export("Hello",
                     "1.2.0",
                     channel="user/testing",
                     settings=[
                         "compiler",
                     ])

        self.client.run('install Hello/1.2.0@user/testing '
                        ' -s compiler="gcc" -s compiler.libcxx=libstdc++11'
                        ' -s compiler.version=7.2 --build')

        self._export("Hello",
                     "1.2.0",
                     channel="user/testing",
                     settings=[
                         "compiler",
                         "cppstd",
                     ])

        with catch_deprecation_warning(self):
            self.client.run(
                'info Hello/1.2.0@user/testing  -s compiler="gcc" '
                '-s compiler.libcxx=libstdc++11  -s compiler.version=7.2 '
                '-s cppstd=gnu14')
        with catch_deprecation_warning(self):
            self.client.run('install Hello/1.2.0@user/testing'
                            ' -s compiler="gcc" -s compiler.libcxx=libstdc++11'
                            ' -s compiler.version=7.2 -s cppstd=gnu14'
                            )  # Default, already built

        # Should NOT have binary available
        with catch_deprecation_warning(self):
            self.client.run(
                'install Hello/1.2.0@user/testing'
                ' -s compiler="gcc" -s compiler.libcxx=libstdc++11'
                ' -s compiler.version=7.2 -s cppstd=gnu11',
                assert_error=True)

        self.assertIn(
            "Missing prebuilt package for 'Hello/1.2.0@user/testing'",
            self.client.out)

    def test_std_non_matching_with_cppstd(self):
        self._export("Hello",
                     "1.2.0",
                     package_id_text="self.info.default_std_non_matching()",
                     channel="user/testing",
                     settings=[
                         "compiler",
                         "cppstd",
                     ])
        self.client.run('install Hello/1.2.0@user/testing'
                        ' -s compiler="gcc" -s compiler.libcxx=libstdc++11'
                        ' -s compiler.version=7.2 --build')

        with catch_deprecation_warning(self, n=1):
            self.client.run('install Hello/1.2.0@user/testing'
                            ' -s compiler="gcc" -s compiler.libcxx=libstdc++11'
                            ' -s compiler.version=7.2 -s cppstd=gnu14',
                            assert_error=True)  # Default
        self.assertIn(
            "Missing prebuilt package for 'Hello/1.2.0@user/testing'",
            self.client.out)

    def test_std_non_matching_with_compiler_cppstd(self):
        self._export("Hello",
                     "1.2.0",
                     package_id_text="self.info.default_std_non_matching()",
                     channel="user/testing",
                     settings=[
                         "compiler",
                     ])
        self.client.run('install Hello/1.2.0@user/testing '
                        ' -s compiler="gcc" -s compiler.libcxx=libstdc++11'
                        ' -s compiler.version=7.2 --build')

        self.client.run(
            'install Hello/1.2.0@user/testing '
            ' -s compiler="gcc" -s compiler.libcxx=libstdc++11'
            ' -s compiler.version=7.2 -s compiler.cppstd=gnu14',
            assert_error=True)
        self.assertIn(
            "Missing prebuilt package for 'Hello/1.2.0@user/testing'",
            self.client.out)

    def test_std_matching_with_compiler_cppstd(self):
        self._export("Hello",
                     "1.2.0",
                     package_id_text="self.info.default_std_matching()",
                     channel="user/testing",
                     settings=[
                         "compiler",
                     ])
        self.client.run('install Hello/1.2.0@user/testing '
                        ' -s compiler="gcc" -s compiler.libcxx=libstdc++11'
                        ' -s compiler.version=7.2 --build')

        self.client.run('install Hello/1.2.0@user/testing '
                        ' -s compiler="gcc" -s compiler.libcxx=libstdc++11'
                        ' -s compiler.version=7.2 -s compiler.cppstd=gnu14')
        self.assertIn("Hello/1.2.0@user/testing: Already installed!",
                      self.client.out)

    def test_package_id_requires_patch_mode(self):
        """ Requirements shown in build missing error, must contains transitive packages
            For this test the follow graph has been used:
            libb <- liba
            libfoo <- libbar
            libc <- libb, libfoo
            libd <- libc
        """

        channel = "user/testing"
        self.client.run(
            "config set general.default_package_id_mode=patch_mode")
        self._export("liba",
                     "0.1.0",
                     channel=channel,
                     package_id_text=None,
                     requires=None)
        self.client.run("create . liba/0.1.0@user/testing")
        self._export("libb",
                     "0.1.0",
                     channel=channel,
                     package_id_text=None,
                     requires=["liba/0.1.0@user/testing"])
        self.client.run("create . libb/0.1.0@user/testing")
        self._export("libbar",
                     "0.1.0",
                     channel=channel,
                     package_id_text=None,
                     requires=None)
        self.client.run("create . libbar/0.1.0@user/testing")
        self._export("libfoo",
                     "0.1.0",
                     channel=channel,
                     package_id_text=None,
                     requires=["libbar/0.1.0@user/testing"])
        self.client.run("create . libfoo/0.1.0@user/testing")
        self._export(
            "libc",
            "0.1.0",
            channel=channel,
            package_id_text=None,
            requires=["libb/0.1.0@user/testing", "libfoo/0.1.0@user/testing"])
        self._export("libd",
                     "0.1.0",
                     channel=channel,
                     package_id_text=None,
                     requires=["libc/0.1.0@user/testing"])
        self.client.run("create . libd/0.1.0@user/testing", assert_error=True)
        self.assertIn(
            """ERROR: Missing binary: libc/0.1.0@user/testing:e12c9d31fa508340bb8d0c4f9dd4c98a5d0ac082

libc/0.1.0@user/testing: WARN: Can't find a 'libc/0.1.0@user/testing' package for the specified settings, options and dependencies:
- Settings:%s
- Options: an_option=off, liba:an_option=off, libb:an_option=off, libbar:an_option=off, libfoo:an_option=off
- Dependencies: libb/0.1.0@user/testing, libfoo/0.1.0@user/testing
- Requirements: liba/0.1.0, libb/0.1.0, libbar/0.1.0, libfoo/0.1.0
- Package ID: e12c9d31fa508340bb8d0c4f9dd4c98a5d0ac082

ERROR: Missing prebuilt package for 'libc/0.1.0@user/testing'""" % " ",
            self.client.out)