"""Integration tests for app.api.v1.literature — feed, search, detail, save, dismiss, export"""

import uuid
import datetime

import pytest
import pytest_asyncio
from sqlalchemy import select

from app.models.literature import GlobalLiterature, GlobalTag, GlobalLiteratureTag
from app.models.interaction import UserFeed, UserLiterature

PMID_1 = 90000100
PMID_2 = 90000101


async def _get_lit_id(db, pmid):
    """Look up a literature ID by PMID."""
    r = await db.execute(select(GlobalLiterature.id).where(GlobalLiterature.pmid == pmid))
    return r.scalar()


@pytest_asyncio.fixture
async def seed_lit(db):
    """Seed two literature items with a tag. Deletes any existing data first."""
    from sqlalchemy import delete
    await db.execute(delete(GlobalLiterature).where(GlobalLiterature.pmid.in_([PMID_1, PMID_2])))
    lit_id_1 = uuid.uuid4()
    lit_id_2 = uuid.uuid4()
    db.add_all([
        GlobalLiterature(
            id=lit_id_1, pmid=PMID_1, title="Test Literature One",
            abstract="Abstract for literature one about cancer research",
            authors=[{"family": "Smith", "affiliation": "Univ A"}],
            journal="Journal of Oncology", doi="10.1000/jonc.001",
            pub_date=datetime.date(2024, 6, 1), pub_year=2024,
            keywords=["cancer", "oncology"], cited_by_count=10,
        ),
        GlobalLiterature(
            id=lit_id_2, pmid=PMID_2, title="Test Literature Two",
            abstract="Abstract for literature two about gene therapy",
            authors=[{"family": "Jones", "affiliation": "Univ B"}],
            journal="Gene Therapy Today", doi="10.1000/gt.002",
            pub_date=datetime.date(2024, 5, 15), pub_year=2024,
            keywords=["gene", "therapy"], cited_by_count=5,
        ),
    ])
    tag = GlobalTag(
        id=uuid.uuid4(), name_zh="癌症", name_en="cancer",
        path="diseases::cancer", tag_category="cancer", level=1,
    )
    db.add(tag)
    await db.flush()
    glt = GlobalLiteratureTag(literature_id=lit_id_1, tag_id=tag.id, is_major=True)
    db.add(glt)
    await db.commit()


@pytest_asyncio.fixture
async def seed_and_auth(client, db, seed_lit):
    """Register a user — returns (headers, user_id). Depends on seed_lit."""
    email = f"t{uuid.uuid4().hex[:6]}@test.cn"
    resp = await client.post("/api/v1/auth/register", json={
        "email": email, "password": "Test1234", "display_name": "TestDoctor"
    })
    assert resp.status_code == 200, f"Register failed: {resp.text}"
    data = resp.json()
    headers = {"Authorization": f"Bearer {data['token']['access_token']}"}
    user_id = uuid.UUID(data["user"]["id"])
    return headers, user_id


# ── Personal Feed ──


@pytest.mark.asyncio
async def test_personal_feed_empty(seed_and_auth, client):
    """Personal feed returns empty when no UserFeed items exist"""
    headers, _ = seed_and_auth
    r = await client.get("/api/v1/literature/feed", headers=headers)
    assert r.status_code == 200
    data = r.json()
    assert data["total"] == 0
    assert data["items"] == []


@pytest.mark.asyncio
async def test_personal_feed_with_items(seed_and_auth, client, db):
    """Personal feed returns feed items"""
    headers, uid = seed_and_auth
    lit_id = await _get_lit_id(db, PMID_1)
    db.add(UserFeed(user_id=uid, literature_id=lit_id, priority="must_read"))
    await db.commit()

    r = await client.get("/api/v1/literature/feed", headers=headers)
    assert r.status_code == 200
    data = r.json()
    assert data["total"] == 1
    assert data["items"][0]["pmid"] == PMID_1
    assert data["items"][0]["priority"] == "must_read"


@pytest.mark.asyncio
async def test_personal_feed_counts(seed_and_auth, client, db):
    """Personal feed returns correct must_read/recommended/related counts"""
    headers, uid = seed_and_auth
    lit_id_1 = await _get_lit_id(db, PMID_1)
    lit_id_2 = await _get_lit_id(db, PMID_2)
    db.add_all([
        UserFeed(user_id=uid, literature_id=lit_id_1, priority="must_read"),
        UserFeed(user_id=uid, literature_id=lit_id_2, priority="related"),
    ])
    await db.commit()

    r = await client.get("/api/v1/literature/feed", headers=headers)
    assert r.status_code == 200
    data = r.json()
    assert data["total"] == 2
    assert data["must_read"] == 1
    assert data["recommended"] == 0
    assert data["related"] == 1


