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

仕様出典: 実装サマリー §E
- E1: weight/alpha パラメータが一時 YAML に正しく書き込まれること
- E2: geometry.meshes の STL パスが一時 YAML からの相対パス（POSIX 区切り）に変換されること
  （旧仕様は絶対パス化．Windows で生成した YAML を研究室サーバで読めないため 2026-09-13 に
  scene/paths.py の relative_mesh_path による YAML 相対パスへ変更）
- 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 フィクスチャ（実ファイル）を使用する（testing.md のモック方針）
"""

from __future__ import annotations

import sys
from pathlib import Path, PurePosixPath, PureWindowsPath
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 が一時 YAML からの相対パス（POSIX 区切り）に変換されること
# ---------------------------------------------------------------------------


class TestGenerateTmpYamlRelativePaths:
    """E2 系: メッシュパスは一時 YAML のディレクトリからの相対パス（POSIX 区切り）で書かれ，
    YAML の場所から解決するとテンプレートと同じ実在のメッシュを指すこと．

    旧仕様（絶対パス化）は Windows で生成した YAML（C:\\Users\\...）を研究室サーバで
    読めないため廃止した（2026-09-13．scene/paths.py relative_mesh_path）．
    """

    @pytest.fixture(autouse=True)
    def _patch_tmp_dir(self, tmp_path, sweep_module):
        """TMP_DIR を tmp_path に差し替える．"""
        if tmp_path.resolve().drive.upper() != _ROOT.drive.upper():
            pytest.skip("tmp_path とリポジトリが別ドライブのため相対パスで表せない環境")
        with mock.patch.object(sweep_module, "TMP_DIR", tmp_path):
            yield

    @staticmethod
    def _generate(sweep_module) -> tuple[Path, list[dict]]:
        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 が空のため検証不可"
        return tmp_yaml, meshes

    def test_no_mesh_path_is_absolute(self, sweep_module):
        """全メッシュの path が（POSIX でも Windows でも）絶対パスでないこと．

        仕様 E2: 絶対パスだと別マシン（研究室サーバ）で読めない
        """
        _, meshes = self._generate(sweep_module)
        for mesh in meshes:
            p = mesh["path"]
            assert not PurePosixPath(p).is_absolute(), f"mesh '{mesh.get('id')}': {p}"
            assert not PureWindowsPath(p).is_absolute(), f"mesh '{mesh.get('id')}': {p}"
            assert not PureWindowsPath(p).drive, f"mesh '{mesh.get('id')}': {p}"

    def test_mesh_paths_use_posix_separator(self, sweep_module):
        """区切り文字は / のみであること（Windows で生成しても Linux で読める）．"""
        _, meshes = self._generate(sweep_module)
        for mesh in meshes:
            assert "\\" not in mesh["path"], f"mesh '{mesh.get('id')}': {mesh['path']}"

    def test_relative_path_resolves_to_template_mesh(self, sweep_module):
        """YAML の場所から解決すると，テンプレート（configs/）基準の元のメッシュを指すこと．

        仕様 E2: template_base = TEMPLATE_YAML.parent（configs/）基準の実体を，
        書き出し先 YAML のディレクトリからの相対パスで表す
        """
        tmp_yaml, meshes = self._generate(sweep_module)
        template = yaml.safe_load(_TEMPLATE_YAML.read_text(encoding="utf-8"))
        originals = {m["id"]: m["path"] for m in template["geometry"]["meshes"]}
        assert {m["id"] for m in meshes} == set(originals)
        for mesh in meshes:
            resolved = (tmp_yaml.parent / mesh["path"]).resolve()
            expected = (_TEMPLATE_YAML.parent / originals[mesh["id"]]).resolve()
            assert resolved == expected, f"mesh '{mesh.get('id')}': {mesh['path']}"
            assert resolved.is_file(), f"mesh '{mesh.get('id')}' の実体がありません: {resolved}"


# ---------------------------------------------------------------------------
# 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 がテンプレートのまま更新されていません"
        )
