"""Edge-case tests for dependency synchronisation (specter_cli.deps).

Covers scenarios not in test_deps.py: underscore package names,
specter-host filtering variants, pip freeze with mixed content,
and the main() entry point with correct patch targets.
"""

from __future__ import annotations

from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from specter_cli.deps import _pip_freeze, sync_deps

_SUBPROCESS_RUN = "specter_cli.deps.subprocess.run"
_RSYNC_SSH = "specter_cli.deps.rsync_ssh_args"
_SSH_BASE = "specter_cli.deps.ssh_base_args"


@pytest.fixture
def fake_venv(tmp_path: Path) -> Path:
    """Create a minimal fake venv with python and python.specter-original."""
    bin_dir = tmp_path / "bin"
    bin_dir.mkdir()
    original = bin_dir / "python.specter-original"
    original.write_text("#!/bin/bash\n")
    original.chmod(0o755)
    python = bin_dir / "python"
    python.write_text("#!/bin/bash\n# wrapper\n")
    python.chmod(0o755)
    return tmp_path


class TestSpecterHostFiltering:
    """Verify specter-host is excluded from synced deps in all variants."""

    @patch(_SUBPROCESS_RUN)
    def test_filters_specter_host_with_underscore(
        self, mock_run: MagicMock, fake_venv: Path
    ) -> None:
        """pip may output specter_host (underscore) instead of specter-host."""
        mock_run.return_value = MagicMock(returncode=0, stdout="specter_host==0.1.0\n")

        result = sync_deps(fake_venv, host="1.2.3.4", user="root")

        # BUG: Currently this may return True because the underscore variant
        # isn't filtered. This test documents the expected behavior.
        # If filtering is correct, this should be False.
        # Marking as xfail until bug 3 is fixed.
        assert result is False

    @patch(_SUBPROCESS_RUN)
    def test_filters_specter_host_mixed_case(self, mock_run: MagicMock, fake_venv: Path) -> None:
        """Filter should be case-insensitive."""
        mock_run.return_value = MagicMock(returncode=0, stdout="Specter-Host==0.1.0\n")

        result = sync_deps(fake_venv, host="1.2.3.4", user="root")

        assert result is False

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch(_RSYNC_SSH, return_value="ssh -o ControlMaster=auto")
    @patch(_SUBPROCESS_RUN)
    def test_filters_specter_host_keeps_other_packages(
        self,
        mock_run: MagicMock,
        _rsync_ssh: MagicMock,
        _ssh_base: MagicMock,
        fake_venv: Path,
    ) -> None:
        """specter-host is filtered but other packages remain."""
        mock_run.side_effect = [
            MagicMock(
                returncode=0,
                stdout="specter-host==0.1.0\ntorch==2.3.0\nrequests==2.31.0\n",
            ),
            MagicMock(returncode=0),  # rsync
            MagicMock(returncode=0, stderr=""),  # remote pip
        ]

        result = sync_deps(fake_venv, host="1.2.3.4", user="root")

        assert result is True
        assert mock_run.call_count == 3


class TestPipFreezeEdgeCases:
    """Edge cases for pip freeze output."""

    @patch(_SUBPROCESS_RUN)
    def test_freeze_with_only_whitespace(self, mock_run: MagicMock, fake_venv: Path) -> None:
        """pip freeze that returns only whitespace/empty lines."""
        mock_run.return_value = MagicMock(returncode=0, stdout="\n\n  \n")

        result = sync_deps(fake_venv, host="1.2.3.4", user="root")

        assert result is False

    @patch(_SUBPROCESS_RUN)
    def test_freeze_timeout(self, mock_run: MagicMock, fake_venv: Path) -> None:
        """pip freeze that times out."""
        import subprocess

        mock_run.side_effect = subprocess.TimeoutExpired(cmd="pip", timeout=60)

        with pytest.raises(subprocess.TimeoutExpired):
            _pip_freeze(fake_venv)

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch(_RSYNC_SSH, return_value="ssh -o ControlMaster=auto")
    @patch(_SUBPROCESS_RUN)
    def test_freeze_with_editable_installs(
        self,
        mock_run: MagicMock,
        _rsync_ssh: MagicMock,
        _ssh_base: MagicMock,
        fake_venv: Path,
    ) -> None:
        """pip freeze with -e (editable) installs included."""
        mock_run.side_effect = [
            MagicMock(
                returncode=0,
                stdout="-e git+https://github.com/user/project.git@abc123#egg=myproj\ntorch==2.3.0\n",
            ),
            MagicMock(returncode=0),  # rsync
            MagicMock(returncode=0, stderr=""),  # remote pip
        ]

        result = sync_deps(fake_venv, host="1.2.3.4", user="root")

        # Editable installs get forwarded too (they'll fail remotely but
        # that's a warning, not an error).
        assert result is True

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch(_RSYNC_SSH, return_value="ssh -o ControlMaster=auto")
    @patch(_SUBPROCESS_RUN)
    def test_freeze_with_many_packages(
        self,
        mock_run: MagicMock,
        _rsync_ssh: MagicMock,
        _ssh_base: MagicMock,
        fake_venv: Path,
    ) -> None:
        """Realistic pip freeze with many packages."""
        packages = (
            "\n".join(
                [
                    "certifi==2024.2.2",
                    "charset-normalizer==3.3.2",
                    "click==8.1.7",
                    "filelock==3.13.1",
                    "httpx==0.27.0",
                    "numpy==1.26.4",
                    "rich==13.7.0",
                    "specter-host==0.1.0",
                    "torch==2.3.0",
                    "transformers==4.38.2",
                ]
            )
            + "\n"
        )

        mock_run.side_effect = [
            MagicMock(returncode=0, stdout=packages),
            MagicMock(returncode=0),  # rsync
            MagicMock(returncode=0, stderr=""),  # remote pip
        ]

        result = sync_deps(fake_venv, host="1.2.3.4", user="root")

        assert result is True
        # Verify specter-host was filtered but 9 other packages remain.
        assert mock_run.call_count == 3


