"""Tests for versioned Python binary patching (python3.X redirect).

Standard venvs created with ``python -m venv`` produce pip entry points
with shebangs like ``#!/path/to/.venv/bin/python3.12``. If ``python3.12``
is not redirected through the wrapper, pip bypasses the remote-first
forwarding entirely.

These tests verify that ``patch_venv()`` redirects ``python3.X`` binaries
to the wrapper and that ``restore_venv()`` restores them.
"""

from __future__ import annotations

from pathlib import Path


class TestVersionedPythonPatch:
    """patch_venv() should redirect python3.X → python (wrapper)."""

    def _make_venv(self, tmp_path: Path) -> Path:
        """Create a realistic venv directory layout with python3.12.

        Uses a real file for the python binary to avoid dangling symlinks
        on systems without /usr/bin/python3.12.
        """
        bin_dir = tmp_path / "bin"
        bin_dir.mkdir()
        # python3.12 — the "real" binary (a dummy file for testing)
        python312 = bin_dir / "python3.12"
        python312.write_text("#!/usr/bin/env python3\n# real python 3.12\n")
        python312.chmod(0o755)
        # python → python3.12 (standard symlink chain)
        python_bin = bin_dir / "python"
        python_bin.symlink_to("python3.12")
        # python3 → python3.12
        python3_bin = bin_dir / "python3"
        python3_bin.symlink_to("python3.12")
        return tmp_path

    def test_patch_redirects_versioned_python(self, tmp_path: Path) -> None:
        """After patching, python3.12 should point to python (the wrapper)."""
        from specter_cli.venv import patch_venv

        venv = self._make_venv(tmp_path)
        bin_dir = venv / "bin"

        patch_venv(venv, "h100")

        python312 = bin_dir / "python3.12"
        assert python312.is_symlink(), "python3.12 should be a symlink"
        assert str(python312.readlink()) == "python", (
            f"python3.12 should point to 'python' (wrapper), not {python312.readlink()}"
        )

    def test_versioned_backup_created(self, tmp_path: Path) -> None:
        """patch_venv() should back up python3.12 as python3.12.specter-original."""
        from specter_cli.venv import patch_venv

        venv = self._make_venv(tmp_path)
        bin_dir = venv / "bin"

        patch_venv(venv, "h100")

        backup = bin_dir / "python3.12.specter-original"
        assert backup.exists(), "versioned backup should exist"

    def test_restore_restores_versioned_python(self, tmp_path: Path) -> None:
        """After restore, python3.12 should point back to system python."""
        from specter_cli.venv import patch_venv, restore_venv

        venv = self._make_venv(tmp_path)
        bin_dir = venv / "bin"

        patch_venv(venv, "h100")
        restored = restore_venv(venv)

        assert restored is True
        python312 = bin_dir / "python3.12"
        # Should be restored to original state (symlink to system python)
        assert python312.exists(), "python3.12 should exist after restore"
        # Should NOT point to "python" (wrapper) anymore
        if python312.is_symlink():
            target = str(python312.readlink())
            assert target != "python", "python3.12 should not point to wrapper after restore"

    def test_pip_shebang_resolves_through_wrapper(self, tmp_path: Path) -> None:
        """A pip shebang like #!/.../python3.12 should resolve through the wrapper."""
        from specter_cli.venv import patch_venv

        venv = self._make_venv(tmp_path)
        bin_dir = venv / "bin"

        # Create pip with shebang pointing to python3.12 (realistic)
        pip_script = bin_dir / "pip"
        pip_script.write_text(f"#!{bin_dir}/python3.12\nimport pip\npip.main()\n")
        pip_script.chmod(0o755)

        patch_venv(venv, "h100")

        # python3.12 → python (wrapper), so pip's shebang now goes through wrapper
        python312 = bin_dir / "python3.12"
        assert python312.is_symlink()
        assert str(python312.readlink()) == "python"

        # Verify wrapper exists and is the bash script
        python_bin = bin_dir / "python"
        content = python_bin.read_text()
        assert content.startswith("#!/bin/bash"), "python should be the wrapper script"

    def test_multiple_versioned_pythons(self, tmp_path: Path) -> None:
        """If both python3.11 and python3.12 exist, both should be redirected."""
        from specter_cli.venv import patch_venv

        bin_dir = tmp_path / "bin"
        bin_dir.mkdir()
        # Real python binary
        python_bin = bin_dir / "python"
        python_bin.write_text("#!/usr/bin/env python3\n# real python\n")
        python_bin.chmod(0o755)
        # Two versioned pythons (use relative symlinks)
        (bin_dir / "python3.11").symlink_to("python")
        (bin_dir / "python3.12").symlink_to("python")
        # Standard symlinks
        (bin_dir / "python3").symlink_to("python")

        patch_venv(tmp_path, "h100")

        for name in ("python3.11", "python3.12"):
            p = bin_dir / name
            assert p.is_symlink(), f"{name} should be a symlink"
            assert str(p.readlink()) == "python", f"{name} should point to 'python' (wrapper)"

    def test_restore_multiple_versioned_pythons(self, tmp_path: Path) -> None:
        """restore_venv() should restore all versioned python binaries."""
        from specter_cli.venv import patch_venv, restore_venv

        bin_dir = tmp_path / "bin"
        bin_dir.mkdir()
        # Use relative symlinks so targets always resolve within the test dir.
        python_bin = bin_dir / "python"
        python_bin.write_text("#!/usr/bin/env python3\n# real python\n")
        python_bin.chmod(0o755)
        (bin_dir / "python3").symlink_to("python")
        (bin_dir / "python3.11").symlink_to("python")
        (bin_dir / "python3.12").symlink_to("python")

        patch_venv(tmp_path, "h100")
        restore_venv(tmp_path)

        for name in ("python3.11", "python3.12"):
            p = bin_dir / name
            # Use is_symlink() since the target may not exist as a real binary
            assert p.exists() or p.is_symlink(), f"{name} should exist after restore"
            backup = bin_dir / f"{name}.specter-original"
            assert not backup.exists(), f"{name} backup should be cleaned up after restore"

    def test_patch_restore_repatch_versioned(self, tmp_path: Path) -> None:
        """Full cycle: patch → restore → re-patch should work for versioned pythons."""
        from specter_cli.venv import patch_venv, restore_venv

        venv = self._make_venv(tmp_path)
        bin_dir = venv / "bin"

        # Patch → restore → re-patch
        patch_venv(venv, "h100")
        restore_venv(venv)
        patch_venv(venv, "a100")

        # python3.12 should point to wrapper again
        python312 = bin_dir / "python3.12"
        assert python312.is_symlink()
        assert str(python312.readlink()) == "python"

        # Backup should exist and NOT point to "python" (wrapper)
        backup = bin_dir / "python3.12.specter-original"
        assert backup.exists()
        if backup.is_symlink():
            target = str(backup.readlink())
            assert target != "python", "versioned backup must not point to wrapper after re-patch"

    def test_idempotent_patch(self, tmp_path: Path) -> None:
        """Patching twice should be safe (idempotent)."""
        from specter_cli.venv import patch_venv

        venv = self._make_venv(tmp_path)
        bin_dir = venv / "bin"

        patch_venv(venv, "h100")
        patch_venv(venv, "h100")  # second patch

        python312 = bin_dir / "python3.12"
        assert python312.is_symlink()
        assert str(python312.readlink()) == "python"

        # Backup should still be valid
        backup = bin_dir / "python3.12.specter-original"
        assert backup.exists()

    def test_no_versioned_python_no_error(self, tmp_path: Path) -> None:
        """Venv without python3.X should patch fine (no crash)."""
        from specter_cli.venv import patch_venv

        bin_dir = tmp_path / "bin"
        bin_dir.mkdir()
        python_bin = bin_dir / "python"
        python_bin.write_text("#!/usr/bin/env python3\n# real python\n")
        python_bin.chmod(0o755)
        python3_bin = bin_dir / "python3"
        python3_bin.symlink_to("python")

        # Should not raise
        patch_venv(tmp_path, "h100")

        # Wrapper should be written
        content = (bin_dir / "python").read_text()
        assert content.startswith("#!/bin/bash")


