"""Gestión de fotos del admin (corregir un QR mal leído, papelera) y compatibilidad con el layout antiguo."""
import io
import re
import uuid

import pytest
from PIL import Image

from core import config
from services import photo_service, school_service
from conftest import DEMO_CODE, LEGACY_CODE, OTHER_CODE, OTHER_SCHOOL, PHOTOGRAPHER, SCHOOL, family_dir, save_jpeg, plain_image


@pytest.fixture(autouse=True)
def _clean_temporary_photos():
    """Las fotos temporales de estos tests no pueden contaminar las galerías de los demás."""
    yield
    for f in list(family_dir(SCHOOL, DEMO_CODE).glob("tmp_*.jpg")) + list(family_dir(SCHOOL, DEMO_CODE).glob("dup*.jpg")):
        f.unlink(missing_ok=True)
    photo_service.invalidate_families_cache(SCHOOL)


def _new_photo(school=SCHOOL, code=DEMO_CODE, name=None, photographer=PHOTOGRAPHER):
    name = name or f"tmp_{uuid.uuid4().hex[:6]}.jpg"
    return save_jpeg(plain_image((400, 300), seed=9), family_dir(school, code, photographer) / name)


async def test_families_listing_is_scoped_to_the_school(client, admin_headers, other_admin_headers):
    mine = {f["qr_code"] for f in (await client.get("/api/admin/photos/families", headers=admin_headers)).json()["families"]}
    theirs = {f["qr_code"] for f in (await client.get("/api/admin/photos/families", headers=other_admin_headers)).json()["families"]}
    assert DEMO_CODE in mine and OTHER_CODE not in mine
    assert OTHER_CODE in theirs and DEMO_CODE not in theirs


async def test_by_date_defaults_to_today_and_thumbs_are_signed_and_work(client, admin_headers):
    photo = _new_photo()
    photo_service.invalidate_families_cache(SCHOOL)
    body = (await client.get("/api/admin/photos/by-date", headers=admin_headers)).json()
    row = next(p for p in body["photos"] if p["filename"] == photo.name)
    assert row["photographer"] == PHOTOGRAPHER and row["qr_code"] == DEMO_CODE and row["kind"] == "original"
    r = await client.get(row["thumb_url"])
    assert r.status_code == 200 and r.headers["content-type"] == "image/webp"
    assert max(Image.open(io.BytesIO(r.content)).size) <= 300
    assert (await client.get(re.sub(r"sig=\w+", "sig=00", row["thumb_url"]))).status_code == 403
    assert (await client.get(re.sub(r"exp=\d+", "exp=1", row["thumb_url"]))).status_code == 403


async def test_by_date_filters_by_date_and_hour(client, admin_headers):
    _new_photo()
    assert (await client.get("/api/admin/photos/by-date?date_from=2001-01-01&date_to=2001-01-02",
                             headers=admin_headers)).json()["photos"] == []
    assert (await client.get("/api/admin/photos/by-date?date_from=2001-01-01&date_to=2001-01-02&time_from=abc",
                             headers=admin_headers)).status_code == 422


async def test_admin_thumb_is_bound_to_its_school_and_path(client, admin_headers):
    from services import media_service
    photo = _new_photo()
    url = media_service.admin_thumb_url(SCHOOL, PHOTOGRAPHER, DEMO_CODE, photo.name)
    assert (await client.get(url)).status_code == 200
    # la firma de un colegio no vale para otro, ni para otra ruta
    assert (await client.get(url.replace(f"/{SCHOOL}/", f"/{OTHER_SCHOOL}/"))).status_code == 403
    assert (await client.get(url.replace(DEMO_CODE, "MAR-OTR-999"))).status_code == 403


