# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc.
# SPDX-License-Identifier: MIT

"""
SSRF (Server-Side Request Forgery) protection tests.

Tests that Qdrant URL validation properly rejects malicious URLs that could:
- Access internal network resources (non-whitelisted hosts)
- Read local files (file:// protocol - TODO(OMN-6655))
- Connect to arbitrary external services
- Scan dangerous ports (SSH, DB, Kafka, etc.)

**IMPORTANT**: Tests validate the whitelist-based URL validation in qdrant_helper.
All tests should PASS (except skipped tests marked as production code TODOs).  # TODO_FORMAT_EXEMPT: documents test status

Current implementation:
  - Whitelist-based host validation
  - Dangerous port blocking (SSH, PostgreSQL, Redis, Kafka, etc.)
  - HTTPS enforcement in production
  - File protocol blocking (TODO(OMN-6655) - currently skipped)
  - Redirect blocking (TODO(OMN-6655) - requires HTTP request mocking, currently skipped)

Created: 2025-11-20
Updated: 2025-11-24 (stabilized environment-dependent tests)
"""

import os
import sys
from pathlib import Path
from unittest.mock import patch

import pytest

sys.path.insert(0, str(Path(__file__).parent.parent.parent / "_shared"))


class TestSSRFProtection:
    """Test SSRF attack prevention in Qdrant URL handling.

    Note: Tests mock ENVIRONMENT to 'development' to ensure consistent behavior
    regardless of the actual environment. Production-specific tests explicitly
    set ENVIRONMENT='production'.
    """

    @pytest.fixture(scope="class")
    def qdrant_helper(self):
        """Import qdrant_helper module with stable path resolution.

        Uses class scope to avoid repeated sys.path manipulation and ensure
        consistent imports across all tests in the class.

        Important: This fixture does NOT set ENVIRONMENT - individual tests
        should mock os.getenv or os.environ to control environment behavior.
        """
        # Save original sys.path to restore after tests
        original_path = sys.path.copy()

        # Ensure _shared directory is in path for consistent imports
        shared_path = str(Path(__file__).parent.parent.parent / "_shared")
        if shared_path not in sys.path:
            sys.path.insert(0, shared_path)

        try:
            import qdrant_helper

            yield qdrant_helper
        finally:
            # Restore original sys.path after all tests complete
            sys.path[:] = original_path

    @pytest.fixture(autouse=True)
    def _mock_environment(self):
        """Mock environment to development for consistent test behavior.

        This fixture ensures tests don't fail due to ENVIRONMENT being
        set to 'production' in the user's shell, which would require HTTPS.
        """
        with patch.dict(os.environ, {"ENVIRONMENT": "development"}, clear=False):
            yield

    def test_non_whitelisted_hosts_blocked(self, qdrant_helper):
        """Test that non-whitelisted hosts are blocked."""
        # These IPs are NOT in the whitelist (localhost and configured QDRANT_ALLOWED_HOSTS ARE allowed)
        malicious_urls = [
            "http://0.0.0.0:6333",  # Not whitelisted
            "http://10.0.0.1:6333",  # Private Class A  # onex-allow-internal-ip
            "http://172.16.0.1:6333",  # Private Class B  # onex-allow-internal-ip
            "http://192.168.1.1:6333",  # Different subnet  # onex-allow-internal-ip
            "http://169.254.169.254",  # AWS metadata service
            "http://internal-admin:6333",  # Internal hostname
            "http://evil.com:6333",  # External malicious
        ]

        for url in malicious_urls:
            if hasattr(qdrant_helper, "validate_qdrant_url"):
                with pytest.raises(ValueError, match=r".*whitelist.*") as exc_info:
                    qdrant_helper.validate_qdrant_url(url)
                # Verify error message mentions whitelist
                assert "whitelist" in str(exc_info.value).lower()

    @pytest.mark.skip(
        reason="File protocol validation not yet implemented - whitelist only validates hostname"
    )
    def test_file_protocol_blocked(self, qdrant_helper):
        """Test that file:// protocol is blocked.

        NOTE: Current implementation only validates hostname against whitelist.
        File protocol URLs would need explicit scheme validation to block.

        TODO(OMN-6655): Add scheme validation to validate_qdrant_url (allow only http/https).
        """
        malicious_urls = [
            "file:///etc/passwd",
            "file:///var/log/system.log",
            "file://localhost/etc/passwd",
        ]

        for url in malicious_urls:
            if hasattr(qdrant_helper, "validate_qdrant_url"):
                with pytest.raises(ValueError, match=r".*"):
                    qdrant_helper.validate_qdrant_url(url)

    @pytest.mark.skip(
        reason="Redirect blocking requires HTTP request mocking - not yet implemented. "
        "URL validation alone cannot prevent redirects; full protection requires "
        "HTTP client configuration (e.g., follow_redirects=False or redirect validation)"
    )
    def test_redirect_blocked(self, qdrant_helper):
        """Test that URL redirects to internal IPs are prevented.

        NOTE: This test is skipped because redirect protection cannot be implemented
        through URL validation alone. It requires:
        1. HTTP client configuration to disable automatic redirects, OR
        2. A custom redirect handler that validates each redirect target

        TODO(OMN-6655): Implement redirect protection in qdrant_helper HTTP client usage:
        - Use urllib with a custom redirect handler
        - Or switch to httpx/requests with follow_redirects=False
        - Validate redirect targets against the same whitelist as initial URLs

        When implemented, this test should verify:
        1. Direct requests to malicious URLs are blocked (already tested elsewhere)
        2. Redirects from allowed hosts to non-allowed hosts are blocked
        3. Redirect chains are properly validated at each hop
        """
        suspicious_urls = [
            "http://evil.com/redirect?url=http://127.0.0.1",
            "http://shorturl.com/abc123",  # Could redirect anywhere
        ]

        # These should be validated by URL structure
        # Full redirect protection requires HTTP client checks
        for url in suspicious_urls:
            if hasattr(qdrant_helper, "validate_qdrant_url"):
                with pytest.raises(ValueError, match=r".*whitelist.*"):
                    qdrant_helper.validate_qdrant_url(url)

    def test_unicode_encoding_blocked(self, qdrant_helper):
        """Test that Unicode-encoded URLs are normalized and validated."""
        # Unicode homograph attacks
        malicious_urls = [
            "http://127.0.0.①:6333",  # Unicode digit
            "http://ⓛocalhost:6333",  # Unicode letters
        ]

        for url in malicious_urls:
            if hasattr(qdrant_helper, "validate_qdrant_url"):
                # These URLs should be rejected (either immediately or after normalization)
                # We accept either:
                # 1. ValueError raised (URL rejected)
                # 2. URL passes but doesn't resolve to localhost (safe behavior)
                validation_failed = False
                try:
                    result = qdrant_helper.validate_qdrant_url(url)
                    # If validation passes, verify the result is NOT localhost
                    # (meaning Unicode was NOT decoded to localhost)
                    assert "localhost" not in result.lower(), (
                        f"Unicode URL {url} was incorrectly normalized to localhost"
                    )
                    assert "127.0.0.1" not in result, (
                        f"Unicode URL {url} was incorrectly normalized to 127.0.0.1"
                    )
                except ValueError:
                    # Expected rejection - Unicode URL was caught
                    validation_failed = True

                # Either validation failed OR the URL was not normalized to localhost
                # Both are acceptable secure behaviors

    def test_whitelisted_urls_allowed(self, qdrant_helper):
        """Test that whitelisted URLs are allowed."""
        # Only whitelisted hosts should be allowed (per whitelist in validate_qdrant_url)
        valid_urls = [
            "http://localhost:6333",  # localhost (whitelisted)
            "http://127.0.0.1:6333",  # 127.0.0.1 (whitelisted)
            "http://qdrant.internal:6333",  # Internal DNS (whitelisted)
        ]

        for url in valid_urls:
            if hasattr(qdrant_helper, "validate_qdrant_url"):
                # These should NOT raise exceptions
                try:
                    result = qdrant_helper.validate_qdrant_url(url)
                    assert result == url, (
                        "validate_qdrant_url should return the URL unchanged"
                    )
                except ValueError as e:
                    pytest.fail(f"Whitelisted URL {url} was incorrectly rejected: {e}")

    def test_dangerous_ports_blocked(self, qdrant_helper):
        """Test that dangerous ports are blocked even for whitelisted hosts."""
        # Attempting to access dangerous ports on whitelisted host
        dangerous_port_urls = [
            "http://localhost:22",  # SSH
            "http://localhost:23",  # Telnet
            "http://localhost:25",  # SMTP
            "http://localhost:3389",  # RDP
            "http://localhost:5432",  # PostgreSQL
            "http://localhost:6379",  # Redis
            "http://localhost:27017",  # MongoDB
            "http://localhost:3306",  # MySQL
            "http://localhost:1521",  # Oracle
            "http://localhost:9092",  # Kafka
        ]

        for url in dangerous_port_urls:
            if hasattr(qdrant_helper, "validate_qdrant_url"):
                with pytest.raises(ValueError, match=r"[Dd]angerous port") as exc_info:
                    qdrant_helper.validate_qdrant_url(url)
                # Verify error message mentions dangerous port
                assert "dangerous port" in str(exc_info.value).lower()

    def test_dns_rebinding_protection(self, qdrant_helper):
        """Test protection against DNS rebinding attacks.

        DNS rebinding attacks involve a domain that initially resolves to a safe
        external IP but later resolves to an internal IP. This test verifies that:
        1. URL validation occurs at configuration time
        2. The helper provides validate_qdrant_url for SSRF protection

        Note: Full DNS rebinding protection requires runtime DNS resolution checks
        at connection time, which is outside the scope of URL validation.
        """
        # Verify validate_qdrant_url exists for URL validation
        assert hasattr(qdrant_helper, "validate_qdrant_url"), (
            "qdrant_helper should provide validate_qdrant_url for SSRF protection"
        )

        # Test that validation properly checks whitelisted hosts
        # (this prevents initial DNS rebinding setup)
        test_url = "http://localhost:6333"
        if hasattr(qdrant_helper, "validate_qdrant_url"):
            # Should pass validation for whitelisted host
            result = qdrant_helper.validate_qdrant_url(test_url)
            assert result.startswith("http://") or result.startswith("https://"), (
                f"URL should start with http:// or https://, got: {result}"
            )


class TestQdrantURLParsing:
    """Test URL parsing and normalization."""

    def test_url_components_extracted(self):
        """Test that URL components are properly extracted."""
        from urllib.parse import urlparse

        test_url = "http://localhost:6333/collections"

        parsed = urlparse(test_url)

        assert parsed.scheme == "http"
        assert parsed.hostname == "localhost"
        assert parsed.port == 6333
        assert parsed.path == "/collections"

    def test_malformed_url_parsing(self):
        """Test that malformed URLs can be detected."""
        from urllib.parse import urlparse

        malformed_urls = [
            "not-a-url",
            "http://",
            "://missing-scheme",
        ]

        for url in malformed_urls:
            try:
                parsed = urlparse(url)
                # Should have no hostname or invalid components
                assert not parsed.hostname or not parsed.scheme, (
                    f"URL {url} should have no hostname or scheme"
                )
            except ValueError:
                # Some malformed URLs raise ValueError during parsing
                pass  # Expected for malformed URLs
