"""Tests for activate script PATH patching and legacy shim cleanup.

Covers the v0.1.7 changes:
1. _patch_activate() — injects specter tool PATH into bin/activate
2. _unpatch_activate() — removes the injection cleanly
3. Idempotency — double-patch doesn't create duplicate PATH entries
4. Legacy shim cleanup — restore_venv removes old bin/nvidia-smi shims
5. Shim relative paths — bin/specter/ shims reference ../python.specter-original
6. Activate fallback — missing anchor line still patches via append
"""

from __future__ import annotations

import stat
from pathlib import Path

import pytest

from specter_cli.venv import (
    _ACTIVATE_BEGIN,
    _ACTIVATE_END,
    _FORWARDED_TOOLS,
    _TOOL_SHIM_DIR,
    _patch_activate,
    _unpatch_activate,
    _write_tool_shims,
    patch_venv,
    restore_venv,
)

# Standard activate PATH line used as anchor for injection.
_ACTIVATE_ANCHOR = 'export PATH="$VIRTUAL_ENV/bin:$PATH"'


def _make_activate(bin_dir: Path, content: str | None = None) -> Path:
    """Create a minimal activate script in bin_dir."""
    activate = bin_dir / "activate"
    if content is None:
        content = (
            'VIRTUAL_ENV="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"\n'
            "export VIRTUAL_ENV\n"
            f"{_ACTIVATE_ANCHOR}\n"
        )
    activate.write_text(content)
    return activate


@pytest.fixture
def bin_dir(tmp_path: Path) -> Path:
    d = tmp_path / "bin"
    d.mkdir()
    return d


@pytest.fixture
def fake_venv(tmp_path: Path) -> Path:
    """Create a minimal fake venv with python binary and activate script."""
    venv = tmp_path / "fakevenv"
    bin_dir = venv / "bin"
    bin_dir.mkdir(parents=True)
    python = bin_dir / "python"
    python.write_text("#!/bin/bash\necho fake python\n")
    python.chmod(0o755)
    python3 = bin_dir / "python3"
    python3.symlink_to("python")
    _make_activate(bin_dir)
    return venv


# ---------------------------------------------------------------------------
# _patch_activate() tests
# ---------------------------------------------------------------------------


class TestPatchActivate:
    """Tests for _patch_activate() — inject specter tool PATH into activate."""

    def test_injects_after_path_export(self, bin_dir: Path) -> None:
        """Should inject specter PATH block after the standard PATH export."""
        _make_activate(bin_dir)
        _patch_activate(bin_dir)

        text = (bin_dir / "activate").read_text()
        assert _ACTIVATE_BEGIN in text
        assert _ACTIVATE_END in text
        assert f"$VIRTUAL_ENV/bin/{_TOOL_SHIM_DIR}" in text

    def test_injection_follows_anchor(self, bin_dir: Path) -> None:
        """The specter block should appear after the PATH export line."""
        _make_activate(bin_dir)
        _patch_activate(bin_dir)

        text = (bin_dir / "activate").read_text()
        anchor_pos = text.index(_ACTIVATE_ANCHOR)
        begin_pos = text.index(_ACTIVATE_BEGIN)
        assert begin_pos > anchor_pos

    def test_idempotent_double_patch(self, bin_dir: Path) -> None:
        """Calling _patch_activate twice should not duplicate the block."""
        _make_activate(bin_dir)
        _patch_activate(bin_dir)
        _patch_activate(bin_dir)

        text = (bin_dir / "activate").read_text()
        assert text.count(_ACTIVATE_BEGIN) == 1
        assert text.count(_ACTIVATE_END) == 1

    def test_idempotent_guard_in_bash(self, bin_dir: Path) -> None:
        """The injected block should have a bash idempotency guard."""
        _make_activate(bin_dir)
        _patch_activate(bin_dir)

        text = (bin_dir / "activate").read_text()
        # The guard prevents duplicate PATH entries when activate is re-sourced.
        assert '":$PATH:"' in text
        assert f"$VIRTUAL_ENV/bin/{_TOOL_SHIM_DIR}" in text

    def test_missing_activate_skips_silently(self, bin_dir: Path) -> None:
        """No activate script → should not crash."""
        # Don't create activate.
        _patch_activate(bin_dir)
        # Should not raise.

    def test_fallback_appends_when_anchor_missing(self, bin_dir: Path) -> None:
        """If the standard PATH export line is missing, append to end."""
        activate = bin_dir / "activate"
        activate.write_text("# custom activate without standard PATH line\n")

        _patch_activate(bin_dir)

        text = activate.read_text()
        assert _ACTIVATE_BEGIN in text
        assert _ACTIVATE_END in text

    def test_preserves_existing_content(self, bin_dir: Path) -> None:
        """Patching should not modify existing activate content."""
        original = (
            "# my custom header\n"
            f"{_ACTIVATE_ANCHOR}\n"
            "# my custom footer\n"
        )
        _make_activate(bin_dir, original)
        _patch_activate(bin_dir)

        text = (bin_dir / "activate").read_text()
        assert "# my custom header" in text
        assert "# my custom footer" in text
        assert _ACTIVATE_ANCHOR in text


