from unittest.mock import MagicMock, patch

import pytest

from backend.app.services.patreon_resolver import (
    resolve_campaign_id,
    resolve_campaign_id_for_source,
    resolve_display_name,
)


def test_resolve_display_name_from_campaign():
    fake = MagicMock()
    fake.status_code = 200
    fake.json.return_value = {"data": [
        {"id": "1", "type": "campaign", "attributes": {"name": "Maewix Studios"}},
    ]}
    with patch("backend.app.services.patreon_resolver.requests.get", return_value=fake):
        assert resolve_display_name("maewix", None) == "Maewix Studios"


def test_resolve_display_name_none_on_empty_or_error():
    empty = MagicMock()
    empty.status_code = 200
    empty.json.return_value = {"data": []}
    with patch("backend.app.services.patreon_resolver.requests.get", return_value=empty):
        assert resolve_display_name("maewix", None) is None
    import requests as _rq
    with patch(
        "backend.app.services.patreon_resolver.requests.get",
        side_effect=_rq.ConnectionError("x"),
    ):
        assert resolve_display_name("maewix", None) is None


@pytest.mark.asyncio
async def test_resolves_on_happy_path():
    fake_response = MagicMock()
    fake_response.status_code = 200
    fake_response.json.return_value = {"data": [{"id": "12345678", "type": "campaign"}]}
    with patch("backend.app.services.patreon_resolver.requests.get", return_value=fake_response):
        result = await resolve_campaign_id("alice", cookies_path=None)
    assert result == "12345678"


@pytest.mark.asyncio
async def test_returns_none_on_401():
    fake_response = MagicMock()
    fake_response.status_code = 401
    fake_response.json.return_value = {}
    with patch("backend.app.services.patreon_resolver.requests.get", return_value=fake_response):
        result = await resolve_campaign_id("alice", cookies_path=None)
    assert result is None


@pytest.mark.asyncio
async def test_returns_none_on_network_error():
    import requests
    with patch(
        "backend.app.services.patreon_resolver.requests.get",
        side_effect=requests.ConnectionError("nope"),
    ):
        result = await resolve_campaign_id("alice", cookies_path=None)
    assert result is None


@pytest.mark.asyncio
async def test_returns_none_on_empty_data():
    fake_response = MagicMock()
    fake_response.status_code = 200
    fake_response.json.return_value = {"data": []}
    with patch("backend.app.services.patreon_resolver.requests.get", return_value=fake_response):
        result = await resolve_campaign_id("alice", cookies_path=None)
    assert result is None


@pytest.mark.asyncio
async def test_returns_none_on_malformed_json():
    fake_response = MagicMock()
    fake_response.status_code = 200
    fake_response.json.side_effect = ValueError("not json")
    with patch("backend.app.services.patreon_resolver.requests.get", return_value=fake_response):
        result = await resolve_campaign_id("alice", cookies_path=None)
    assert result is None


@pytest.mark.asyncio
async def test_missing_cookies_file_does_not_raise(tmp_path):
    fake_response = MagicMock()
    fake_response.status_code = 200
    fake_response.json.return_value = {"data": [{"id": "1", "type": "campaign"}]}
    with patch("backend.app.services.patreon_resolver.requests.get", return_value=fake_response):
        result = await resolve_campaign_id(
            "alice", cookies_path=str(tmp_path / "does-not-exist.txt")
        )
    assert result == "1"


@pytest.mark.asyncio
async def test_loads_cookies_file_when_present(tmp_path):
    cookies = tmp_path / "cookies.txt"
    cookies.write_text(
        "# Netscape HTTP Cookie File\n"
        ".patreon.com\tTRUE\t/\tTRUE\t1700000000\tsession_id\tabc\n"
    )
    fake_response = MagicMock()
    fake_response.status_code = 200
    fake_response.json.return_value = {"data": [{"id": "9", "type": "campaign"}]}
    with patch(
        "backend.app.services.patreon_resolver.requests.get",
        return_value=fake_response,
    ) as mock_get:
        result = await resolve_campaign_id("alice", cookies_path=str(cookies))
    assert result == "9"
    _, kwargs = mock_get.call_args
    assert kwargs.get("cookies") is not None


