"""Tests for fetch.py — HTTP transport layer with fallback chain."""

import os
import tempfile
from unittest.mock import patch, MagicMock, PropertyMock
import requests as real_requests
from scripts.fetch import fetch_page, extract_pdf_text, try_wayback


# ---------------------------------------------------------------------------
# fetch_page: snapshot fallback
# ---------------------------------------------------------------------------

def test_fetch_page_snapshot_fallback_when_no_requests():
    """When requests is None and snapshot is provided, use snapshot."""
    with patch("proof_citations.fetch.requests", None):
        text, mode, err = fetch_page(
            "https://example.com",
            snapshot="<html>snapshot content</html>",
        )
    assert text == "<html>snapshot content</html>"
    assert mode == "snapshot"
    assert err is None


def test_fetch_page_snapshot_fallback_after_live_failure():
    """When live fetch raises an error, fall back to snapshot."""
    mock_requests = MagicMock()
    mock_requests.get.side_effect = real_requests.exceptions.ConnectionError("refused")
    mock_requests.exceptions = real_requests.exceptions

    with patch("proof_citations.fetch.requests", mock_requests):
        text, mode, err = fetch_page(
            "https://example.com",
            snapshot="<html>snapshot</html>",
        )
    assert text == "<html>snapshot</html>"
    assert mode == "snapshot"
    assert err is None


# ---------------------------------------------------------------------------
# fetch_page: no requests, no snapshot
# ---------------------------------------------------------------------------

def test_fetch_page_no_requests_no_snapshot():
    """When requests is None and no snapshot, return fetch_failed."""
    with patch("proof_citations.fetch.requests", None):
        text, mode, err = fetch_page("https://example.com")
    assert text is None
    assert mode == "fetch_failed"
    assert "requests" in err.lower()


# ---------------------------------------------------------------------------
# fetch_page: live success
# ---------------------------------------------------------------------------

def test_fetch_page_live_html_success():
    """Successful live HTML fetch returns page text."""
    mock_resp = MagicMock()
    mock_resp.text = "<html>live content</html>"
    mock_resp.headers = {"Content-Type": "text/html"}
    mock_resp.raise_for_status = MagicMock()

    mock_requests = MagicMock()
    mock_requests.get.return_value = mock_resp
    mock_requests.exceptions = real_requests.exceptions

    with patch("proof_citations.fetch.requests", mock_requests):
        text, mode, err = fetch_page("https://example.com")
    assert text == "<html>live content</html>"
    assert mode == "live"
    assert err is None


# ---------------------------------------------------------------------------
# fetch_page: HTTP error codes
# ---------------------------------------------------------------------------

def test_fetch_page_http_403_falls_to_snapshot():
    """HTTP 403 on live fetch should fall back to snapshot."""
    mock_resp = MagicMock()
    mock_resp.status_code = 403
    mock_resp.raise_for_status.side_effect = real_requests.exceptions.HTTPError(
        response=mock_resp
    )

    mock_requests = MagicMock()
    mock_requests.get.return_value = mock_resp
    mock_requests.exceptions = real_requests.exceptions

    with patch("proof_citations.fetch.requests", mock_requests):
        text, mode, err = fetch_page(
            "https://example.com",
            snapshot="<html>snapshot</html>",
        )
    assert text == "<html>snapshot</html>"
    assert mode == "snapshot"


def test_fetch_page_http_404_no_fallback():
    """HTTP 404 with no snapshot or wayback returns fetch_failed."""
    mock_resp = MagicMock()
    mock_resp.status_code = 404
    mock_resp.raise_for_status.side_effect = real_requests.exceptions.HTTPError(
        response=mock_resp
    )

    mock_requests = MagicMock()
    mock_requests.get.return_value = mock_resp
    mock_requests.exceptions = real_requests.exceptions

    with patch("proof_citations.fetch.requests", mock_requests):
        text, mode, err = fetch_page("https://example.com")
    assert text is None
    assert mode == "fetch_failed"
    assert "404" in err


# ---------------------------------------------------------------------------
# fetch_page: timeout
# ---------------------------------------------------------------------------

