"""scripts/sweep_blend_bsdf.py の _generate_tmp_yaml 仕様ベーステスト．

仕様出典: 実装サマリー §E
- E1: weight/alpha パラメータが一時 YAML に正しく書き込まれること
- E2: geometry.meshes の STL パスが絶対パスに変換されること
- E3: 一時 YAML が BOM なし UTF-8 で書かれること
- E4: name フィールドが "baseline_minitias_blend_{job_name}" 形式に更新されること

テスト戦略:
- subprocess（sm-render 実行）は含まない．_generate_tmp_yaml の純変換ロジックのみをテストする
- 実際の TEMPLATE_YAML（configs/scene_baseline_blend.yaml）を使用する
- 出力先は tmp_path フィクスチャ（実ファイル）を使用する（GUIDE_08 ファイル I/O はモックしない方針）
"""

from __future__ import annotations

import importlib
import sys
from pathlib import Path
from types import ModuleType
from unittest import mock

import pytest
import yaml

# ---------------------------------------------------------------------------
# sweep_blend_bsdf モジュールをインポートするヘルパ
# ---------------------------------------------------------------------------

_ROOT = Path(__file__).resolve().parents[2]
_SCRIPT_PATH = _ROOT / "scripts" / "sweep_blend_bsdf.py"

_TEMPLATE_YAML = _ROOT / "configs" / "scene_baseline_blend.yaml"


def _load_sweep_module() -> ModuleType:
    """sweep_blend_bsdf.py を直接インポートする．

    scripts/ は通常のパッケージではないため importlib.util で直接ロードする．
    モジュールキャッシュに登録して再 import を防ぐ．
    """
    import importlib.util

    spec = importlib.util.spec_from_file_location("sweep_blend_bsdf", _SCRIPT_PATH)
    assert spec is not None, f"スクリプトが見つかりません: {_SCRIPT_PATH}"
    module = importlib.util.module_from_spec(spec)
    # モジュールキャッシュに登録（2回目以降の import を安全にする）
    sys.modules["sweep_blend_bsdf"] = module
    assert spec.loader is not None
    spec.loader.exec_module(module)  # type: ignore[union-attr]
    return module


@pytest.fixture(scope="module")
def sweep_module() -> ModuleType:
    """sweep_blend_bsdf モジュールを返すフィクスチャ（モジュールスコープ）．"""
    return _load_sweep_module()


# ---------------------------------------------------------------------------
# テンプレート YAML の存在確認（前提条件チェック）
# ---------------------------------------------------------------------------


def test_template_yaml_exists():
    """configs/scene_baseline_blend.yaml が存在すること（テストの前提条件）．

    このファイルがなければ E 系テスト全体が無効になる．
    """
    assert _TEMPLATE_YAML.exists(), (
        f"テンプレート YAML が見つかりません: {_TEMPLATE_YAML}\n"
        "configs/scene_baseline_blend.yaml を生成してからテストを実行してください．"
    )


# ---------------------------------------------------------------------------
# E1: weight/alpha が一時 YAML に正しく書き込まれる
# ---------------------------------------------------------------------------