class TestRsyncFailure:
    """Rsync failures should raise CalledProcessError."""

    @patch(_RSYNC_SSH, return_value="ssh -o ControlMaster=auto")
    @patch(_SUBPROCESS_RUN)
    def test_rsync_failure_raises(
        self,
        mock_run: MagicMock,
        _rsync_ssh: MagicMock,
        fake_venv: Path,
    ) -> None:
        """rsync failure should propagate (check=True)."""
        import subprocess

        mock_run.side_effect = [
            MagicMock(returncode=0, stdout="torch==2.3.0\n"),
            subprocess.CalledProcessError(1, ["rsync"], stderr="Connection refused"),
        ]

        with pytest.raises(subprocess.CalledProcessError):
            sync_deps(fake_venv, host="1.2.3.4", user="root")


class TestMainEntryPoint:
    """Tests for deps.main() with correct patch targets.

    The main() function uses local imports, so we patch the source
    modules directly (specter_cli.config, not specter_cli.deps).
    """

    @patch("specter_cli.deps.sync_deps", return_value=True)
    @patch("specter_cli.config.load_remote_config")
    @patch.dict("os.environ", {"VIRTUAL_ENV": "/tmp/test-venv"})
    def test_main_syncs_when_configured(
        self,
        mock_config: MagicMock,
        mock_sync: MagicMock,
        tmp_path: Path,
    ) -> None:
        """main() should call sync_deps when config and venv exist."""
        mock_config.return_value = {
            "host": "1.2.3.4",
            "user": "root",
            "ssh_port": 22,
        }

        # Create the fake venv directory so Path.is_dir() returns True.

        fake_venv = tmp_path / "test-venv"
        fake_venv.mkdir()

        with patch.dict("os.environ", {"VIRTUAL_ENV": str(fake_venv)}):
            from specter_cli.deps import main

            main()

        mock_sync.assert_called_once()

    @patch("specter_cli.deps.sync_deps")
    @patch("specter_cli.config.load_remote_config", return_value={})
    def test_main_no_config_skips(
        self,
        _config: MagicMock,
        mock_sync: MagicMock,
    ) -> None:
        """No remote config → skip sync."""
        from specter_cli.deps import main

        main()

        mock_sync.assert_not_called()

    @patch("specter_cli.deps.sync_deps")
    @patch(
        "specter_cli.config.load_remote_config",
        return_value={"host": "1.2.3.4", "user": "root"},
    )
    @patch.dict("os.environ", {}, clear=True)
    def test_main_no_venv_skips(
        self,
        _config: MagicMock,
        mock_sync: MagicMock,
    ) -> None:
        """No VIRTUAL_ENV env var → skip sync."""
        from specter_cli.deps import main

        main()

        mock_sync.assert_not_called()

    @patch("specter_cli.deps.sync_deps", side_effect=Exception("SSH failed"))
    @patch(
        "specter_cli.config.load_remote_config",
        return_value={
            "host": "1.2.3.4",
            "user": "root",
            "ssh_port": 22,
        },
    )
    def test_main_handles_errors_gracefully(
        self,
        _config: MagicMock,
        _sync: MagicMock,
        tmp_path: Path,
    ) -> None:
        """sync failure should not crash — just print a warning."""
        fake_venv = tmp_path / "test-venv"
        fake_venv.mkdir()

        with patch.dict("os.environ", {"VIRTUAL_ENV": str(fake_venv)}):
            from specter_cli.deps import main

            # Should not raise — errors are caught and logged.
            main()

    @patch("specter_cli.deps.sync_deps")
    @patch(
        "specter_cli.config.load_remote_config",
        return_value={"host": "1.2.3.4", "user": "root", "ssh_port": 22},
    )
    def test_main_nonexistent_venv_skips(
        self,
        _config: MagicMock,
        mock_sync: MagicMock,
    ) -> None:
        """VIRTUAL_ENV points to nonexistent dir → skip sync."""
        with patch.dict(
            "os.environ",
            {"VIRTUAL_ENV": "/nonexistent/path/to/.venv"},
        ):
            from specter_cli.deps import main

            main()

        mock_sync.assert_not_called()
