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

仕様出典:
  - src/sm_tias_lightsim/geometry/subdivide.py (subdivide_mesh_to_max_edge docstring)
  - CLAUDE.md 実装サマリー（--bake-subdivide 機能）

テスト方針（testing.md 準拠）:
  - 純関数の単体テスト（pyvista / Mitsuba 不要）
  - trimesh 依存のため trimesh をインポートしてテスト
  - 決定論性・dtype・インデックス整合性を必ず検証する
"""

from __future__ import annotations

import numpy as np
import pytest

from sm_tias_lightsim.geometry.subdivide import subdivide_mesh_to_max_edge

# ===========================================================================
# フィクスチャ
# ===========================================================================


@pytest.fixture
def unit_triangle_m():
    """1 m × 1 m の正三角形（m 単位，辺長 ≈1 m）."""
    vertices = np.array(
        [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]],
        dtype=np.float64,
    )
    faces = np.array([[0, 1, 2]], dtype=np.int64)
    return vertices, faces


@pytest.fixture
def small_triangle_m():
    """辺長 5 mm（0.005 m）の小さな三角形（m 単位）."""
    vertices = np.array(
        [[0.0, 0.0, 0.0], [0.005, 0.0, 0.0], [0.0, 0.005, 0.0]],
        dtype=np.float64,
    )
    faces = np.array([[0, 1, 2]], dtype=np.int64)
    return vertices, faces


@pytest.fixture
def quad_mesh_m():
    """2 つの大きな三角形で構成される四角形メッシュ（各辺 1 m）."""
    vertices = np.array(
        [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0]],
        dtype=np.float64,
    )
    faces = np.array([[0, 1, 2], [0, 2, 3]], dtype=np.int64)
    return vertices, faces


# ===========================================================================
# 1. エラー処理
# ===========================================================================


class TestSubdivideErrors:
    """max_edge バリデーションのテスト."""

    def test_max_edge_zero_raises_value_error(self, unit_triangle_m):
        """max_edge=0 のとき ValueError が発生すること."""
        verts, faces = unit_triangle_m
        with pytest.raises(ValueError, match="正の値"):
            subdivide_mesh_to_max_edge(verts, faces, 0.0)

    def test_max_edge_negative_raises_value_error(self, unit_triangle_m):
        """max_edge<0 のとき ValueError が発生すること."""
        verts, faces = unit_triangle_m
        with pytest.raises(ValueError, match="正の値"):
            subdivide_mesh_to_max_edge(verts, faces, -1.0)

    def test_max_edge_very_small_negative_raises(self, unit_triangle_m):
        """max_edge が微小負値（-1e-15）でも ValueError が発生すること."""
        verts, faces = unit_triangle_m
        with pytest.raises(ValueError):
            subdivide_mesh_to_max_edge(verts, faces, -1e-15)


# ===========================================================================
# 2. 分割の基本動作
# ===========================================================================


class TestSubdivideBasicBehavior:
    """分割の基本動作テスト."""

    def test_large_triangle_face_count_increases(self, unit_triangle_m):
        """辺長 1 m の三角形を max_edge=0.3 m で分割すると face 数が増えること."""
        verts, faces = unit_triangle_m
        new_verts, new_faces = subdivide_mesh_to_max_edge(verts, faces, 0.3)
        assert len(new_faces) > 1, f"face 数が増えること: {len(new_faces)}"

    def test_small_triangle_not_subdivided(self, small_triangle_m):
        """辺長 5 mm の三角形を max_edge=10 mm で分割すると face 数が変わらないこと."""
        verts, faces = small_triangle_m
        new_verts, new_faces = subdivide_mesh_to_max_edge(verts, faces, 0.010)
        # すでに辺長が max_edge 以下なので分割なし
        assert len(new_faces) == 1

    def test_more_subdivision_more_faces(self, unit_triangle_m):
        """max_edge を小さくするほど face 数が多くなること."""
        verts, faces = unit_triangle_m
        _, faces_coarse = subdivide_mesh_to_max_edge(verts, faces, 0.5)
        _, faces_fine = subdivide_mesh_to_max_edge(verts, faces, 0.2)
        assert len(faces_fine) >= len(faces_coarse), (
            f"細かい分割の方が face 数が多いこと: "
            f"coarse={len(faces_coarse)}, fine={len(faces_fine)}"
        )

    def test_quad_mesh_both_faces_subdivided(self, quad_mesh_m):
        """2 面のメッシュで両面が分割されること（face 数 > 2）."""
        verts, faces = quad_mesh_m
        new_verts, new_faces = subdivide_mesh_to_max_edge(verts, faces, 0.3)
        assert len(new_faces) > 2


# ===========================================================================
# 3. 出力の整合性
# ===========================================================================


class TestSubdivideOutputIntegrity:
    """出力配列の整合性テスト."""

    def test_output_vertices_dtype_float64(self, unit_triangle_m):
        """出力頂点配列の dtype が float64 であること."""
        verts, faces = unit_triangle_m
        new_verts, _ = subdivide_mesh_to_max_edge(verts, faces, 0.3)
        assert new_verts.dtype == np.float64

    def test_output_faces_dtype_int64(self, unit_triangle_m):
        """出力面配列の dtype が int64 であること."""
        verts, faces = unit_triangle_m
        _, new_faces = subdivide_mesh_to_max_edge(verts, faces, 0.3)
        assert new_faces.dtype == np.int64

    def test_vertices_shape_is_n_by_3(self, unit_triangle_m):
        """出力頂点の shape が (N_v', 3) であること."""
        verts, faces = unit_triangle_m
        new_verts, _ = subdivide_mesh_to_max_edge(verts, faces, 0.3)
        assert new_verts.ndim == 2
        assert new_verts.shape[1] == 3

    def test_faces_shape_is_n_by_3(self, unit_triangle_m):
        """出力面の shape が (N_f', 3) であること."""
        verts, faces = unit_triangle_m
        _, new_faces = subdivide_mesh_to_max_edge(verts, faces, 0.3)
        assert new_faces.ndim == 2
        assert new_faces.shape[1] == 3

    def test_face_indices_in_vertex_bounds(self, unit_triangle_m):
        """出力面のインデックスが頂点配列の範囲内であること."""
        verts, faces = unit_triangle_m
        new_verts, new_faces = subdivide_mesh_to_max_edge(verts, faces, 0.3)
        n_verts = len(new_verts)
        assert new_faces.min() >= 0, f"最小インデックス >= 0: {new_faces.min()}"
        assert new_faces.max() < n_verts, (
            f"最大インデックス < n_verts: {new_faces.max()} < {n_verts}"
        )

    def test_float32_input_vertices_work(self, unit_triangle_m):
        """float32 の頂点配列を渡しても動作し、float64 で返ること（内部でキャスト）."""
        verts, faces = unit_triangle_m
        verts_f32 = verts.astype(np.float32)
        new_verts, new_faces = subdivide_mesh_to_max_edge(verts_f32, faces, 0.3)
        assert new_verts.dtype == np.float64  # 内部で float64 にキャスト

    def test_int32_input_faces_work(self, unit_triangle_m):
        """int32 の面配列を渡しても動作し、int64 で返ること（内部でキャスト）."""
        verts, faces = unit_triangle_m
        faces_i32 = faces.astype(np.int32)
        new_verts, new_faces = subdivide_mesh_to_max_edge(verts, faces_i32, 0.3)
        assert new_faces.dtype == np.int64


# ===========================================================================
# 4. 決定論性
# ===========================================================================


class TestSubdivideDeterminism:
    """同一入力に対して常に同一の出力を返すこと（決定論性）."""

    def test_same_input_same_output_vertices(self, unit_triangle_m):
        """同一入力を 2 回渡すと同一の頂点を返すこと."""
        verts, faces = unit_triangle_m
        v1, _ = subdivide_mesh_to_max_edge(verts, faces, 0.3)
        v2, _ = subdivide_mesh_to_max_edge(verts, faces, 0.3)
        np.testing.assert_array_equal(v1, v2)

    def test_same_input_same_output_faces(self, unit_triangle_m):
        """同一入力を 2 回渡すと同一の面を返すこと."""
        verts, faces = unit_triangle_m
        _, f1 = subdivide_mesh_to_max_edge(verts, faces, 0.3)
        _, f2 = subdivide_mesh_to_max_edge(verts, faces, 0.3)
        np.testing.assert_array_equal(f1, f2)

    def test_different_max_edge_different_output(self, unit_triangle_m):
        """異なる max_edge では異なる出力を返すこと."""
        verts, faces = unit_triangle_m
        _, f1 = subdivide_mesh_to_max_edge(verts, faces, 0.5)
        _, f2 = subdivide_mesh_to_max_edge(verts, faces, 0.2)
        # face 数が異なるか、または同じでも配列が異なること
        assert not (len(f1) == len(f2) and np.array_equal(f1, f2))


# ===========================================================================
# 5. geometry パッケージの公開 API
# ===========================================================================


class TestGeometryPackageExport:
    """geometry パッケージからの公開 API テスト."""

    def test_import_from_geometry_package(self):
        """geometry パッケージから subdivide_mesh_to_max_edge をインポートできること."""
        from sm_tias_lightsim.geometry import subdivide_mesh_to_max_edge as f
        assert callable(f)

    def test_geometry_all_includes_subdivide(self):
        """geometry パッケージの __all__ に subdivide_mesh_to_max_edge が含まれること."""
        import sm_tias_lightsim.geometry as geo
        assert "subdivide_mesh_to_max_edge" in geo.__all__