class TestGenerateTmpYamlParameters:
    """E1 系: weight と alpha が一時 YAML の正しいパスに書き込まれること．"""

    @pytest.fixture(autouse=True)
    def _patch_tmp_dir(self, tmp_path, sweep_module):
        """TMP_DIR を tmp_path に差し替えて一時 YAML の出力先をテスト用にする．"""
        with mock.patch.object(sweep_module, "TMP_DIR", tmp_path):
            yield

    def test_weight_written_to_printer_white(self, sweep_module):
        """生成された YAML の materials.printer_white.weight が入力値 0.15 と一致すること．

        仕様 E1: weight=0.15 が一時 YAML の printer_white.weight に書き込まれる
        """
        tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
        with tmp_yaml.open(encoding="utf-8") as f:
            data = yaml.safe_load(f)
        assert data["materials"]["printer_white"]["weight"] == pytest.approx(0.15)

    def test_alpha_written_to_roughconductor_component(self, sweep_module):
        """生成された YAML の components[1].alpha が入力値 0.3 と一致すること．

        仕様 E1: alpha=0.3 が components[1].alpha に書き込まれる
        """
        tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
        with tmp_yaml.open(encoding="utf-8") as f:
            data = yaml.safe_load(f)
        assert data["materials"]["printer_white"]["components"][1]["alpha"] == pytest.approx(0.3)

    def test_weight_0_05_written_correctly(self, sweep_module):
        """weight=0.05 が正しく書き込まれること（スイープ最小値）．

        仕様 E1: スイープグリッドの全値で正しく書き込まれること
        """
        tmp_yaml = sweep_module._generate_tmp_yaml(0.05, 0.1)
        with tmp_yaml.open(encoding="utf-8") as f:
            data = yaml.safe_load(f)
        assert data["materials"]["printer_white"]["weight"] == pytest.approx(0.05)

    def test_alpha_0_6_written_correctly(self, sweep_module):
        """alpha=0.6 が正しく書き込まれること（スイープ最大値）．

        仕様 E1: スイープグリッドの全値で正しく書き込まれること
        """
        tmp_yaml = sweep_module._generate_tmp_yaml(0.30, 0.6)
        with tmp_yaml.open(encoding="utf-8") as f:
            data = yaml.safe_load(f)
        assert data["materials"]["printer_white"]["components"][1]["alpha"] == pytest.approx(0.6)

    def test_weight_0_30_written_correctly(self, sweep_module):
        """weight=0.30 が正しく書き込まれること（スイープ最大値）．

        仕様 E1: スイープグリッドの全値で正しく書き込まれること
        """
        tmp_yaml = sweep_module._generate_tmp_yaml(0.30, 0.3)
        with tmp_yaml.open(encoding="utf-8") as f:
            data = yaml.safe_load(f)
        assert data["materials"]["printer_white"]["weight"] == pytest.approx(0.30)

    def test_component0_type_still_diffuse_after_write(self, sweep_module):
        """components[0].type が "diffuse" のまま保持されること（破壊的書き換えがないこと）．

        仕様 E1: alpha 書き換えは components[1] のみに影響する
        """
        tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
        with tmp_yaml.open(encoding="utf-8") as f:
            data = yaml.safe_load(f)
        assert data["materials"]["printer_white"]["components"][0]["type"] == "diffuse"

    def test_component1_type_still_roughconductor_after_write(self, sweep_module):
        """components[1].type が "roughconductor" のまま保持されること．

        仕様 E1: alpha 書き換えは値のみで type フィールドは変更しない
        """
        tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
        with tmp_yaml.open(encoding="utf-8") as f:
            data = yaml.safe_load(f)
        assert data["materials"]["printer_white"]["components"][1]["type"] == "roughconductor"


# ---------------------------------------------------------------------------
# E2: geometry.meshes の path が絶対パスに変換されること
# ---------------------------------------------------------------------------


