"""Comment attachments (04 addendum).

The security assertions matter more than the happy path: a file's URL is not a
capability. Every read re-checks the OWNING STAGE, so someone who can't see a
stage can't fetch its screenshots even holding the exact link.
"""

import io
import itertools

import pytest

from app.services import attachments as att
from tests.conftest import as_user

_n = itertools.count(1)

PNG = bytes.fromhex("89504e470d0a1a0a") + b"\x00" * 64
PDF = b"%PDF-1.4\n" + b"\x00" * 32


def _project(client, agency):
    as_user(client, agency)
    r = client.post("/api/projects", json={"display_name": f"Att Project {next(_n)}",
                                           "template_key": "website_build_v1"})
    pid = r.json()["id"]
    stages = client.get(f"/api/projects/{pid}").json()["stages"]
    return pid, stages[0]["id"]


def _upload(client, stage_id, name, data, ctype="image/png"):
    return client.post(f"/api/attachments/{stage_id}",
                       files={"file": (name, io.BytesIO(data), ctype)})


# ── the pure service ────────────────────────────────────────────────────

def test_rejects_type_not_on_the_allow_list(tmp_path):
    with pytest.raises(att.AttachmentError) as e:
        att.save(tmp_path, "s1", "payload.svg", b"<svg/>")
    assert e.value.code == "UNSUPPORTED_TYPE"


def test_rejects_content_that_lies_about_its_extension(tmp_path):
    """A .png whose bytes are a script is the whole reason for magic checks."""
    with pytest.raises(att.AttachmentError) as e:
        att.save(tmp_path, "s1", "shell.png", b"<?php system($_GET[0]); ?>")
    assert e.value.code == "CONTENT_MISMATCH"


def test_rejects_oversize(tmp_path):
    with pytest.raises(att.AttachmentError) as e:
        att.save(tmp_path, "s1", "big.png", PNG + b"\x00" * att.MAX_BYTES)
    assert e.value.code == "TOO_LARGE"


def test_stored_name_is_never_the_uploaded_name(tmp_path):
    """Traversal, collisions and content-sniffing by name all die here."""
    saved = att.save(tmp_path, "s1", "../../etc/passwd.png", PNG)
    assert "/" not in saved["name"] and ".." not in saved["name"]
    assert saved["name"].endswith(".png")
    assert saved["display"] == "passwd.png"      # the label survives
    assert (tmp_path / "s1" / saved["name"]).exists()


def test_resolve_refuses_to_escape_its_subject_directory(tmp_path):
    att.save(tmp_path, "s1", "a.png", PNG)
    with pytest.raises(att.AttachmentError):
        att.resolve(tmp_path, "s1", "../s2/a.png")


def test_resolve_cannot_cross_subjects(tmp_path):
    saved = att.save(tmp_path, "s1", "a.png", PNG)
    with pytest.raises(att.AttachmentError) as e:
        att.resolve(tmp_path, "s2", saved["name"])
    assert e.value.code == "NO_SUCH_ATTACHMENT"


def test_per_subject_file_cap(tmp_path, monkeypatch):
    monkeypatch.setattr(att, "MAX_FILES_PER_SUBJECT", 2)
    att.save(tmp_path, "s1", "a.png", PNG)
    att.save(tmp_path, "s1", "b.png", PNG)
    with pytest.raises(att.AttachmentError) as e:
        att.save(tmp_path, "s1", "c.png", PNG)
    assert e.value.code == "SUBJECT_FULL"


# ── through the API, where visibility is enforced ───────────────────────

def test_upload_then_fetch_round_trips(client, agency):
    _, sid = _project(client, agency)
    up = _upload(client, sid, "shot.png", PNG)
    assert up.status_code == 201, up.text
    a = up.json()["attachment"]
    assert a["is_image"] is True
    assert a["markdown"].startswith("![")

    got = client.get(a["url"])
    assert got.status_code == 200
    assert got.content == PNG
    # A stored image must never be sniffed into something executable.
    assert got.headers["x-content-type-options"] == "nosniff"
    assert got.headers["content-type"].startswith("image/png")


def test_a_pdf_renders_as_a_link_not_an_image(client, agency):
    _, sid = _project(client, agency)
    a = _upload(client, sid, "spec.pdf", PDF, "application/pdf").json()["attachment"]
    assert a["is_image"] is False
    assert a["markdown"].startswith("[")     # not "!["


def test_a_client_on_the_project_can_fetch_a_visible_stages_file(client, kit, agency):
    pid, sid = _project(client, agency)
    a = _upload(client, sid, "shot.png", PNG).json()["attachment"]

    kit.member("cleo", "member")
    kit.grant("cleo", pid, "member")
    as_user(client, "cleo")
    assert client.get(a["url"]).status_code == 200


def test_a_client_cannot_touch_an_internal_stage(client, kit, agency, session):
    """404, never 403 — an invisible stage must not be confirmed to exist."""
    pid, sid = _project(client, agency)
    a = _upload(client, sid, "shot.png", PNG).json()["attachment"]

    from app.models import Stage
    session.get(Stage, sid).client_visible = False
    session.commit()

    kit.member("cleo", "member")
    kit.grant("cleo", pid, "member")
    as_user(client, "cleo")
    assert _upload(client, sid, "x.png", PNG).status_code == 404
    assert client.get(a["url"]).status_code == 404


def test_the_url_is_not_a_capability(client, kit, agency):
    """The exact link, in the wrong hands, still 404s."""
    _, sid = _project(client, agency)
    a = _upload(client, sid, "shot.png", PNG).json()["attachment"]

    kit.member("nosy", "member")             # on no project at all
    as_user(client, "nosy")
    assert client.get(a["url"]).status_code == 404


def test_a_comment_carries_its_attachment(client, agency):
    pid, sid = _project(client, agency)
    a = _upload(client, sid, "shot.png", PNG).json()["attachment"]
    r = client.post(f"/api/projects/{pid}/stages/{sid}/comments",
                    json={"body": f"Here's what I mean\n{a['markdown']}"})
    assert r.status_code == 201, r.text
    shown = client.get(f"/api/projects/{pid}/stages/{sid}/comments").json()["comments"]
    assert a["url"] in shown[-1]["body"]
