"""
Regression-prevention fixtures for the SSRF hardening (PR #3873, #3882).

This is a defensive regression net: if a future change to ``validate_url``
accidentally rejects URLs LDR is documented to fetch, these tests fail
loudly. Patterns extracted from a real codebase audit of:

- ``src/local_deep_research/research_library/downloaders/`` (academic)
- ``src/local_deep_research/web_search_engines/engines/`` (search)
- ``src/local_deep_research/llm/providers/implementations/`` (LLM)
- ``src/local_deep_research/notifications/`` (Apprise)

Plus a complementary list of attack URLs that MUST stay blocked, and a
behaviour-change lock-in class for the deliberate semantic changes in
PR #3873 (None handling, whitespace stripping) and PR #3882 (log
redaction).
"""

import time

import pytest
from unittest.mock import patch


# DNS resolution mock — return a public IP so the validation pipeline
# reaches the IP-block check (which is the only thing that needs network).
_PUBLIC_DNS_RESPONSE = [(2, 1, 6, "", ("93.184.216.34", 0))]


# -----------------------------------------------------------------------
# REAL-WORLD URLS THAT MUST PASS
# -----------------------------------------------------------------------
# If any of these stops passing validate_url, an LDR user feature breaks.
# Categories: academic, search, llm, notifications, idn, edge, ipv6.

REAL_WORLD_URLS_THAT_MUST_PASS = [
    # ---- Academic paper sources ----
    ("https://arxiv.org/abs/2401.12345", "academic"),
    ("https://arxiv.org/pdf/2401.12345v1.pdf", "academic"),
    ("https://export.arxiv.org/api/query?id_list=2401.12345", "academic"),
    ("https://pubmed.ncbi.nlm.nih.gov/35123456/", "academic"),
    ("https://www.ncbi.nlm.nih.gov/pmc/articles/PMC1234567/", "academic"),
    (
        "https://www.ebi.ac.uk/europepmc/webservices/rest/search",
        "academic",
    ),
    (
        "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/elink.fcgi",
        "academic",
    ),
    (
        "https://www.biorxiv.org/content/10.1101/2024.01.01.123456v1.full.pdf",
        "academic",
    ),
    (
        "https://api.openalex.org/works?filter=doi:10.1038/nature.2024.12345",
        "academic",
    ),
    (
        "https://api.semanticscholar.org/graph/v1/paper/CORPUSID:12345",
        "academic",
    ),
    ("https://doi.org/10.1038/nature12345", "academic"),
    ("https://ui.adsabs.harvard.edu/abs/2024ApJ...123..456A", "academic"),
    # ---- Search / reference ----
    ("https://en.wikipedia.org/wiki/Machine_learning", "search"),
    # encoded umlaut — common Wikipedia article URL form
    ("https://en.wikipedia.org/wiki/M%C3%BCnchen", "search"),
    (
        "https://web.archive.org/cdx/search/cdx?url=example.com",
        "search",
    ),
    ("https://api.tavily.com/search", "search"),
    ("https://api.exa.ai/search", "search"),
    (
        "https://openlibrary.org/api/books?bibkeys=ISBN:0451524934",
        "search",
    ),
    ("https://www.gutenberg.org/ebooks/12345", "search"),
    ("https://content.guardianapis.com/search", "search"),
    (
        "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/2244/JSON",
        "search",
    ),
    # ---- LLM provider default endpoints (from openai_base/google/etc.) ----
    # If these stop passing, LDR cannot talk to its own configured providers.
    ("https://api.openai.com/v1/models", "llm"),
    ("https://api.anthropic.com/v1/messages", "llm"),
    ("https://openrouter.ai/api/v1/chat/completions", "llm"),
    ("https://api.x.ai/v1/chat/completions", "llm"),
    ("https://generativelanguage.googleapis.com/v1beta/openai", "llm"),
    ("https://openai.inference.de-txl.ionos.com/v1", "llm"),
    # ---- IDN / non-Latin domains (urllib3 auto-Punycodes) ----
    # These exercise the urllib3 host-extraction path and confirm that
    # users in Asia / Cyrillic / Han regions are not blocked.
    ("https://例え.jp/", "idn"),
    ("https://привет.рф/", "idn"),
    ("https://中国.cn/", "idn"),
    ("https://xn--mnchen-3ya.de/", "idn"),  # pre-Punycoded München
    # ---- Edge cases — RFC-legal patterns ----
    ("https://api.example.com/v1?keys[]=foo&keys[]=bar", "edge"),
    (
        "https://api.example.com/v1/items?since=2024-01-01T00:00:00Z",
        "edge",
    ),
    ("https://example.com/path/with+plus", "edge"),
    ("https://example.com/?q=hello+world", "edge"),
    ("https://user:pass@example.com/", "edge"),
    ("https://example.com./", "edge"),  # FQDN trailing dot
    ("https://example.com/file.pdf;jsessionid=abc123", "edge"),
    # encoded backslash in PATH is RFC-legal — distinct from %5C in netloc
    ("https://example.com/path%5Cfile", "edge"),
    (
        "https://example.com/path/with-hyphens_and_underscores.html",
        "edge",
    ),
    # ---- IPv6 public addresses ----
    ("https://[2001:db8::1]/", "ipv6"),
    ("https://[2001:db8::1]:8080/", "ipv6"),
]


