"""scene/sensors.py の仕様ベーステスト．

仕様出典:
- docs/04_SPEC/SPEC_02_シーン定義仕様.md（sensor セクション）
- docs/03_PLAN/PLAN_02_開発ステップ.md（Step 4: scene/sensors.py の役割）

build_sensor_dict の戻り値 dict 構造のみを検証する．Mitsuba の load_dict は呼ばない．
ただし mi.ScalarTransform4f を内部で使うため mitsuba のインポートは発生する．
"""

from __future__ import annotations

import pytest

from sm_tias_lightsim.scene.schema import FilmConfig, SensorConfig
from sm_tias_lightsim.scene.sensors import build_sensor_dict


def _make_sensor(
    *,
    position=(0.0, 0.0, 100.0),
    target=(0.0, 0.0, 0.0),
    up=(0.0, 1.0, 0.0),
    fov_deg=60.0,
    width=640,
    height=480,
    rfilter="gaussian",
) -> SensorConfig:
    """テスト用 perspective SensorConfig を組み立てるヘルパ．"""
    return SensorConfig(
        type="perspective",
        position=position,
        target=target,
        up=up,
        fov_deg=fov_deg,
        film=FilmConfig(width=width, height=height, rfilter=rfilter),
    )


def _make_ortho_sensor(
    *,
    position=(0.0, 0.0, 100.0),
    target=(0.0, 0.0, 0.0),
    up=(0.0, 1.0, 0.0),
    ortho_scale=(50.0, 52.0),
    width=640,
    height=480,
    rfilter="gaussian",
) -> SensorConfig:
    """テスト用 orthographic SensorConfig を組み立てるヘルパ．"""
    return SensorConfig(
        type="orthographic",
        position=position,
        target=target,
        up=up,
        fov_deg=None,
        ortho_scale=ortho_scale,
        film=FilmConfig(width=width, height=height, rfilter=rfilter),
    )


# ---------------------------------------------------------------------------
# type
# ---------------------------------------------------------------------------


def test_sensor_type_is_perspective():
    """build_sensor_dict の戻り値 type が "perspective" であること．

    仕様: sensor.type は perspective のみ対応 (SPEC_02 sensor セクション)
    """
    sensor = _make_sensor()
    result = build_sensor_dict(sensor, position_scale=0.001)
    assert result["type"] == "perspective"


# ---------------------------------------------------------------------------
# fov
# ---------------------------------------------------------------------------


def test_sensor_fov_matches_fov_deg():
    """fov_deg がそのまま fov キーに格納されること．

    仕様: fov_deg → fov (SPEC_02 sensor セクション)
    """
    sensor = _make_sensor(fov_deg=45.0)
    result = build_sensor_dict(sensor, position_scale=0.001)
    assert result["fov"] == pytest.approx(45.0)


# ---------------------------------------------------------------------------
# film
# ---------------------------------------------------------------------------


def test_sensor_film_width_and_height():
    """film の width / height が正しく dict に格納されること．

    仕様: film.width, film.height (SPEC_02 sensor セクション)
    """
    sensor = _make_sensor(width=1920, height=1080)
    result = build_sensor_dict(sensor, position_scale=0.001)
    assert result["film"]["width"] == 1920
    assert result["film"]["height"] == 1080


def test_sensor_film_type_is_hdrfilm():
    """film の type が "hdrfilm" であること．

    仕様: Mitsuba の HDR フィルム型 (SPEC_01 render/output 責務)
    """
    sensor = _make_sensor()
    result = build_sensor_dict(sensor, position_scale=0.001)
    assert result["film"]["type"] == "hdrfilm"


def test_sensor_film_rfilter_gaussian():
    """rfilter が "gaussian" のとき rfilter dict の type が "gaussian" になること．

    仕様: film.rfilter (SPEC_02 sensor セクション)
    """
    sensor = _make_sensor(rfilter="gaussian")
    result = build_sensor_dict(sensor, position_scale=0.001)
    assert result["film"]["rfilter"]["type"] == "gaussian"


def test_sensor_film_rfilter_box():
    """rfilter が "box" のとき rfilter dict の type が "box" になること．

    仕様: film.rfilter: gaussian | box | tent (SPEC_02 sensor セクション FilmConfig)
    """
    sensor = _make_sensor(rfilter="box")
    result = build_sensor_dict(sensor, position_scale=0.001)
    assert result["film"]["rfilter"]["type"] == "box"


# ---------------------------------------------------------------------------
# to_world
# ---------------------------------------------------------------------------


def test_sensor_to_world_key_exists():
    """build_sensor_dict の戻り値に to_world キーが存在すること．

    仕様: perspective sensor は to_world で視点変換を持つ (SPEC_01 scene/sensors 責務)
    """
    sensor = _make_sensor()
    result = build_sensor_dict(sensor, position_scale=0.001)
    assert "to_world" in result


