"""Tests for mesh generation."""

import struct

import numpy as np
import pytest

from pywolken.core.pointcloud import PointCloud
from pywolken.mesh.triangulate import Mesh, triangulate_2d


@pytest.fixture
def grid_pc():
    """Regular 5x5 grid point cloud."""
    x, y = np.meshgrid(np.arange(5, dtype=np.float64), np.arange(5, dtype=np.float64))
    x, y = x.ravel(), y.ravel()
    z = np.zeros_like(x)
    return PointCloud.from_dict({"X": x, "Y": y, "Z": z})


@pytest.fixture
def sloped_pc():
    """Sloped surface: Z = X + Y."""
    x, y = np.meshgrid(
        np.linspace(0, 10, 10, dtype=np.float64),
        np.linspace(0, 10, 10, dtype=np.float64),
    )
    x, y = x.ravel(), y.ravel()
    z = x + y
    return PointCloud.from_dict({"X": x, "Y": y, "Z": z})


class TestTriangulate2D:
    def test_basic_triangulation(self, grid_pc):
        mesh = triangulate_2d(grid_pc)
        assert mesh.num_vertices == 25
        assert mesh.num_faces > 0
        # A 5x5 grid should produce 4*4*2 = 32 triangles
        assert mesh.num_faces == 32

    def test_vertices_match_input(self, grid_pc):
        mesh = triangulate_2d(grid_pc)
        np.testing.assert_allclose(mesh.vertices[:, 0], grid_pc["X"])
        np.testing.assert_allclose(mesh.vertices[:, 1], grid_pc["Y"])
        np.testing.assert_allclose(mesh.vertices[:, 2], grid_pc["Z"])

    def test_sloped_surface(self, sloped_pc):
        mesh = triangulate_2d(sloped_pc)
        assert mesh.num_vertices == 100
        assert mesh.num_faces > 0

    def test_max_edge_length(self):
        """Sparse points with max_edge_length should remove long triangles."""
        pc = PointCloud.from_dict({
            "X": np.array([0.0, 1.0, 0.5, 100.0]),
            "Y": np.array([0.0, 0.0, 1.0, 100.0]),
            "Z": np.array([0.0, 0.0, 0.0, 0.0]),
        })
        mesh_full = triangulate_2d(pc)
        mesh_filtered = triangulate_2d(pc, max_edge_length=5.0)
        assert mesh_filtered.num_faces < mesh_full.num_faces

    def test_with_colors(self):
        pc = PointCloud.from_dict({
            "X": np.array([0.0, 1.0, 0.5]),
            "Y": np.array([0.0, 0.0, 1.0]),
            "Z": np.array([0.0, 0.0, 0.0]),
            "Red": np.array([255, 128, 0], dtype=np.uint16),
            "Green": np.array([0, 128, 255], dtype=np.uint16),
            "Blue": np.array([100, 50, 200], dtype=np.uint16),
        })
        mesh = triangulate_2d(pc)
        assert mesh.vertex_colors is not None
        assert mesh.vertex_colors.shape == (3, 3)

    def test_repr(self, grid_pc):
        mesh = triangulate_2d(grid_pc)
        r = repr(mesh)
        assert "25" in r
        assert "32" in r


class TestMeshExport:
    def test_write_obj(self, grid_pc, tmp_path):
        mesh = triangulate_2d(grid_pc)
        path = str(tmp_path / "test.obj")
        mesh.write_obj(path)

        content = open(path).read()
        assert content.startswith("# Generated by pywolken")
        assert content.count("\nv ") == 25
        assert content.count("\nf ") == 32

    def test_write_stl(self, grid_pc, tmp_path):
        mesh = triangulate_2d(grid_pc)
        path = str(tmp_path / "test.stl")
        mesh.write_stl(path)

        with open(path, "rb") as f:
            f.read(80)  # header
            num_triangles = struct.unpack("<I", f.read(4))[0]
        assert num_triangles == 32

    def test_write_ply(self, grid_pc, tmp_path):
        mesh = triangulate_2d(grid_pc)
        path = str(tmp_path / "test.ply")
        mesh.write_ply(path)

        content = open(path).read()
        assert "ply" in content
        assert "element vertex 25" in content
        assert "element face 32" in content

    def test_write_auto_detect(self, grid_pc, tmp_path):
        mesh = triangulate_2d(grid_pc)
        for ext in [".obj", ".stl", ".ply"]:
            path = str(tmp_path / f"test{ext}")
            mesh.write(path)
            assert (tmp_path / f"test{ext}").exists()

    def test_write_unsupported_raises(self, grid_pc, tmp_path):
        mesh = triangulate_2d(grid_pc)
        with pytest.raises(ValueError, match="Unsupported"):
            mesh.write(str(tmp_path / "test.xyz"))


class TestMeshDecimate:
    def test_decimate_reduces_faces(self, grid_pc):
        mesh = triangulate_2d(grid_pc)
        decimated = mesh.decimate(10)
        assert decimated.num_faces == 10
        assert decimated.num_vertices <= mesh.num_vertices

    def test_decimate_no_op_if_target_larger(self, grid_pc):
        mesh = triangulate_2d(grid_pc)
        decimated = mesh.decimate(1000)
        assert decimated.num_faces == mesh.num_faces
