import pytest
from fastapi import HTTPException

from agent.dashboard import review_api
from agent.dashboard.review_api import (
    _ALLOWED_IMAGE_CONTENT_TYPES,
    _finding_counts,
    _is_allowed_image_url,
    _require_image_in_pr,
    _serialize_diff_groups,
    _serialize_finding,
    _thread_review_summary,
    classify_finding,
    get_pr_head_sha,
    reviewer_thread_id,
)
from agent.webapp import generate_reviewer_thread_id


def test_classify_finding():
    assert classify_finding({"severity": "critical", "confidence": "high"}) == "bug"
    assert classify_finding({"severity": "high", "confidence": "high"}) == "bug"
    assert classify_finding({"severity": "high", "confidence": "medium"}) == "investigate"
    assert classify_finding({"severity": "medium", "confidence": "high"}) == "investigate"
    assert classify_finding({"severity": "low", "confidence": "high"}) == "informational"


def test_finding_counts_only_open_in_groups():
    findings = [
        {"id": "f_1", "severity": "high", "confidence": "high", "status": "open"},
        {"id": "f_2", "severity": "medium", "confidence": "high", "status": "open"},
        {"id": "f_3", "severity": "high", "confidence": "high", "status": "resolved"},
        {"id": "f_4", "severity": "low", "confidence": "low", "status": "dismissed"},
    ]
    counts = _finding_counts(findings)
    assert counts == {"open": 2, "resolved": 1, "dismissed": 1, "bugs": 1, "flags": 1}


def test_serialize_finding_outdated():
    finding = {"id": "f_1", "last_confirmed_sha": "aaa"}
    assert _serialize_finding(finding, "bbb")["outdated"] is True
    assert _serialize_finding(finding, "aaa")["outdated"] is False
    assert _serialize_finding(finding, None)["outdated"] is False
    assert _serialize_finding({"id": "f_2"}, "bbb")["outdated"] is False


def test_thread_review_summary():
    thread = {
        "thread_id": "t1",
        "status": "idle",
        "updated_at": "2026-06-10T00:00:00Z",
        "metadata": {
            "kind": "reviewer",
            "pr": {
                "owner": "acme",
                "name": "repo",
                "number": 7,
                "title": "Fix things",
                "head_ref": "fix",
                "base_ref": "main",
            },
            "head_sha": "abc",
            "watch": True,
            "latest_run_status": "success",
            "findings": [{"id": "f_1", "severity": "high", "confidence": "high", "status": "open"}],
        },
    }
    summary = _thread_review_summary(thread)
    assert summary is not None
    assert summary["owner"] == "acme"
    assert summary["number"] == 7
    assert summary["status"] == "idle"
    assert summary["watch"] is True
    assert summary["counts"]["bugs"] == 1


def test_thread_review_summary_requires_pr_meta():
    assert _thread_review_summary({"metadata": {"kind": "reviewer"}}) is None


def test_is_allowed_image_url_accepts_github_hosts():
    assert _is_allowed_image_url("https://github.com/user-attachments/assets/abc-123")
    assert _is_allowed_image_url("https://private-user-images.githubusercontent.com/1/x.png?jwt=y")
    assert _is_allowed_image_url("https://user-images.githubusercontent.com/1/x.png")


def test_is_allowed_image_url_rejects_unsafe_urls():
    # Non-https scheme.
    assert not _is_allowed_image_url("http://github.com/user-attachments/assets/x")
    # github.com but not a user-attachment path.
    assert not _is_allowed_image_url("https://github.com/langchain-ai/open-swe")
    # Arbitrary external host (SSRF guard).
    assert not _is_allowed_image_url("https://evil.example.com/x.png")
    # Lookalike host that merely contains the suffix substring.
    assert not _is_allowed_image_url("https://githubusercontent.com.evil.com/x.png")
    # Internal address.
    assert not _is_allowed_image_url("https://169.254.169.254/latest/meta-data")


def test_image_content_type_allowlist_excludes_svg():
    # SVG can execute script in our origin, so it must never be served.
    assert "image/svg+xml" not in _ALLOWED_IMAGE_CONTENT_TYPES
    assert "image/png" in _ALLOWED_IMAGE_CONTENT_TYPES


@pytest.mark.asyncio
async def test_get_pr_head_sha_returns_head(monkeypatch):
    async def fake_token():
        return "tok"

    async def fake_get(path, token, **kwargs):
        assert path == "/repos/acme/repo/pulls/7"
        return {"head": {"sha": "abc123"}}

    monkeypatch.setattr(review_api, "_require_app_token", fake_token)
    monkeypatch.setattr(review_api, "_github_get", fake_get)
    assert await get_pr_head_sha("acme", "repo", 7) == "abc123"


@pytest.mark.asyncio
async def test_get_pr_head_sha_empty_on_failure(monkeypatch):
    async def fake_token():
        return "tok"

    async def fake_get(path, token, **kwargs):
        raise HTTPException(404, "not found")

    monkeypatch.setattr(review_api, "_require_app_token", fake_token)
    monkeypatch.setattr(review_api, "_github_get", fake_get)
    assert await get_pr_head_sha("acme", "repo", 7) == ""


async def test_require_image_in_pr_rejects_unreferenced_url(monkeypatch):
    async def fake_github_get(path, token, **kwargs):
        return {"body": "see ![diagram](https://x.githubusercontent.com/a.png)"}

    monkeypatch.setattr("agent.dashboard.review_api._github_get", fake_github_get)

    # A URL not present in the PR body (cross-repo IDOR attempt) is rejected.
    with pytest.raises(HTTPException) as exc:
        await _require_image_in_pr(
            "acme", "repo", 7, "https://x.githubusercontent.com/other-repo.png", "tok"
        )
    assert exc.value.status_code == 403

    # A URL actually embedded in the PR body is allowed.
    await _require_image_in_pr("acme", "repo", 7, "https://x.githubusercontent.com/a.png", "tok")


def test_reviewer_thread_id_matches_webapp():
    assert reviewer_thread_id("acme", "repo", 7) == generate_reviewer_thread_id("acme", "repo", 7)


def test_serialize_diff_groups_assigns_index_and_drops_invalid():
    metadata = {
        "diff_groups": {
            "head_sha": "abc",
            "groups": [
                {"title": "Feature", "summary": "Adds it", "files": ["a.py", "b.py"]},
                {"title": "   ", "summary": "x", "files": ["c.py"]},
                {"title": "Empty", "summary": "", "files": []},
                {"title": "Tests", "summary": "Covers it", "files": ["t.py", 5]},
            ],
        }
    }
    groups, stale = _serialize_diff_groups(metadata, "abc")
    assert stale is False
    assert groups == [
        {"index": 1, "title": "Feature", "summary": "Adds it", "files": ["a.py", "b.py"]},
        {"index": 2, "title": "Tests", "summary": "Covers it", "files": ["t.py"]},
    ]


def test_serialize_diff_groups_marks_stale_on_head_mismatch():
    metadata = {
        "diff_groups": {
            "head_sha": "old",
            "groups": [{"title": "T", "summary": "", "files": ["a.py"]}],
        }
    }
    groups, stale = _serialize_diff_groups(metadata, "new")
    assert stale is True
    assert groups[0]["index"] == 1


def test_serialize_diff_groups_handles_missing():
    assert _serialize_diff_groups({}, "abc") == ([], False)
    assert _serialize_diff_groups({"diff_groups": {"groups": "nope"}}, "abc") == ([], False)
