import asyncio

import httpx
import pytest

from src import gitea_proxy, main


@pytest.mark.anyio
async def test_review_detail_endpoint_returns_normalized_no_store_payload(monkeypatch):
    async def requested(repository, number):
        return (repository, number) == ("stackchain/api", 7)

    async def detail(repository, number):
        assert (repository, number) == ("stackchain/api", 7)
        return {
            "repository": repository,
            "number": number,
            "title": "Review API",
            "body": "Check retries",
            "url": "https://forge.example/stackchain/api/pulls/7",
            "author": "alex",
            "ci_state": "success",
            "files": [{"filename": "src/api.py", "status": "modified", "additions": 8, "deletions": 2}],
            "reviews": [{"user": {"login": "sam"}, "state": "APPROVED", "body": "Good"}],
        }

    monkeypatch.setattr(main, "is_requested_review", requested)
    monkeypatch.setattr(main, "pull_review_detail", detail)
    transport = httpx.ASGITransport(app=main.app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.get("/api/v1/repos/stackchain/api/pulls/7/review")

    assert response.status_code == 200
    assert response.headers["cache-control"] == "no-store"
    assert response.json()["files"][0]["filename"] == "src/api.py"


@pytest.mark.anyio
async def test_requested_review_checks_endpoint_authorizes_and_returns_status_only(monkeypatch):
    calls = []

    async def requested(repository, number):
        calls.append(("requested", repository, number))
        return True

    async def checks(repository, number):
        calls.append(("checks", repository, number))
        return {"head_sha": "def456", "ci_state": "success", "checks": []}

    monkeypatch.setattr(main, "is_requested_review", requested)
    monkeypatch.setattr(main.gitea_proxy, "pull_check_status", checks)
    transport = httpx.ASGITransport(app=main.app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.get("/api/v1/repos/stackchain/api/pulls/7/review/checks")

    assert response.status_code == 200
    assert response.headers["cache-control"] == "no-store"
    assert response.json() == {"head_sha": "def456", "ci_state": "success", "checks": []}
    assert calls == [
        ("requested", "stackchain/api", 7),
        ("checks", "stackchain/api", 7),
    ]


@pytest.mark.anyio
async def test_gitea_review_detail_returns_bounded_actionable_checks(monkeypatch):
    monkeypatch.setattr(gitea_proxy, "GITEA_URL", "https://forge.example/git")

    async def handler(request):
        path = request.url.path
        if path.endswith("/pulls/7"):
            return httpx.Response(200, json={
                "title": "Review API", "head": {"sha": "abc123"},
                "user": {"login": "alex"},
            })
        if path.endswith("/pulls/7/files") or path.endswith("/pulls/7/reviews"):
            return httpx.Response(200, json=[])
        if path.endswith("/commits/abc123/status"):
            return httpx.Response(200, json={
                "state": "failure",
                "statuses": [
                    {
                        "context": "lint/<unsafe>", "status": "failure",
                        "description": "Formatting failed " + "x" * 300,
                        "target_url": "https://forge.example/git/stackchain/api/actions/runs/9/jobs/4",
                    },
                    {
                        "context": "external", "status": "pending",
                        "description": "Waiting", "target_url": "https://evil.example/job/1",
                    },
                    {"context": "build-release", "status": "success", "description": "Passed"},
                ] + [{"context": f"extra-{index}", "status": "success"} for index in range(30)],
            })
        if path.endswith("/pulls/7.diff"):
            return httpx.Response(200, text="")
        raise AssertionError(f"unexpected request: {request.method} {path}")

    gitea_proxy.start_client(transport=httpx.MockTransport(handler))
    try:
        detail = await gitea_proxy.pull_review_detail("stackchain/api", 7)
    finally:
        await gitea_proxy.stop_client()

    assert len(detail["checks"]) == 20
    assert detail["checks"][:3] == [
        {
            "name": "lint/<unsafe>", "state": "failure",
            "description": "Formatting failed " + "x" * 222,
            "url": "https://forge.example/git/stackchain/api/actions/runs/9/jobs/4",
        },
        {"name": "external", "state": "pending", "description": "Waiting", "url": ""},
        {"name": "build-release", "state": "success", "description": "Passed", "url": ""},
    ]


@pytest.mark.anyio
async def test_review_detail_rejects_pulls_not_requested_from_service_user(monkeypatch):
    async def requested(repository, number):
        return False

    monkeypatch.setattr(main, "is_requested_review", requested)
    transport = httpx.ASGITransport(app=main.app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.get("/api/v1/repos/private/secret/pulls/9/review")

    assert response.status_code == 404
    assert response.headers["cache-control"] == "no-store"


@pytest.mark.anyio
async def test_review_detail_has_one_retryable_deadline_and_cancels_pending_work(monkeypatch):
    cancelled = asyncio.Event()

    async def requested(repository, number):
        try:
            await asyncio.Event().wait()
        finally:
            cancelled.set()

    monkeypatch.setattr(main, "is_requested_review", requested)
    monkeypatch.setattr(main, "REVIEW_DETAIL_TIMEOUT_SECONDS", 0.01)
    transport = httpx.ASGITransport(app=main.app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.get("/api/v1/repos/stackchain/api/pulls/7/review")

    assert response.status_code == 503
    assert response.headers["cache-control"] == "no-store"
    assert response.headers["retry-after"] == "1"
    assert response.json() == {
        "error": "Pull request review details timed out. Please retry."
    }
    assert cancelled.is_set()


@pytest.mark.anyio
async def test_review_submission_posts_validated_decision_for_requested_current_head(monkeypatch):
    calls = []

    async def requested(repository, number):
        return (repository, number) == ("stackchain/api", 7)

    async def submit(repository, number, expected_head_sha, decision, body):
        calls.append((repository, number, expected_head_sha, decision, body))
        return {"id": 91, "state": "APPROVED", "url": "https://forge.example/reviews/91"}

    monkeypatch.setattr(main, "is_requested_review", requested)
    monkeypatch.setattr(main.gitea_proxy, "submit_pull_review", submit, raising=False)
    transport = httpx.ASGITransport(app=main.app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.post(
            "/api/v1/repos/stackchain/api/pulls/7/review",
            json={
                "decision": "approve",
                "body": "Looks good on mobile.",
                "expected_head_sha": "abc123",
            },
        )

    assert response.status_code == 201
    assert response.headers["cache-control"] == "no-store"
    assert response.json() == {
        "id": 91,
        "state": "APPROVED",
        "url": "https://forge.example/reviews/91",
    }
    assert calls == [
        ("stackchain/api", 7, "abc123", "approve", "Looks good on mobile.")
    ]


@pytest.mark.anyio
async def test_review_submission_forwards_valid_inline_comments(monkeypatch):
    calls = []

    async def requested(repository, number):
        return True

    async def submit(repository, number, expected_head_sha, decision, body, comments):
        calls.append((repository, number, expected_head_sha, decision, body, comments))
        return {"id": 92, "state": "REQUEST_CHANGES", "url": "https://forge.example/reviews/92"}

    monkeypatch.setattr(main, "is_requested_review", requested)
    monkeypatch.setattr(main.gitea_proxy, "submit_pull_review", submit)
    transport = httpx.ASGITransport(app=main.app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.post(
            "/api/v1/repos/stackchain/api/pulls/7/review",
            json={
                "decision": "request_changes",
                "body": "One inline blocker.",
                "expected_head_sha": "abc123",
                "comments": [{
                    "path": "src/api.py", "body": "Handle the empty value.",
                    "new_position": 42,
                }],
            },
        )

    assert response.status_code == 201
    assert calls == [(
        "stackchain/api", 7, "abc123", "request_changes", "One inline blocker.",
        [{"path": "src/api.py", "body": "Handle the empty value.", "new_position": 42, "old_position": None}],
    )]


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

    async def handler(request):
        requests.append(request)
        if request.method == "GET":
            return httpx.Response(200, json={"head": {"sha": "abc123"}})
        return httpx.Response(
            200,
            json={
                "id": 91,
                "state": "APPROVED",
                "html_url": "http://127.0.0.1:3000/reviews/91",
            },
        )

    gitea_proxy.start_client(transport=httpx.MockTransport(handler))
    try:
        result = await gitea_proxy.submit_pull_review(
            "stackchain/api", 7, "abc123", "approve", "Looks good."
        )
    finally:
        await gitea_proxy.stop_client()

    assert [(request.method, request.url.path) for request in requests] == [
        ("GET", "/api/v1/repos/stackchain/api/pulls/7"),
        ("POST", "/api/v1/repos/stackchain/api/pulls/7/reviews"),
    ]
    assert requests[1].content == b'{"body":"Looks good.","event":"APPROVE","commit_id":"abc123"}'
    assert result == {
        "id": 91,
        "state": "APPROVED",
        "url": "http://127.0.0.1:3000/reviews/91",
    }


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

    async def handler(request):
        requests.append(request)
        if request.url.path.endswith("/files"):
            return httpx.Response(200, json=[{"filename": "src/api.py"}])
        if request.method == "GET":
            return httpx.Response(200, json={"head": {"sha": "abc123"}})
        return httpx.Response(200, json={"id": 93, "state": "COMMENT"})

    gitea_proxy.start_client(transport=httpx.MockTransport(handler))
    try:
        await gitea_proxy.submit_pull_review(
            "stackchain/api", 7, "abc123", "comment", "Summary",
            [{"path": "src/api.py", "body": "Handle empty values.", "new_position": 42, "old_position": None}],
        )
    finally:
        await gitea_proxy.stop_client()

    assert [(request.method, request.url.path) for request in requests] == [
        ("GET", "/api/v1/repos/stackchain/api/pulls/7"),
        ("GET", "/api/v1/repos/stackchain/api/pulls/7/files"),
        ("POST", "/api/v1/repos/stackchain/api/pulls/7/reviews"),
    ]
    assert requests[-1].content == (
        b'{"body":"Summary","event":"COMMENT","commit_id":"abc123","comments":'
        b'[{"path":"src/api.py","body":"Handle empty values.","new_position":42,"old_position":null}]}'
    )


@pytest.mark.anyio
async def test_gitea_review_submission_rejects_changed_head_without_posting():
    methods = []

    async def handler(request):
        methods.append(request.method)
        return httpx.Response(200, json={"head": {"sha": "new456"}})

    gitea_proxy.start_client(transport=httpx.MockTransport(handler))
    try:
        with pytest.raises(gitea_proxy.StaleReviewError):
            await gitea_proxy.submit_pull_review(
                "stackchain/api", 7, "abc123", "request_changes", "Please revise."
            )
    finally:
        await gitea_proxy.stop_client()

    assert methods == ["GET"]


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

    async def handler(request):
        requests.append((request.method, request.url.path))
        if request.url.path.endswith("/files"):
            return httpx.Response(200, json=[{"filename": "src/api.py"}])
        return httpx.Response(200, json={"head": {"sha": "abc123"}})

    gitea_proxy.start_client(transport=httpx.MockTransport(handler))
    try:
        with pytest.raises(gitea_proxy.InvalidReviewCommentError):
            await gitea_proxy.submit_pull_review(
                "stackchain/api", 7, "abc123", "comment", "Review note.",
                [{"path": "src/unknown.py", "body": "Not in this change", "new_position": 4}],
            )
    finally:
        await gitea_proxy.stop_client()

    assert requests == [
        ("GET", "/api/v1/repos/stackchain/api/pulls/7"),
        ("GET", "/api/v1/repos/stackchain/api/pulls/7/files"),
    ]


@pytest.mark.anyio
async def test_review_submission_returns_conflict_when_pull_head_changed(monkeypatch):
    async def requested(repository, number):
        return True

    async def submit(*args):
        raise gitea_proxy.StaleReviewError("changed")

    monkeypatch.setattr(main, "is_requested_review", requested)
    monkeypatch.setattr(main.gitea_proxy, "submit_pull_review", submit)
    transport = httpx.ASGITransport(app=main.app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.post(
            "/api/v1/repos/stackchain/api/pulls/7/review",
            json={
                "decision": "request_changes",
                "body": "Please revise.",
                "expected_head_sha": "abc123",
            },
        )

    assert response.status_code == 409
    assert response.json() == {
        "error": "New commits were pushed. Refresh the review before submitting."
    }


@pytest.mark.anyio
async def test_review_submission_returns_validation_error_when_inline_path_is_stale(monkeypatch):
    async def requested(repository, number):
        return True

    async def submit(*args):
        raise gitea_proxy.InvalidReviewCommentError("unknown path")

    monkeypatch.setattr(main, "is_requested_review", requested)
    monkeypatch.setattr(main.gitea_proxy, "submit_pull_review", submit)
    transport = httpx.ASGITransport(app=main.app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.post(
            "/api/v1/repos/stackchain/api/pulls/7/review",
            json={
                "decision": "comment", "body": "Note", "expected_head_sha": "abc123",
                "comments": [{"path": "gone.py", "body": "Stale", "new_position": 1}],
            },
        )

    assert response.status_code == 422
    assert response.json() == {
        "error": "An inline comment no longer matches this pull request. Refresh the review."
    }


@pytest.mark.anyio
async def test_review_submission_rejects_unsupported_decision_before_upstream(monkeypatch):
    called = False

    async def requested(repository, number):
        nonlocal called
        called = True
        return True

    monkeypatch.setattr(main, "is_requested_review", requested)
    transport = httpx.ASGITransport(app=main.app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.post(
            "/api/v1/repos/stackchain/api/pulls/7/review",
            json={
                "decision": "merge",
                "body": "Ship it.",
                "expected_head_sha": "abc123",
            },
        )

    assert response.status_code == 422
    assert called is False


@pytest.mark.anyio
async def test_review_submission_rejects_oversized_feedback_before_upstream(monkeypatch):
    called = False

    async def requested(repository, number):
        nonlocal called
        called = True
        return True

    monkeypatch.setattr(main, "is_requested_review", requested)
    transport = httpx.ASGITransport(app=main.app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.post(
            "/api/v1/repos/stackchain/api/pulls/7/review",
            json={
                "decision": "comment",
                "body": "x" * 10_001,
                "expected_head_sha": "abc123",
            },
        )

    assert response.status_code == 422
    assert called is False


@pytest.mark.anyio
async def test_review_submission_deadline_cancels_request_check(monkeypatch):
    cancelled = asyncio.Event()

    async def requested(repository, number):
        try:
            await asyncio.Event().wait()
        finally:
            cancelled.set()

    monkeypatch.setattr(main, "is_requested_review", requested)
    monkeypatch.setattr(main, "REVIEW_DETAIL_TIMEOUT_SECONDS", 0.01)
    transport = httpx.ASGITransport(app=main.app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        response = await asyncio.wait_for(
            client.post(
                "/api/v1/repos/stackchain/api/pulls/7/review",
                json={
                    "decision": "comment",
                    "body": "Review note.",
                    "expected_head_sha": "abc123",
                },
            ),
            timeout=0.2,
        )

    assert response.status_code == 503
    assert response.headers["retry-after"] == "1"
    assert cancelled.is_set()

