"""Task B1 — gpt 사이트 wrapper 치환 call-args characterization 테스트.

stochastic 모델이라 출력 PNG 동일성은 검증 불가 → "요청 kwargs 동일"이 결정론 기준
(Codex 합의). 이 테스트는 각 사이트가 ``openai_client.images.{generate,edit}`` 를
어떤 method/kwargs/image 형태로 호출하는지 고정한다. 치환 **전** 현재 코드에서 GREEN 을
확인한 뒤(=현 동작 캡처), wrapper 로 치환해도 동일 kwargs 면 GREEN 이 유지된다
(=byte-identical 보존 증명). file handle 객체 동일성은 비교 안 하고 .name 순서로 검증.
"""

import base64
from types import SimpleNamespace

_BIG = b"\x89PNG\r\n\x1a\n" + b"\x00" * 4096  # decode 후 >1024 bytes


class RecordingImages:
    def __init__(self, decoded: bytes = _BIG):
        self._b64 = base64.b64encode(decoded).decode("ascii")
        self.decoded = decoded
        self.calls = []  # (method, kwargs(no image), 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):
        assert "image" not in kwargs
        self.calls.append(("generate", dict(kwargs), None))
        return SimpleNamespace(data=[SimpleNamespace(b64_json=self._b64)])

    def edit(self, **kwargs):
        image = kwargs.pop("image")
        self.calls.append(("edit", dict(kwargs), self._image_repr(image)))
        return SimpleNamespace(data=[SimpleNamespace(b64_json=self._b64)])


class RecordingClient:
    def __init__(self, decoded: bytes = _BIG):
        self.images = RecordingImages(decoded)


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


# ─────────────────────────── floor_plan_render ───────────────────────────

def _run_fp(client, tmp_path, ref_paths):
    from app.modules.pipeline.floor_plan_render import render_one_floor_plan

    return render_one_floor_plan(
        openai_client=client,
        image_model="gpt-image-2.5-sunburst",
        prompt="draw a floor plan",
        out_path=tmp_path / "fp_out.png",
        ref_paths=ref_paths,
        fp_id="fp1",
    )


def test_floor_plan_generate_callargs(tmp_path):
    client = RecordingClient()
    res = _run_fp(client, tmp_path, [])
    assert res.status == "ok"
    method, kwargs, image_repr = client.images.calls[0]
    assert method == "generate"
    assert image_repr is None
    assert kwargs == {
        "model": "gpt-image-2.5-sunburst", "prompt": "draw a floor plan",
        "size": "1024x1024", "quality": "high", "n": 1,
    }
    assert (tmp_path / "fp_out.png").read_bytes() == _BIG


def test_floor_plan_edit_single_callargs(tmp_path):
    client = RecordingClient()
    ref = _ref(tmp_path, "r1.png")
    res = _run_fp(client, tmp_path, [ref])
    assert res.status == "ok"
    method, kwargs, image_repr = client.images.calls[0]
    assert method == "edit"
    assert image_repr == ("single", str(ref))
    assert kwargs == {
        "model": "gpt-image-2.5-sunburst", "prompt": "draw a floor plan",
        "size": "1024x1024", "quality": "high", "n": 1,
    }


def test_floor_plan_edit_multi_callargs_in_order(tmp_path):
    client = RecordingClient()
    f1 = _ref(tmp_path, "a.png", b"AAAA")
    f2 = _ref(tmp_path, "b.png", b"BBBB")
    res = _run_fp(client, tmp_path, [f1, f2])
    assert res.status == "ok"
    method, kwargs, image_repr = client.images.calls[0]
    assert method == "edit"
    assert image_repr == ("list", [str(f1), str(f2)])
    assert kwargs == {
        "model": "gpt-image-2.5-sunburst", "prompt": "draw a floor plan",
        "size": "1024x1024", "quality": "high", "n": 1,
    }


# ─────────────────────────── background_render ───────────────────────────

