"""
T039 — upload with tags → tags persisted and returned
T040 — duplicate upload → existing record returned, tags unchanged
T057 — PATCH replaces tags, old tags unlinked, new tags upserted
T058 — PATCH with invalid tag → 422 invalid_tag
T073 — GET /api/v1/tags returns all tags alphabetically with correct image_count
T074 — GET /api/v1/tags?q=ca returns only tags prefixed "ca"
T001 — GET /api/v1/tags?sort=count_desc returns tags ordered highest-count-first
T002 — GET /api/v1/tags?min_count=N excludes tags with image_count < N
"""
import io

import pytest


def _minimal_png() -> bytes:
    import struct
    import zlib
    def chunk(name: bytes, data: bytes) -> bytes:
        c = name + data
        return struct.pack(">I", len(data)) + c + struct.pack(">I", zlib.crc32(c) & 0xFFFFFFFF)

    ihdr = struct.pack(">IIBBBBB", 1, 1, 8, 2, 0, 0, 0)
    idat_data = zlib.compress(b"\x00\xFF\xFF\xFF")
    return (
        b"\x89PNG\r\n\x1a\n"
        + chunk(b"IHDR", ihdr)
        + chunk(b"IDAT", idat_data)
        + chunk(b"IEND", b"")
    )


@pytest.mark.asyncio
async def test_upload_with_tags_persists_tags(authed_client):
    client, token = authed_client
    data = _minimal_png()
    response = await client.post(
        "/api/v1/images",
        files={"file": ("img.png", io.BytesIO(data), "image/png")},
        data={"tags": "cat,funny"},
        headers={"Authorization": f"Bearer {token}"},
    )
    assert response.status_code == 201
    body = response.json()
    assert set(body["tags"]) == {"cat", "funny"}


@pytest.mark.asyncio
async def test_duplicate_upload_tags_unchanged(authed_client):
    client, token = authed_client
    headers = {"Authorization": f"Bearer {token}"}
    data = _minimal_png()
    r1 = await client.post(
        "/api/v1/images",
        files={"file": ("img.png", io.BytesIO(data), "image/png")},
        data={"tags": "original-tag"},
        headers=headers,
    )
    assert r1.status_code in (200, 201)
    original_tags = set(r1.json()["tags"])

    r2 = await client.post(
        "/api/v1/images",
        files={"file": ("img.png", io.BytesIO(data), "image/png")},
        data={"tags": "different-tag"},
        headers=headers,
    )
    assert r2.status_code == 200
    assert r2.json()["duplicate"] is True
    assert set(r2.json()["tags"]) == original_tags


@pytest.mark.asyncio
async def test_patch_replaces_tag_set(authed_client):
    client, token = authed_client
    headers = {"Authorization": f"Bearer {token}"}
    data = _minimal_png()
    r1 = await client.post(
        "/api/v1/images",
        files={"file": ("patch-test.png", io.BytesIO(data), "image/png")},
        data={"tags": "old-tag"},
        headers=headers,
    )
    image_id = r1.json()["short_id"]

    patch = await client.patch(
        f"/api/v1/i/{image_id}/tags",
        json={"tags": ["new-tag", "another"]},
        headers=headers,
    )
    assert patch.status_code == 200
    body = patch.json()
    assert "old-tag" not in body["tags"]
    assert set(body["tags"]) == {"new-tag", "another"}


@pytest.mark.asyncio
async def test_patch_invalid_tag_returns_422(authed_client):
    client, token = authed_client
    headers = {"Authorization": f"Bearer {token}"}
    data = _minimal_png()
    r1 = await client.post(
        "/api/v1/images",
        files={"file": ("invalid-tag-test.png", io.BytesIO(data), "image/png")},
        headers=headers,
    )
    image_id = r1.json()["short_id"]

    patch = await client.patch(
        f"/api/v1/i/{image_id}/tags",
        json={"tags": ["valid", "INVALID TAG WITH SPACES!"]},
        headers=headers,
    )
    assert patch.status_code == 422
    body = patch.json()
    assert body["code"] == "invalid_tag"


