"""Smoke tests F9 — fotos + daily log endpoints.

Uso: python smoke_test_f9.py (asume servidor corriendo en localhost:8080)
"""
from __future__ import annotations

import http.cookiejar
import io
import json
import os
import sys
import urllib.request
import urllib.error
import uuid
from pathlib import Path

# Dep opcional: solo para crear JPG con EXIF sintetico.
try:
    from PIL import Image  # type: ignore
    import piexif  # type: ignore
    _HAS_PIEXIF = True
except ImportError:
    try:
        from PIL import Image  # type: ignore
        _HAS_PIEXIF = False
    except ImportError:
        print("ERROR: instala Pillow primero.")
        sys.exit(1)

BASE = "http://localhost:8080"
OBRA = "VITR1"
ADMIN = {"email": "admin@demo.local", "password": "demo1234"}

# Opener global con cookiejar para mantener la sesion admin en todas las llamadas.
_COOKIE_JAR = http.cookiejar.CookieJar()
_OPENER = urllib.request.build_opener(
    urllib.request.HTTPCookieProcessor(_COOKIE_JAR)
)


def _http(method: str, path: str, *, body: bytes | None = None,
          headers: dict[str, str] | None = None):
    req = urllib.request.Request(
        BASE + path, data=body, method=method, headers=headers or {},
    )
    try:
        with _OPENER.open(req, timeout=10) as resp:
            return resp.status, resp.read(), dict(resp.headers)
    except urllib.error.HTTPError as e:
        return e.code, e.read(), dict(e.headers) if e.headers else {}


def _login_admin() -> bool:
    """Login admin y carga cookie en el jar compartido. True si OK."""
    payload = json.dumps(ADMIN).encode("utf-8")
    st, _, _ = _http(
        "POST", "/api/auth/login", body=payload,
        headers={"Content-Type": "application/json"},
    )
    return st == 200


def _build_jpg(size: int = 800) -> bytes:
    """Crea un JPG minimal en memoria usando Pillow."""
    img = Image.new("RGB", (50, 50), color=(200, 150, 80))
    buf = io.BytesIO()
    img.save(buf, format="JPEG", quality=70)
    data = buf.getvalue()
    # Aumentar tamaño si se pide mas grande (padding con pixel noise)
    if size > len(data):
        img = Image.new("RGB", (int(size ** 0.5) + 10, int(size ** 0.5) + 10),
                        color=(100, 100, 100))
        buf = io.BytesIO()
        img.save(buf, format="JPEG", quality=90)
        data = buf.getvalue()
    return data


def _multipart_body(fields: dict[str, str], file_field: str,
                    filename: str, filedata: bytes, filemime: str) -> tuple[bytes, str]:
    boundary = "----dg-test-" + uuid.uuid4().hex
    out = io.BytesIO()
    for k, v in fields.items():
        out.write(f"--{boundary}\r\n".encode())
        out.write(f'Content-Disposition: form-data; name="{k}"\r\n\r\n'.encode())
        out.write(v.encode("utf-8"))
        out.write(b"\r\n")
    out.write(f"--{boundary}\r\n".encode())
    out.write(
        f'Content-Disposition: form-data; name="{file_field}"; filename="{filename}"\r\n'
        .encode())
    out.write(f"Content-Type: {filemime}\r\n\r\n".encode())
    out.write(filedata)
    out.write(b"\r\n")
    out.write(f"--{boundary}--\r\n".encode())
    return out.getvalue(), f"multipart/form-data; boundary={boundary}"