# ---------------------------------------------------------------------------
# _unpatch_activate() tests
# ---------------------------------------------------------------------------


class TestUnpatchActivate:
    """Tests for _unpatch_activate() — remove specter PATH from activate."""

    def test_removes_injected_block(self, bin_dir: Path) -> None:
        """Should remove the entire specter block."""
        _make_activate(bin_dir)
        _patch_activate(bin_dir)

        text = (bin_dir / "activate").read_text()
        assert _ACTIVATE_BEGIN in text

        _unpatch_activate(bin_dir)

        text = (bin_dir / "activate").read_text()
        assert _ACTIVATE_BEGIN not in text
        assert _ACTIVATE_END not in text
        assert _TOOL_SHIM_DIR not in text

    def test_preserves_original_content(self, bin_dir: Path) -> None:
        """Unpatching should restore activate to (roughly) original state."""
        original = (
            "# header\n"
            f"{_ACTIVATE_ANCHOR}\n"
            "# footer\n"
        )
        _make_activate(bin_dir, original)
        _patch_activate(bin_dir)
        _unpatch_activate(bin_dir)

        text = (bin_dir / "activate").read_text()
        assert "# header" in text
        assert "# footer" in text
        assert _ACTIVATE_ANCHOR in text

    def test_noop_when_not_patched(self, bin_dir: Path) -> None:
        """Unpatching an unpatched activate should be a no-op."""
        _make_activate(bin_dir)
        original_text = (bin_dir / "activate").read_text()

        _unpatch_activate(bin_dir)

        assert (bin_dir / "activate").read_text() == original_text

    def test_noop_when_no_activate(self, bin_dir: Path) -> None:
        """No activate file → should not crash."""
        _unpatch_activate(bin_dir)
        # Should not raise.

    def test_double_unpatch_is_safe(self, bin_dir: Path) -> None:
        """Calling _unpatch_activate twice should not crash."""
        _make_activate(bin_dir)
        _patch_activate(bin_dir)
        _unpatch_activate(bin_dir)
        _unpatch_activate(bin_dir)

        text = (bin_dir / "activate").read_text()
        assert _ACTIVATE_BEGIN not in text


# ---------------------------------------------------------------------------
# Legacy shim cleanup
# ---------------------------------------------------------------------------


class TestLegacyShimCleanup:
    """restore_venv should clean up both old (bin/) and new (bin/specter/) shims."""

    def test_restore_removes_legacy_bin_shim(self, fake_venv: Path) -> None:
        """Legacy bin/nvidia-smi from pre-v0.1.7 should be removed by restore."""
        patch_venv(fake_venv, "h100")

        # Simulate a legacy shim (pre-v0.1.7 placed directly in bin/).
        legacy_shim = fake_venv / "bin" / "nvidia-smi"
        legacy_shim.write_text("#!/bin/bash\necho legacy shim\n")
        legacy_shim.chmod(0o755)

        restore_venv(fake_venv)

        assert not legacy_shim.exists(), "Legacy bin/nvidia-smi should be removed"

    def test_restore_removes_both_legacy_and_new_shims(self, fake_venv: Path) -> None:
        """Both bin/nvidia-smi and bin/specter/nvidia-smi should be cleaned."""
        patch_venv(fake_venv, "h100")

        # Create a legacy shim alongside the new one.
        legacy_shim = fake_venv / "bin" / "nvidia-smi"
        legacy_shim.write_text("#!/bin/bash\necho legacy\n")
        legacy_shim.chmod(0o755)

        new_shim = fake_venv / "bin" / "specter" / "nvidia-smi"
        assert new_shim.exists(), "New shim should exist before restore"

        restore_venv(fake_venv)

        assert not legacy_shim.exists()
        assert not new_shim.exists()
        assert not (fake_venv / "bin" / "specter").exists(), "specter/ dir should be removed"

    def test_restore_tolerates_missing_specter_dir(self, fake_venv: Path) -> None:
        """restore_venv should not crash if bin/specter/ was already removed."""
        patch_venv(fake_venv, "h100")

        import shutil

        shutil.rmtree(fake_venv / "bin" / "specter")

        # Should not raise.
        restore_venv(fake_venv)


