# Copyright 2022-2025 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2

import io
import os
import sys
import tempfile
from unittest.mock import MagicMock, call, patch

from portage.const import BINREPOS_CONF_FILE
from portage.dbapi.bintree import binarytree
from portage.tests import TestCase


class BinarytreeTestCase(TestCase):
    @classmethod
    def setUpClass(cls):
        """
        Create a temporary TMPDIR which prevents test
        methods of this class from leaving behind an empty
        /tmp/Packages file if TMPDIR is initially unset.
        """
        cls._orig_tmpdir = os.environ.get("TMPDIR")
        cls._tmpdir = tempfile.TemporaryDirectory()
        os.environ["TMPDIR"] = cls._tmpdir.name

    @classmethod
    def tearDownClass(cls):
        cls._tmpdir.cleanup()
        if cls._orig_tmpdir is None:
            os.environ.pop("TMPDIR", None)
        else:
            os.environ["TMPDIR"] = cls._orig_tmpdir
        del cls._orig_tmpdir, cls._tmpdir

    def test_required_init_params(self):
        with self.assertRaises(TypeError) as cm:
            binarytree()
        self.assertEqual(str(cm.exception), "pkgdir parameter is required")
        with self.assertRaises(TypeError) as cm:
            binarytree(pkgdir=os.getenv("TMPDIR", "/tmp"))
        self.assertEqual(str(cm.exception), "settings parameter is required")

    def test_init_with_legacy_params_warns(self):
        with self.assertWarns(DeprecationWarning):
            binarytree(
                _unused=None, pkgdir=os.getenv("TMPDIR", "/tmp"), settings=MagicMock()
            )
        with self.assertWarns(DeprecationWarning):
            binarytree(
                virtual=None, pkgdir=os.getenv("TMPDIR", "/tmp"), settings=MagicMock()
            )

    def test_instance_has_required_attrs(self):
        # Quite smoky test. What would it be a better testing strategy?
        # Not sure yet...
        required_attrs_no_multi_instance = {
            "pkgdir",
            "_multi_instance",
            "dbapi",
            "update_ents",
            "move_slot_ent",
            "populated",
            "tree",
            "_binrepos_conf",
            "_remote_has_index",
            "_remotepkgs",
            "_additional_pkgs",
            "invalids",
            "invalid_paths",
            "settings",
            "_pkg_paths",
            "_populating",
            "_all_directory",
            "_pkgindex_version",
            "_pkgindex_hashes",
            "_pkgindex_file",
            "_pkgindex_keys",
            "_pkgindex_aux_keys",
            "_pkgindex_use_evaluated_keys",
            "_pkgindex_header",
            "_pkgindex_header_keys",
            "_pkgindex_default_pkg_data",
            "_pkgindex_inherited_keys",
            "_pkgindex_default_header_data",
            "_pkgindex_translated_keys",
            "_pkgindex_allowed_pkg_keys",
        }
        no_multi_instance_settings = MagicMock()
        no_multi_instance_settings.features = ""
        no_multi_instance_bt = binarytree(
            pkgdir=os.getenv("TMPDIR", "/tmp"), settings=no_multi_instance_settings
        )
        multi_instance_settings = MagicMock()
        multi_instance_settings.features = "binpkg-multi-instance"
        multi_instance_bt = binarytree(
            pkgdir=os.getenv("TMPDIR", "/tmp"), settings=multi_instance_settings
        )
        for attr in required_attrs_no_multi_instance:
            getattr(no_multi_instance_bt, attr)
            getattr(multi_instance_bt, attr)
        # The next attribute is the difference between multi instance
        # and no multi instance:
        multi_instance_bt._allocate_filename

    @patch("portage.dbapi.bintree.binarytree._populate_local")
    def test_populate_without_updates_repos_nor_getbinspkgs(self, ppopulate_local):
        bt = binarytree(pkgdir=os.getenv("TMPDIR", "/tmp"), settings=MagicMock())
        ppopulate_local.return_value = {}
        bt.populate()
        ppopulate_local.assert_called_once_with(reindex=True, invalid_errors=True)
        self.assertFalse(bt._populating)
        self.assertTrue(bt.populated)

    @patch("portage.dbapi.bintree.binarytree._populate_local")
    def test_populate_calls_twice_populate_local_if_updates(self, ppopulate_local):
        bt = binarytree(pkgdir=os.getenv("TMPDIR", "/tmp"), settings=MagicMock())
        bt.populate()
        self.assertIn(
            call(reindex=True, invalid_errors=True), ppopulate_local.mock_calls
        )
        self.assertIn(call(), ppopulate_local.mock_calls)
        self.assertEqual(ppopulate_local.call_count, 2)

    @patch("portage.dbapi.bintree.binarytree._populate_additional")
    @patch("portage.dbapi.bintree.binarytree._populate_local")
    def test_populate_with_repos(self, ppopulate_local, ppopulate_additional):
        repos = ("one", "two")
        bt = binarytree(pkgdir=os.getenv("TMPDIR", "/tmp"), settings=MagicMock())
        bt.populate(add_repos=repos)
        ppopulate_additional.assert_called_once_with(repos)

    @patch("portage.dbapi.bintree.BinRepoConfigLoader")
    @patch("portage.dbapi.bintree.binarytree._populate_remote")
    @patch("portage.dbapi.bintree.binarytree._populate_local")
    def test_populate_with_getbinpkgs(
        self, ppopulate_local, ppopulate_remote, pBinRepoConfigLoader
    ):
        refresh = "something"
        settings = MagicMock()
        settings.__getitem__.return_value = "/some/path"
        bt = binarytree(pkgdir=os.getenv("TMPDIR", "/tmp"), settings=settings)
        bt.populate(getbinpkgs=True, getbinpkg_refresh=refresh)
        ppopulate_remote.assert_called_once_with(
            getbinpkg_refresh=refresh,
            pretend=False,
            verbose=False,
            getbinpkg_exclude=None,
            getbinpkg_include=None,
        )

    @patch("portage.dbapi.bintree.BinRepoConfigLoader")
    @patch("portage.dbapi.bintree.binarytree._populate_remote")
    @patch("portage.dbapi.bintree.binarytree._populate_local")
    def test_populate_with_getbinpkgs_and_not_BinRepoConfigLoader(
        self, ppopulate_local, ppopulate_remote, pBinRepoConfigLoader
    ):
        refresh = "something"
        settings = MagicMock()
        portage_root = "/some/path"
        settings.__getitem__.return_value = portage_root
        pBinRepoConfigLoader.return_value = None
        conf_file = os.path.join(portage_root, BINREPOS_CONF_FILE)
        bt = binarytree(pkgdir=os.getenv("TMPDIR", "/tmp"), settings=settings)
        out = io.StringIO()
        stderr_orig = sys.stderr
        try:
            sys.stderr = out
            bt.populate(getbinpkgs=True, getbinpkg_refresh=refresh)
        finally:
            sys.stderr = stderr_orig

        ppopulate_remote.assert_not_called()
        self.assertEqual(
            out.getvalue(),
            "!!! binrepos.conf is missing (or PORTAGE_BINHOST is unset), but use is requested.\n",
        )

    @patch("portage.dbapi.bintree.BinRepoConfigLoader")
    @patch("portage.dbapi.bintree.binarytree._populate_remote")
    @patch("portage.dbapi.bintree.binarytree._populate_local")
    def test_default_getbinpkg_refresh_in_populate(
        self, ppopulate_local, ppopulate_remote, pBinRepoConfigLoader
    ):
        """Bug #864259
        This test fixes the bug. It requires that
        ``_emerge.actions.run_action`` calls ``binarytree.populate``
        explicitly with ``getbinpkg_refresh=True``
        """
        settings = MagicMock()
        settings.__getitem__.return_value = "/some/path"
        bt = binarytree(pkgdir=os.getenv("TMPDIR", "/tmp"), settings=settings)
        bt.populate(getbinpkgs=True)
        ppopulate_remote.assert_called_once_with(
            getbinpkg_refresh=False,
            pretend=False,
            verbose=False,
            getbinpkg_exclude=None,
            getbinpkg_include=None,
        )

    @patch("portage.data.secpass", 2)
    @patch("portage.dbapi.bintree.BinRepoConfigLoader")
    @patch("portage.dbapi.bintree.binarytree._run_trust_helper")
    def test_default_getbinpkg_refresh_in_populate_trusthelper(
        self, run_trust_helper, pBinRepoConfigLoader
    ):
        """
        Test for bug #915842.

        Verify that we call the trust helper in non-pretend mode.
        """
        settings = MagicMock()
        settings.features = ["binpkg-request-signature"]
        settings.__getitem__.return_value = "/some/path"

        d = tempfile.TemporaryDirectory()
        try:
            bt = binarytree(pkgdir=d.name, settings=settings)
            bt.populate(getbinpkgs=True, pretend=False)
            run_trust_helper.assert_called_once()
        finally:
            d.cleanup()

    @patch("portage.dbapi.bintree.BinRepoConfigLoader")
    @patch("portage.dbapi.bintree.binarytree._run_trust_helper")
    def test_default_getbinpkg_refresh_in_populate_trusthelper_pretend(
        self, run_trust_helper, pBinRepoConfigLoader
    ):
        """
        Test for bug #915842.

        Verify we do not call the trust helper in pretend mode.
        """
        settings = MagicMock()
        settings.features = ["binpkg-request-signature"]
        settings.__getitem__.return_value = "/some/path"

        d = tempfile.TemporaryDirectory()
        try:
            bt = binarytree(pkgdir=d.name, settings=settings)
            bt.populate(getbinpkgs=True, pretend=True)
            run_trust_helper.assert_not_called()
        finally:
            d.cleanup()