async def test_move_photo_to_another_family_creates_the_folder_and_drops_derivatives(client, admin_headers):
    photo = _new_photo()
    deriv = photo_service.mirror_path(photo, ".wm.1000.webp")
    deriv.parent.mkdir(parents=True, exist_ok=True)
    deriv.write_bytes(b"x")
    r = await client.post("/api/admin/photos/move", headers=admin_headers, json={
        "photographer": PHOTOGRAPHER, "qr_code": DEMO_CODE, "filename": photo.name, "target_qr_code": "mar nw1 001"})
    assert r.status_code == 200, r.text
    assert r.json()["qr_code"] == "MARNW1001"
    moved = family_dir(SCHOOL, "MARNW1001") / photo.name
    assert moved.exists() and not photo.exists() and not deriv.exists()
    # mover otra a la MISMA carpeta ignorando guiones reutiliza la carpeta, no crea otra
    photo2 = _new_photo()
    r = await client.post("/api/admin/photos/move", headers=admin_headers, json={
        "photographer": PHOTOGRAPHER, "qr_code": DEMO_CODE, "filename": photo2.name, "target_qr_code": "MARNW1001"})
    assert r.json()["qr_code"] == "MARNW1001"


async def test_move_never_overwrites_a_file_with_the_same_name(client, admin_headers):
    a = _new_photo(name="dup.jpg")
    other = family_dir(SCHOOL, "MAR-DUP-001")
    save_jpeg(plain_image((100, 100), seed=1), other / "dup.jpg")
    r = await client.post("/api/admin/photos/move", headers=admin_headers, json={
        "photographer": PHOTOGRAPHER, "qr_code": DEMO_CODE, "filename": "dup.jpg", "target_qr_code": "MAR-DUP-001"})
    assert r.status_code == 200 and len(list(other.glob("dup*.jpg"))) == 2


async def test_move_rejects_traversal_unknown_photographer_and_missing_file(client, admin_headers):
    photo = _new_photo()
    base = {"photographer": PHOTOGRAPHER, "qr_code": DEMO_CODE, "filename": photo.name, "target_qr_code": "MAR-XXX-111"}
    for patch, status in (({"target_qr_code": "../../etc"}, 400), ({"filename": "../x.jpg"}, 400),
                          ({"photographer": ".."}, 400), ({"target_photographer": "no-existe"}, 400),
                          ({"filename": "inexistente.jpg"}, 404)):
        r = await client.post("/api/admin/photos/move", headers=admin_headers, json={**base, **patch})
        assert r.status_code == status, (patch, r.text)
    assert photo.exists()


async def test_an_operator_cannot_move_photos_of_another_school(client, other_admin_headers):
    photo = _new_photo()
    r = await client.post("/api/admin/photos/move", headers=other_admin_headers, json={
        "photographer": PHOTOGRAPHER, "qr_code": DEMO_CODE, "filename": photo.name, "target_qr_code": "MAR-HAK-001"})
    assert r.status_code == 404 and photo.exists()


async def test_delete_goes_to_the_trash_and_can_be_restored(client, admin_headers):
    photo = _new_photo()
    r = await client.delete(f"/api/admin/photos/{PHOTOGRAPHER}/{DEMO_CODE}/{photo.name}", headers=admin_headers)
    assert r.status_code == 200 and not photo.exists()
    entries = (await client.get("/api/admin/photos/trash", headers=admin_headers)).json()["entries"]
    entry = next(e for e in entries if e["kind"].startswith("photo-") and e["file_count"] == 1)
    assert entry["expires_in_days"] <= config.TRASH_RETENTION_DAYS
    # la papelera NO aparece como familia ni se sirve
    fams = {f["qr_code"] for f in (await client.get("/api/admin/photos/families", headers=admin_headers)).json()["families"]}
    assert not any(c.startswith(".") for c in fams)
    r = await client.post(f"/api/admin/photos/trash/{entry['id']}/restore", headers=admin_headers)
    assert r.status_code == 200 and r.json()["restored"] == 1 and photo.exists()


async def test_delete_family_is_one_restorable_batch(client, admin_headers):
    code = "MAR-FAM-001"
    for cam in ("cam1", "cam2"):
        save_jpeg(plain_image((100, 100), seed=2), family_dir(SCHOOL, code, cam) / f"{cam}.jpg")
    photo_service.invalidate_families_cache(SCHOOL)
    r = await client.delete(f"/api/admin/photos/family/{code}", headers=admin_headers)
    assert r.json()["deleted"] == 2 and not family_dir(SCHOOL, code).exists()
    entries = (await client.get("/api/admin/photos/trash", headers=admin_headers)).json()["entries"]
    entry = next(e for e in entries if e["kind"].startswith("family-MARFAM001"))
    await client.post(f"/api/admin/photos/trash/{entry['id']}/restore", headers=admin_headers)
    assert (family_dir(SCHOOL, code, "cam1") / "cam1.jpg").exists() and (family_dir(SCHOOL, code, "cam2") / "cam2.jpg").exists()


