"""Task B0 — call_gpt_image_bytes primitive wrapper 결정론 테스트.

stochastic 모델이라 출력 PNG 동일성은 검증 불가 → "요청 kwargs 동일"이 결정론 기준
(Codex 합의). 따라서 mock client 로 images.generate/edit 가:
  - 같은 method name
  - 같은 prompt/model/size/quality/n
  - 같은 image 인자 개수(단일 핸들 vs 리스트) + 파일 순서
로 호출되는지 assert 한다. file handle 객체 동일성은 비교하지 않고 .name(파일 경로)
순서로 검증한다.

capture 는 wrapper 단일점 + capture-only min-size 가드: scope 가 열렸고 bytes 가
min_capture_bytes 이상일 때만 enqueue, 작으면 capture 만 skip 하고 bytes 는 그대로
반환(사이트 성공/실패 semantics 불변). budget reserve/out_path/검증은 wrapper 책임 아님.
"""

import base64
from types import SimpleNamespace

import pytest

from app.modules.llm.gpt_image_primitive import call_gpt_image_bytes
from app.services.image_capture.context import generation_context
from app.services.image_capture.queue import CAPTURE_DIAG


def _resp(b64):
    return SimpleNamespace(data=[SimpleNamespace(b64_json=b64)])


class _RecordingImages:
    """openai_client.images 대역 — 호출 method/kwargs/image 형태를 기록."""

    def __init__(self, decoded: bytes):
        self._b64 = base64.b64encode(decoded).decode("ascii")
        self.calls = []  # (method, kwargs, image_repr)

    def _image_repr(self, image):
        if isinstance(image, list):
            return ("list", [getattr(h, "name", None) for h in image])
        return ("single", getattr(image, "name", None))

    def generate(self, **kwargs):
        # generate 에는 image 인자가 없어야 한다.
        assert "image" not in kwargs
        self.calls.append(("generate", dict(kwargs), None))
        return _resp(self._b64)

    def edit(self, **kwargs):
        image = kwargs.pop("image")
        self.calls.append(("edit", dict(kwargs), self._image_repr(image)))
        return _resp(self._b64)


class _RecordingClient:
    def __init__(self, decoded: bytes = b"\x89PNG\r\n" + b"\x00" * 4096):
        self.images = _RecordingImages(decoded)


def _write_ref(tmp_path, name, content=b"REF"):
    p = tmp_path / name
    p.write_bytes(content)
    return p


def test_generate_mode_calls_generate_with_passthrough_kwargs():
    client = _RecordingClient()
    call_kwargs = {"model": "gpt-image-2.5-sunburst", "size": "1024x1024", "quality": "high", "n": 1}
    out = call_gpt_image_bytes(
        client, mode="generate", prompt="a prompt", ref_paths=None,
        call_kwargs=call_kwargs, capture_role="floor_plan",
    )
    assert len(client.images.calls) == 1
    method, kwargs, image_repr = client.images.calls[0]
    assert method == "generate"
    assert image_repr is None  # image 인자 없음
    assert kwargs["prompt"] == "a prompt"
    assert kwargs["model"] == "gpt-image-2.5-sunburst"
    assert kwargs["size"] == "1024x1024"
    assert kwargs["quality"] == "high"
    assert kwargs["n"] == 1
    # 반환 = decode 된 bytes
    assert out == b"\x89PNG\r\n" + b"\x00" * 4096


def test_edit_single_passes_single_handle_not_list(tmp_path):
    client = _RecordingClient()
    ref = _write_ref(tmp_path, "base.png")
    call_gpt_image_bytes(
        client, mode="edit", prompt="edit it", ref_paths=[ref],
        call_kwargs={"model": "gpt-image-2.5-sunburst", "size": "1536x1024"},
        capture_role="space_set_bg_i2i",
    )
    method, kwargs, image_repr = client.images.calls[0]
    assert method == "edit"
    assert image_repr[0] == "single"  # 단일 핸들, 리스트 아님
    assert image_repr[1] == str(ref)
    # space_set 은 quality/n 미전달 — pass-through 가 강제 주입하지 않음
    assert "quality" not in kwargs
    assert "n" not in kwargs
    assert kwargs["size"] == "1536x1024"