@pytest.mark.asyncio
async def test_personal_feed_dismissed_excluded(seed_and_auth, client, db):
    """Dismissed feed items are excluded from feed"""
    headers, uid = seed_and_auth
    lit_id = await _get_lit_id(db, PMID_1)
    db.add(UserFeed(user_id=uid, literature_id=lit_id,
                    priority="must_read", is_dismissed=True))
    await db.commit()

    r = await client.get("/api/v1/literature/feed", headers=headers)
    assert r.status_code == 200
    data = r.json()
    assert data["total"] == 0


@pytest.mark.asyncio
async def test_personal_feed_pagination(seed_and_auth, client, db):
    """Personal feed respects pagination params"""
    headers, uid = seed_and_auth
    lit_id = await _get_lit_id(db, PMID_1)
    db.add(UserFeed(user_id=uid, literature_id=lit_id, priority="must_read"))
    await db.commit()

    r = await client.get("/api/v1/literature/feed?page=1&page_size=5", headers=headers)
    assert r.status_code == 200
    assert r.json()["total"] == 1

    r2 = await client.get("/api/v1/literature/feed?page=2&page_size=5", headers=headers)
    assert r2.status_code == 200
    assert r2.json()["items"] == []


# ── Search ──


@pytest.mark.asyncio
async def test_search_empty_query(auth_headers, client):
    """Search with empty query returns empty result"""
    r = await client.get("/api/v1/literature/search?q=", headers=auth_headers)
    assert r.status_code == 200
    data = r.json()
    assert data["items"] == []
    assert data["total"] == 0


@pytest.mark.asyncio
async def test_search_with_results(seed_lit, auth_headers, client):
    """Search returns matching literature"""
    r = await client.get("/api/v1/literature/search?q=cancer", headers=auth_headers)
    assert r.status_code == 200
    data = r.json()
    assert data["total"] >= 1


@pytest.mark.asyncio
async def test_search_no_results(auth_headers, client):
    """Search with unmatched query returns empty"""
    r = await client.get("/api/v1/literature/search?q=zzzzzzzzNOTHING", headers=auth_headers)
    assert r.status_code == 200
    data = r.json()
    assert data["items"] == []
    assert data["total"] == 0


@pytest.mark.asyncio
async def test_search_pagination(seed_lit, auth_headers, client):
    """Search respects pagination"""
    r = await client.get("/api/v1/literature/search?q=test&page_size=1", headers=auth_headers)
    assert r.status_code == 200
    data = r.json()
    assert data["total"] >= 2
    assert len(data["items"]) == 1


# ── Literature Detail ──


@pytest.mark.asyncio
async def test_literature_detail_not_found(seed_and_auth, client):
    """Detail returns 404 for non-existent PMID"""
    headers, _ = seed_and_auth
    r = await client.get("/api/v1/literature/99999999", headers=headers)
    assert r.status_code == 404


@pytest.mark.asyncio
async def test_literature_detail_found(seed_and_auth, client):
    """Detail returns literature with tags"""
    headers, _ = seed_and_auth
    r = await client.get(f"/api/v1/literature/{PMID_1}", headers=headers)
    assert r.status_code == 200
    data = r.json()
    assert data["pmid"] == PMID_1
    assert data["title"] == "Test Literature One"
    assert data["abstract"] is not None
    assert len(data["tags"]) >= 1
    assert any(t["name_en"] == "cancer" for t in data["tags"])


# ── Save Literature ──


@pytest.mark.asyncio
async def test_save_literature(seed_and_auth, client):
    """Save a literature item"""
    headers, _ = seed_and_auth
    r = await client.post(f"/api/v1/literature/{PMID_1}/save", headers=headers)
    assert r.status_code == 200
    assert r.json()["status"] == "saved"


@pytest.mark.asyncio
async def test_save_literature_already_saved(seed_and_auth, client, db):
    """Save again returns already_saved"""
    headers, uid = seed_and_auth
    lit_id = await _get_lit_id(db, PMID_1)
    db.add(UserLiterature(user_id=uid, literature_id=lit_id))
    await db.commit()

    r = await client.post(f"/api/v1/literature/{PMID_1}/save", headers=headers)
    assert r.status_code == 200
    assert r.json()["status"] == "already_saved"


@pytest.mark.asyncio
async def test_save_literature_not_found(seed_and_auth, client):
    """Save non-existent PMID returns 404"""
    headers, _ = seed_and_auth
    r = await client.post("/api/v1/literature/99999999/save", headers=headers)
    assert r.status_code == 404


# ── Dismiss Feed ──