def test_sensor_position_scale_affects_to_world():
    """position_scale が異なると to_world が変わること（文字列化で差分確認）．

    仕様: position_scale で position/target が変換される (SPEC_01 scene/sensors 責務)
    """
    sensor = _make_sensor(position=(0.0, 0.0, 100.0), target=(0.0, 0.0, 0.0))
    result_mm = build_sensor_dict(sensor, position_scale=0.001)
    result_m = build_sensor_dict(sensor, position_scale=1.0)
    assert str(result_mm["to_world"]) != str(result_m["to_world"])


# ---------------------------------------------------------------------------
# sampler
# ---------------------------------------------------------------------------


def test_sensor_sampler_type_is_independent():
    """sampler の type が "independent" であること．

    仕様: spp は render 時に渡す (runner.py 設計)
    """
    sensor = _make_sensor()
    result = build_sensor_dict(sensor, position_scale=0.001)
    assert result["sampler"]["type"] == "independent"


# ---------------------------------------------------------------------------
# 境界値: 様々な position_scale
# ---------------------------------------------------------------------------


def test_sensor_position_scale_1_leaves_position_in_to_world():
    """position_scale=1.0 のとき position_scale=0.001 と to_world が異なること．

    座標が 1000 倍差があれば to_world 行列も異なるはず．
    """
    sensor_mm = _make_sensor(position=(0.0, 0.0, 500.0), target=(0.0, 0.0, 0.0))
    result_mm = build_sensor_dict(sensor_mm, position_scale=0.001)
    result_m = build_sensor_dict(sensor_mm, position_scale=1.0)
    assert str(result_mm["to_world"]) != str(result_m["to_world"])


# ---------------------------------------------------------------------------
# orthographic センサ対応（Step 8 追加）
# ---------------------------------------------------------------------------


def test_ortho_sensor_type_is_orthographic():
    """orthographic SensorConfig の build_sensor_dict 戻り値 type が "orthographic" であること．

    仕様: sensor.type = orthographic → Mitsuba sensor dict の type = "orthographic"
         （sensors.py build_sensor_dict 分岐）
    """
    sensor = _make_ortho_sensor()
    result = build_sensor_dict(sensor, position_scale=0.001)
    assert result["type"] == "orthographic"


def test_ortho_sensor_has_no_fov_key():
    """orthographic センサの dict に "fov" キーが存在しないこと．

    仕様: orthographic では fov は不要（Mitsuba orthographic は scale で撮影範囲を決める）
         （sensors.py build_sensor_dict 分岐）
    """
    sensor = _make_ortho_sensor()
    result = build_sensor_dict(sensor, position_scale=0.001)
    assert "fov" not in result


def test_ortho_sensor_has_to_world_key():
    """orthographic センサの dict に "to_world" キーが存在すること．

    仕様: orthographic sensor は look_at × scale で to_world を生成する
         （sensors.py build_sensor_dict 分岐）
    """
    sensor = _make_ortho_sensor()
    result = build_sensor_dict(sensor, position_scale=0.001)
    assert "to_world" in result


def test_ortho_sensor_has_film_and_sampler():
    """orthographic センサの dict に "film" と "sampler" が含まれること．

    仕様: perspective / orthographic 共通で film: hdrfilm, sampler: independent を持つ
         （sensors.py build_sensor_dict）
    """
    sensor = _make_ortho_sensor()
    result = build_sensor_dict(sensor, position_scale=0.001)
    assert result["film"]["type"] == "hdrfilm"
    assert result["sampler"]["type"] == "independent"


def test_ortho_sensor_to_world_scale_applied():
    """ortho_scale=(50, 52)+position_scale=0.001 のとき，perspective と to_world が異なること．

    仕様: orthographic の to_world は look_at × scale(sx*ps, sy*ps, 1.0) で構築される
         perspective の to_world は look_at のみのため，両者は必ず異なる
         （sensors.py build_sensor_dict 分岐）
    """
    position = (0.0, 0.0, 100.0)
    target = (0.0, 0.0, 0.0)
    up = (0.0, 1.0, 0.0)
    ortho_sensor = _make_ortho_sensor(
        position=position, target=target, up=up, ortho_scale=(50.0, 52.0)
    )
    persp_sensor = _make_sensor(position=position, target=target, up=up, fov_deg=60.0)
    ortho_result = build_sensor_dict(ortho_sensor, position_scale=0.001)
    persp_result = build_sensor_dict(persp_sensor, position_scale=0.001)
    # scale が追加された分，to_world 文字列表現が異なるはず
    assert str(ortho_result["to_world"]) != str(persp_result["to_world"])


def test_ortho_sensor_different_scales_produce_different_to_world():
    """ortho_scale が異なると to_world が変わること．

    仕様: ortho_scale の値が to_world の scale 変換に反映される
         （sensors.py build_sensor_dict: sx_m = ortho_scale[0] * position_scale）
    """
    sensor_small = _make_ortho_sensor(ortho_scale=(25.0, 26.0))
    sensor_large = _make_ortho_sensor(ortho_scale=(50.0, 52.0))
    result_small = build_sensor_dict(sensor_small, position_scale=0.001)
    result_large = build_sensor_dict(sensor_large, position_scale=0.001)
    assert str(result_small["to_world"]) != str(result_large["to_world"])
