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

仕様出典: docs/04_SPEC/SPEC_02_シーン定義仕様.md
"""

import copy

import pytest
import yaml
from pydantic import ValidationError

from sm_tias_lightsim.scene.schema import (
    AreaLight,
    DielectricMaterial,
    DiffuseMaterial,
    DirectionalLight,
    PointLight,
    SceneConfig,
    ThinDielectricMaterial,
)

# ---------------------------------------------------------------------------
# 正常系: 最小構成
# ---------------------------------------------------------------------------


def test_minimal_valid_yaml_parses(valid_minimal_yaml):
    """必須 6 セクションのみで SceneConfig が生成されること．

    仕様: name, geometry, materials, lights, sensor, render が必須 (SPEC_02 トップレベル構造)
    """
    data = yaml.safe_load(valid_minimal_yaml)
    config = SceneConfig.model_validate(data)
    assert config.name == "test_scene"
    assert len(config.geometry.meshes) == 1
    assert len(config.lights) == 1


def test_optional_evaluation_section(valid_minimal_dict):
    """evaluation を省略しても SceneConfig が生成されること．

    仕様: evaluation は任意（SPEC_02 evaluation セクション）
    """
    d = copy.deepcopy(valid_minimal_dict)
    d.pop("evaluation", None)
    config = SceneConfig.model_validate(d)
    assert config.evaluation is None


def test_optional_description(valid_minimal_dict):
    """description を省略しても SceneConfig が生成されること．

    仕様: description は任意（SPEC_02 トップレベル構造）
    """
    d = copy.deepcopy(valid_minimal_dict)
    d.pop("description", None)
    config = SceneConfig.model_validate(d)
    assert config.description is None


def test_render_defaults(valid_minimal_dict):
    """render セクションを省略しても既定値が設定されること．

    仕様: render.variant デフォルト llvm_ad_rgb, spp デフォルト 256 (SPEC_02 render セクション)
    """
    d = copy.deepcopy(valid_minimal_dict)
    d.pop("render", None)
    config = SceneConfig.model_validate(d)
    assert config.render.variant == "llvm_ad_rgb"
    assert config.render.spp == 256
    assert config.render.integrator == "path"
    assert config.render.max_depth == 8


# ---------------------------------------------------------------------------
# 正常系: マテリアル
# ---------------------------------------------------------------------------


def test_diffuse_material_with_scalar_reflectance(valid_minimal_dict):
    """reflectance をスカラ値で指定できること．

    仕様: reflectance スカラ可 (SPEC_02 materials セクション)
    """
    d = copy.deepcopy(valid_minimal_dict)
    d["materials"]["mat_diffuse"] = {"type": "diffuse", "reflectance": 0.95}
    config = SceneConfig.model_validate(d)
    mat = config.materials["mat_diffuse"]
    assert isinstance(mat, DiffuseMaterial)
    assert mat.reflectance == pytest.approx(0.95)


def test_diffuse_material_with_rgb_reflectance(valid_minimal_dict):
    """reflectance を RGB リストで指定できること．

    仕様: reflectance: [0.05, 0.05, 0.05] の RGB 表記 (SPEC_02 materials セクション)
    """
    d = copy.deepcopy(valid_minimal_dict)
    d["materials"]["mat_diffuse"] = {"type": "diffuse", "reflectance": [0.05, 0.05, 0.05]}
    config = SceneConfig.model_validate(d)
    mat = config.materials["mat_diffuse"]
    assert isinstance(mat, DiffuseMaterial)
    assert mat.reflectance == pytest.approx((0.05, 0.05, 0.05))


def test_dielectric_and_thindielectric_materials(valid_minimal_dict):
    """dielectric / thindielectric の type が正しく識別されること．

    仕様: type: dielectric / thindielectric の discriminated union (SPEC_02 materials セクション)
    """
    d = copy.deepcopy(valid_minimal_dict)
    d["materials"]["mat_dielectric"] = {
        "type": "dielectric",
        "int_ior": 1.5,
        "ext_ior": 1.0,
    }
    d["materials"]["mat_thin"] = {
        "type": "thindielectric",
        "int_ior": 1.49,
        "ext_ior": 1.0,
    }
    d["geometry"]["meshes"].append(
        {"id": "mesh2", "path": "m.stl", "unit": "mm", "material": "mat_dielectric"}
    )
    d["geometry"]["meshes"].append(
        {"id": "mesh3", "path": "m.stl", "unit": "mm", "material": "mat_thin"}
    )
    config = SceneConfig.model_validate(d)
    assert isinstance(config.materials["mat_dielectric"], DielectricMaterial)
    assert isinstance(config.materials["mat_thin"], ThinDielectricMaterial)


# ---------------------------------------------------------------------------
# 正常系: ライト
# ---------------------------------------------------------------------------


def test_point_light_parses(valid_minimal_dict):
    """point 光源の必須フィールドが正しくパースされること．

    仕様: type: point, position, intensity, profile (SPEC_02 lights セクション)
    """
    d = copy.deepcopy(valid_minimal_dict)
    config = SceneConfig.model_validate(d)
    light = config.lights[0]
    assert isinstance(light, PointLight)
    assert light.id == "led_top"
    assert light.type == "point"
    assert light.position == pytest.approx((0.0, 0.0, 50.0))
    assert light.profile == "lambertian"


def test_area_light_parses(valid_minimal_dict):
    """area 光源の必須フィールドが正しくパースされること．

    仕様: type: area, shape, center, size, direction, radiance (SPEC_02 lights セクション)
    """
    d = copy.deepcopy(valid_minimal_dict)
    d["lights"] = [
        {
            "id": "led_area",
            "type": "area",
            "shape": "rectangle",
            "center": [0.0, 0.0, 20.0],
            "size": [10.0, 10.0],
            "direction": [0.0, 0.0, -1.0],
            "radiance": [50.0, 50.0, 50.0],
        }
    ]
    config = SceneConfig.model_validate(d)
    light = config.lights[0]
    assert isinstance(light, AreaLight)
    assert light.id == "led_area"
    assert light.shape == "rectangle"
    assert light.size == pytest.approx((10.0, 10.0))
    assert light.radiance == pytest.approx((50.0, 50.0, 50.0))


def test_directional_light_parses(valid_minimal_dict):
    """directional 光源が正しくパースされること．

    仕様: type: directional, direction, irradiance (SPEC_02 lights セクション)
    """
    d = copy.deepcopy(valid_minimal_dict)
    d["lights"] = [
        {
            "id": "sun",
            "type": "directional",
            "direction": [0.0, 0.0, -1.0],
            "irradiance": 1.0,
        }
    ]
    config = SceneConfig.model_validate(d)
    light = config.lights[0]
    assert isinstance(light, DirectionalLight)
    assert light.id == "sun"
    assert light.direction == pytest.approx((0.0, 0.0, -1.0))


# ---------------------------------------------------------------------------
# 正常系: evaluation / zones
# ---------------------------------------------------------------------------


def test_zones_default_boundaries(valid_minimal_dict):
    """boundaries を省略したとき既定値 (0.33, 0.66) が設定されること．

    仕様: boundaries デフォルト [0.33, 0.66] (SPEC_02 evaluation セクション)
    """
    d = copy.deepcopy(valid_minimal_dict)
    d["evaluation"] = {
        "roi": {"type": "image_pixel", "x": 0, "y": 0, "width": 100, "height": 100},
        "zones": {"enabled": True},
    }
    config = SceneConfig.model_validate(d)
    assert config.evaluation is not None
    assert config.evaluation.zones is not None
    assert config.evaluation.zones.boundaries == pytest.approx((0.33, 0.66))


# ---------------------------------------------------------------------------
# 異常系: 必須キー欠落
# ---------------------------------------------------------------------------


def test_missing_name_raises(valid_minimal_dict):
    """name が欠落したとき ValidationError が発生すること．

    仕様: name 必須 (SPEC_02 トップレベル構造)
    """
    d = copy.deepcopy(valid_minimal_dict)
    del d["name"]
    with pytest.raises(ValidationError):
        SceneConfig.model_validate(d)


def test_missing_geometry_raises(valid_minimal_dict):
    """geometry が欠落したとき ValidationError が発生すること．

    仕様: geometry 必須 (SPEC_02 トップレベル構造)
    """
    d = copy.deepcopy(valid_minimal_dict)
    del d["geometry"]
    with pytest.raises(ValidationError):
        SceneConfig.model_validate(d)


def test_missing_materials_raises(valid_minimal_dict):
    """materials が欠落したとき ValidationError が発生すること．

    仕様: materials 必須 (SPEC_02 トップレベル構造)
    """
    d = copy.deepcopy(valid_minimal_dict)
    del d["materials"]
    with pytest.raises(ValidationError):
        SceneConfig.model_validate(d)


def test_missing_lights_raises(valid_minimal_dict):
    """lights が欠落したとき ValidationError が発生すること．

    仕様: lights 必須 (SPEC_02 トップレベル構造)
    """
    d = copy.deepcopy(valid_minimal_dict)
    del d["lights"]
    with pytest.raises(ValidationError):
        SceneConfig.model_validate(d)


def test_missing_sensor_raises(valid_minimal_dict):
    """sensor が欠落したとき ValidationError が発生すること．

    仕様: sensor 必須 (SPEC_02 トップレベル構造)
    """
    d = copy.deepcopy(valid_minimal_dict)
    del d["sensor"]
    with pytest.raises(ValidationError):
        SceneConfig.model_validate(d)


def test_empty_lights_list_raises(valid_minimal_dict):
    """lights が空リストのとき ValidationError が発生すること．

    仕様: lights は 1 個以上必須 (SPEC_02 lights セクション)
    """
    d = copy.deepcopy(valid_minimal_dict)
    d["lights"] = []
    with pytest.raises(ValidationError):
        SceneConfig.model_validate(d)


def test_empty_meshes_list_raises(valid_minimal_dict):
    """geometry.meshes が空リストのとき ValidationError が発生すること．

    仕様: meshes は 1 個以上必須（min_length=1）
    """
    d = copy.deepcopy(valid_minimal_dict)
    d["geometry"]["meshes"] = []
    with pytest.raises(ValidationError):
        SceneConfig.model_validate(d)


# ---------------------------------------------------------------------------
# 異常系: 参照整合性・ID 重複
# ---------------------------------------------------------------------------


def test_dangling_material_reference_raises(valid_minimal_dict):
    """存在しない material を mesh が参照したとき ValidationError が発生し，
    エラーメッセージに mesh id と material name が含まれること．

    仕様: 参照整合性チェック (SPEC_02 バリデーション節)
    """
    d = copy.deepcopy(valid_minimal_dict)
    d["geometry"]["meshes"][0]["material"] = "nonexistent_material"
    with pytest.raises(ValidationError) as exc_info:
        SceneConfig.model_validate(d)
    error_str = str(exc_info.value)
    assert "housing" in error_str
    assert "nonexistent_material" in error_str


def test_duplicate_mesh_id_raises(valid_minimal_dict):
    """同じ mesh.id が 2 つあるとき ValidationError が発生すること．

    仕様: ID の重複チェック (SPEC_02 バリデーション節)
    """
    d = copy.deepcopy(valid_minimal_dict)
    d["geometry"]["meshes"].append(
        {
            "id": "housing",  # 重複
            "path": "other.stl",
            "unit": "mm",
            "material": "mat_diffuse",
        }
    )
    with pytest.raises(ValidationError):
        SceneConfig.model_validate(d)


def test_duplicate_light_id_raises(valid_minimal_dict):
    """同じ light.id が 2 つあるとき ValidationError が発生すること．

    仕様: ID の重複チェック (SPEC_02 バリデーション節)
    """
    d = copy.deepcopy(valid_minimal_dict)
    d["lights"].append(
        {
            "id": "led_top",  # 重複
            "type": "point",
            "position": [1.0, 0.0, 50.0],
            "intensity": 1.0,
            "profile": "isotropic",
        }
    )
    with pytest.raises(ValidationError):
        SceneConfig.model_validate(d)


# ---------------------------------------------------------------------------
# 異常系: 型・値範囲
# ---------------------------------------------------------------------------


def test_invalid_unit_cm_raises(valid_minimal_dict):
    """unit: cm を指定したとき ValidationError が発生すること．

    仕様: unit は mm | m のみ (SPEC_02 geometry セクション)
    """
    d = copy.deepcopy(valid_minimal_dict)
    d["geometry"]["meshes"][0]["unit"] = "cm"
    with pytest.raises(ValidationError):
        SceneConfig.model_validate(d)


def test_invalid_material_type_raises(valid_minimal_dict):
    """未対応の material type（mask 等）で ValidationError が発生すること．

    仕様: type は diffuse / dielectric / thindielectric の discriminated union (SPEC_02)
    注: SPEC_02 では mask が将来拡張として記載されているが，現在はサポート外
    """
    d = copy.deepcopy(valid_minimal_dict)
    d["materials"]["mat_diffuse"] = {"type": "mask", "opacity": 0.5}
    with pytest.raises(ValidationError):
        SceneConfig.model_validate(d)


def test_invalid_light_type_raises(valid_minimal_dict):
    """未対応の light type（laser 等）で ValidationError が発生すること．

    仕様: type は point / area / directional の discriminated union (SPEC_02 lights セクション)
    """
    d = copy.deepcopy(valid_minimal_dict)
    d["lights"] = [
        {
            "id": "laser_1",
            "type": "laser",
            "position": [0.0, 0.0, 50.0],
            "intensity": 1.0,
        }
    ]
    with pytest.raises(ValidationError):
        SceneConfig.model_validate(d)


def test_invalid_sensor_type_raises(valid_minimal_dict):
    """type: orthographic を指定したとき ValidationError が発生すること．

    仕様: sensor.type は perspective のみ (SPEC_02 sensor セクション)
    """
    d = copy.deepcopy(valid_minimal_dict)
    d["sensor"]["type"] = "orthographic"
    with pytest.raises(ValidationError):
        SceneConfig.model_validate(d)


def test_fov_zero_raises(valid_minimal_dict):
    """fov_deg: 0 のとき ValidationError が発生すること．

    仕様: 0 < fov_deg < 180 (SPEC_02 バリデーション節)
    """
    d = copy.deepcopy(valid_minimal_dict)
    d["sensor"]["fov_deg"] = 0.0
    with pytest.raises(ValidationError):
        SceneConfig.model_validate(d)


def test_fov_180_raises(valid_minimal_dict):
    """fov_deg: 180 のとき ValidationError が発生すること．

    仕様: 0 < fov_deg < 180（端点は含まない）(SPEC_02 バリデーション節)
    """
    d = copy.deepcopy(valid_minimal_dict)
    d["sensor"]["fov_deg"] = 180.0
    with pytest.raises(ValidationError):
        SceneConfig.model_validate(d)


def test_zones_invalid_boundaries_raises(valid_minimal_dict):
    """boundaries: [0.66, 0.33]（逆順）のとき ValidationError が発生すること．

    仕様: 0 < b1 < b2 < 1 (SPEC_02 evaluation/zones セクション)
    """
    d = copy.deepcopy(valid_minimal_dict)
    d["evaluation"] = {
        "roi": {"type": "image_pixel", "x": 0, "y": 0, "width": 100, "height": 100},
        "zones": {"enabled": True, "boundaries": [0.66, 0.33]},
    }
    with pytest.raises(ValidationError):
        SceneConfig.model_validate(d)


def test_zones_boundaries_out_of_range_raises(valid_minimal_dict):
    """boundaries: [0.0, 1.0]（範囲外）のとき ValidationError が発生すること．

    仕様: 0 < b1 < b2 < 1（端点 0 と 1 は含まない）(SPEC_02 evaluation/zones セクション)
    """
    d = copy.deepcopy(valid_minimal_dict)
    d["evaluation"] = {
        "roi": {"type": "image_pixel", "x": 0, "y": 0, "width": 100, "height": 100},
        "zones": {"enabled": True, "boundaries": [0.0, 1.0]},
    }
    with pytest.raises(ValidationError):
        SceneConfig.model_validate(d)


# ---------------------------------------------------------------------------
# 異常系: フィールドの細部
# ---------------------------------------------------------------------------


def test_film_dimensions_must_be_positive(valid_minimal_dict):
    """film.width: 0 のとき ValidationError が発生すること．

    仕様: width >= 1 (ge=1)
    """
    d = copy.deepcopy(valid_minimal_dict)
    d["sensor"]["film"]["width"] = 0
    with pytest.raises(ValidationError):
        SceneConfig.model_validate(d)


def test_max_depth_must_be_positive(valid_minimal_dict):
    """render.max_depth: 0 のとき ValidationError が発生すること．

    仕様: max_depth >= 1 (ge=1)
    """
    d = copy.deepcopy(valid_minimal_dict)
    d["render"]["max_depth"] = 0
    with pytest.raises(ValidationError):
        SceneConfig.model_validate(d)


def test_spp_must_be_positive(valid_minimal_dict):
    """render.spp: 0 のとき ValidationError が発生すること．

    仕様: spp >= 1 (ge=1)
    """
    d = copy.deepcopy(valid_minimal_dict)
    d["render"]["spp"] = 0
    with pytest.raises(ValidationError):
        SceneConfig.model_validate(d)


# ---------------------------------------------------------------------------
# イミュータビリティ
# ---------------------------------------------------------------------------


def test_scene_config_is_frozen(valid_minimal_dict):
    """パース後の SceneConfig インスタンスへの代入で ValidationError が発生すること．

    仕様: frozen=True (schema.py ConfigDict)
    """
    config = SceneConfig.model_validate(valid_minimal_dict)
    with pytest.raises(ValidationError):
        config.name = "modified"  # type: ignore[misc]