def test_fetch_page_timeout_falls_to_snapshot():
    """Timeout on live fetch should fall back to snapshot."""
    mock_requests = MagicMock()
    mock_requests.get.side_effect = real_requests.exceptions.Timeout("timed out")
    mock_requests.exceptions = real_requests.exceptions

    with patch("proof_citations.fetch.requests", mock_requests):
        text, mode, err = fetch_page(
            "https://example.com",
            snapshot="<html>snapshot</html>",
        )
    assert text == "<html>snapshot</html>"
    assert mode == "snapshot"


# ---------------------------------------------------------------------------
# fetch_page: Wayback fallback
# ---------------------------------------------------------------------------

def test_fetch_page_wayback_fallback_success():
    """When live and snapshot both fail, Wayback returns content."""
    mock_requests = MagicMock()
    mock_requests.get.side_effect = [
        # First call: live fetch fails
        real_requests.exceptions.ConnectionError("refused"),
        # Second call: Wayback succeeds
        MagicMock(text="<html>wayback</html>", raise_for_status=MagicMock()),
    ]
    mock_requests.exceptions = real_requests.exceptions

    with patch("proof_citations.fetch.requests", mock_requests):
        text, mode, err = fetch_page(
            "https://example.com",
            wayback_fallback=True,
        )
    assert text == "<html>wayback</html>"
    assert mode == "wayback"
    assert err is None


def test_fetch_page_wayback_not_tried_without_flag():
    """Wayback should not be tried when wayback_fallback=False."""
    mock_requests = MagicMock()
    mock_requests.get.side_effect = real_requests.exceptions.ConnectionError("refused")
    mock_requests.exceptions = real_requests.exceptions

    with patch("proof_citations.fetch.requests", mock_requests):
        text, mode, err = fetch_page("https://example.com", wayback_fallback=False)
    assert text is None
    assert mode == "fetch_failed"
    # Only one call (live), no Wayback attempt
    assert mock_requests.get.call_count == 1


# ---------------------------------------------------------------------------
# fetch_page: PDF detection
# ---------------------------------------------------------------------------

def test_fetch_page_pdf_by_content_type():
    """PDF content type should trigger PDF extraction."""
    mock_resp = MagicMock()
    mock_resp.headers = {"Content-Type": "application/pdf"}
    mock_resp.content = b"fake pdf content"
    mock_resp.raise_for_status = MagicMock()

    mock_requests = MagicMock()
    mock_requests.get.return_value = mock_resp
    mock_requests.exceptions = real_requests.exceptions

    with patch("proof_citations.fetch.requests", mock_requests), \
         patch("proof_citations.fetch.extract_pdf_text", return_value="extracted text"):
        text, mode, err = fetch_page("https://example.com/paper")
    assert text == "extracted text"
    assert mode == "live"


def test_fetch_page_pdf_extraction_fails():
    """PDF detected but extraction fails should fall to snapshot."""
    mock_resp = MagicMock()
    mock_resp.headers = {"Content-Type": "application/pdf"}
    mock_resp.content = b"bad pdf"
    mock_resp.raise_for_status = MagicMock()

    mock_requests = MagicMock()
    mock_requests.get.return_value = mock_resp
    mock_requests.exceptions = real_requests.exceptions

    with patch("proof_citations.fetch.requests", mock_requests), \
         patch("proof_citations.fetch.extract_pdf_text", return_value=None):
        text, mode, err = fetch_page(
            "https://example.com/paper.pdf",
            snapshot="<html>snapshot</html>",
        )
    assert text == "<html>snapshot</html>"
    assert mode == "snapshot"


# ---------------------------------------------------------------------------
# extract_pdf_text
# ---------------------------------------------------------------------------

def test_extract_pdf_text_no_libraries():
    """When no PDF library is available, returns None."""
    with patch.dict("sys.modules", {"pdfplumber": None, "PyPDF2": None}):
        result = extract_pdf_text(b"fake pdf content")
    assert result is None


# ---------------------------------------------------------------------------
# try_github_raw
# ---------------------------------------------------------------------------

