"""スイープ生成スクリプトが一時 YAML に書くメッシュパスの仕様ベーステスト．

仕様出典:
- 実装サマリー 仕様 10: スイープ生成スクリプトは _tmp YAML のメッシュパスを
  YAML ファイルからの相対パス（POSIX 区切り）で書く（scene/paths.py relative_mesh_path）．
  以前は絶対パスだった
- docs/02_ENV/ENV_04_開発コマンド.md「研究室サーバでの実行」注意
  （Windows 絶対パスの YAML はサーバで読めない．push 時に警告が出る）

対象（9 本）: sanity_led_groups / sweep_max_depth / sweep_led_distribution / sweep_reflectance /
sweep_blend_bsdf / sweep_bsdf_grid_fix / sweep_sphere_radius / sweep_cube_size / sweep_offaxis_led

テスト方針:
- sm-render は呼ばない．YAML 生成関数だけを呼び，出力先は tmp_path に差し替える
- 「YAML の場所から解決すると元のメッシュ（テンプレートの実在メッシュ・生成メッシュ）を指す」
  「どの OS でも相対パスとして読める（区切りは /，ドライブ無し）」
  「push の Windows 絶対パス警告に掛からない」の 3 点を確かめる
"""

from __future__ import annotations

import importlib.util
import sys
from pathlib import Path, PurePosixPath, PureWindowsPath
from types import ModuleType
from unittest import mock

import pytest
import yaml

from sm_tias_lightsim.remote.files import PushEntry, find_yaml_with_windows_paths

_ROOT = Path(__file__).resolve().parents[2]
_SCRIPTS = _ROOT / "scripts"


def _load(name: str) -> ModuleType:
    """scripts/<name>.py を import する（既に読み込み済みならそれを使う）．"""
    path = (_SCRIPTS / f"{name}.py").resolve()
    cached = sys.modules.get(name)
    if cached is not None and Path(getattr(cached, "__file__", "")).resolve() == path:
        return cached
    spec = importlib.util.spec_from_file_location(name, path)
    assert spec is not None and spec.loader is not None
    module = importlib.util.module_from_spec(spec)
    sys.modules[name] = module
    spec.loader.exec_module(module)
    return module


@pytest.fixture(autouse=True)
def _same_drive(tmp_path):
    if tmp_path.resolve().drive.upper() != _ROOT.drive.upper():
        pytest.skip("tmp_path とリポジトリが別ドライブのため相対パスで表せない環境")


def _template_targets(template: Path) -> dict[str, Path]:
    """テンプレート YAML のメッシュ id → 実体（テンプレートのディレクトリ基準で解決）．"""
    data = yaml.safe_load(template.read_text(encoding="utf-8"))
    return {m["id"]: (template.parent / m["path"]).resolve() for m in data["geometry"]["meshes"]}


def _assert_mesh_paths(yaml_path: Path, expected: dict[str, Path]) -> None:
    data = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
    meshes = data["geometry"]["meshes"]
    assert {m["id"] for m in meshes} == set(expected)
    for mesh in meshes:
        path = mesh["path"]
        assert isinstance(path, str)
        assert "\\" not in path, f"{mesh['id']}: POSIX 区切りでない: {path}"
        assert not PurePosixPath(path).is_absolute(), f"{mesh['id']}: 絶対パス: {path}"
        assert not PureWindowsPath(path).is_absolute(), f"{mesh['id']}: 絶対パス: {path}"
        assert not PureWindowsPath(path).drive, f"{mesh['id']}: ドライブ付き: {path}"
        assert (yaml_path.parent / path).resolve() == expected[mesh["id"]], mesh["id"]
    entry = PushEntry(path=yaml_path, arcname=f"configs/_tmp_x/{yaml_path.name}", size=0)
    assert find_yaml_with_windows_paths([entry]) == []


def _require_existing(targets: dict[str, Path]) -> None:
    missing = [str(p) for p in targets.values() if not p.is_file()]
    if missing:
        pytest.skip(f"テンプレートのメッシュが実在しない環境: {missing}")