@pytest.mark.asyncio
async def test_falls_back_to_creator_page_when_api_empty():
    """When the campaigns API returns empty data (its known failure mode), the
    resolver scrapes the campaign id out of the creator page (gallery-dl's
    method)."""
    api_resp = MagicMock()
    api_resp.status_code = 200
    api_resp.json.return_value = {"data": []}
    page_resp = MagicMock()
    page_resp.status_code = 200
    page_resp.text = (
        '<html><script>window.patreon={"bootstrap":{"campaign":'
        '{"data":{"id":"7654321","type":"campaign"}}}}</script></html>'
    )

    def _get(url, **kwargs):
        return api_resp if "/api/campaigns" in url else page_resp

    with patch(
        "backend.app.services.patreon_resolver.requests.get", side_effect=_get
    ):
        result = await resolve_campaign_id("alice", cookies_path=None)
    assert result == "7654321"


def test_scrape_campaign_id_patterns():
    from backend.app.services.patreon_resolver import _scrape_campaign_id

    assert _scrape_campaign_id('foo /api/campaigns/424242 bar') == "424242"
    assert _scrape_campaign_id('"campaign_id":"99"') == "99"
    assert _scrape_campaign_id('{"id":"5","type":"campaign"}') == "5"
    assert _scrape_campaign_id("no id here") is None
    assert _scrape_campaign_id(None) is None


# -- resolve_campaign_id_for_source (shared by download + verify) ----------


@pytest.mark.asyncio
async def test_for_source_uses_cached_override_without_lookup():
    with patch("backend.app.services.patreon_resolver.requests.get") as mock_get:
        cid, resolved = await resolve_campaign_id_for_source(
            "https://patreon.com/alice", None, {"patreon_campaign_id": "999"},
        )
    assert cid == "999"
    assert resolved is None  # cached → no newly-resolved id to cache
    mock_get.assert_not_called()


@pytest.mark.asyncio
async def test_for_source_extracts_id_url_without_lookup():
    with patch("backend.app.services.patreon_resolver.requests.get") as mock_get:
        cid, resolved = await resolve_campaign_id_for_source(
            "https://www.patreon.com/id:4242", None, {},
        )
    assert cid == "4242"
    assert resolved is None
    mock_get.assert_not_called()


@pytest.mark.asyncio
async def test_for_source_resolves_vanity_and_reports_it():
    fake = MagicMock()
    fake.status_code = 200
    fake.json.return_value = {"data": [{"id": "777", "type": "campaign"}]}
    with patch("backend.app.services.patreon_resolver.requests.get", return_value=fake):
        cid, resolved = await resolve_campaign_id_for_source(
            "https://patreon.com/alice", None, {},
        )
    assert cid == "777"
    assert resolved == "777"  # newly resolved → caller caches it


@pytest.mark.asyncio
async def test_for_source_unresolvable_returns_none():
    cid, resolved = await resolve_campaign_id_for_source("not-a-patreon-url", None, {})
    assert cid is None
    assert resolved is None


def test_extract_vanity_handles_all_path_prefixes():
    """Patreon serves the vanity bare and under c/ and cw/ prefixes; all must
    yield the creator slug, not the prefix (operator-flagged 2026-06-07: a
    /cw/Atole source resolved vanity='cw')."""
    from backend.app.services.patreon_resolver import extract_vanity

    assert extract_vanity("https://www.patreon.com/Atole") == "Atole"
    assert extract_vanity("https://www.patreon.com/c/Atole") == "Atole"
    assert extract_vanity("https://www.patreon.com/cw/Atole") == "Atole"
    assert extract_vanity("https://patreon.com/cw/Atole") == "Atole"
    # id: URLs are NOT vanities (handled by the id path).
    assert extract_vanity("https://www.patreon.com/id:123") is None
    # /posts/ permalinks are NOT vanities (handled by the post path) — must not
    # masquerade as a creator slug named "posts".
    assert extract_vanity(
        "https://www.patreon.com/posts/mimic-in-dungeon-158372536"
    ) is None


@pytest.mark.asyncio
async def test_for_source_resolves_post_url_to_campaign():
    """A /posts/<slug>-<id> permalink resolves to the OWNING creator's campaign
    (a subscription walks the whole feed, not one post)."""
    post_resp = MagicMock()
    post_resp.status_code = 200
    post_resp.json.return_value = {
        "data": {
            "id": "158372536", "type": "post",
            "relationships": {"campaign": {"data": {"id": "9988", "type": "campaign"}}},
        }
    }
    calls = []

    def _get(url, **kwargs):
        calls.append(url)
        return post_resp

    with patch("backend.app.services.patreon_resolver.requests.get", side_effect=_get):
        cid, resolved = await resolve_campaign_id_for_source(
            "https://www.patreon.com/posts/mimic-in-dungeon-158372536", None, {},
        )
    assert cid == "9988"
    assert resolved == "9988"  # newly resolved → caller caches it
    assert calls and "/api/posts/158372536" in calls[0]
