"""geometry/transforms.py の仕様ベーステスト．

仕様出典:
- docs/04_SPEC/SPEC_01_アーキテクチャ設計.md（geometry 層の責務）
- docs/03_PLAN/PLAN_02_開発ステップ.md（Step 3 完了条件: 単位変換が可逆）
- docs/01_GUIDE/GUIDE_08_テスト方針.md（Mandatory: データ変換「変換が可逆であること」）
"""

import pathlib

import numpy
import pytest

from sm_tias_lightsim.geometry import LoadedMesh, mm_to_m, scale_vertices

# ---------------------------------------------------------------------------
# テストヘルパー
# ---------------------------------------------------------------------------


def _make_mesh(
    vertices: numpy.ndarray,
    unit: str = "mm",
) -> LoadedMesh:
    """テスト用 LoadedMesh を生成するヘルパー．faces はダミーの 1 面."""
    faces = numpy.array([[0, 1, 2]], dtype=numpy.int64)
    return LoadedMesh(
        vertices=vertices,
        faces=faces,
        unit=unit,  # type: ignore[arg-type]
        source_path=pathlib.Path("/dummy/mesh.stl"),
    )


# ---------------------------------------------------------------------------
# mm_to_m: スカラ
# ---------------------------------------------------------------------------


def test_mm_to_m_scalar_1000():
    """mm_to_m(1000.0) == 1.0 であること．

    仕様: mm → m 変換（× 0.001）(transforms.py mm_to_m docstring)
    """
    assert mm_to_m(1000.0) == pytest.approx(1.0)


def test_mm_to_m_scalar_2500_5():
    """mm_to_m(2500.5) == 2.5005 であること（小数点以下 4 桁）."""
    assert mm_to_m(2500.5) == pytest.approx(2.5005)


def test_mm_to_m_zero():
    """mm_to_m(0) == 0 であること．

    仕様: 0 ミリメートルは 0 メートル（恒等変換の特殊ケース）
    """
    assert mm_to_m(0) == pytest.approx(0.0)


# ---------------------------------------------------------------------------
# mm_to_m: ndarray
# ---------------------------------------------------------------------------


def test_mm_to_m_ndarray():
    """mm_to_m(ndarray) が要素ごとに × 0.001 した配列を返すこと．

    仕様: スカラ・ndarray 両対応 (transforms.py mm_to_m シグネチャ)
    """
    input_arr = numpy.array([1000.0, 2000.0], dtype=numpy.float64)
    result = mm_to_m(input_arr)
    expected = numpy.array([1.0, 2.0], dtype=numpy.float64)
    numpy.testing.assert_array_almost_equal(result, expected)


def test_mm_to_m_ndarray_shape_preserved():
    """mm_to_m が入力と同じ shape の配列を返すこと."""
    arr = numpy.ones((3, 3), dtype=numpy.float64) * 500.0
    result = mm_to_m(arr)
    assert result.shape == (3, 3)


# ---------------------------------------------------------------------------
# scale_vertices: mm → m 変換
# ---------------------------------------------------------------------------


def test_scale_vertices_mm_to_m():
    """mm → m 変換で頂点値が 1/1000 になること．

    仕様: mm → m 変換（× 0.001）(transforms.py scale_vertices)
    """
    vertices_mm = numpy.array(
        [[1000.0, 2000.0, 500.0], [0.0, 3000.0, 1500.0], [500.0, 500.0, 0.0]],
        dtype=numpy.float64,
    )
    mesh = _make_mesh(vertices_mm, unit="mm")
    scaled = scale_vertices(mesh, "m")
    expected = vertices_mm * 0.001
    numpy.testing.assert_array_almost_equal(scaled.vertices, expected)


# ---------------------------------------------------------------------------
# scale_vertices: m → mm 変換
# ---------------------------------------------------------------------------


def test_scale_vertices_m_to_mm():
    """m → mm 変換で頂点値が 1000 倍になること．

    仕様: m → mm 変換（× 1000）(transforms.py scale_vertices)
    """
    vertices_m = numpy.array(
        [[1.0, 2.0, 0.5], [0.0, 3.0, 1.5], [0.5, 0.5, 0.0]],
        dtype=numpy.float64,
    )
    mesh = _make_mesh(vertices_m, unit="m")
    scaled = scale_vertices(mesh, "mm")
    expected = vertices_m * 1000.0
    numpy.testing.assert_array_almost_equal(scaled.vertices, expected)


# ---------------------------------------------------------------------------
# scale_vertices: 同一単位
# ---------------------------------------------------------------------------


def test_scale_vertices_same_unit_values_unchanged():
    """mm → mm では頂点値が変わらないこと."""
    vertices = numpy.array([[100.0, 200.0, 300.0]], dtype=numpy.float64)
    mesh = _make_mesh(vertices, unit="mm")
    scaled = scale_vertices(mesh, "mm")
    numpy.testing.assert_array_equal(scaled.vertices, vertices)