# -----------------------------------------------------------------------
# REAL-WORLD URLS THAT MUST FAIL (security sentinels)
# -----------------------------------------------------------------------
# If any of these starts passing, the SSRF hardening has regressed.

REAL_WORLD_URLS_THAT_MUST_FAIL = [
    # ---- GHSA-g23j-2vwm-5c25 canonical ----
    ("http://127.0.0.1:6666\\@1.1.1.1", "advisory_canonical"),
    ("http://127.0.0.1:6666/%5C@1.1.1.1", "advisory_post_prepare"),
    # ---- IPv6 unspecified bypass (caught in PR #3873 review) ----
    ("http://[::]/", "ipv6_unspecified"),
    ("http://[0::]/", "ipv6_unspecified_alt"),
    ("http://[0:0:0:0:0:0:0:0]/", "ipv6_unspecified_full"),
    # ---- Cloud metadata — always blocked under every flag ----
    ("http://169.254.169.254/latest/meta-data/", "aws_imds"),
    ("http://169.254.170.2/v2/credentials/", "aws_ecs_v3"),
    ("http://169.254.170.23/v4/credentials/", "aws_ecs_v4"),
    ("http://169.254.0.23/", "tencent"),
    ("http://100.100.100.200/latest/meta-data/", "alibaba"),
    # ---- Loopback / private (default flags) ----
    ("http://127.0.0.1/", "ipv4_loopback"),
    ("http://[::1]/", "ipv6_loopback"),
    # ---- Forbidden chars (Layer 1) ----
    ("http://example.com/path with space", "whitespace"),
    ("http://example.com\t/", "tab"),
    ("http://example.com\n/", "newline"),
]


class TestRealWorldUrlsRegressionPrevention:
    """Lock in that legitimate URL patterns LDR fetches keep working."""

    @pytest.mark.parametrize("url,category", REAL_WORLD_URLS_THAT_MUST_PASS)
    def test_legitimate_url_passes(self, url, category):
        from local_deep_research.security.ssrf_validator import (
            validate_url,
        )

        with patch("socket.getaddrinfo", return_value=_PUBLIC_DNS_RESPONSE):
            assert validate_url(url) is True, (
                f"Legitimate {category} URL {url!r} unexpectedly "
                f"rejected. This breaks an LDR user flow."
            )


class TestSecuritySentinelsStayBlocked:
    """Lock in that the SSRF fix continues to block known attack
    payloads. If any of these starts passing, the hardening has
    silently regressed."""

    @pytest.mark.parametrize("url,category", REAL_WORLD_URLS_THAT_MUST_FAIL)
    def test_attack_url_blocked(self, url, category):
        from local_deep_research.security.ssrf_validator import (
            validate_url,
        )

        assert validate_url(url) is False, (
            f"{category} attack URL {url!r} unexpectedly passed. "
            f"SSRF hardening has regressed."
        )


