"""Tests for the 2026-05-26 gpt-image-2 substrate improvements:

- ``_validate_gpt_image_2_size_override``: enforces the OpenAI custom-size
  constraints (≤3840 edge, ≤8.29 MP, multiples of 16, ≤3:1 AR).
- ``_resolve_gpt_image_2_size_with_hints``: hint wins when valid, falls back
  to the preset map when missing or invalid.
- ``_estimate_gpt_image_2_cost``: per-tier base × pixel-count ratio. Used in
  place of fal billing extraction because fal does not expose gpt-image-2
  cost in the response body or headers.
"""

from __future__ import annotations

from types import SimpleNamespace

from recoil.execution.providers.fal import (
    _estimate_gpt_image_2_cost,
    _resolve_gpt_image_2_size_with_hints,
    _validate_gpt_image_2_size_override,
)


# ---------------------------------------------------------------------------
# size override validator
# ---------------------------------------------------------------------------


def test_validator_accepts_canonical_2k_landscape():
    assert _validate_gpt_image_2_size_override("2048x1152") == "2048x1152"


def test_validator_accepts_canonical_4k_landscape():
    assert _validate_gpt_image_2_size_override("3840x2160") == "3840x2160"


def test_validator_rejects_edge_over_3840():
    assert _validate_gpt_image_2_size_override("4096x1024") is None


def test_validator_rejects_pixels_over_4k_ceiling():
    # 3840 × 2192 = 8,417,280 > 8,294,400 (4K ceiling)
    assert _validate_gpt_image_2_size_override("3840x2192") is None


def test_validator_rejects_non_multiple_of_16():
    assert _validate_gpt_image_2_size_override("2050x1152") is None
    assert _validate_gpt_image_2_size_override("2048x1150") is None


def test_validator_rejects_aspect_ratio_over_3_to_1():
    # 3072 × 1008 = 3,096,576 px (OK), but 3072/1008 ≈ 3.05 (FAIL)
    assert _validate_gpt_image_2_size_override("3072x1008") is None


def test_validator_rejects_malformed_strings():
    for bad in ("", "1024", "1024x", "x1024", "1024xfoo", "1024x1024x16"):
        assert _validate_gpt_image_2_size_override(bad) is None, bad


def test_validator_rejects_non_str_inputs():
    assert _validate_gpt_image_2_size_override(None) is None  # type: ignore[arg-type]
    assert _validate_gpt_image_2_size_override(1024) is None  # type: ignore[arg-type]


def test_validator_normalizes_case():
    assert _validate_gpt_image_2_size_override("2048X1152") == "2048x1152"


# ---------------------------------------------------------------------------
# hint-aware size resolver
# ---------------------------------------------------------------------------


def _mk_payload(*, aspect_ratio="16:9", hints=None):
    return SimpleNamespace(aspect_ratio=aspect_ratio, hints=hints)


def test_resolver_prefers_valid_hint_over_aspect_ratio():
    p = _mk_payload(aspect_ratio="1:1", hints={"size_override": "2048x1152"})
    assert _resolve_gpt_image_2_size_with_hints(p) == "2048x1152"


def test_resolver_falls_back_when_hint_missing():
    p = _mk_payload(aspect_ratio="16:9", hints={})
    assert _resolve_gpt_image_2_size_with_hints(p) == "1536x1024"


def test_resolver_falls_back_when_hint_invalid():
    # 4096 exceeds the 3840 edge cap → fall through to preset
    p = _mk_payload(aspect_ratio="9:16", hints={"size_override": "4096x1024"})
    assert _resolve_gpt_image_2_size_with_hints(p) == "1024x1536"


def test_resolver_handles_none_hints():
    p = _mk_payload(aspect_ratio="1:1", hints=None)
    assert _resolve_gpt_image_2_size_with_hints(p) == "1024x1024"


# ---------------------------------------------------------------------------
# cost estimator
# ---------------------------------------------------------------------------