@pytest.mark.asyncio
async def test_list_tags_alphabetical_with_counts(authed_client):
    client, token = authed_client
    data = _minimal_png()
    await client.post(
        "/api/v1/images",
        files={"file": ("tag-list-test.png", io.BytesIO(data), "image/png")},
        data={"tags": "zebra,apple"},
        headers={"Authorization": f"Bearer {token}"},
    )
    response = await client.get("/api/v1/tags")
    assert response.status_code == 200
    body = response.json()
    names = [item["name"] for item in body["items"]]
    assert names == sorted(names)
    for item in body["items"]:
        assert "image_count" in item
        assert item["image_count"] >= 0


@pytest.mark.asyncio
async def test_list_tags_prefix_filter(authed_client):
    client, token = authed_client
    data = _minimal_png()
    await client.post(
        "/api/v1/images",
        files={"file": ("prefix-test.png", io.BytesIO(data), "image/png")},
        data={"tags": "cat,catfish,caterpillar,dog"},
        headers={"Authorization": f"Bearer {token}"},
    )
    response = await client.get("/api/v1/tags?q=cat")
    assert response.status_code == 200
    body = response.json()
    for item in body["items"]:
        assert item["name"].startswith("cat")
    assert not any(item["name"] == "dog" for item in body["items"])


def _unique_png(seed: int) -> bytes:
    """Generate a 1x1 PNG with a seed-determined pixel so each seed produces a distinct hash."""
    import struct
    import zlib
    def chunk(name: bytes, data: bytes) -> bytes:
        c = name + data
        return struct.pack(">I", len(data)) + c + struct.pack(">I", zlib.crc32(c) & 0xFFFFFFFF)
    ihdr = struct.pack(">IIBBBBB", 1, 1, 8, 2, 0, 0, 0)
    r, g, b = (seed * 37) % 256, (seed * 53) % 256, (seed * 71) % 256
    idat_data = zlib.compress(bytes([0, r, g, b]))
    return (
        b"\x89PNG\r\n\x1a\n"
        + chunk(b"IHDR", ihdr)
        + chunk(b"IDAT", idat_data)
        + chunk(b"IEND", b"")
    )


@pytest.mark.asyncio
async def test_list_tags_sort_count_desc(authed_client):
    client, token = authed_client
    headers = {"Authorization": f"Bearer {token}"}
    # popular-sort-tag appears on 2 images, rare-sort-tag on 1 — verify count_desc ordering
    for seed in (100, 101):
        await client.post(
            "/api/v1/images",
            files={"file": (f"sort-{seed}.png", io.BytesIO(_unique_png(seed)), "image/png")},
            data={"tags": "popular-sort-tag,rare-sort-tag" if seed == 100 else "popular-sort-tag"},
            headers=headers,
        )
    response = await client.get("/api/v1/tags?sort=count_desc")
    assert response.status_code == 200
    items = response.json()["items"]
    sort_items = [i for i in items if i["name"] in ("popular-sort-tag", "rare-sort-tag")]
    assert len(sort_items) == 2
    # popular-sort-tag (count=2) must come before rare-sort-tag (count=1)
    names = [i["name"] for i in sort_items]
    assert names.index("popular-sort-tag") < names.index("rare-sort-tag")
    # Counts must be non-increasing
    counts = [i["image_count"] for i in items]
    assert counts == sorted(counts, reverse=True)


@pytest.mark.asyncio
async def test_list_tags_min_count_excludes_below_threshold(authed_client):
    client, token = authed_client
    headers = {"Authorization": f"Bearer {token}"}
    # common-min-tag appears on 2 images, uncommon-min-tag on 1
    for seed in (200, 201):
        await client.post(
            "/api/v1/images",
            files={"file": (f"min-{seed}.png", io.BytesIO(_unique_png(seed)), "image/png")},
            data={"tags": "common-min-tag,uncommon-min-tag" if seed == 200 else "common-min-tag"},
            headers=headers,
        )
    # min_count=2 should exclude uncommon-min-tag (count=1) but keep common-min-tag (count=2)
    response = await client.get("/api/v1/tags?min_count=2")
    assert response.status_code == 200
    items = response.json()["items"]
    names = [i["name"] for i in items]
    assert "common-min-tag" in names
    assert "uncommon-min-tag" not in names
    # All returned tags must have image_count >= 2
    for item in items:
        assert item["image_count"] >= 2
