"""Attachments in comments are a client-facing upload surface (R2 task T5), so the hostile
cases are the tests: the type is decided by the bytes (a script named .png is refused, so is
a Word file, which the to-do hand-in accepts but a comment does not), the stored name is ours
and only our name shape ever resolves to a file, the served type comes from our table with
nosniff, the size cap holds, and visibility follows the owning subject: a running-list subject
is 404 for a client, before anything is read or written. In-process on the accounts suite's
SQLite, files under a temporary root.
"""

import io

import pytest

from app.models import AccountLevel
from app.routers import attachments as att
from app.services import accounts
from tests import _accounts as T

PNG = b"\x89PNG\r\n\x1a\n" + b"\0" * 64
PDF = b"%PDF-1.7\n%\xe2\xe3\xcf\xd3\n1 0 obj<<>>endobj\n"


@pytest.fixture
def world(monkeypatch, tmp_path):
    T.fresh(monkeypatch)
    T.person("rian", display_name="Rian")
    from tests.kit import _env
    with _env.TestSessionLocal() as db:
        db.add(AccountLevel(name="admin", permissions=list(accounts.SEED_LEVELS["admin"]["permissions"]), assignable=[]))
        db.commit()
    T.person("adam", level="admin", display_name="Adam")
    monkeypatch.setattr(att, "ROOT", tmp_path / "attachments")
    # A real re-encode needs Pillow and a real image; the seam is exercised by its own tests.
    monkeypatch.setattr(att.up, "strip_image_metadata", lambda data, ct: data)
    yield tmp_path


def upload(c, subject: str, name: str, data: bytes):
    return c.post(f"/api/attachments/{subject}", files={"file": (name, io.BytesIO(data), "application/octet-stream")})


class TestUpload:
    def test_a_png_is_stored_under_our_name_and_answers_markdown(self, world):
        c = T.client()
        T.as_user(c, "adam")
        r = upload(c, "quote/guides", "../../shelf photo.png", PNG)
        assert r.status_code == 201, r.text
        body = r.json()
        assert body["is_image"] and body["display"] == "shelf photo.png"
        assert att.NAME.match(body["name"]) and body["url"] == f"/api/attachments/quote/guides/{body['name']}"
        assert body["markdown"] == f"![shelf photo.png]({body['url']})"
        assert (world / "attachments" / "quote" / "guides" / body["name"]).read_bytes() == PNG
        # A PDF is a link, not an image.
        r = upload(c, "quote/guides", "signed.pdf", PDF)
        assert r.status_code == 201 and r.json()["markdown"].startswith("[signed.pdf](/api/attachments/quote/guides/")

    def test_the_bytes_decide_the_type_and_a_comment_takes_images_and_pdf_only(self, world):
        c = T.client()
        T.as_user(c, "adam")
        assert upload(c, "quote/guides", "photo.png", b"<script>alert(1)</script>" + b"\0" * 8).status_code == 415
        assert upload(c, "quote/guides", "photo.png", b"\x7fELF\x02\x01\x01" + b"\0" * 20).status_code == 415
        assert upload(c, "quote/guides", "notes.txt", b"plain words").status_code == 415  # text: the to-do hand-in's, not a comment's
        assert upload(c, "quote/guides", "empty.png", b"").status_code == 422
        assert upload(c, "quote/guides", "big.png", PNG + b"\0" * att.MAX_BYTES).status_code == 413
        assert not (world / "attachments").exists() or not any((world / "attachments").rglob("*.*"))

    def test_a_subject_the_caller_cannot_see_is_404_before_any_write(self, world):
        c = T.client()
        T.as_user(c, "adam")
        assert upload(c, "item/decide-x", "photo.png", PNG).status_code == 404
        assert upload(c, "stage/1", "photo.png", PNG).status_code == 404
        assert not (world / "attachments").exists()
        T.as_user(c, "rian")
        assert upload(c, "item/decide-x", "photo.png", PNG).status_code == 201


class TestServe:
    def test_served_with_our_type_and_nosniff_and_only_our_name_shape_resolves(self, world):
        c = T.client()
        T.as_user(c, "adam")
        name = upload(c, "structure/urls", "a.png", PNG).json()["name"]
        r = c.get(f"/api/attachments/structure/urls/{name}")
        assert r.status_code == 200 and r.content == PNG
        assert r.headers["content-type"] == "image/png" and r.headers["x-content-type-options"] == "nosniff"
        assert r.headers["cache-control"] == "private, max-age=3600"
        # The name shape is the traversal guard.
        (world / "attachments" / "structure" / "urls" / "secret.txt").write_text("not for you")
        # (A literal `../` is normalised away by the client before it is sent; the encoded form
        # reaches the server as `..`, which the name shape refuses.)
        for bad in ("secret.txt", "%2e%2e", name + ".exe", "x.png", name.upper()):
            assert c.get(f"/api/attachments/structure/urls/{bad}").status_code == 404, bad
        # An encoded slash decodes into a path no route matches; the access policy refuses it
        # before any handler runs (ROUTE_NOT_CLASSIFIED), which is the other fail-safe.
        assert c.get("/api/attachments/structure/urls/..%2F..%2Fetc%2Fpasswd").status_code in (403, 404)
        assert c.get(f"/api/attachments/structure/urls/{name.replace('.png', '.pdf')}").status_code == 404

    def test_visibility_is_the_owning_subjects(self, world):
        c = T.client()
        T.as_user(c, "rian")
        name = upload(c, "item/decide-x", "a.png", PNG).json()["name"]
        assert c.get(f"/api/attachments/item/decide-x/{name}").status_code == 200
        T.as_user(c, "adam")
        assert c.get(f"/api/attachments/item/decide-x/{name}").status_code == 404
        # Anonymous: the policy's 401, before the route.
        anon = T.client()
        assert anon.get(f"/api/attachments/item/decide-x/{name}").status_code == 401