def test_edit_single_as_list_when_flag_set(tmp_path):
    """edit_image_as_list=True 면 ref 1개라도 image=[핸들] (location_floor_plan 보존)."""
    client = _RecordingClient()
    ref = _write_ref(tmp_path, "only.png")
    call_gpt_image_bytes(
        client, mode="edit", prompt="p", ref_paths=[ref],
        call_kwargs={"model": "gpt-image-2.5-sunburst", "size": "1024x1024", "quality": "high", "n": 1},
        capture_role="floor_plan", edit_image_as_list=True,
    )
    method, kwargs, image_repr = client.images.calls[0]
    assert method == "edit"
    assert image_repr == ("list", [str(ref)])  # 단일핸들 아님 — 리스트


def test_edit_multi_passes_list_in_order(tmp_path):
    client = _RecordingClient()
    f1 = _write_ref(tmp_path, "fp.png", b"FP")
    f2 = _write_ref(tmp_path, "prior.png", b"PRIOR")
    call_gpt_image_bytes(
        client, mode="edit", prompt="multi", ref_paths=[f1, f2],
        call_kwargs={"model": "gpt-image-2.5-sunburst", "size": "1536x864", "quality": "high", "n": 1},
        capture_role="background_render",
    )
    method, kwargs, image_repr = client.images.calls[0]
    assert method == "edit"
    assert image_repr[0] == "list"
    assert image_repr[1] == [str(f1), str(f2)]  # 순서 보존


def test_captures_inside_scope_once_with_metadata():
    client = _RecordingClient()
    with generation_context("p-b0", "e-b0", "floor_plan") as q:
        out = call_gpt_image_bytes(
            client, mode="generate", prompt="p", ref_paths=None,
            call_kwargs={"model": "gpt-image-2.5-sunburst", "size": "1024x1024", "quality": "high", "n": 1},
            capture_role="floor_plan",
            capture_metadata={"budget_source": "location_floor_plan.generate", "ref_count": 0},
        )
        assert len(q._items) == 1
        _spool, meta = q._items[0]
        assert meta["pipeline_role"] == "floor_plan"
        assert meta["prompt"] == "p"
        assert meta["pipeline_metadata"] == {
            "budget_source": "location_floor_plan.generate", "ref_count": 0
        }
        assert out  # bytes 반환


def test_no_capture_outside_scope_but_returns_bytes():
    client = _RecordingClient()
    before = CAPTURE_DIAG["skipped_no_context"]
    out = call_gpt_image_bytes(
        client, mode="generate", prompt="p", ref_paths=None,
        call_kwargs={"model": "gpt-image-2.5-sunburst", "size": "1024x1024"},
        capture_role="floor_plan",
    )
    assert CAPTURE_DIAG["skipped_no_context"] == before + 1
    assert out  # production 경로(scope 없음)도 bytes 정상 반환


def test_min_size_guard_skips_capture_but_returns_small_bytes():
    """bytes 가 min_capture_bytes 미만이면 capture 만 skip, bytes 는 그대로 반환."""
    client = _RecordingClient(decoded=b"tiny")  # 4 bytes < 1024
    with generation_context("p-b0s", "e-b0s", "floor_plan") as q:
        out = call_gpt_image_bytes(
            client, mode="generate", prompt="p", ref_paths=None,
            call_kwargs={"model": "gpt-image-2.5-sunburst", "size": "1024x1024"},
            capture_role="floor_plan",
        )
        assert len(q._items) == 0  # capture skip
    assert out == b"tiny"  # 사이트가 too-small 검증하도록 bytes 는 반환


def test_empty_response_returns_empty_bytes_no_raise():
    """resp.data 비었으면 b'' 반환(사이트가 자기 error type 으로 raise)."""
    client = _RecordingClient()
    client.images.generate = lambda **kw: SimpleNamespace(data=[])  # 빈 응답
    out = call_gpt_image_bytes(
        client, mode="generate", prompt="p", ref_paths=None,
        call_kwargs={"model": "gpt-image-2.5-sunburst", "size": "1024x1024"},
        capture_role="floor_plan",
    )
    assert out == b""


def test_edit_mode_without_refs_raises_value_error():
    """mode='edit' 인데 ref_paths 가 비면 caller 오류 — 명시적 ValueError."""
    client = _RecordingClient()
    with pytest.raises(ValueError):
        call_gpt_image_bytes(
            client, mode="edit", prompt="p", ref_paths=[],
            call_kwargs={"model": "gpt-image-2.5-sunburst"}, capture_role="x",
        )
