import httpx
import pytest

from src import gitea_proxy


@pytest.mark.anyio
async def test_notification_page_preserves_upstream_total_without_loading_other_pages():
    requests = []

    def upstream(request):
        requests.append(str(request.url))
        return httpx.Response(
            200,
            headers={"X-Total-Count": "125"},
            json=[{
                "id": 42,
                "unread": True,
                "repository": {"full_name": "stackchain/api"},
                "subject": {
                    "title": "Retry failed deploy",
                    "type": "Issue",
                    "html_url": "http://127.0.0.1:3000/stackchain/api/issues/7",
                },
            }],
        )

    gitea_proxy.start_client(transport=httpx.MockTransport(upstream))
    try:
        result = await gitea_proxy.notification_page(page=2)
    finally:
        await gitea_proxy.stop_client()

    assert requests == [
        "http://127.0.0.1:3000/api/v1/notifications?status-types=unread&limit=50&page=2"
    ]
    assert result["page"] == 2
    assert result["total"] == 125
    assert result["has_more"] is True
    assert [item["id"] for item in result["items"]] == [42]


@pytest.mark.anyio
async def test_unread_notification_snapshot_loads_every_page_with_bounded_concurrency():
    active = 0
    peak = 0
    requested_pages = []

    async def upstream(request):
        nonlocal active, peak
        page = int(request.url.params["page"])
        requested_pages.append(page)
        active += 1
        peak = max(peak, active)
        await __import__("asyncio").sleep(0)
        active -= 1
        start = (page - 1) * 50 + 1
        end = min(start + 50, 126)
        return httpx.Response(
            200,
            headers={"X-Total-Count": "125"},
            json=[{
                "id": thread_id,
                "updated_at": f"2026-08-11T12:{thread_id % 60:02d}:00Z",
                "subject": {},
                "repository": {},
            } for thread_id in range(start, end)],
        )

    gitea_proxy.start_client(transport=httpx.MockTransport(upstream))
    try:
        result = await gitea_proxy.unread_notification_snapshot(
            max_pages=3, max_concurrency=2, deadline_seconds=1
        )
    finally:
        await gitea_proxy.stop_client()

    assert requested_pages[0] == 1
    assert sorted(requested_pages) == [1, 2, 3]
    assert peak == 2
    assert result["complete"] is True
    assert result["total"] == 125
    assert [item["id"] for item in result["items"]] == list(range(1, 126))


@pytest.mark.anyio
async def test_unread_notification_snapshot_rejects_pagination_that_changes_mid_scan():
    def upstream(request):
        page = int(request.url.params["page"])
        total = 51 if page == 1 else 50
        start = (page - 1) * 50 + 1
        return httpx.Response(
            200,
            headers={"X-Total-Count": str(total)},
            json=[{
                "id": thread_id,
                "subject": {},
                "repository": {},
            } for thread_id in range(start, min(start + 50, total + 1))],
        )

    gitea_proxy.start_client(transport=httpx.MockTransport(upstream))
    try:
        with pytest.raises(ValueError, match="changed during the scan"):
            await gitea_proxy.unread_notification_snapshot(deadline_seconds=1)
    finally:
        await gitea_proxy.stop_client()


@pytest.mark.anyio
async def test_unread_notification_snapshot_rejects_missing_or_duplicate_threads():
    def upstream(_request):
        return httpx.Response(
            200,
            headers={"X-Total-Count": "3"},
            json=[
                {"id": 7, "subject": {}, "repository": {}},
                {"id": 7, "subject": {}, "repository": {}},
                "malformed",
            ],
        )

    gitea_proxy.start_client(transport=httpx.MockTransport(upstream))
    try:
        with pytest.raises(ValueError, match="incomplete thread set"):
            await gitea_proxy.unread_notification_snapshot(deadline_seconds=1)
    finally:
        await gitea_proxy.stop_client()


@pytest.mark.anyio
async def test_unread_notification_snapshot_fails_before_exceeding_the_scan_limit():
    requested_pages = []

    def upstream(request):
        page = int(request.url.params["page"])
        requested_pages.append(page)
        start = (page - 1) * 50 + 1
        return httpx.Response(
            200,
            headers={"X-Total-Count": "101"},
            json=[
                {"id": thread_id, "subject": {}, "repository": {}}
                for thread_id in range(start, min(start + 50, 102))
            ],
        )

    gitea_proxy.start_client(transport=httpx.MockTransport(upstream))
    try:
        with pytest.raises(ValueError, match="scan limit"):
            await gitea_proxy.unread_notification_snapshot(
                max_pages=2, deadline_seconds=1
            )
    finally:
        await gitea_proxy.stop_client()

    assert requested_pages == [1]