def _run_bg(client, tmp_path, fp_path, prior_bg_paths):
    from app.modules.pipeline.background_render import render_one_background

    return render_one_background(
        openai_client=client,
        image_model="gpt-image-2.5-sunburst",
        prompt="a background plate",
        out_path=tmp_path / "bg_out.png",
        fp_path=fp_path,
        prior_bg_paths=prior_bg_paths,
        bg_id="bg1",
    )


def test_background_generate_callargs(tmp_path):
    client = RecordingClient()
    info = _run_bg(client, tmp_path, None, [])
    assert info["status"] == "ok"
    method, kwargs, image_repr = client.images.calls[0]
    assert method == "generate"
    assert image_repr is None
    assert kwargs == {
        "model": "gpt-image-2.5-sunburst", "prompt": "a background plate",
        "size": "1536x864", "quality": "high", "n": 1,
    }
    assert (tmp_path / "bg_out.png").read_bytes() == _BIG


def test_background_edit_single_callargs(tmp_path):
    client = RecordingClient()
    fp = _ref(tmp_path, "fp.png")
    info = _run_bg(client, tmp_path, fp, [])
    assert info["status"] == "ok"
    method, kwargs, image_repr = client.images.calls[0]
    assert method == "edit"
    assert image_repr == ("single", str(fp))
    assert kwargs == {
        "model": "gpt-image-2.5-sunburst", "prompt": "a background plate",
        "size": "1536x864", "quality": "high", "n": 1,
    }


def test_background_edit_multi_callargs_fp_then_prior(tmp_path):
    client = RecordingClient()
    fp = _ref(tmp_path, "fp.png", b"FPFP")
    prior = _ref(tmp_path, "prior.png", b"PRIOR")
    info = _run_bg(client, tmp_path, fp, [prior])
    assert info["status"] == "ok"
    method, kwargs, image_repr = client.images.calls[0]
    assert method == "edit"
    assert image_repr == ("list", [str(fp), str(prior)])  # fp 1순위 → prior


# ─────────────────────────── background_chain_render ───────────────────────

def _run_chain(client, tmp_path, ref_paths):
    from app.modules.pipeline.background_chain_render import render_node_image

    return render_node_image(
        openai_client=client,
        image_model="gpt-image-2.5-sunburst",
        prompt="a chained background node",
        out_path=tmp_path / "node_out.png",
        ref_paths=ref_paths,
    )


def test_chain_generate_callargs(tmp_path):
    client = RecordingClient()
    info = _run_chain(client, tmp_path, [])
    assert info["status"] == "ok"
    method, kwargs, image_repr = client.images.calls[0]
    assert method == "generate"
    assert image_repr is None
    assert kwargs == {
        "model": "gpt-image-2.5-sunburst", "prompt": "a chained background node",
        "size": "1024x1024", "quality": "high", "n": 1,
    }
    assert (tmp_path / "node_out.png").read_bytes() == _BIG


def test_chain_edit_single_callargs(tmp_path):
    client = RecordingClient()
    ref = _ref(tmp_path, "parent.png")
    info = _run_chain(client, tmp_path, [ref])
    assert info["status"] == "ok"
    method, kwargs, image_repr = client.images.calls[0]
    assert method == "edit"
    assert image_repr == ("single", str(ref))
    assert kwargs == {
        "model": "gpt-image-2.5-sunburst", "prompt": "a chained background node",
        "size": "1024x1024", "quality": "high", "n": 1,
    }


def test_chain_edit_multi_callargs_in_order(tmp_path):
    client = RecordingClient()
    f1 = _ref(tmp_path, "p1.png", b"P1P1")
    f2 = _ref(tmp_path, "p2.png", b"P2P2")
    info = _run_chain(client, tmp_path, [f1, f2])
    assert info["status"] == "ok"
    method, kwargs, image_repr = client.images.calls[0]
    assert method == "edit"
    assert image_repr == ("list", [str(f1), str(f2)])


# ─────────────────────── location_floor_plan ───────────────────────
# ★ 이 사이트는 ref 1개라도 image=[리스트] (항상 multi-edit). valid_refs 필터는
#   파일 존재 + size >= 1024 bytes → ref 콘텐츠를 1024+ 로 만든다.