async def test_delete_all_needs_the_confirmation_word_and_only_touches_its_school(client, admin_token):
    sid = f"del-{uuid.uuid4().hex[:5]}"
    school_service.create_school("Borrado", slug=sid)
    headers = {"Authorization": f"Bearer {admin_token}", "X-School": sid}
    save_jpeg(plain_image((100, 100)), family_dir(sid, "MAR-DEL-001") / "a.jpg")
    assert (await client.delete("/api/admin/photos/all", headers=headers)).status_code == 400
    assert (await client.delete("/api/admin/photos/all?confirm=no", headers=headers)).status_code == 400
    assert family_dir(sid, "MAR-DEL-001").exists()
    r = await client.delete("/api/admin/photos/all?confirm=borrar", headers=headers)
    assert r.status_code == 200 and r.json()["deleted"] == 1 and not family_dir(sid, "MAR-DEL-001").exists()
    assert (family_dir(SCHOOL, DEMO_CODE) / "portrait.jpg").exists()            # las de otro colegio, intactas


async def test_trash_restore_rejects_path_traversal(client, admin_headers):
    for bad in ("..%2F..%2Fphotos", "x%2F..%2F..", "no-existe"):
        r = await client.post(f"/api/admin/photos/trash/{bad}/restore", headers=admin_headers)
        assert r.status_code in (400, 404, 422), bad


async def test_photo_admin_endpoints_require_the_operator_role(client, director_headers):
    for method, path in (("get", "/api/admin/photos/families"), ("get", "/api/admin/photos/by-date"),
                         ("get", "/api/admin/photos/trash"), ("delete", "/api/admin/photos/all?confirm=BORRAR")):
        r = await getattr(client, method)(path, headers=director_headers)
        assert r.status_code == 403, path
        assert (await getattr(client, method)(path)).status_code == 401, path


# ─── Layout antiguo (photos/{CÓDIGO}/) ────────────────────────────────────────

async def test_families_with_the_old_layout_still_see_and_buy_their_photos(client):
    photos = (await client.get(f"/api/portal/photos?code={LEGACY_CODE}")).json()["photos"]
    assert [p["filename"] for p in photos] == ["vieille.jpg"]
    assert (await client.get(photos[0]["url"])).status_code == 200


async def test_a_family_with_both_layouts_sees_the_union(client, admin_headers):
    save_jpeg(plain_image((200, 200), seed=4), family_dir(SCHOOL, LEGACY_CODE) / "nueva.jpg")
    names = {p["filename"] for p in (await client.get(f"/api/portal/photos?code={LEGACY_CODE}")).json()["photos"]}
    assert names == {"vieille.jpg", "nueva.jpg"}
    (family_dir(SCHOOL, LEGACY_CODE) / "nueva.jpg").unlink()


async def test_deleting_a_legacy_photo_goes_to_the_trash_and_comes_back_to_the_old_place(client, admin_headers):
    legacy = config.PHOTOS_DIR / LEGACY_CODE / "temporal_legacy.jpg"
    save_jpeg(plain_image((100, 100)), legacy)
    r = await client.delete(f"/api/admin/photos/_/{LEGACY_CODE}/temporal_legacy.jpg", headers=admin_headers)
    assert r.status_code == 200 and not legacy.exists()
    entries = (await client.get("/api/admin/photos/trash", headers=admin_headers)).json()["entries"]
    entry = next(e for e in entries if "temporal" not in e["kind"] and e["file_count"] == 1 and e["kind"].startswith("photo-OLD7007"))
    await client.post(f"/api/admin/photos/trash/{entry['id']}/restore", headers=admin_headers)
    assert legacy.exists()
    legacy.unlink()


async def test_stats_count_photos_of_both_layouts(client, admin_headers):
    stats = (await client.get("/api/admin/stats", headers=admin_headers)).json()
    assert stats["photos_received"] >= 3          # portrait + fratrie (nuevo) + vieille (antiguo)