class TestGenerateTmpYamlAbsolutePaths:
    """E2 系: geometry.meshes の STL パスが絶対パスに変換されること．"""

    @pytest.fixture(autouse=True)
    def _patch_tmp_dir(self, tmp_path, sweep_module):
        """TMP_DIR を tmp_path に差し替える．"""
        with mock.patch.object(sweep_module, "TMP_DIR", tmp_path):
            yield

    def test_all_mesh_paths_are_absolute(self, sweep_module):
        """全メッシュの path が絶対パスになっていること．

        仕様 E2: 一時 YAML の geometry.meshes の各 path が絶対パスに変換されている
        """
        tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
        with tmp_yaml.open(encoding="utf-8") as f:
            data = yaml.safe_load(f)
        meshes = data["geometry"]["meshes"]
        assert len(meshes) > 0, "meshes が空のため検証不可"
        for mesh in meshes:
            p = Path(mesh["path"])
            assert p.is_absolute(), f"mesh '{mesh.get('id')}' の path が絶対パスでない: {mesh['path']}"

    def test_absolute_path_resolves_relative_to_template_dir(self, sweep_module):
        """絶対化されたパスがテンプレート YAML の親ディレクトリ（configs/）基準で解決されること．

        仕様 E2: template_base = TEMPLATE_YAML.parent（configs/）を基準に resolve
        """
        tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
        with tmp_yaml.open(encoding="utf-8") as f:
            data = yaml.safe_load(f)
        meshes = data["geometry"]["meshes"]
        template_base = _TEMPLATE_YAML.parent  # configs/
        for mesh in meshes:
            p = Path(mesh["path"])
            # 絶対化されたパスは template_base 配下またはそのサブディレクトリにあること
            # （relative path が "meshes/xxx.stl" → configs/meshes/xxx.stl の absolute）
            try:
                p.relative_to(template_base)
            except ValueError:
                pytest.fail(
                    f"mesh '{mesh.get('id')}' の絶対パス '{p}' が template_base '{template_base}' の"
                    "下にありません（パス解決ロジックの不整合）"
                )

    def test_no_mesh_path_is_relative(self, sweep_module):
        """一時 YAML のメッシュパスに相対パスが含まれないこと．

        仕様 E2: 相対パスが残っていないことを確認
        """
        tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
        with tmp_yaml.open(encoding="utf-8") as f:
            data = yaml.safe_load(f)
        meshes = data["geometry"]["meshes"]
        for mesh in meshes:
            assert Path(mesh["path"]).is_absolute(), (
                f"mesh '{mesh.get('id')}' の path が相対パス: {mesh['path']}"
            )


# ---------------------------------------------------------------------------
# E3: 一時 YAML が BOM なし UTF-8 で書かれること
# ---------------------------------------------------------------------------


class TestGenerateTmpYamlEncoding:
    """E3 系: 一時 YAML のエンコーディング仕様．"""

    @pytest.fixture(autouse=True)
    def _patch_tmp_dir(self, tmp_path, sweep_module):
        """TMP_DIR を tmp_path に差し替える．"""
        with mock.patch.object(sweep_module, "TMP_DIR", tmp_path):
            yield

    def test_output_file_has_no_bom(self, sweep_module):
        """一時 YAML ファイルが UTF-8 BOM（EF BB BF）で始まっていないこと．

        仕様 E3: BOM なし UTF-8（Mitsuba YAML パーサは BOM 付きで失敗するため）
        """
        tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
        raw = tmp_yaml.read_bytes()
        # UTF-8 BOM は 0xEF 0xBB 0xBF
        bom = b"\xef\xbb\xbf"
        assert not raw.startswith(bom), (
            "一時 YAML が UTF-8 BOM で始まっています．BOM なし UTF-8 で書き込んでください．"
        )

    def test_output_file_is_valid_utf8(self, sweep_module):
        """一時 YAML ファイルが UTF-8 として正常にデコードできること．

        仕様 E3: UTF-8 エンコーディングで書かれていること
        """
        tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
        raw = tmp_yaml.read_bytes()
        # UnicodeDecodeError が発生しなければ OK
        decoded = raw.decode("utf-8")
        assert len(decoded) > 0

    def test_output_file_parseable_as_yaml(self, sweep_module):
        """一時 YAML ファイルが YAML として正常にパースできること．

        仕様 E3: 書き込み後に yaml.safe_load で再読み込みできること（エンコーディング崩れなし）
        """
        tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
        with tmp_yaml.open(encoding="utf-8") as f:
            data = yaml.safe_load(f)
        assert isinstance(data, dict)
        assert "name" in data


