"""Тесты для download_plan_image() из flat_plans.py (issue #2440).

Покрывает:
  - успешное скачивание: файл на диске, width/height/size_bytes через Pillow.
  - idempotency: повторный вызов не качает заново (download_binary не дёргается).
  - ошибка скачивания → None, файл не создаётся.
  - _safe_ods_filename / _ext_from_url helpers.

Использует фейковый BrowserSession с in-memory PNG (Pillow генерит 3x2 картинку) —
без сети и без реального Playwright.
"""

from __future__ import annotations

import io
from pathlib import Path
from typing import Any

import pytest
from PIL import Image

from app.services.scrapers.flat_plans import (
    _ext_from_url,
    _safe_ods_filename,
    download_plan_image,
)


def _png_bytes(w: int = 3, h: int = 2) -> bytes:
    buf = io.BytesIO()
    Image.new("RGB", (w, h), (10, 20, 30)).save(buf, format="PNG")
    return buf.getvalue()


class _FakeSession:
    """Минимальный BrowserSession-дублёр: download_binary отдаёт заданные байты."""

    def __init__(self, payload: bytes | None, raise_exc: Exception | None = None) -> None:
        self._payload = payload
        self._raise = raise_exc
        self.calls = 0

    async def download_binary(self, url: str) -> bytes:
        self.calls += 1
        if self._raise is not None:
            raise self._raise
        assert self._payload is not None
        return self._payload


# ── helpers ───────────────────────────────────────────────────────────────────


@pytest.mark.parametrize(
    "ods,expected",
    [
        ("65136/1/1.4.3", "65136_1_1.4.3"),
        ("plain", "plain"),
        ("a//b", "a_b"),
        ("///", "plan"),
    ],
)
def test_safe_ods_filename(ods: str, expected: str) -> None:
    assert _safe_ods_filename(ods) == expected


@pytest.mark.parametrize(
    "url,expected",
    [
        ("https://x/plan.png", ".png"),
        ("https://x/plan.JPG", ".jpg"),
        ("https://x/plan.jpeg", ".jpg"),
        ("https://x/api/ext/file/abc", ".png"),  # no ext → default png
        ("https://x/plan.svg", ".svg"),
    ],
)
def test_ext_from_url(url: str, expected: str) -> None:
    assert _ext_from_url(url) == expected


# ── download ──────────────────────────────────────────────────────────────────


def _read(path: str) -> bytes:
    """Sync file read — вынесено из async-теста, чтобы не тревожить ASYNC240."""
    return Path(path).read_bytes()


def _exists(path: Path) -> bool:
    return path.exists()


async def test_download_writes_file_and_dims(tmp_path: Path) -> None:
    png = _png_bytes(3, 2)
    sess: Any = _FakeSession(png)
    meta = await download_plan_image(
        sess, "https://x/api/ext/file/plan1.png", "65136/1/1.4.3", 65136, base_dir=tmp_path
    )
    assert meta is not None
    assert meta["width_px"] == 3
    assert meta["height_px"] == 2
    assert meta["size_bytes"] == len(png)
    assert meta["downloaded_at"] is not None
    p = Path(meta["local_path"])
    assert _read(str(p)) == png
    # путь: {base}/{obj_id}/{safe_ods}.png
    assert p.parent.name == "65136"
    assert p.name == "65136_1_1.4.3.png"


async def test_download_idempotent_skips_second_fetch(tmp_path: Path) -> None:
    png = _png_bytes(4, 4)
    sess: Any = _FakeSession(png)
    m1 = await download_plan_image(sess, "https://x/plan.png", "1/2/3", 42, base_dir=tmp_path)
    assert m1 is not None
    assert sess.calls == 1
    # второй вызов: файл уже на диске → download_binary НЕ дёргается
    m2 = await download_plan_image(sess, "https://x/plan.png", "1/2/3", 42, base_dir=tmp_path)
    assert m2 is not None
    assert sess.calls == 1  # не увеличилось
    assert m2["local_path"] == m1["local_path"]
    assert m2["size_bytes"] == len(png)


async def test_download_failure_returns_none(tmp_path: Path) -> None:
    sess: Any = _FakeSession(None, raise_exc=RuntimeError("binary http 403"))
    meta = await download_plan_image(sess, "https://x/plan.png", "9/9/9", 99, base_dir=tmp_path)
    assert meta is None
    # файл не создан
    assert not _exists(tmp_path / "99")


async def test_download_empty_body_returns_none(tmp_path: Path) -> None:
    sess: Any = _FakeSession(b"")
    meta = await download_plan_image(sess, "https://x/plan.png", "9/9/9", 99, base_dir=tmp_path)
    assert meta is None