def run():
    passed = 0
    failed = 0
    out_foto_id = None

    # T0: login admin (prerequisito F13+: rutas de fotos/daily-log exigen sesion).
    if not _login_admin():
        print("  [FAIL] T0 login admin - aborta smoke test")
        return False
    print("  [OK] T0 login admin")
    passed += 1

    def check(name, cond, detail=""):
        nonlocal passed, failed
        if cond:
            print(f"  [OK] {name}")
            passed += 1
        else:
            print(f"  [FAIL] {name} | {detail}")
            failed += 1

    # T1: POST foto valida
    jpg = _build_jpg()
    body, ctype = _multipart_body(
        {"fecha": "2026-04-21", "sector": "Torre A", "piso": "3",
         "nota": "Test foto F9", "usuario": "smoke_test"},
        "foto", "test.jpg", jpg, "image/jpeg")
    st, data, _ = _http("POST", f"/api/obras/{OBRA}/fotos", body=body,
                         headers={"Content-Type": ctype,
                                  "Content-Length": str(len(body))})
    ok = st == 201
    parsed = json.loads(data) if data else {}
    check("T1 POST foto valida -> 201", ok, f"status={st} body={data[:200]}")
    if ok:
        out_foto_id = parsed["foto"]["id"]

    # T2: POST sin field 'foto'
    body, ctype = _multipart_body(
        {"fecha": "2026-04-21"}, "otro", "x.jpg", b"xxx", "image/jpeg")
    st, data, _ = _http("POST", f"/api/obras/{OBRA}/fotos", body=body,
                         headers={"Content-Type": ctype,
                                  "Content-Length": str(len(body))})
    check("T2 POST sin campo 'foto' -> 400", st == 400, f"status={st}")

    # T3: POST fecha invalida
    body, ctype = _multipart_body(
        {"fecha": "hoy"}, "foto", "t.jpg", jpg, "image/jpeg")
    st, data, _ = _http("POST", f"/api/obras/{OBRA}/fotos", body=body,
                         headers={"Content-Type": ctype,
                                  "Content-Length": str(len(body))})
    check("T3 POST fecha 'hoy' -> 400", st == 400, f"status={st}")

    # T4: POST obra inexistente
    body, ctype = _multipart_body(
        {"fecha": "2026-04-21"}, "foto", "t.jpg", jpg, "image/jpeg")
    st, data, _ = _http("POST", "/api/obras/NOPE99/fotos", body=body,
                         headers={"Content-Type": ctype,
                                  "Content-Length": str(len(body))})
    check("T4 POST obra NOPE99 -> 404", st == 404, f"status={st}")

    # T5: GET lista
    st, data, _ = _http("GET", f"/api/obras/{OBRA}/fotos")
    parsed = json.loads(data) if data else []
    check("T5 GET /fotos lista incluye foto T1", st == 200 and any(
        f.get("id") == out_foto_id for f in parsed) if out_foto_id else st == 200,
          f"status={st}, count={len(parsed)}")

    # T6: GET blob
    if out_foto_id:
        st, data, hdrs = _http("GET", f"/api/obras/{OBRA}/fotos/{out_foto_id}/blob")
        ctype = hdrs.get("Content-Type", "")
        check("T6 GET /blob -> 200 + image/jpeg",
              st == 200 and ctype.startswith("image/") and data[:2] == b"\xff\xd8",
              f"status={st} type={ctype} head={data[:4].hex() if data else 'empty'}")

    # T7: GET filtros fecha
    st, data, _ = _http("GET", f"/api/obras/{OBRA}/fotos?desde=2026-04-21&hasta=2026-04-21")
    parsed = json.loads(data) if data else []
    check("T7 GET con filtros fecha",
          st == 200 and all(f.get("fecha") == "2026-04-21" for f in parsed),
          f"status={st}, count={len(parsed)}")

    # T8: POST daily-log valido
    payload = json.dumps({
        "fecha": "2026-04-21", "autor": "smoke_test",
        "actividades": "Hormigonado losa piso 3",
        "observaciones": "Sin novedad", "clima": "Soleado 28C",
        "decisiones_key": "OK a continuar albanileria"
    }).encode("utf-8")
    st, data, _ = _http("POST", f"/api/obras/{OBRA}/daily-log", body=payload,
                         headers={"Content-Type": "application/json",
                                  "Content-Length": str(len(payload))})
    parsed = json.loads(data) if data else {}
    check("T8 POST daily-log valido -> 201", st == 201,
          f"status={st} body={data[:200]}")
    entry_id = parsed.get("entry", {}).get("id") if st == 201 else None

    # T9: POST daily-log sin actividades ni observaciones
    payload = json.dumps({"fecha": "2026-04-21"}).encode("utf-8")
    st, data, _ = _http("POST", f"/api/obras/{OBRA}/daily-log", body=payload,
                         headers={"Content-Type": "application/json",
                                  "Content-Length": str(len(payload))})
    check("T9 POST daily-log vacio -> 400", st == 400, f"status={st}")

    # T10: POST daily-log fecha invalida
    payload = json.dumps({"fecha": "ayer", "actividades": "test"}).encode("utf-8")
    st, data, _ = _http("POST", f"/api/obras/{OBRA}/daily-log", body=payload,
                         headers={"Content-Type": "application/json",
                                  "Content-Length": str(len(payload))})
    check("T10 POST daily-log fecha 'ayer' -> 400", st == 400, f"status={st}")

    # T11: GET daily-log lista
    st, data, _ = _http("GET", f"/api/obras/{OBRA}/daily-log")
    parsed = json.loads(data) if data else []
    check("T11 GET /daily-log incluye entry T8",
          st == 200 and any(e.get("id") == entry_id for e in parsed) if entry_id else st == 200,
          f"status={st}, count={len(parsed)}")

    # T12: obra inexistente -> 404 para GET fotos y daily-log tambien.
    st1, _, _ = _http("GET", "/api/obras/NOPE99/fotos")
    st2, _, _ = _http("GET", "/api/obras/NOPE99/daily-log")
    check("T12 GET obra inexistente -> 404 (ambos)",
          st1 == 404 and st2 == 404, f"fotos={st1} daily={st2}")

    # T13: guard tamano excesivo -> rechazo (criterio 6 §2.4 del prompt).
    # Uso 21MB (limite=20MB) para que el drain del server termine rapido.
    # En Windows, urllib a veces eleva ConnectionAbortedError si el server
    # responde y cierra durante upload — ambos outcomes cuentan como rechazo.
    big = b"\xff\xd8\xff\xe0" + (b"A" * (21 * 1024 * 1024))
    body, ctype = _multipart_body(
        {"fecha": "2026-04-21"}, "foto", "huge.jpg", big, "image/jpeg")
    rejected = False
    reason = ""
    try:
        st, data, _ = _http("POST", f"/api/obras/{OBRA}/fotos", body=body,
                             headers={"Content-Type": ctype,
                                      "Content-Length": str(len(body))})
        msg = data.decode("utf-8", errors="replace") if data else ""
        rejected = st == 400 and ("grande" in msg.lower() or "max" in msg.lower())
        reason = f"status={st} msg={msg[:200]}"
    except (ConnectionAbortedError, ConnectionResetError, BrokenPipeError) as e:
        rejected = True
        reason = f"server cerro conexion (rechazo Windows): {type(e).__name__}"
    except urllib.error.URLError as e:
        if "ConnectionAbortedError" in str(e) or "Connection" in str(e):
            rejected = True
            reason = f"URLError por conexion: {e}"
        else:
            reason = f"URLError inesperado: {e}"
    check("T13 POST >20MB -> rechazo", rejected, reason)

    print(f"\n  TOTAL: {passed}/{passed+failed} pasados")
    return failed == 0


if __name__ == "__main__":
    ok = run()
    sys.exit(0 if ok else 1)