# ---------------------------------------------------------------------------
# E4: name フィールドが "baseline_minitias_blend_{job_name}" 形式になること
# ---------------------------------------------------------------------------


class TestGenerateTmpYamlName:
    """E4 系: 一時 YAML の name フィールド仕様．"""

    @pytest.fixture(autouse=True)
    def _patch_tmp_dir(self, tmp_path, sweep_module):
        """TMP_DIR を tmp_path に差し替える．"""
        with mock.patch.object(sweep_module, "TMP_DIR", tmp_path):
            yield

    def test_name_starts_with_baseline_minitias_blend(self, sweep_module):
        """name が "baseline_minitias_blend_" で始まること．

        仕様 E4: name フィールドが "baseline_minitias_blend_{job_name}" 形式
        """
        tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
        with tmp_yaml.open(encoding="utf-8") as f:
            data = yaml.safe_load(f)
        assert data["name"].startswith("baseline_minitias_blend_")

    def test_name_contains_weight_and_alpha(self, sweep_module):
        """name に weight と alpha の値が含まれること．

        仕様 E4: job_name = f"w{weight}_a{alpha}" 形式
        """
        tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
        with tmp_yaml.open(encoding="utf-8") as f:
            data = yaml.safe_load(f)
        # job_name は "w0.15_a0.3" 相当
        name = data["name"]
        assert "0.15" in name, f"weight 0.15 が name に含まれない: {name}"
        assert "0.3" in name, f"alpha 0.3 が name に含まれない: {name}"

    def test_name_format_for_weight_0_05_alpha_0_1(self, sweep_module):
        """weight=0.05, alpha=0.1 のとき name が適切な形式であること．

        仕様 E4: スイープグリッドの全組合せで name が正しい形式になる
        """
        tmp_yaml = sweep_module._generate_tmp_yaml(0.05, 0.1)
        with tmp_yaml.open(encoding="utf-8") as f:
            data = yaml.safe_load(f)
        name = data["name"]
        assert name.startswith("baseline_minitias_blend_")
        assert "0.05" in name
        assert "0.1" in name

    def test_name_format_for_weight_0_30_alpha_0_6(self, sweep_module):
        """weight=0.30, alpha=0.6 のとき name が適切な形式であること．

        仕様 E4: スイープグリッドの最大値でも name が正しい形式になる
        """
        tmp_yaml = sweep_module._generate_tmp_yaml(0.30, 0.6)
        with tmp_yaml.open(encoding="utf-8") as f:
            data = yaml.safe_load(f)
        name = data["name"]
        assert name.startswith("baseline_minitias_blend_")
        assert "0.3" in name  # 0.30 → "0.3" が含まれれば OK
        assert "0.6" in name

    def test_name_differs_across_different_params(self, sweep_module):
        """異なるパラメータ組み合わせで生成された name が異なること．

        仕様 E4: name がジョブ識別子を含むため各ジョブで一意になる
        """
        tmp1 = sweep_module._generate_tmp_yaml(0.05, 0.1)
        tmp2 = sweep_module._generate_tmp_yaml(0.30, 0.6)
        with tmp1.open(encoding="utf-8") as f:
            d1 = yaml.safe_load(f)
        with tmp2.open(encoding="utf-8") as f:
            d2 = yaml.safe_load(f)
        assert d1["name"] != d2["name"], "異なるパラメータで同じ name が生成されています"

    def test_original_template_name_is_not_used(self, sweep_module):
        """テンプレートの元の name ("baseline_minitias_blend") がそのまま使われないこと．

        仕様 E4: name フィールドがジョブごとに更新される
        """
        tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
        with tmp_yaml.open(encoding="utf-8") as f:
            data = yaml.safe_load(f)
        # テンプレートの元 name は "baseline_minitias_blend"（サフィックスなし）
        assert data["name"] != "baseline_minitias_blend", (
            "name がテンプレートのまま更新されていません"
        )