class TestBehaviorChangeLockIn:
    """Lock in deliberate behaviour changes from PR #3873 / #3882 so a
    future revert doesn't silently undo them."""

    def test_validate_url_with_none_returns_false_not_raises(self):
        """PR #3873 changed ``validate_url(None)`` from raising
        ``TypeError`` to returning ``False``. Callers that depended on
        the exception would already be broken; lock in the new contract.
        """
        from local_deep_research.security.ssrf_validator import (
            validate_url,
        )

        assert validate_url(None) is False
        assert validate_url(123) is False
        assert validate_url([]) is False

    def test_validate_url_strips_surrounding_whitespace(self):
        """PR #3873 added ``url.strip()`` at the top so URLs pasted from
        clipboard with surrounding whitespace are accepted."""
        from local_deep_research.security.ssrf_validator import (
            validate_url,
        )

        with patch("socket.getaddrinfo", return_value=_PUBLIC_DNS_RESPONSE):
            assert validate_url("  https://example.com/  ") is True
            assert validate_url("\thttps://example.com/\n") is True

    def test_validate_url_internal_whitespace_still_rejected(self):
        """Strip handles SURROUNDING whitespace; INTERIOR whitespace is
        still an RFC 3986 violation and Layer 1 rejects it."""
        from local_deep_research.security.ssrf_validator import (
            validate_url,
        )

        assert validate_url("https://example.com/ /path") is False
        assert validate_url("https://example.com/\tpath") is False

    def test_redact_url_for_log_normalizes_to_origin(self):
        """Helper strips userinfo, path, query, AND fragment — leaving
        only ``scheme://host[:port]`` (the URL origin per RFC 6454)."""
        from local_deep_research.security.ssrf_validator import (
            redact_url_for_log,
        )

        assert (
            redact_url_for_log("http://user:pass@example.com/p?q=1#f")
            == "http://example.com"
        )

    def test_redact_url_for_log_preserves_port(self):
        from local_deep_research.security.ssrf_validator import (
            redact_url_for_log,
        )

        assert (
            redact_url_for_log("http://example.com:8080/path")
            == "http://example.com:8080"
        )

    def test_redact_url_for_log_handles_ipv6(self):
        from local_deep_research.security.ssrf_validator import (
            redact_url_for_log,
        )

        assert redact_url_for_log("http://[::1]:8080/") == "http://[::1]:8080"

    def test_idn_domain_auto_punycoded_via_urllib3(self):
        """urllib3 auto-Punycodes raw IDN before the ASCII guard. Lock
        in this behaviour — if a future urllib3 stops doing this, IDN
        URLs would silently break for users in Asia/Cyrillic regions."""
        from urllib3.util import parse_url

        u = parse_url("http://例え.jp/")
        assert u.host == "xn--r8jz45g.jp", (
            "urllib3 changed its Punycode behaviour. IDN URLs may now "
            "break in LDR's SSRF validation — file an issue."
        )


class TestPerformance:
    """Sanity check: validate_url must be cheap. ~10k calls in a
    research session shouldn't add meaningful latency."""

    def test_validate_url_under_5ms_per_call(self):
        """Generous 5ms-per-call budget absorbs noisy CI runners while
        still catching genuine regressions. Local measurement is ~63µs,
        so 5ms is ~80× headroom. A 100-URL research session at the
        threshold would add 500ms; a real regression that breached it
        would be worth investigating."""
        from local_deep_research.security.ssrf_validator import (
            validate_url,
        )

        url = "https://api.openalex.org/works?filter=doi:10.1038/nature"
        with patch("socket.getaddrinfo", return_value=_PUBLIC_DNS_RESPONSE):
            t0 = time.perf_counter()
            for _ in range(1000):
                validate_url(url)
            elapsed = time.perf_counter() - t0
        per_call_us = elapsed * 1000  # 1000 calls -> µs per call
        assert per_call_us < 5000, (
            f"validate_url is too slow: {per_call_us:.1f}µs per call "
            f"(target: <5000µs). 100-URL research session would add "
            f"{per_call_us / 10:.1f}ms latency."
        )
