"""Tests for Feature 17: Web Fetch Tool.

Tests SSRF prevention, rate limiting, URL validation, HTML stripping,
and approval integration.
"""

import asyncio
import re
import time
from unittest.mock import AsyncMock, MagicMock, patch

import pytest


# ---------------------------------------------------------------------------
# SSRF prevention
# ---------------------------------------------------------------------------

class TestSSRFBlocking:
    """Test that private/internal IPs are blocked."""

    @pytest.fixture(autouse=True)
    def _setup_web_fetch(self):
        """Set up the web_fetch function for testing.

        We re-create the core logic inline rather than importing from the
        deeply nested closure in langgraph_runner. This tests the same patterns.
        """
        self._BLOCKED_HOSTS = frozenset({
            "127.0.0.1", "localhost", "0.0.0.0",
            "169.254.", "::1", "fe80::",
        })

        def is_blocked(host: str) -> bool:
            for blocked in self._BLOCKED_HOSTS:
                if host.startswith(blocked) or host == blocked:
                    return True
            if host.startswith("10.") or host.startswith("192.168."):
                return True
            if re.match(r"172\.(1[6-9]|2\d|3[01])\.", host):
                return True
            return False

        self.is_blocked = is_blocked

    def test_blocks_localhost(self):
        assert self.is_blocked("localhost")

    def test_blocks_127_0_0_1(self):
        assert self.is_blocked("127.0.0.1")

    def test_blocks_0_0_0_0(self):
        assert self.is_blocked("0.0.0.0")

    def test_blocks_10_x(self):
        assert self.is_blocked("10.0.0.1")
        assert self.is_blocked("10.255.255.255")

    def test_blocks_192_168_x(self):
        assert self.is_blocked("192.168.0.1")
        assert self.is_blocked("192.168.100.200")

    def test_blocks_172_16_to_31(self):
        assert self.is_blocked("172.16.0.1")
        assert self.is_blocked("172.20.5.5")
        assert self.is_blocked("172.31.255.255")

    def test_allows_172_outside_private(self):
        assert not self.is_blocked("172.15.0.1")
        assert not self.is_blocked("172.32.0.1")

    def test_blocks_link_local(self):
        assert self.is_blocked("169.254.1.1")

    def test_blocks_ipv6_loopback(self):
        assert self.is_blocked("::1")

    def test_blocks_ipv6_link_local(self):
        assert self.is_blocked("fe80::1")

    def test_allows_public_ip(self):
        assert not self.is_blocked("8.8.8.8")
        assert not self.is_blocked("1.1.1.1")
        assert not self.is_blocked("203.0.113.50")

    def test_allows_public_hostname(self):
        assert not self.is_blocked("example.com")
        assert not self.is_blocked("docs.python.org")


# ---------------------------------------------------------------------------
# Rate limiting
# ---------------------------------------------------------------------------

class TestRateLimiting:
    """Test per-host rate limiting logic."""

    def test_rate_limit_enforcement(self):
        timestamps: dict[str, list[float]] = {}
        rate_limit = 10
        window_s = 60.0

        host = "example.com"
        now = time.time()

        # Fill up the rate limit
        for i in range(rate_limit):
            host_times = timestamps.setdefault(host, [])
            host_times[:] = [t for t in host_times if now - t < window_s]
            assert len(host_times) < rate_limit, f"Should not be rate limited at request {i}"
            host_times.append(now)

        # Next request should be blocked
        host_times = timestamps[host]
        host_times[:] = [t for t in host_times if now - t < window_s]
        assert len(host_times) >= rate_limit

    def test_rate_limit_window_expiry(self):
        timestamps: dict[str, list[float]] = {}
        rate_limit = 10
        window_s = 60.0

        host = "example.com"
        old_time = time.time() - 120  # 2 minutes ago

        # Add expired timestamps
        timestamps[host] = [old_time] * rate_limit
        now = time.time()
        host_times = timestamps[host]
        host_times[:] = [t for t in host_times if now - t < window_s]
        assert len(host_times) == 0  # All expired

    def test_rate_limits_are_per_host(self):
        timestamps: dict[str, list[float]] = {}
        rate_limit = 10
        window_s = 60.0

        now = time.time()
        # Fill up example.com
        timestamps["example.com"] = [now] * rate_limit
        # other.com should not be affected
        host_times = timestamps.setdefault("other.com", [])
        host_times[:] = [t for t in host_times if now - t < window_s]
        assert len(host_times) < rate_limit