@pytest.mark.asyncio
async def test_dismiss_feed(seed_and_auth, client, db):
    """Dismiss a feed item"""
    headers, uid = seed_and_auth
    lit_id = await _get_lit_id(db, PMID_1)
    db.add(UserFeed(user_id=uid, literature_id=lit_id, priority="must_read"))
    await db.commit()

    r = await client.post(f"/api/v1/literature/{PMID_1}/dismiss", headers=headers)
    assert r.status_code == 200
    assert r.json()["status"] == "dismissed"

    # Verify feed no longer visible
    r2 = await client.get("/api/v1/literature/feed", headers=headers)
    assert r2.json()["total"] == 0


@pytest.mark.asyncio
async def test_dismiss_feed_no_item(seed_and_auth, client):
    """Dismiss works even if no feed item exists"""
    headers, _ = seed_and_auth
    r = await client.post(f"/api/v1/literature/{PMID_1}/dismiss", headers=headers)
    assert r.status_code == 200
    assert r.json()["status"] == "dismissed"


@pytest.mark.asyncio
async def test_dismiss_feed_not_found(seed_and_auth, client):
    """Dismiss non-existent PMID returns 404"""
    headers, _ = seed_and_auth
    r = await client.post("/api/v1/literature/99999999/dismiss", headers=headers)
    assert r.status_code == 404


# ── Saved List ──


@pytest.mark.asyncio
async def test_saved_list_empty(seed_and_auth, client):
    """Saved list returns empty when nothing saved"""
    headers, _ = seed_and_auth
    r = await client.get("/api/v1/literature/saved", headers=headers)
    assert r.status_code == 200
    data = r.json()
    assert data["items"] == []
    assert data["total"] == 0


@pytest.mark.asyncio
async def test_saved_list_with_items(seed_and_auth, client, db):
    """Saved list returns saved literature"""
    headers, uid = seed_and_auth
    lit_id = await _get_lit_id(db, PMID_1)
    db.add(UserLiterature(user_id=uid, literature_id=lit_id))
    await db.commit()

    r = await client.get("/api/v1/literature/saved", headers=headers)
    assert r.status_code == 200
    data = r.json()
    assert data["total"] == 1
    assert data["items"][0]["pmid"] == PMID_1


# ── Bulk Save ──


@pytest.mark.asyncio
async def test_bulk_save(seed_and_auth, client):
    """Bulk save multiple PMIDs"""
    headers, _ = seed_and_auth
    r = await client.post(
        "/api/v1/literature/bulk/save",
        json={"pmids": [PMID_1, PMID_2]},
        headers=headers,
    )
    assert r.status_code == 200
    data = r.json()
    assert data["status"] == "saved"
    assert data["count"] == 2


@pytest.mark.asyncio
async def test_bulk_save_duplicates(seed_and_auth, client, db):
    """Bulk save skips already saved items"""
    headers, uid = seed_and_auth
    lit_id = await _get_lit_id(db, PMID_1)
    db.add(UserLiterature(user_id=uid, literature_id=lit_id))
    await db.commit()

    r = await client.post(
        "/api/v1/literature/bulk/save",
        json={"pmids": [PMID_1, PMID_2]},
        headers=headers,
    )
    assert r.status_code == 200
    assert r.json()["count"] == 1


@pytest.mark.asyncio
async def test_bulk_save_non_existent_pmid(seed_and_auth, client):
    """Bulk save skips PMIDs that don't exist"""
    headers, _ = seed_and_auth
    r = await client.post(
        "/api/v1/literature/bulk/save",
        json={"pmids": [99999999, PMID_1]},
        headers=headers,
    )
    assert r.status_code == 200
    assert r.json()["count"] == 1


# ── Export ──


@pytest.mark.asyncio
async def test_export_single_bibtex(seed_lit, auth_headers, client):
    """Export single article in BibTeX format"""
    r = await client.get(f"/api/v1/literature/export/{PMID_1}/bibtex", headers=auth_headers)
    assert r.status_code == 200
    assert r.headers["content-type"] == "text/plain; charset=utf-8"
    assert "bib" in r.headers.get("content-disposition", "")
    assert "@article" in r.text


@pytest.mark.asyncio
async def test_export_single_ris(seed_lit, auth_headers, client):
    """Export single article in RIS format"""
    r = await client.get(f"/api/v1/literature/export/{PMID_1}/ris", headers=auth_headers)
    assert r.status_code == 200
    assert "ris" in r.headers.get("content-disposition", "")


@pytest.mark.asyncio
async def test_export_single_endnote(seed_lit, auth_headers, client):
    """Export single article in EndNote format"""
    r = await client.get(f"/api/v1/literature/export/{PMID_1}/endnote", headers=auth_headers)
    assert r.status_code == 200
    assert "enw" in r.headers.get("content-disposition", "")


@pytest.mark.asyncio
async def test_export_single_csv(seed_lit, auth_headers, client):
    """Export single article in CSV format"""
    r = await client.get(f"/api/v1/literature/export/{PMID_1}/csv", headers=auth_headers)
    assert r.status_code == 200
    assert "csv" in r.headers.get("content-disposition", "")
    assert "Test Literature One" in r.text