# ---------------------------------------------------------------------------
# テンプレートのメッシュをそのまま参照する 5 本（_generate_tmp_yaml）
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    ("script", "args"),
    [
        ("sanity_led_groups", ("bottom",)),
        ("sweep_max_depth", (16,)),
        ("sweep_led_distribution", (90,)),
        ("sweep_reflectance", (0.85,)),
        ("sweep_blend_bsdf", (0.15, 0.3)),
    ],
)
def test_generate_tmp_yaml_writes_yaml_relative_paths(tmp_path, script, args):
    module = _load(script)
    tmp_dir = tmp_path / "configs" / f"_tmp_{script}"
    with mock.patch.object(module, "TMP_DIR", tmp_dir):
        yaml_path = module._generate_tmp_yaml(*args)
    assert yaml_path.parent == tmp_dir
    expected = _template_targets(module.TEMPLATE_YAML)
    _require_existing(expected)
    _assert_mesh_paths(yaml_path, expected)


def test_sweep_bsdf_grid_fix_writes_yaml_relative_paths(tmp_path):
    module = _load("sweep_bsdf_grid_fix")
    if not module.TEMPLATE_YAML.is_file():
        pytest.skip(f"テンプレートがありません: {module.TEMPLATE_YAML}")
    expected = _template_targets(module.TEMPLATE_YAML)
    _require_existing(expected)
    dest = tmp_path / "configs" / "_tmp_bsdf_grid" / "r0.85_w0.05_a0.1" / "scene.yaml"
    module.build_scene_yaml(0.85, 0.05, 0.1, dest)
    _assert_mesh_paths(dest, expected)


# ---------------------------------------------------------------------------
# 生成メッシュ（筐体・バッフル）を参照する 3 本（build_scene_yaml）
# ---------------------------------------------------------------------------


@pytest.mark.parametrize("with_baffle", [True, False])
def test_sweep_sphere_radius_writes_yaml_relative_paths(tmp_path, with_baffle):
    module = _load("sweep_sphere_radius")
    tmp_dir = tmp_path / "configs" / "_tmp_sphere_radius"
    mesh = tmp_dir / "meshes" / "球_r100.stl"
    baffle = tmp_dir / "meshes" / "バッフル_r100.stl"
    dest = tmp_dir / "board_baffle" / "scene_r100.yaml"
    module.build_scene_yaml(
        100.0, mesh, dest, "board_baffle", "similar_radial", baffle if with_baffle else None
    )
    expected = _template_targets(module.TEMPLATE_YAML)
    expected["sphere_housing"] = mesh.resolve()
    if with_baffle:
        expected["baffle"] = baffle.resolve()
    _assert_mesh_paths(dest, expected)
    paths = {
        m["id"]: m["path"]
        for m in yaml.safe_load(dest.read_text(encoding="utf-8"))["geometry"]["meshes"]
    }
    assert paths["sphere_housing"] == "../meshes/球_r100.stl"
    if with_baffle:
        assert paths["baffle"] == "../meshes/バッフル_r100.stl"


def test_sweep_cube_size_writes_yaml_relative_paths(tmp_path):
    module = _load("sweep_cube_size")
    tmp_dir = tmp_path / "configs" / "_tmp_cube_size"
    mesh = tmp_dir / "meshes" / "立方体_L120.stl"
    baffle = tmp_dir / "meshes" / "バッフル_L120.stl"
    dest = tmp_dir / "scene_L120.yaml"
    module.build_scene_yaml(120.0, mesh, dest, baffle)
    expected = _template_targets(module.TEMPLATE_YAML)
    expected.pop("sphere_housing")
    expected["cube_housing"] = mesh.resolve()
    expected["baffle"] = baffle.resolve()
    _assert_mesh_paths(dest, expected)


@pytest.mark.parametrize(("shape", "size"), [("cube", 120.0), ("sphere", 100.0)])
def test_sweep_offaxis_led_writes_yaml_relative_paths(tmp_path, shape, size):
    module = _load("sweep_offaxis_led")
    tmp_dir = tmp_path / "configs" / "_tmp_offaxis"
    name = module.job_name(shape, size)
    mesh = tmp_dir / "meshes" / f"{name}.stl"
    baffle = tmp_dir / "meshes" / f"{name}_baffle.stl"
    dest = tmp_dir / f"scene_{name}.yaml"
    module.build_scene_yaml(shape, size, mesh, baffle, module.led_geometry(shape, size), dest)
    expected = _template_targets(module.TEMPLATE_YAML)
    expected.pop("sphere_housing")
    expected["cube_housing" if shape == "cube" else "sphere_housing"] = mesh.resolve()
    expected["baffle"] = baffle.resolve()
    _assert_mesh_paths(dest, expected)