@pytest.mark.anyio
async def test_unread_notifications_are_normalized_for_mobile_handoff():
    result = gitea_proxy._normalize_notifications(
        [
            {
                "id": 42,
                "unread": True,
                "updated_at": "2026-08-06T12:30:00Z",
                "repository": {"full_name": "stackchain/api"},
                "subject": {
                    "title": "Retry failed deploy",
                    "type": "Issue",
                    "state": "open",
                    "html_url": "http://127.0.0.1:3000/stackchain/api/issues/7",
                    "latest_comment_html_url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9",
                },
            },
            {"id": 43, "repository": None, "subject": None},
            {
                "id": 44,
                "repository": {"full_name": "stackchain/web"},
                "subject": {"title": "Unsafe", "html_url": "javascript:alert(1)"},
            },
            "malformed",
        ]
    )
    assert result == [
        {
            "id": 42,
            "unread": True,
            "updated_at": "2026-08-06T12:30:00Z",
            "repository": "stackchain/api",
            "number": 7,
            "title": "Retry failed deploy",
            "subject_type": "Issue",
            "state": "open",
            "url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9",
            "subject_url": "http://127.0.0.1:3000/stackchain/api/issues/7",
        },
        {
            "id": 43,
            "unread": False,
            "updated_at": "",
            "repository": "",
            "number": None,
            "title": "Untitled update",
            "subject_type": "Update",
            "state": "",
            "url": "",
            "subject_url": "",
        },
        {
            "id": 44,
            "unread": False,
            "updated_at": "",
            "repository": "stackchain/web",
            "number": None,
            "title": "Unsafe",
            "subject_type": "Update",
            "state": "",
            "url": "",
            "subject_url": "",
        },
    ]


@pytest.mark.anyio
async def test_notification_collection_rejects_non_list_payload():
    with pytest.raises(ValueError, match="notification response was not a list"):
        gitea_proxy._normalize_notifications({"message": "unexpected"})


@pytest.mark.anyio
async def test_notification_detail_loads_subject_and_latest_comment_for_inbox_reader():
    requests = []

    def upstream(request):
        requests.append(str(request.url))
        if request.url.path.endswith("/notifications/threads/42"):
            return httpx.Response(200, json={
                "id": 42,
                "repository": {"full_name": "stackchain/api"},
                "subject": {
                    "title": "Retry failed deploy",
                    "type": "Issue",
                    "state": "open",
                    "url": "http://127.0.0.1:3000/api/v1/repos/stackchain/api/issues/7",
                    "latest_comment_url": "http://127.0.0.1:3000/api/v1/repos/stackchain/api/issues/comments/9",
                    "html_url": "http://127.0.0.1:3000/stackchain/api/issues/7",
                    "latest_comment_html_url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9",
                },
            })
        if request.url.path.endswith("/issues/7"):
            return httpx.Response(200, json={
                "number": 7, "state": "open", "assignees": [],
                "body": "Deploy fails after **three** retries.",
            })
        if request.url.path.endswith("/issues/7/subscriptions/check"):
            return httpx.Response(200, json={"subscribed": True, "ignored": False})
        if request.url.path.endswith("/issues/comments/9"):
            return httpx.Response(200, json={
                "id": 9,
                "body": "Logs point to the worker timeout.",
                "created_at": "2026-08-06T12:30:00Z",
                "user": {"login": "alexander"},
                "html_url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9",
            })
        if request.url.path.endswith("/issues/7/comments"):
            return httpx.Response(200, json=[{
                "id": 9,
                "body": "Logs point to the worker timeout.",
                "created_at": "2026-08-06T12:30:00Z",
                "user": {"login": "alexander"},
                "html_url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9",
            }], headers={"X-Total-Count": "1"})
        return httpx.Response(404)

    gitea_proxy.start_client(transport=httpx.MockTransport(upstream))
    try:
        result = await gitea_proxy.notification_detail(42)
    finally:
        await gitea_proxy.stop_client()

    assert requests == [
        "http://127.0.0.1:3000/api/v1/notifications/threads/42",
        "http://127.0.0.1:3000/api/v1/repos/stackchain/api/issues/7",
        "http://127.0.0.1:3000/api/v1/repos/stackchain/api/issues/comments/9",
        "http://127.0.0.1:3000/api/v1/repos/stackchain/api/issues/7/subscriptions/check",
    ]
    assert result == {
        "id": 42,
        "repository": "stackchain/api",
        "title": "Retry failed deploy",
        "subject_type": "Issue",
        "state": "open",
        "url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9",
        "subject_body": "Deploy fails after **three** retries.",
        "latest_comment": {
            "author": "alexander",
            "body": "Logs point to the worker timeout.",
            "created_at": "2026-08-06T12:30:00Z",
            "url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9",
        },
        "issue": {"number": 7, "assignees": [], "claimable": True},
        "acknowledge_supported": True,
        "mute_supported": True,
        "conversation_available": True,
    }