def test_scale_vertices_same_unit_returns_copy():
    """mm → mm では値は同じだが配列オブジェクトは別であること．

    仕様: 元と同じ単位が指定された場合も頂点配列はコピーされる（関数の純粋性）
          (transforms.py scale_vertices docstring)
    """
    vertices = numpy.array([[100.0, 200.0, 300.0]], dtype=numpy.float64)
    mesh = _make_mesh(vertices, unit="mm")
    scaled = scale_vertices(mesh, "mm")
    assert scaled.vertices is not mesh.vertices


# ---------------------------------------------------------------------------
# scale_vertices: unit フィールドの更新
# ---------------------------------------------------------------------------


def test_scale_vertices_unit_field_updated_mm_to_m():
    """mm → m 変換後の unit が "m" になること．

    仕様: unit フィールドは target_unit に更新される (transforms.py scale_vertices docstring)
    """
    vertices = numpy.array([[1000.0, 0.0, 0.0]], dtype=numpy.float64)
    mesh = _make_mesh(vertices, unit="mm")
    scaled = scale_vertices(mesh, "m")
    assert scaled.unit == "m"


def test_scale_vertices_unit_field_updated_m_to_mm():
    """m → mm 変換後の unit が "mm" になること."""
    vertices = numpy.array([[1.0, 0.0, 0.0]], dtype=numpy.float64)
    mesh = _make_mesh(vertices, unit="m")
    scaled = scale_vertices(mesh, "mm")
    assert scaled.unit == "mm"


# ---------------------------------------------------------------------------
# scale_vertices: 可逆性（PLAN_02 完了条件「単位変換が可逆」）
# ---------------------------------------------------------------------------


def test_scale_vertices_roundtrip_is_identity():
    """mm → m → mm で元の頂点とほぼ一致すること（単位変換の可逆性）．

    仕様: 単位変換が可逆 (PLAN_02_開発ステップ.md Step 3 完了条件)
          GUIDE_08 Mandatory: 「データ変換: 変換が可逆であること」
    """
    vertices_mm = numpy.array(
        [
            [0.0, 0.0, 0.0],
            [1000.0, 0.0, 0.0],
            [500.0, 1000.0, 0.0],
            [500.0, 500.0, 1000.0],
        ],
        dtype=numpy.float64,
    )
    original = _make_mesh(vertices_mm, unit="mm")
    converted = scale_vertices(original, "m")
    round_tripped = scale_vertices(converted, "mm")
    assert numpy.allclose(round_tripped.vertices, original.vertices)


# ---------------------------------------------------------------------------
# scale_vertices: インスタンスの独立性
# ---------------------------------------------------------------------------


def test_scale_vertices_returns_new_instance():
    """scale_vertices の戻り値が元と異なるインスタンスであること．

    仕様: frozen dataclass の不変性を維持するため，常に新規インスタンスを返す
          (transforms.py scale_vertices docstring)
    """
    vertices = numpy.array([[1000.0, 2000.0, 0.0]], dtype=numpy.float64)
    mesh = _make_mesh(vertices, unit="mm")
    scaled = scale_vertices(mesh, "m")
    assert scaled is not mesh


# ---------------------------------------------------------------------------
# scale_vertices: faces の不変性
# ---------------------------------------------------------------------------


def test_scale_vertices_faces_preserved():
    """単位変換で faces 配列の値が変わらないこと．

    仕様: 頂点座標のみスケール変換する（面インデックスは変換対象外）
          (SPEC_01 geometry 層の責務)
    """
    faces = numpy.array([[0, 1, 2], [1, 2, 3]], dtype=numpy.int64)
    vertices = numpy.array(
        [
            [1000.0, 0.0, 0.0],
            [0.0, 1000.0, 0.0],
            [0.0, 0.0, 1000.0],
            [1000.0, 1000.0, 0.0],
        ],
        dtype=numpy.float64,
    )
    mesh = LoadedMesh(
        vertices=vertices,
        faces=faces,
        unit="mm",
        source_path=pathlib.Path("/dummy/mesh.stl"),
    )
    scaled = scale_vertices(mesh, "m")
    numpy.testing.assert_array_equal(scaled.faces, faces)


# ---------------------------------------------------------------------------
# scale_vertices: 元メッシュの faces 参照が同一かコピーかの確認
# ---------------------------------------------------------------------------


def test_scale_vertices_original_vertices_unchanged():
    """scale_vertices 後に元メッシュの頂点が変わっていないこと（純粋関数性）．

    仕様: 元の LoadedMesh は変更しない（frozen=True かつ純粋関数）
    """
    vertices = numpy.array([[1000.0, 2000.0, 3000.0]], dtype=numpy.float64)
    original_copy = vertices.copy()
    mesh = _make_mesh(vertices, unit="mm")
    scale_vertices(mesh, "m")
    numpy.testing.assert_array_equal(mesh.vertices, original_copy)