def _big_ref(tmp_path, name):
    p = tmp_path / name
    p.write_bytes(b"X" * 1100)
    return p


def _run_lfp(client, ref_paths):
    from app.modules.pipeline.location_floor_plan import generate_floor_plan_image

    return generate_floor_plan_image(
        prompt="draw building floor plan",
        openai_client=client,
        ref_paths=ref_paths,
        model="gpt-image-2.5-sunburst",
        size="1024x1024",
        quality="high",
    )


def test_location_fp_generate_callargs():
    client = RecordingClient()
    out = _run_lfp(client, None)
    assert out == _BIG
    method, kwargs, image_repr = client.images.calls[0]
    assert method == "generate"
    assert image_repr is None
    assert kwargs == {
        "model": "gpt-image-2.5-sunburst", "prompt": "draw building floor plan",
        "size": "1024x1024", "quality": "high", "n": 1,
    }


def test_location_fp_single_ref_uses_list(tmp_path):
    client = RecordingClient()
    ref = _big_ref(tmp_path, "prev.png")
    out = _run_lfp(client, [ref])
    assert out == _BIG
    method, kwargs, image_repr = client.images.calls[0]
    assert method == "edit"
    assert image_repr == ("list", [str(ref)])  # ★ 1개라도 리스트
    assert kwargs == {
        "model": "gpt-image-2.5-sunburst", "prompt": "draw building floor plan",
        "size": "1024x1024", "quality": "high", "n": 1,
    }


def test_location_fp_multi_ref_in_order(tmp_path):
    client = RecordingClient()
    f1 = _big_ref(tmp_path, "g1.png")
    f2 = _big_ref(tmp_path, "g2.png")
    out = _run_lfp(client, [f1, f2])
    assert out == _BIG
    method, kwargs, image_repr = client.images.calls[0]
    assert method == "edit"
    assert image_repr == ("list", [str(f1), str(f2)])


# ─────────────────────── space_set_bg_provider ───────────────────────
# ★ _openai_client() 를 내부 생성 → monkeypatch 로 RecordingClient 주입.
#   quality/n 미전달(현 동작). image_edit 는 단일 핸들(image=f).

def _patch_ssp_client(monkeypatch, client):
    import app.modules.pipeline.space_set_bg_provider as ssp

    monkeypatch.setattr(ssp, "_openai_client", lambda: client)
    return ssp


def test_space_set_generate_callargs_no_quality(tmp_path, monkeypatch):
    client = RecordingClient()
    ssp = _patch_ssp_client(monkeypatch, client)
    out = tmp_path / "t2i.png"
    ssp.image_generate(prompt="t2i bg", out_path=str(out), model="gpt-image-2.5-sunburst", size="1536x1024")
    method, kwargs, image_repr = client.images.calls[0]
    assert method == "generate"
    assert image_repr is None
    # quality/n 미주입 — pass-through 가 강제하지 않음
    assert kwargs == {"model": "gpt-image-2.5-sunburst", "prompt": "t2i bg", "size": "1536x1024"}
    assert out.read_bytes() == _BIG


def test_space_set_edit_callargs_single_handle(tmp_path, monkeypatch):
    client = RecordingClient()
    ssp = _patch_ssp_client(monkeypatch, client)
    base = tmp_path / "base.png"
    base.write_bytes(b"BASEDATA")
    out = tmp_path / "edited.png"
    ssp.image_edit(
        base_image_path=str(base), prompt="i2i edit",
        out_path=str(out), model="gpt-image-2.5-sunburst", size="1536x1024",
    )
    method, kwargs, image_repr = client.images.calls[0]
    assert method == "edit"
    assert image_repr == ("single", str(base))  # 단일 핸들
    assert kwargs == {"model": "gpt-image-2.5-sunburst", "prompt": "i2i edit", "size": "1536x1024"}
    assert out.read_bytes() == _BIG