@pytest.mark.anyio
async def test_notification_detail_never_follows_foreign_api_urls():
    requests = []

    def upstream(request):
        requests.append(str(request.url))
        return httpx.Response(200, json={
            "id": 42,
            "repository": {"full_name": "stackchain/api"},
            "subject": {
                "title": "Suspicious update",
                "url": "https://evil.example/api/v1/user",
                "latest_comment_url": "https://evil.example/api/v1/admin/users",
                "html_url": "javascript:alert(1)",
            },
        })

    gitea_proxy.start_client(transport=httpx.MockTransport(upstream))
    try:
        result = await gitea_proxy.notification_detail(42)
    finally:
        await gitea_proxy.stop_client()

    assert requests == [
        "http://127.0.0.1:3000/api/v1/notifications/threads/42"
    ]
    assert result["url"] == ""
    assert result["subject_body"] == ""
    assert result["latest_comment"]["body"] == ""


@pytest.mark.anyio
@pytest.mark.parametrize("subject_kind", ["issues", "pulls"])
async def test_reply_to_notification_posts_to_its_issue_conversation(subject_kind):
    requests = []

    def upstream(request):
        requests.append((request.method, str(request.url), request.content))
        if request.method == "GET":
            return httpx.Response(200, json={
                "id": 42,
                "repository": {"full_name": "stackchain/api"},
                "subject": {
                    "type": "Pull" if subject_kind == "pulls" else "Issue",
                    "url": (
                        "http://127.0.0.1:3000/api/v1/repos/stackchain/api/"
                        f"{subject_kind}/7"
                    ),
                },
            })
        return httpx.Response(201, json={
            "id": 91,
            "user": {"login": "timmy"},
            "body": "Please retry the worker.",
            "created_at": "2026-08-07T19:00:00Z",
            "html_url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-91",
        })

    gitea_proxy.start_client(transport=httpx.MockTransport(upstream))
    try:
        result = await gitea_proxy.reply_to_notification(42, "Please retry the worker.")
    finally:
        await gitea_proxy.stop_client()

    assert requests == [
        (
            "GET",
            "http://127.0.0.1:3000/api/v1/notifications/threads/42",
            b"",
        ),
        (
            "POST",
            "http://127.0.0.1:3000/api/v1/repos/stackchain/api/issues/7/comments",
            b'{"body":"Please retry the worker."}',
        ),
    ]
    assert result == {
        "id": 91,
        "author": "timmy",
        "body": "Please retry the worker.",
        "created_at": "2026-08-07T19:00:00Z",
        "url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-91",
    }


@pytest.mark.anyio
async def test_mute_notification_unsubscribes_current_user_from_trusted_thread_target():
    requests = []

    def upstream(request):
        requests.append((request.method, request.url.path))
        if request.url.path.endswith("/notifications/threads/42"):
            return httpx.Response(200, json={
                "repository": {"full_name": "stackchain/api"},
                "subject": {
                    "type": "Pull",
                    "url": "http://127.0.0.1:3000/api/v1/repos/stackchain/api/pulls/7",
                },
            })
        if request.url.path.endswith("/user"):
            return httpx.Response(200, json={"login": "timmy"})
        if request.method == "DELETE":
            return httpx.Response(204)
        return httpx.Response(404)

    gitea_proxy.start_client(transport=httpx.MockTransport(upstream))
    try:
        result = await gitea_proxy.mute_notification(42)
    finally:
        await gitea_proxy.stop_client()

    assert result == {"id": 42, "repository": "stackchain/api", "number": 7, "muted": True}
    assert requests == [
        ("GET", "/api/v1/notifications/threads/42"),
        ("GET", "/api/v1/user"),
        ("DELETE", "/api/v1/repos/stackchain/api/issues/7/subscriptions/timmy"),
    ]