def test_cost_low_quality_at_base_size():
    # fal.ai documented tariff (2026-05-28)
    assert _estimate_gpt_image_2_cost("low", "1024x1024") == 0.01


def test_cost_medium_quality_at_base_size():
    assert _estimate_gpt_image_2_cost("medium", "1024x1024") == 0.06


def test_cost_high_quality_at_base_size():
    assert _estimate_gpt_image_2_cost("high", "1024x1024") == 0.22


def test_cost_4k_high_matches_published_tariff():
    # The headline number — 4K high is $0.41, NOT $1.66 the old
    # linear-pixel-scaling estimator produced. 4× difference matters.
    assert _estimate_gpt_image_2_cost("high", "3840x2160") == 0.41


def test_cost_documented_intermediate_sizes():
    # Spot-check a few more rows from the published table
    assert _estimate_gpt_image_2_cost("medium", "1920x1080") == 0.04
    assert _estimate_gpt_image_2_cost("high", "2560x1440") == 0.23
    assert _estimate_gpt_image_2_cost("low", "3840x2160") == 0.02


def test_cost_custom_size_rounds_up_to_next_documented_size():
    # Conservative: custom sizes match the smallest documented size whose
    # pixel count is >= the requested. 2048×1152 = 2.36M px → next up is
    # 2560×1440 = 3.69M px, so use that tariff.
    cost = _estimate_gpt_image_2_cost("high", "2048x1152")
    assert cost == 0.23  # 2560×1440 high tariff


def test_cost_unknown_quality_defaults_to_medium():
    # Defensive — caller passed an unexpected tier
    assert _estimate_gpt_image_2_cost("ultra", "1024x1024") == 0.06
    assert _estimate_gpt_image_2_cost("", "1024x1024") == 0.06
    assert _estimate_gpt_image_2_cost(None, "1024x1024") == 0.06  # type: ignore[arg-type]


def test_cost_malformed_size_falls_back_to_base_tariff():
    # Defensive — caller passed a malformed size string
    assert _estimate_gpt_image_2_cost("medium", "garbage") == 0.06
    assert _estimate_gpt_image_2_cost("medium", "") == 0.06


# ---------------------------------------------------------------------------
# pre-submit cost-cap enforcement (C1 fix, 2026-05-28)
# ---------------------------------------------------------------------------


def test_cost_cap_blocks_submit_when_estimate_exceeds_cap(monkeypatch):
    """When the estimated cost > max_cost_per_shot_usd, the adapter must
    raise BEFORE submitting (not after). Monkeypatches the cap low instead
    of relying on tariff math — keeps the test robust to future tariff
    changes and decouples the enforcement contract from absolute prices.

    Pre-2026-05-28 this test asserted 4K-high blocks at the $1.00 cap
    because the old linear-pixel estimator returned $1.66. After the
    fal-blog tariff rebuild (2026-05-28), 4K-high is $0.41 — well under
    the cap. The enforcement code is unchanged; the test now uses a
    synthetic cap to exercise the block path.
    """
    from types import SimpleNamespace
    from recoil.execution.providers.fal import FalAdapter
    import recoil.execution.providers.fal as fal_mod
    from recoil.core import model_profiles

    submit_called = {"count": 0}

    class _MockTransport:
        def submit_queue(self, *_a, **_kw):
            submit_called["count"] += 1
            return {"request_id": "should-never-fire"}

    monkeypatch.setattr(fal_mod._fal_transport, "FalTransport", _MockTransport)
    # Synthetic profile with cap below the cheapest documented tier ($0.01).
    monkeypatch.setattr(
        model_profiles, "get_profile",
        lambda model: {"max_cost_per_shot_usd": 0.005},
    )

    payload = SimpleNamespace(
        prompt="x",
        aspect_ratio="1:1",
        reference_images=None,
        model_id="gpt-image-2",
        hints={"quality": "low"},
        quality=None,
    )

    import pytest
    with pytest.raises(RuntimeError, match="max_cost_per_shot_usd"):
        FalAdapter().direct_submit_image(payload)
    assert submit_called["count"] == 0, "submit_queue must NOT be called when cap exceeded"


