"""W20E5 — floor_plan_render image_call_budget wiring (RED).

Mirrors test_background_render_cap.py for ``render_one_floor_plan``. Each
``openai_client.images.edit`` / ``openai_client.images.generate`` call must
reserve one budget unit before the request; cap exhaustion raises
``ImageCallBudgetExceeded``.
"""
from __future__ import annotations

from unittest.mock import MagicMock

import pytest

from app.core.image_call_budget import (
    ImageCallBudget,
    ImageCallBudgetExceeded,
    install_budget,
    uninstall_budget,
)
from app.modules.pipeline.floor_plan_render import render_one_floor_plan


@pytest.fixture(autouse=True)
def _isolate_budget():
    uninstall_budget()
    yield
    uninstall_budget()


def _ok_response(b64: str = "aGVsbG8="):
    resp = MagicMock()
    resp.data = [MagicMock(b64_json=b64)]
    return resp


def test_no_budget_installed_keeps_legacy_behaviour(tmp_path):
    client = MagicMock()
    client.images.generate.return_value = _ok_response()
    out = tmp_path / "fp.png"
    res = render_one_floor_plan(
        openai_client=client, image_model="gpt-image-2.5-sunburst", prompt="x",
        out_path=out, ref_paths=[], max_attempts=1,
    )
    assert res.status == "ok"
    assert client.images.generate.call_count == 1


def test_cap_zero_blocks_text_only_generate(tmp_path):
    install_budget(ImageCallBudget(cap=0))
    client = MagicMock()
    client.images.generate.return_value = _ok_response()
    out = tmp_path / "fp.png"
    with pytest.raises(ImageCallBudgetExceeded):
        render_one_floor_plan(
            openai_client=client, image_model="gpt-image-2.5-sunburst", prompt="x",
            out_path=out, ref_paths=[], max_attempts=1,
        )
    assert client.images.generate.call_count == 0


def test_cap_zero_blocks_single_edit(tmp_path):
    install_budget(ImageCallBudget(cap=0))
    client = MagicMock()
    client.images.edit.return_value = _ok_response()
    ref = tmp_path / "r.png"
    ref.write_bytes(b"R")
    out = tmp_path / "fp.png"
    with pytest.raises(ImageCallBudgetExceeded):
        render_one_floor_plan(
            openai_client=client, image_model="gpt-image-2.5-sunburst", prompt="x",
            out_path=out, ref_paths=[ref], max_attempts=1,
        )
    assert client.images.edit.call_count == 0


def test_cap_zero_blocks_multi_edit(tmp_path):
    install_budget(ImageCallBudget(cap=0))
    client = MagicMock()
    client.images.edit.return_value = _ok_response()
    r1 = tmp_path / "a.png"; r1.write_bytes(b"A")
    r2 = tmp_path / "b.png"; r2.write_bytes(b"B")
    out = tmp_path / "fp.png"
    with pytest.raises(ImageCallBudgetExceeded):
        render_one_floor_plan(
            openai_client=client, image_model="gpt-image-2.5-sunburst", prompt="x",
            out_path=out, ref_paths=[r1, r2], max_attempts=1,
        )
    assert client.images.edit.call_count == 0


def test_cap_one_allows_exactly_one_successful_call(tmp_path):
    budget = ImageCallBudget(cap=1)
    install_budget(budget)
    client = MagicMock()
    client.images.generate.return_value = _ok_response()
    out = tmp_path / "fp.png"
    res = render_one_floor_plan(
        openai_client=client, image_model="gpt-image-2.5-sunburst", prompt="x",
        out_path=out, ref_paths=[], max_attempts=3,
    )
    assert res.status == "ok"
    assert client.images.generate.call_count == 1
    assert budget.snapshot()["used"] == 1


def test_cap_two_exhausts_during_transient_retries(tmp_path):
    budget = ImageCallBudget(cap=2)
    install_budget(budget)
    client = MagicMock()
    client.images.generate.side_effect = RuntimeError("upstream 503")
    out = tmp_path / "fp.png"
    with pytest.raises(ImageCallBudgetExceeded):
        render_one_floor_plan(
            openai_client=client, image_model="gpt-image-2.5-sunburst", prompt="x",
            out_path=out, ref_paths=[], max_attempts=4,
        )
    assert client.images.generate.call_count == 2
    assert budget.snapshot()["used"] == 2
    assert budget.snapshot()["denied"] == 1
