"""scene/paths.py（relative_mesh_path）の仕様ベーステスト．

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

テスト方針:
- 相対パスは「YAML のディレクトリから解決すると元のメッシュを指す」ことで検証する
- 相対化できない場合（Windows で別ドライブ）は，警告を出して絶対パスを返す
  （docstring に明記された従来挙動へのフォールバック）．この扱いをテストで明示する
"""

from __future__ import annotations

import logging
import os
import string
from pathlib import Path, PurePosixPath, PureWindowsPath

import pytest

from sm_tias_lightsim.remote.files import PushEntry, find_yaml_with_windows_paths
from sm_tias_lightsim.scene import paths
from sm_tias_lightsim.scene.paths import relative_mesh_path

_ROOT = Path(__file__).resolve().parents[2]


def _assert_portable(rel: str) -> None:
    """どの OS でも相対パスとして解釈される文字列であること．"""
    assert "\\" not in rel, f"POSIX 区切りでない: {rel}"
    assert not PurePosixPath(rel).is_absolute(), rel
    assert not PureWindowsPath(rel).is_absolute(), rel
    assert not PureWindowsPath(rel).drive, rel


@pytest.mark.parametrize(
    ("target", "yaml_dir", "expected"),
    [
        ("configs/scene.stl", "configs", "scene.stl"),
        ("configs/meshes/前側.stl", "configs", "meshes/前側.stl"),
        ("configs/meshes/前側.stl", "configs/_tmp_offaxis", "../meshes/前側.stl"),
        (
            "configs/_tmp_offaxis/meshes/立方体_L120.stl",
            "configs/_tmp_offaxis",
            "meshes/立方体_L120.stl",
        ),
        ("configs/meshes/a.stl", "configs/_tmp_x/deep/er", "../../../meshes/a.stl"),
        ("other/b.stl", "configs/_tmp_x", "../../other/b.stl"),
    ],
)
def test_relative_to_yaml_dir(tmp_path, target, yaml_dir, expected):
    """YAML のディレクトリからの相対パスを POSIX 区切りで返すこと．"""
    rel = relative_mesh_path(tmp_path / target, tmp_path / yaml_dir)
    assert rel == expected
    _assert_portable(rel)


def test_resolves_back_to_target(tmp_path):
    """YAML のディレクトリ基準で解決すると実在のメッシュを指すこと．

    YAML のディレクトリは実在させておく（実運用でも YAML を書き出す場所なので必ずある）．
    POSIX ではパス中の `..` をカーネルがたどるため，手前のディレクトリが無いと
    `_tmp_blend/../meshes/...` へのアクセスが ENOENT になる（Windows は `..` を文字列上で
    先に畳むので通ってしまい，差が出る）．
    """
    mesh = tmp_path / "configs" / "meshes" / "照明ムラ補正ボード.stl"
    mesh.parent.mkdir(parents=True)
    mesh.write_text("solid", encoding="utf-8")
    yaml_dir = tmp_path / "configs" / "_tmp_blend"
    yaml_dir.mkdir(parents=True)
    rel = relative_mesh_path(mesh, yaml_dir)
    assert (yaml_dir / rel).resolve() == mesh.resolve()
    assert (yaml_dir / rel).is_file()


def test_yaml_dir_need_not_exist(tmp_path):
    """書き出し前（YAML ディレクトリ未作成）でも計算できること．"""
    rel = relative_mesh_path(tmp_path / "meshes" / "a.stl", tmp_path / "not" / "yet")
    assert rel == "../../meshes/a.stl"


def test_relative_target_is_resolved_from_cwd(tmp_path, monkeypatch):
    """target が相対パスなら CWD 基準で解釈すること．"""
    monkeypatch.chdir(tmp_path)
    rel = relative_mesh_path(Path("meshes") / "a.stl", tmp_path / "cfg")
    assert rel == "../meshes/a.stl"


def test_repository_layout_gives_short_relative_path():
    """実リポジトリの configs/_tmp_* → configs/meshes は `../meshes/...` になること
    （サーバの任意の場所に置いたリポジトリでもそのまま解決できる）．"""
    rel = relative_mesh_path(
        _ROOT / "configs" / "meshes" / "前側.stl", _ROOT / "configs" / "_tmp_offaxis"
    )
    assert rel == "../meshes/前側.stl"


def test_portable_path_is_not_flagged_by_push_warning(tmp_path):
    """相対パスで書いた YAML は push の Windows 絶対パス警告に引っかからないこと．"""
    rel = relative_mesh_path(tmp_path / "configs" / "meshes" / "a.stl", tmp_path / "configs" / "_t")
    yaml_path = tmp_path / "configs" / "_t" / "scene.yaml"
    yaml_path.parent.mkdir(parents=True)
    yaml_path.write_text(f"geometry:\n  meshes:\n  - id: a\n    path: {rel}\n", encoding="utf-8")
    entry = PushEntry(path=yaml_path, arcname="configs/_t/scene.yaml", size=1)
    assert find_yaml_with_windows_paths([entry]) == []


# ---------------------------------------------------------------------------
# 相対化できない場合（Windows の別ドライブ）の扱い
# ---------------------------------------------------------------------------


def test_unrelatable_path_falls_back_to_absolute_with_warning(tmp_path, monkeypatch, caplog):
    """os.path.relpath が ValueError（別ドライブ）のときは例外にせず，警告を出して
    解決済みの絶対パスを返すこと（従来挙動へのフォールバック）．"""

    def _raise(*_args, **_kwargs):
        raise ValueError("path is on mount 'D:', start on mount 'C:'")

    monkeypatch.setattr(paths.os.path, "relpath", _raise)
    target = tmp_path / "meshes" / "a.stl"
    with caplog.at_level(logging.WARNING, logger=paths.__name__):
        result = relative_mesh_path(target, tmp_path / "cfg")
    assert result == str(target.resolve())
    assert Path(result).is_absolute()
    assert any(r.levelno == logging.WARNING for r in caplog.records)


def _other_drive_letter() -> str:
    current = Path.cwd().drive.rstrip(":").upper()
    for letter in reversed(string.ascii_uppercase):
        if letter != current and letter != Path(os.environ.get("TEMP", "C:")).drive[:1].upper():
            return letter
    raise RuntimeError("別ドライブ文字が見つかりません")


@pytest.mark.skipif(os.name != "nt", reason="ドライブ文字は Windows のみ")
def test_windows_other_drive_returns_absolute_and_push_warns(tmp_path, caplog):
    """Windows で YAML とメッシュのドライブが違う場合: 絶対パスを返し（警告ログ付き），
    その YAML を push すると Windows 絶対パスとして警告対象になること（安全網が効く）．"""
    letter = _other_drive_letter()
    target = Path(f"{letter}:\\meshes\\a.stl")
    with caplog.at_level(logging.WARNING, logger=paths.__name__):
        result = relative_mesh_path(target, tmp_path)
    assert PureWindowsPath(result).drive.upper() == f"{letter}:"
    assert PureWindowsPath(result).is_absolute()
    assert any(r.levelno == logging.WARNING for r in caplog.records)

    yaml_path = tmp_path / "scene.yaml"
    yaml_path.write_text(f"geometry:\n  meshes:\n  - id: a\n    path: {result}\n", encoding="utf-8")
    entry = PushEntry(path=yaml_path, arcname="configs/_tmp_x/scene.yaml", size=1)
    assert find_yaml_with_windows_paths([entry]) == ["configs/_tmp_x/scene.yaml"]