class TestRealPythonChainIntegrity:
    """Verify REAL_PYTHON (python.specter-original) resolves to real binary.

    The wrapper uses ``$(dirname "$0")/python.specter-original`` as REAL_PYTHON.
    If the backup is a relative symlink and the target gets redirected,
    REAL_PYTHON can loop back to the wrapper → infinite recursion.

    These tests verify the backup resolves to a real binary, not the wrapper.
    """

    def _make_standard_venv(self, tmp_path: Path) -> Path:
        """Standard venv layout: python → python3.12 → system python.

        Uses a "system python" outside the venv (like /usr/bin/python3.12)
        so that resolve() gives a stable absolute path that never gets
        modified by patch_venv().
        """
        # Simulate system python outside the venv
        system_dir = tmp_path / "usr" / "bin"
        system_dir.mkdir(parents=True)
        system_python = system_dir / "python3.12"
        system_python.write_text("#!/usr/bin/env python3\n# system python 3.12\n")
        system_python.chmod(0o755)

        bin_dir = tmp_path / "venv" / "bin"
        bin_dir.mkdir(parents=True)
        # python3.12 → system python (realistic venv symlink)
        (bin_dir / "python3.12").symlink_to(str(system_python))
        # Standard symlink chain
        (bin_dir / "python").symlink_to("python3.12")
        (bin_dir / "python3").symlink_to("python3.12")
        return tmp_path / "venv"

    def test_real_python_not_wrapper_after_patch(self, tmp_path: Path) -> None:
        """After patching, python.specter-original must resolve to real binary.

        This catches the infinite recursion bug: if python.specter-original
        is a relative symlink to python3.12, and python3.12 gets redirected
        to python (wrapper), then REAL_PYTHON → python3.12 → python (wrapper)
        → calls REAL_PYTHON → infinite loop.
        """
        from specter_cli.venv import patch_venv

        venv = self._make_standard_venv(tmp_path)
        bin_dir = venv / "bin"

        patch_venv(venv, "h100")

        # Follow the REAL_PYTHON chain to its final target
        real_python = bin_dir / "python.specter-original"
        assert real_python.exists() or real_python.is_symlink(), (
            "python.specter-original must exist"
        )

        # Resolve the full chain
        resolved = real_python.resolve()
        # The resolved path must NOT be the wrapper script
        if resolved.exists():
            content = resolved.read_text()
            assert "#!/bin/bash" not in content, (
                f"REAL_PYTHON resolves to the wrapper script at {resolved} — "
                f"this causes infinite recursion! "
                f"Chain: python.specter-original → ... → python (wrapper)"
            )

    def test_real_python_not_wrapper_after_repatch(self, tmp_path: Path) -> None:
        """After patch → restore → re-patch, REAL_PYTHON must still be real binary."""
        from specter_cli.venv import patch_venv, restore_venv

        venv = self._make_standard_venv(tmp_path)
        bin_dir = venv / "bin"

        patch_venv(venv, "h100")
        restore_venv(venv)
        patch_venv(venv, "a100")

        real_python = bin_dir / "python.specter-original"
        resolved = real_python.resolve()
        if resolved.exists():
            content = resolved.read_text()
            assert "#!/bin/bash" not in content, (
                f"REAL_PYTHON resolves to wrapper after re-patch — infinite recursion! "
                f"Resolved: {resolved}"
            )

    def test_repatch_without_restore_preserves_real_backup(self, tmp_path: Path) -> None:
        """patch → patch (without restore) must NOT back up the wrapper as the 'original'.

        Regression test for the "Argument list too long" bug:
        1. First patch: python.specter-original = symlink to /usr/bin/python3.12
        2. Second patch (no restore): compat symlink guard deletes the REAL
           backup, then backs up the wrapper script as python.specter-original
        3. Wrapper calls wrapper → infinite recursion → PYTHONPATH grows
           on every call → E2BIG ("Argument list too long")
        """
        from specter_cli.venv import patch_venv

        venv = self._make_standard_venv(tmp_path)
        bin_dir = venv / "bin"

        # First patch — creates real backup symlink
        patch_venv(venv, "h100")

        # Verify backup is a real python (not wrapper)
        backup = bin_dir / "python.specter-original"
        assert backup.exists() or backup.is_symlink()
        if backup.exists():
            content = backup.read_bytes()[:50]
            assert b"#!/bin/bash" not in content, "backup should be real python, not wrapper"

        # Second patch WITHOUT restore — simulates `specter install` on
        # an already-patched venv (user confirmed "Continue anyway?")
        patch_venv(venv, "a100")

        # The backup must STILL be the real python, not the wrapper
        backup = bin_dir / "python.specter-original"
        assert backup.exists() or backup.is_symlink(), (
            "python.specter-original must exist after re-patch"
        )
        resolved = backup.resolve()
        if resolved.exists():
            content = resolved.read_text()
            assert "#!/bin/bash" not in content, (
                f"CRITICAL: python.specter-original is the wrapper script! "
                f"This causes infinite recursion → 'Argument list too long'. "
                f"Resolved: {resolved}"
            )
            assert "specter" not in content.lower(), (
                f"CRITICAL: python.specter-original contains 'specter' — "
                f"it's the wrapper, not the real python binary. "
                f"Resolved: {resolved}"
            )