def test_try_github_raw_bare_repo_url():
    """Bare github.com/owner/repo URL fetches raw README."""
    mock_resp = MagicMock()
    mock_resp.text = "# My Project\nSome README content"
    mock_resp.raise_for_status = MagicMock()

    mock_requests = MagicMock()
    mock_requests.get.return_value = mock_resp
    mock_requests.exceptions = real_requests.exceptions

    with patch("proof_citations.fetch.requests", mock_requests):
        from scripts.fetch import try_github_raw
        result = try_github_raw("https://github.com/owner/repo")
    assert result == "# My Project\nSome README content"
    call_url = mock_requests.get.call_args[0][0]
    assert call_url == "https://raw.githubusercontent.com/owner/repo/HEAD/README.md"


def test_try_github_raw_trailing_slash():
    """Trailing slash on repo URL still rewrites correctly."""
    mock_resp = MagicMock()
    mock_resp.text = "readme content"
    mock_resp.raise_for_status = MagicMock()

    mock_requests = MagicMock()
    mock_requests.get.return_value = mock_resp
    mock_requests.exceptions = real_requests.exceptions

    with patch("proof_citations.fetch.requests", mock_requests):
        from scripts.fetch import try_github_raw
        result = try_github_raw("https://github.com/owner/repo/")
    assert result == "readme content"


def test_try_github_raw_file_path_returns_none():
    """URL with file path should not be rewritten."""
    from scripts.fetch import try_github_raw
    result = try_github_raw("https://github.com/owner/repo/blob/main/file.py")
    assert result is None


def test_try_github_raw_non_github_returns_none():
    """Non-GitHub URL returns None."""
    from scripts.fetch import try_github_raw
    result = try_github_raw("https://example.com/some/page")
    assert result is None


def test_try_github_raw_falls_back_to_rst():
    """When README.md 404s, should try README.rst and succeed."""
    def mock_get(url, **kwargs):
        resp = MagicMock()
        if url.endswith("README.md"):
            raise real_requests.exceptions.HTTPError("404")
        resp.text = "RST readme content"
        resp.raise_for_status = MagicMock()
        return resp

    mock_requests = MagicMock()
    mock_requests.get.side_effect = mock_get
    mock_requests.exceptions = real_requests.exceptions

    with patch("proof_citations.fetch.requests", mock_requests):
        from scripts.fetch import try_github_raw
        result = try_github_raw("https://github.com/owner/repo")
    assert result == "RST readme content"
    # Second call should be README.rst
    call_url = mock_requests.get.call_args_list[1][0][0]
    assert "README.rst" in call_url


def test_try_github_raw_all_404_returns_none():
    """404 on all README variants returns None."""
    mock_requests = MagicMock()
    mock_requests.get.side_effect = real_requests.exceptions.HTTPError("404")
    mock_requests.exceptions = real_requests.exceptions

    with patch("proof_citations.fetch.requests", mock_requests):
        from scripts.fetch import try_github_raw
        result = try_github_raw("https://github.com/owner/repo")
    assert result is None


def test_try_github_raw_no_requests_returns_none():
    """When requests is None, returns None."""
    with patch("proof_citations.fetch.requests", None):
        from scripts.fetch import try_github_raw
        result = try_github_raw("https://github.com/owner/repo")
    assert result is None


def test_fetch_page_github_falls_back_to_raw_on_js_shell():
    """GitHub repo URL with JS-shell response should fall back to raw README."""
    js_shell = "<html><div id='react-root'></div><script src='app.js'></script></html>"
    readme_text = "# My Project\nReal README content"

    call_count = [0]
    def mock_get(url, **kwargs):
        call_count[0] += 1
        resp = MagicMock()
        resp.headers = {"Content-Type": "text/html"}
        if "raw.githubusercontent.com" in url:
            resp.text = readme_text
        else:
            resp.text = js_shell
        resp.raise_for_status = MagicMock()
        return resp

    mock_requests = MagicMock()
    mock_requests.get.side_effect = mock_get
    mock_requests.exceptions = real_requests.exceptions

    with patch("proof_citations.fetch.requests", mock_requests):
        text, mode, err = fetch_page("https://github.com/owner/repo")
    assert text == readme_text
    assert mode == "github_raw"
    assert err is None


