import os
import tempfile
import unittest
from unittest.mock import patch

import pkgmgr.actions.install as install_module
from pkgmgr.actions.install import install_repos
from pkgmgr.actions.install.installers.base import BaseInstaller


class DummyInstaller(BaseInstaller):
    """
    Minimal installer used to ensure that the installation pipeline runs
    without executing any real external commands.
    """

    layer = None

    def supports(self, ctx):  # type: ignore[override]
        return True

    def run(self, ctx):  # type: ignore[override]
        return


class TestInstallReposIntegration(unittest.TestCase):
    @patch("pkgmgr.actions.install.verify_repository")
    @patch("pkgmgr.actions.install.clone_repos")
    @patch("pkgmgr.actions.install.get_repo_dir")
    @patch("pkgmgr.actions.install.get_repo_identifier")
    def test_system_binary_vs_nix_binary(
        self,
        mock_get_repo_identifier,
        mock_get_repo_dir,
        mock_clone_repos,
        mock_verify_repository,
    ) -> None:
        """
        Integration test:

        We do NOT re-test the low-level implementation details of
        resolve_command_for_repo() here (that is covered by unit tests).

        Instead, we assert the high-level behavior of install_repos() +
        InstallationPipeline + create_ink():

          * If resolve_command_for_repo(...) returns None:
              → install_repos() must NOT create a symlink for that repo.

          * If resolve_command_for_repo(...) returns a path:
              → install_repos() must create exactly one symlink in bin_dir
                that points to this path.

        Concretely in this test:

          * repo-system:
              fake resolver → returns None
              → no symlink in bin_dir for this repo.

          * repo-nix:
              fake resolver → returns "/nix/profile/bin/repo-nix"
              → exactly one symlink in bin_dir pointing to that path.
        """

        # Repositories must have provider/account/repository so that get_repo_dir()
        # does not crash when called from create_ink().
        repo_system = {
            "name": "repo-system",
            "provider": "github.com",
            "account": "dummy",
            "repository": "repo-system",
        }
        repo_nix = {
            "name": "repo-nix",
            "provider": "github.com",
            "account": "dummy",
            "repository": "repo-nix",
        }

        selected_repos = [repo_system, repo_nix]
        all_repos = selected_repos

        with (
            tempfile.TemporaryDirectory() as tmp_base,
            tempfile.TemporaryDirectory() as tmp_bin,
        ):
            # Fake repo directories (what get_repo_dir will return)
            repo_system_dir = os.path.join(tmp_base, "repo-system")
            repo_nix_dir = os.path.join(tmp_base, "repo-nix")
            os.makedirs(repo_system_dir, exist_ok=True)
            os.makedirs(repo_nix_dir, exist_ok=True)

            # Identifiers and repo dirs used inside install_repos()
            mock_get_repo_identifier.side_effect = ["repo-system", "repo-nix"]
            mock_get_repo_dir.side_effect = [repo_system_dir, repo_nix_dir]

            # Repository verification always succeeds
            mock_verify_repository.return_value = (True, [], "commit", "key")
            mock_clone_repos.return_value = None

            # Pretend this is the "Nix binary" path for repo-nix
            nix_tool_path = "/nix/profile/bin/repo-nix"

            # Patch resolve_command_for_repo at the *pipeline* module level,
            # because InstallationPipeline imports it there.
            with (
                patch(
                    "pkgmgr.actions.install.pipeline.resolve_command_for_repo"
                ) as mock_resolve,
                patch("pkgmgr.actions.install.os.path.exists") as mock_exists_install,
            ):

                def fake_resolve(repo, repo_identifier: str, repo_dir: str):
                    """
                    High-level behavior stub:

                      - For repo-system: act as if a system package owns the command
                        → return None (no symlink).

                      - For repo-nix: act as if a Nix profile binary is the entrypoint
                        → return nix_tool_path (symlink should be created).
                    """
                    name = repo.get("name")
                    if name == "repo-system":
                        return None
                    if name == "repo-nix":
                        return nix_tool_path
                    return None

                def fake_exists_install(path: str) -> bool:
                    """
                    Make _ensure_repo_dir() believe that the repo directories
                    already exist so that it does not attempt cloning.
                    """
                    return path in (repo_system_dir, repo_nix_dir)

                mock_resolve.side_effect = fake_resolve
                mock_exists_install.side_effect = fake_exists_install

                # Use only DummyInstaller so we focus on link creation, not installer behavior
                old_installers = install_module.INSTALLERS
                install_module.INSTALLERS = [DummyInstaller()]
                try:
                    install_repos(
                        selected_repos=selected_repos,
                        repositories_base_dir=tmp_base,
                        bin_dir=tmp_bin,
                        all_repos=all_repos,
                        no_verification=False,
                        preview=False,
                        quiet=False,
                        clone_mode="shallow",
                        update_dependencies=False,
                    )
                finally:
                    install_module.INSTALLERS = old_installers

            # ------------------------------------------------------------------
            # Inspect bin_dir: exactly one symlink must exist, pointing to Nix.
            # ------------------------------------------------------------------
            symlink_paths = []
            for entry in os.listdir(tmp_bin):
                full = os.path.join(tmp_bin, entry)
                if os.path.islink(full):
                    symlink_paths.append(full)

            # There must be exactly one symlink (for repo-nix)
            self.assertEqual(
                len(symlink_paths),
                1,
                f"Expected exactly one symlink in {tmp_bin}, found {symlink_paths}",
            )

            target = os.readlink(symlink_paths[0])

            # That symlink must point to the "Nix" path returned by the resolver stub
            self.assertEqual(
                target,
                nix_tool_path,
                f"Expected symlink target to be Nix binary {nix_tool_path}, got {target}",
            )


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