# ---------------------------------------------------------------------------
# Shim relative path correctness
# ---------------------------------------------------------------------------


class TestShimRelativePaths:
    """Shims in bin/specter/ must reference ../python.specter-original."""

    def test_shim_references_parent_python(self, bin_dir: Path) -> None:
        """Shim should use ../python.specter-original (one level up)."""
        _write_tool_shims(bin_dir, "/fake/lib")

        content = (bin_dir / "specter" / "nvidia-smi").read_text()
        # The shim is in bin/specter/, so python.specter-original is at ../
        assert "../python.specter-original" in content

    def test_shim_virtual_env_goes_up_two_levels(self, bin_dir: Path) -> None:
        """VIRTUAL_ENV should resolve to ../../ from bin/specter/."""
        _write_tool_shims(bin_dir, "/fake/lib")

        content = (bin_dir / "specter" / "nvidia-smi").read_text()
        # From bin/specter/, need to go up two levels to reach venv root.
        assert "../.." in content

    def test_specter_dir_created(self, bin_dir: Path) -> None:
        """_write_tool_shims should create bin/specter/ directory."""
        assert not (bin_dir / "specter").exists()

        _write_tool_shims(bin_dir, "/fake/lib")

        assert (bin_dir / "specter").is_dir()

    def test_specter_dir_exist_ok(self, bin_dir: Path) -> None:
        """Calling _write_tool_shims twice should not crash (exist_ok)."""
        _write_tool_shims(bin_dir, "/fake/lib")
        _write_tool_shims(bin_dir, "/fake/lib")

        assert (bin_dir / "specter" / "nvidia-smi").exists()


# ---------------------------------------------------------------------------
# Full patch/restore cycle with activate
# ---------------------------------------------------------------------------


class TestFullPatchRestoreCycle:
    """End-to-end: patch_venv patches activate, restore_venv cleans it."""

    def test_patch_adds_activate_block(self, fake_venv: Path) -> None:
        """patch_venv should inject the specter PATH block into activate."""
        patch_venv(fake_venv, "h100")

        text = (fake_venv / "bin" / "activate").read_text()
        assert _ACTIVATE_BEGIN in text
        assert f"$VIRTUAL_ENV/bin/{_TOOL_SHIM_DIR}" in text

    def test_restore_removes_activate_block(self, fake_venv: Path) -> None:
        """restore_venv should remove the specter PATH block from activate."""
        patch_venv(fake_venv, "h100")
        restore_venv(fake_venv)

        text = (fake_venv / "bin" / "activate").read_text()
        assert _ACTIVATE_BEGIN not in text
        assert _ACTIVATE_END not in text

    def test_re_patch_after_restore(self, fake_venv: Path) -> None:
        """patch → restore → patch should result in clean single patch."""
        patch_venv(fake_venv, "h100")
        restore_venv(fake_venv)
        patch_venv(fake_venv, "a100")

        text = (fake_venv / "bin" / "activate").read_text()
        assert text.count(_ACTIVATE_BEGIN) == 1
        assert (fake_venv / "bin" / "specter" / "nvidia-smi").exists()

    def test_re_patch_without_restore_is_clean(self, fake_venv: Path) -> None:
        """Double patch (auto-restore) should result in single activate block."""
        patch_venv(fake_venv, "h100")
        patch_venv(fake_venv, "a100")  # triggers auto-restore

        text = (fake_venv / "bin" / "activate").read_text()
        assert text.count(_ACTIVATE_BEGIN) == 1

    def test_full_structure_after_patch(self, fake_venv: Path) -> None:
        """Verify complete directory structure after patch_venv."""
        patch_venv(fake_venv, "h100")

        # Shim directory exists.
        specter_dir = fake_venv / "bin" / "specter"
        assert specter_dir.is_dir()

        # All forwarded tools have shims.
        for tool in _FORWARDED_TOOLS:
            shim = specter_dir / tool
            assert shim.exists()
            assert shim.stat().st_mode & stat.S_IEXEC

        # Activate is patched.
        text = (fake_venv / "bin" / "activate").read_text()
        assert _ACTIVATE_BEGIN in text

        # No legacy shims in bin/ directly.
        for tool in _FORWARDED_TOOLS:
            assert not (fake_venv / "bin" / tool).exists()