def test_fetch_page_github_uses_live_when_content_present():
    """GitHub repo URL with real content should use live fetch, not raw fallback."""
    real_page = "<html><body>" + "x" * 600 + "</body></html>"

    mock_resp = MagicMock()
    mock_resp.text = real_page
    mock_resp.headers = {"Content-Type": "text/html"}
    mock_resp.raise_for_status = MagicMock()

    mock_requests = MagicMock()
    mock_requests.get.return_value = mock_resp
    mock_requests.exceptions = real_requests.exceptions

    with patch("proof_citations.fetch.requests", mock_requests):
        text, mode, err = fetch_page("https://github.com/owner/repo")
    assert mode == "live"  # Did NOT fall back to github_raw


def test_fetch_page_non_github_skips_raw():
    """Non-GitHub URLs should not attempt GitHub raw fetch."""
    mock_resp = MagicMock()
    mock_resp.text = "<html>normal page</html>"
    mock_resp.headers = {"Content-Type": "text/html"}
    mock_resp.raise_for_status = MagicMock()

    mock_requests = MagicMock()
    mock_requests.get.return_value = mock_resp
    mock_requests.exceptions = real_requests.exceptions

    with patch("proof_citations.fetch.requests", mock_requests):
        text, mode, err = fetch_page("https://example.com")
    assert mode == "live"


# ---------------------------------------------------------------------------
# fetch_page: snapshot_file fallback
# ---------------------------------------------------------------------------

def test_fetch_page_snapshot_file_fallback_when_live_fails():
    """When live fetch fails and snapshot_file exists, use file contents."""
    with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
        f.write("snapshot file content for verification")
        f.flush()
        tmppath = f.name
    try:
        mock_requests = MagicMock()
        mock_requests.get.side_effect = real_requests.exceptions.ConnectionError("refused")
        mock_requests.exceptions = real_requests.exceptions

        with patch("proof_citations.fetch.requests", mock_requests):
            text, mode, err = fetch_page(
                "https://example.com",
                snapshot_file=tmppath,
            )
        assert text == "snapshot file content for verification"
        assert mode == "snapshot"
        assert err is None
    finally:
        os.unlink(tmppath)


def test_fetch_page_snapshot_file_missing_continues_to_wayback():
    """When snapshot_file path doesn't exist, skip it and try Wayback."""
    mock_requests = MagicMock()
    mock_requests.get.side_effect = [
        # Live fetch fails
        real_requests.exceptions.ConnectionError("refused"),
        # Wayback succeeds
        MagicMock(text="<html>wayback</html>", raise_for_status=MagicMock()),
    ]
    mock_requests.exceptions = real_requests.exceptions

    with patch("proof_citations.fetch.requests", mock_requests):
        text, mode, err = fetch_page(
            "https://example.com",
            snapshot_file="/nonexistent/path/snapshot.txt",
            wayback_fallback=True,
        )
    assert text == "<html>wayback</html>"
    assert mode == "wayback"


def test_fetch_page_snapshot_file_missing_no_wayback_returns_fetch_failed():
    """Missing snapshot_file with no Wayback returns fetch_failed with specific message."""
    mock_requests = MagicMock()
    mock_requests.get.side_effect = real_requests.exceptions.ConnectionError("refused")
    mock_requests.exceptions = real_requests.exceptions

    with patch("proof_citations.fetch.requests", mock_requests):
        text, mode, err = fetch_page(
            "https://example.com",
            snapshot_file="/nonexistent/snapshots/B2_snapshot.txt",
        )
    assert text is None
    assert mode == "fetch_failed"
    assert "snapshot_file" in err
    assert "not found" in err.lower()


def test_fetch_page_inline_snapshot_takes_precedence_over_snapshot_file():
    """Inline snapshot takes precedence over snapshot_file."""
    with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
        f.write("file content")
        f.flush()
        tmppath = f.name
    try:
        mock_requests = MagicMock()
        mock_requests.get.side_effect = real_requests.exceptions.ConnectionError("refused")
        mock_requests.exceptions = real_requests.exceptions

        with patch("proof_citations.fetch.requests", mock_requests):
            text, mode, err = fetch_page(
                "https://example.com",
                snapshot="inline content",
                snapshot_file=tmppath,
            )
        assert text == "inline content"
        assert mode == "snapshot"
        assert err is None
    finally:
        os.unlink(tmppath)
