"""面光源のサイズ→世界座標軸マッピングの再発防止テスト．

仕様出典:
- docs/06_TEST/TEST_08_面光源スケール軸バグの発見と修正.md
- docs/06_TEST/TEST_13_物理モデル健全性の一括監査.md

TEST_08 のバグは `scale([sw / 2.0, 1.0, sh / 2.0])` と，発光面の高さを
縦(Y) ではなく奥行き(Z) に入れていたもの．LED の実寸 2.8 x 3.5 mm が
2.8 x (潰れた厚み) になり，下灯だけ 2.1 倍明るいという不一致を生んだ．
エラーは出ず，しかも TEST_07 でその出力が「物理的に正しい」と一度は正当化された．

既存の tests/scene/test_lights.py は to_world キーの存在と，position_scale を
変えたときに文字列表現が変わることしか検証しておらず，**軸を入れ替えても検出できない**．
本モジュールは単位図形の頂点を実際に to_world で変換し，世界座標での寸法を検証する．
"""

from __future__ import annotations

import numpy as np
import pytest

from sm_tias_lightsim.scene.lights import build_light_dict
from sm_tias_lightsim.scene.schema import AreaLight

# Mitsuba の rectangle は XY 平面上の ±1 正方形（法線 +Z）．
# 発光面は direction=(0,0,-1) で世界座標の XY 平面に載るので，
# 変換後の X / Y 方向の広がりがそのまま実寸になる．
_UNIT_RECTANGLE_CORNERS = [(-1.0, -1.0, 0.0), (1.0, -1.0, 0.0), (1.0, 1.0, 0.0), (-1.0, 1.0, 0.0)]

_MM_TO_M = 0.001


def _world_extents(light: AreaLight) -> tuple[float, float, float]:
    """発光面の単位図形を to_world で変換し，世界座標での XYZ の広がりを返す．

    Args:
        light: 検査対象の AreaLight．

    Returns:
        (X の広がり, Y の広がり, Z の広がり)．単位は m．
    """
    import mitsuba as mi

    to_world = build_light_dict(light, position_scale=_MM_TO_M)["to_world"]
    points = np.array(
        [np.asarray(to_world @ mi.ScalarPoint3f(*c)).reshape(3) for c in _UNIT_RECTANGLE_CORNERS]
    )
    return (
        float(np.ptp(points[:, 0])),
        float(np.ptp(points[:, 1])),
        float(np.ptp(points[:, 2])),
    )


def _rectangle(size: tuple[float, float]) -> AreaLight:
    """XY 平面に載る矩形発光面を作る（direction は評価板と反対向きの基準設計と同じ）．"""
    return AreaLight(
        id="led",
        type="area",
        shape="rectangle",
        center=(0.0, 0.0, 0.0),
        size=size,
        direction=(0.0, 0.0, -1.0),
        radiance=1.0,
    )


def test_size_width_maps_to_world_x_extent():
    """size[0]（幅）が世界座標 X 方向の広がりになること．

    仕様: SPEC_02 の size は [幅, 高さ]．TEST_08 で高さが Z に入る取り違えがあった．
    """
    extent_x, _, _ = _world_extents(_rectangle((2.8, 3.5)))
    assert extent_x == pytest.approx(2.8 * _MM_TO_M)


def test_size_height_maps_to_world_y_extent():
    """size[1]（高さ）が世界座標 Y 方向の広がりになること．

    これが TEST_08 のバグそのもの（高さが Y ではなく Z に入っていた）．
    """
    _, extent_y, _ = _world_extents(_rectangle((2.8, 3.5)))
    assert extent_y == pytest.approx(3.5 * _MM_TO_M)


def test_emitter_has_no_thickness_in_normal_direction():
    """発光面は法線方向（Z）に厚みを持たないこと．

    高さが Z に入っていると，この広がりが 0 でなくなる．
    """
    _, _, extent_z = _world_extents(_rectangle((2.8, 3.5)))
    assert extent_z == pytest.approx(0.0, abs=1e-12)


def test_swapping_size_swaps_world_extents():
    """size を入れ替えると世界座標の広がりも入れ替わること．

    軸マッピングが size に実際に追随していることの確認（定数を返す実装を排除する）．
    """
    x1, y1, _ = _world_extents(_rectangle((2.8, 3.5)))
    x2, y2, _ = _world_extents(_rectangle((3.5, 2.8)))
    assert x2 == pytest.approx(y1)
    assert y2 == pytest.approx(x1)


def test_emitter_area_matches_physical_size():
    """発光面の面積が実寸どおりであること．

    面積は放射束 Phi = pi * L * A を決めるので，ここがずれると
    絶対輝度が系統的にずれる（TEST_08 の「下灯 2.1 倍」の正体）．
    """
    extent_x, extent_y, _ = _world_extents(_rectangle((2.8, 3.5)))
    assert extent_x * extent_y == pytest.approx(2.8 * 3.5 * _MM_TO_M**2)


@pytest.mark.parametrize(("width", "height"), [(1.0, 1.0), (2.8, 3.5), (3.5, 2.8), (10.0, 0.5)])
def test_extents_match_size_for_various_aspect_ratios(width, height):
    """様々なアスペクト比で幅・高さが正しく対応すること．"""
    extent_x, extent_y, _ = _world_extents(_rectangle((width, height)))
    assert extent_x == pytest.approx(width * _MM_TO_M)
    assert extent_y == pytest.approx(height * _MM_TO_M)


def test_disk_extents_are_the_diameter_on_both_axes():
    """円板発光面は両軸とも直径 2r の広がりを持つこと．"""
    light = AreaLight(
        id="disk",
        type="area",
        shape="disk",
        center=(0.0, 0.0, 0.0),
        radius=5.0,
        direction=(0.0, 0.0, -1.0),
        radiance=1.0,
    )
    # 単位円板の外接正方形（±1）が半径でスケールされる
    extent_x, extent_y, _ = _world_extents(light)
    assert extent_x == pytest.approx(2 * 5.0 * _MM_TO_M)
    assert extent_y == pytest.approx(2 * 5.0 * _MM_TO_M)
