#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Integration tests for recursive capability resolution and installer shadowing.

These tests verify that, given different repository layouts (Makefile, pyproject,
flake.nix, PKGBUILD), only the expected installers are executed based on the
capabilities provided by higher layers.

Layer order (strongest → weakest):

  OS-PACKAGES  >  NIX  >  PYTHON  >  MAKEFILE
"""

import os
import shutil
import tempfile
import unittest
from typing import List, Sequence, Tuple
from unittest.mock import patch

import pkgmgr.actions.install as install_mod
from pkgmgr.actions.install import install_repos
from pkgmgr.actions.install.installers.makefile import MakefileInstaller
from pkgmgr.actions.install.installers.nix import NixFlakeInstaller
from pkgmgr.actions.install.installers.os_packages.arch_pkgbuild import (
    ArchPkgbuildInstaller,
)
from pkgmgr.actions.install.installers.python import PythonInstaller


InstallerSpec = Tuple[str, object]


class TestRecursiveCapabilitiesIntegration(unittest.TestCase):
    def setUp(self) -> None:
        self.tmp_root = tempfile.mkdtemp(prefix="pkgmgr-recursive-caps-")
        self.bin_dir = os.path.join(self.tmp_root, "bin")
        os.makedirs(self.bin_dir, exist_ok=True)

    def tearDown(self) -> None:
        shutil.rmtree(self.tmp_root)

    # ------------------------------------------------------------------ helpers

    def _new_repo(self) -> str:
        """
        Create a fresh temporary repo directory under self.tmp_root.
        """
        return tempfile.mkdtemp(prefix="repo-", dir=self.tmp_root)

    def _run_with_installers(
        self,
        repo_dir: str,
        installers: Sequence[InstallerSpec],
        selected_repos=None,
    ) -> List[str]:
        """
        Run install_repos() with a custom INSTALLERS list and capture which
        installer labels actually run.

        We override each installer's supports() to always return True and
        override run() to append its label to called_installers.
        """
        if selected_repos is None:
            repo = {"repository": "dummy"}
            selected_repos = [repo]
            all_repos = [repo]
        else:
            all_repos = selected_repos

        called_installers: List[str] = []

        patched_installers = []
        for label, inst in installers:

            def always_supports(self, ctx):
                return True

            def make_run(label_name: str):
                def _run(self, ctx):
                    called_installers.append(label_name)

                return _run

            inst.supports = always_supports.__get__(inst, inst.__class__)  # type: ignore[assignment]
            inst.run = make_run(label).__get__(inst, inst.__class__)  # type: ignore[assignment]
            patched_installers.append(inst)

        with (
            patch.object(install_mod, "INSTALLERS", patched_installers),
            patch.object(install_mod, "get_repo_identifier", return_value="dummy-repo"),
            patch.object(install_mod, "get_repo_dir", return_value=repo_dir),
            patch.object(
                install_mod, "verify_repository", return_value=(True, [], None, None)
            ),
            patch.object(install_mod, "clone_repos"),
        ):
            install_repos(
                selected_repos=selected_repos,
                repositories_base_dir=self.tmp_root,
                bin_dir=self.bin_dir,
                all_repos=all_repos,
                no_verification=True,
                preview=False,
                quiet=False,
                clone_mode="ssh",
                update_dependencies=False,
            )

        return called_installers

    # ----------------------------------------------------------------- scenarios

    def test_only_makefile_installer_runs(self) -> None:
        """
        With only a Makefile present, only the MakefileInstaller should run.
        """
        repo_dir = self._new_repo()

        with open(os.path.join(repo_dir, "Makefile"), "w", encoding="utf-8") as f:
            f.write("install:\n\t@echo 'make install'\n")

        mk_inst = MakefileInstaller()
        installers: Sequence[InstallerSpec] = [("makefile", mk_inst)]

        called = self._run_with_installers(repo_dir, installers)

        self.assertEqual(
            called,
            ["makefile"],
            "With only a Makefile, the MakefileInstaller should run exactly once.",
        )

    def test_python_and_makefile_both_run_when_caps_disjoint(self) -> None:
        """
        If Python and Makefile have disjoint capabilities, both installers run.
        """
        repo_dir = self._new_repo()

        # pyproject.toml without any explicit "make install" hint
        with open(os.path.join(repo_dir, "pyproject.toml"), "w", encoding="utf-8") as f:
            f.write("name = 'dummy'\n")

        with open(os.path.join(repo_dir, "Makefile"), "w", encoding="utf-8") as f:
            f.write("install:\n\t@echo 'make install'\n")

        py_inst = PythonInstaller()
        mk_inst = MakefileInstaller()
        installers: Sequence[InstallerSpec] = [
            ("python", py_inst),
            ("makefile", mk_inst),
        ]

        called = self._run_with_installers(repo_dir, installers)

        self.assertEqual(
            called,
            ["python", "makefile"],
            "PythonInstaller and MakefileInstaller should both run when their "
            "capabilities are disjoint.",
        )

    def test_python_shadows_makefile_when_pyproject_mentions_make_install(self) -> None:
        """
        If the Python layer advertises a 'make-install' capability (pyproject
        explicitly hints at 'make install'), the Makefile layer must be skipped.
        """
        repo_dir = self._new_repo()

        with open(os.path.join(repo_dir, "pyproject.toml"), "w", encoding="utf-8") as f:
            f.write(
                "name = 'dummy'\n"
                "\n"
                "# Hint for MakeInstallCapability on layer 'python'\n"
                "make install\n"
            )

        with open(os.path.join(repo_dir, "Makefile"), "w", encoding="utf-8") as f:
            f.write("install:\n\t@echo 'make install'\n")

        py_inst = PythonInstaller()
        mk_inst = MakefileInstaller()
        installers: Sequence[InstallerSpec] = [
            ("python", py_inst),
            ("makefile", mk_inst),
        ]

        called = self._run_with_installers(repo_dir, installers)

        self.assertIn("python", called, "PythonInstaller should have run.")
        self.assertNotIn(
            "makefile",
            called,
            "MakefileInstaller should be skipped because its 'make-install' "
            "capability is already provided by Python.",
        )

    def test_nix_shadows_python_and_makefile(self) -> None:
        """
        If a Nix flake advertises both python-runtime and make-install
        capabilities, Python and Makefile installers must be skipped.
        """
        repo_dir = self._new_repo()

        with open(os.path.join(repo_dir, "pyproject.toml"), "w", encoding="utf-8") as f:
            f.write("name = 'dummy'\n")

        with open(os.path.join(repo_dir, "Makefile"), "w", encoding="utf-8") as f:
            f.write("install:\n\t@echo 'make install'\n")

        with open(os.path.join(repo_dir, "flake.nix"), "w", encoding="utf-8") as f:
            f.write(
                '  description = "integration test flake";\n'
                "}\n"
                "\n"
                "# Hint for PythonRuntimeCapability on layer 'nix'\n"
                "buildPythonApplication something\n"
                "\n"
                "# Hint for MakeInstallCapability on layer 'nix'\n"
                "make install\n"
            )

        nix_inst = NixFlakeInstaller()
        py_inst = PythonInstaller()
        mk_inst = MakefileInstaller()
        installers: Sequence[InstallerSpec] = [
            ("nix", nix_inst),
            ("python", py_inst),
            ("makefile", mk_inst),
        ]

        called = self._run_with_installers(repo_dir, installers)

        self.assertIn("nix", called, "NixFlakeInstaller should have run.")
        self.assertNotIn(
            "python",
            called,
            "PythonInstaller should be skipped because its python-runtime "
            "capability is already provided by Nix.",
        )
        self.assertNotIn(
            "makefile",
            called,
            "MakefileInstaller should be skipped because its make-install "
            "capability is already provided by Nix.",
        )

    def test_os_packages_shadow_nix_python_and_makefile(self) -> None:
        """
        If an OS package layer (PKGBUILD) advertises all capabilities,
        all lower layers (Nix, Python, Makefile) must be skipped.
        """
        repo_dir = self._new_repo()

        with open(os.path.join(repo_dir, "pyproject.toml"), "w", encoding="utf-8") as f:
            f.write("name = 'dummy'\n")

        with open(os.path.join(repo_dir, "Makefile"), "w", encoding="utf-8") as f:
            f.write("install:\n\t@echo 'make install'\n")

        with open(os.path.join(repo_dir, "flake.nix"), "w", encoding="utf-8") as f:
            f.write(
                '  description = "integration test flake";\n'
                "}\n"
                "\n"
                "buildPythonApplication something\n"
                "make install\n"
            )

        with open(os.path.join(repo_dir, "PKGBUILD"), "w", encoding="utf-8") as f:
            f.write(
                "pkgver=0.1\n"
                "pkgrel=1\n"
                "pkgdesc='dummy pkg for integration test'\n"
                "arch=('any')\n"
                "source=()\n"
                "sha256sums=()\n"
                "\n"
                "build() {\n"
                "  echo 'build phase'\n"
                "}\n"
                "\n"
                "package() {\n"
                "  echo 'install via pip and make and nix'\n"
                "  pip install .\n"
                "  make install\n"
                "  nix profile list || true\n"
                "}\n"
            )

        os_inst = ArchPkgbuildInstaller()
        nix_inst = NixFlakeInstaller()
        py_inst = PythonInstaller()
        mk_inst = MakefileInstaller()
        installers: Sequence[InstallerSpec] = [
            ("os-packages", os_inst),
            ("nix", nix_inst),
            ("python", py_inst),
            ("makefile", mk_inst),
        ]

        called = self._run_with_installers(repo_dir, installers)

        self.assertIn("os-packages", called, "ArchPkgbuildInstaller should have run.")
        self.assertNotIn(
            "nix",
            called,
            "NixFlakeInstaller should be skipped because all its capabilities "
            "are already provided by os-packages.",
        )
        self.assertNotIn(
            "python",
            called,
            "PythonInstaller should be skipped because python-runtime is already "
            "provided by os-packages.",
        )
        self.assertNotIn(
            "makefile",
            called,
            "MakefileInstaller should be skipped because make-install is already "
            "provided by os-packages.",
        )


if __name__ == "__main__":
    unittest.main()