@pytest.mark.asyncio
async def test_export_single_unsupported_format(seed_lit, auth_headers, client):
    """Export with unsupported format returns 400"""
    r = await client.get(f"/api/v1/literature/export/{PMID_1}/xml", headers=auth_headers)
    assert r.status_code == 400


@pytest.mark.asyncio
async def test_export_single_not_found(auth_headers, client):
    """Export non-existent PMID returns 404"""
    r = await client.get("/api/v1/literature/export/99999999/bibtex", headers=auth_headers)
    assert r.status_code == 404


@pytest.mark.asyncio
async def test_export_batch(seed_lit, auth_headers, client):
    """Batch export multiple PMIDs"""
    r = await client.post(
        "/api/v1/literature/export/batch/bibtex",
        params={"pmids": [PMID_1, PMID_2]},
        headers=auth_headers,
    )
    assert r.status_code == 200


@pytest.mark.asyncio
async def test_export_batch_unsupported_format(seed_lit, auth_headers, client):
    """Batch export with unsupported format returns 400"""
    r = await client.post(
        "/api/v1/literature/export/batch/xml",
        params={"pmids": [PMID_1]},
        headers=auth_headers,
    )
    assert r.status_code == 400


@pytest.mark.asyncio
async def test_export_batch_no_pmids(client, auth_headers):
    """Batch export with no PMIDs returns 400"""
    r = await client.post(
        "/api/v1/literature/export/batch/bibtex",
        params={"pmids": []},
        headers=auth_headers,
    )
    assert r.status_code == 400
    assert "No PMIDs" in r.json().get("error", {}).get("message", "")


# ─── Related Literature ───


@pytest.mark.asyncio
async def test_related_literature_found(seed_and_auth, client):
    """Related returns articles sharing tags with the source."""
    headers, _ = seed_and_auth
    r = await client.get(f"/api/v1/literature/{PMID_1}/related", headers=headers)
    assert r.status_code == 200
    data = r.json()
    assert "items" in data
    assert "source" in data
    assert isinstance(data["items"], list)


@pytest.mark.asyncio
async def test_related_literature_not_found(seed_and_auth, client):
    """Related returns 404 for non-existent PMID."""
    headers, _ = seed_and_auth
    r = await client.get("/api/v1/literature/99999999/related", headers=headers)
    assert r.status_code == 404


@pytest.mark.asyncio
async def test_related_literature_with_shared_tag(seed_and_auth, client, db):
    """Related returns articles that share tags, sorted by overlap."""
    from app.models.literature import GlobalLiterature, GlobalTag, GlobalLiteratureTag

    headers, uid = seed_and_auth

    # Get PMID_2's literature record
    lit2 = (await db.execute(
        select(GlobalLiterature).where(GlobalLiterature.pmid == PMID_2)
    )).scalar()
    assert lit2 is not None

    # Get PMID_1's tag link (the tag created by this seed_lit run)
    src_tag = (await db.execute(
        select(GlobalLiteratureTag)
        .join(GlobalLiterature, GlobalLiteratureTag.literature_id == GlobalLiterature.id)
        .where(GlobalLiterature.pmid == PMID_1)
        .limit(1)
    )).scalar()
    assert src_tag is not None, "PMID_1 has no tag in seed_lit"

    # Add the same tag to PMID_2
    from sqlalchemy import delete as sql_delete
    await db.execute(
        sql_delete(GlobalLiteratureTag).where(GlobalLiteratureTag.literature_id == lit2.id)
    )
    db.add(GlobalLiteratureTag(literature_id=lit2.id, tag_id=src_tag.tag_id, is_major=False))
    await db.commit()

    # Verify via HTTP that the tag is visible
    detail_r = await client.get(f"/api/v1/literature/{PMID_2}", headers=headers)
    assert detail_r.status_code == 200
    lit2_tags = detail_r.json().get("tags", [])
    assert len(lit2_tags) >= 1, f"PMID_2 should have tags, got {lit2_tags}"

    r = await client.get(f"/api/v1/literature/{PMID_1}/related", headers=headers)
    assert r.status_code == 200
    data = r.json()
    assert len(data["items"]) >= 1, f"Expected related items, got {data}"
    pmids = {i["pmid"] for i in data["items"]}
    assert PMID_2 in pmids, f"PMID_2 should be in related, got pmids={pmids}"
    assert data["source"] == "tags"


@pytest.mark.asyncio
async def test_related_literature_no_auth(client):
    """Related without auth returns 401."""
    r = await client.get(f"/api/v1/literature/{PMID_1}/related")
    assert r.status_code == 401