def test_wire_shape_image_size_is_object_not_string():
    """REGRESSION: fal's gpt-image-2 endpoints take image_size as
    {"width": W, "height": H}, NOT a "WxH" string named 'size'.

    From 2026-05-19 through 2026-05-28 12:57 UTC the adapter sent
    body["size"]="1024x1024" — wrong field name + wrong shape — which
    fal silently ignored, defaulting all outputs to landscape_4_3
    (1024×768) regardless of the size requested. Every gpt-image-2 fire
    in that window was 1024×768 instead of the requested resolution.
    JT caught it via PIL on the v1+v2 location-sheet fires.

    Lock the wire shape so the bug class can't silently re-emerge.
    """
    from types import SimpleNamespace
    from recoil.execution.providers.fal import _build_gpt_image_2_body

    payload = SimpleNamespace(
        prompt="x",
        aspect_ratio="16:9",
        reference_images=None,
        model_id="gpt-image-2",
        hints={"quality": "high", "size_override": "3840x2160"},
        quality=None,
    )
    _endpoint, body = _build_gpt_image_2_body(payload)

    # Must use the canonical field name + object shape.
    assert "image_size" in body, "fal wire field is image_size, not size"
    assert "size" not in body, "string 'size' field is silently ignored by fal"
    assert body["image_size"] == {"width": 3840, "height": 2160}


def test_wire_shape_image_size_for_preset_aspect():
    """Same wire-shape regression for the standard aspect-ratio preset path
    (no size_override). 16:9 → 1536×1024."""
    from types import SimpleNamespace
    from recoil.execution.providers.fal import _build_gpt_image_2_body

    payload = SimpleNamespace(
        prompt="x",
        aspect_ratio="16:9",
        reference_images=None,
        model_id="gpt-image-2",
        hints={"quality": "medium"},
        quality=None,
    )
    _endpoint, body = _build_gpt_image_2_body(payload)
    assert body["image_size"] == {"width": 1536, "height": 1024}
    assert "size" not in body


def test_cost_cap_high_at_preset_size_allowed(monkeypatch):
    """high quality at 1024x1024 = $0.22 — well under $1.00 cap. Should submit normally."""
    from types import SimpleNamespace
    from recoil.execution.providers.fal import FalAdapter
    import recoil.execution.providers.fal as fal_mod

    class _MockTransport:
        def submit_queue(self, *_a, **_kw):
            return {"request_id": "ok", "status_url": "u", "response_url": "u"}

        def poll_status(self, *_a, **_kw):
            return {"status": "COMPLETED"}

        def fetch_result(self, *_a, **_kw):
            return {"images": [{"url": "https://cdn.fal.ai/x.png"}]}

    monkeypatch.setattr(fal_mod._fal_transport, "FalTransport", _MockTransport)

    class _Resp:
        def read(self):
            return b"\x89PNG"

        def __enter__(self):
            return self

        def __exit__(self, *a):
            return False

    monkeypatch.setattr(fal_mod.urllib.request, "urlopen", lambda *_a, **_kw: _Resp())

    payload = SimpleNamespace(
        prompt="x",
        aspect_ratio="1:1",
        reference_images=None,
        model_id="gpt-image-2",
        hints={"quality": "high"},
        quality=None,
    )
    # Should not raise
    raw = FalAdapter().direct_submit_image(payload)
    assert raw["cost_usd"] == 0.22  # 1024×1024 high per fal-blog tariff
    assert raw["metadata"]["cost_estimator_inputs"]["quality"] == "high"
    assert raw["metadata"]["cost_estimator_inputs"]["size"] == "1024x1024"