# ---------------------------------------------------------------------------
# URL validation
# ---------------------------------------------------------------------------

class TestURLValidation:

    def test_rejects_non_http(self):
        for url in ["ftp://example.com", "file:///etc/passwd", "javascript:alert(1)", ""]:
            assert not url.startswith(("http://", "https://"))

    def test_accepts_http(self):
        assert "http://example.com".startswith(("http://", "https://"))

    def test_accepts_https(self):
        assert "https://docs.python.org".startswith(("http://", "https://"))


# ---------------------------------------------------------------------------
# HTML stripping
# ---------------------------------------------------------------------------

class TestHTMLStripping:
    """Test the HTML→text stripping logic used in web_fetch."""

    def _strip_html(self, body: str) -> str:
        body = re.sub(r'<script[^>]*>.*?</script>', '', body, flags=re.DOTALL | re.IGNORECASE)
        body = re.sub(r'<style[^>]*>.*?</style>', '', body, flags=re.DOTALL | re.IGNORECASE)
        body = re.sub(r'<[^>]+>', ' ', body)
        body = re.sub(r'\s+', ' ', body).strip()
        return body

    def test_strips_basic_tags(self):
        assert self._strip_html("<p>Hello</p>") == "Hello"

    def test_strips_script_tags(self):
        html = '<p>text</p><script>alert("xss")</script><p>more</p>'
        result = self._strip_html(html)
        assert "alert" not in result
        assert "text" in result
        assert "more" in result

    def test_strips_style_tags(self):
        html = '<style>body{color:red}</style><p>visible</p>'
        result = self._strip_html(html)
        assert "color" not in result
        assert "visible" in result

    def test_collapses_whitespace(self):
        html = "<p>foo</p>   \n\n   <p>bar</p>"
        result = self._strip_html(html)
        assert "  " not in result

    def test_handles_empty_html(self):
        assert self._strip_html("") == ""

    def test_plain_text_passthrough(self):
        assert self._strip_html("just text") == "just text"


# ---------------------------------------------------------------------------
# Approval integration
# ---------------------------------------------------------------------------

class TestWebFetchApproval:
    """Test that web_fetch requires approval in DEFAULT mode."""

    def test_web_fetch_requires_approval_in_default_mode(self):
        from autodebug.agent.approval import ApprovalMode, PolicyDecision, PolicyEngine

        engine = PolicyEngine(mode=ApprovalMode.DEFAULT)
        decision = engine.check("web_fetch", {"url": "https://example.com"})
        assert decision == PolicyDecision.ASK_USER

    def test_web_fetch_allowed_in_yolo_mode(self):
        from autodebug.agent.approval import ApprovalMode, PolicyDecision, PolicyEngine

        engine = PolicyEngine(mode=ApprovalMode.YOLO)
        decision = engine.check("web_fetch", {"url": "https://example.com"})
        assert decision == PolicyDecision.ALLOW

    def test_web_fetch_denied_in_plan_mode(self):
        from autodebug.agent.approval import ApprovalMode, PolicyDecision, PolicyEngine

        engine = PolicyEngine(mode=ApprovalMode.PLAN)
        decision = engine.check("web_fetch", {"url": "https://example.com"})
        assert decision == PolicyDecision.DENY


# ---------------------------------------------------------------------------
# Content truncation
# ---------------------------------------------------------------------------

class TestContentTruncation:

    def test_truncates_large_content(self):
        max_chars = 100
        body = "x" * 500
        if len(body) > max_chars:
            body = body[:max_chars] + f"\n\n... [truncated at {max_chars} chars]"
        assert len(body) < 500
        assert "truncated" in body

    def test_does_not_truncate_small_content(self):
        max_chars = 100_000
        body = "short"
        if len(body) > max_chars:
            body = body[:max_chars] + f"\n\n... [truncated at {max_chars} chars]"
        assert body == "short"
