"""Tests for Laplace solver."""

import numpy as np
import pytest


class SimpleGeometry:
    """Simple circle geometry for testing using trapezoidal quadrature.

    Note: This uses uniform (trapezoidal) quadrature which is lower-order
    than the Gauss-Legendre quadrature used by superellipse's panel_discretization.
    Tests using this geometry should have looser tolerances.
    """

    def __init__(self, n=128, radius=1.0):
        theta = np.linspace(0, 2 * np.pi, n, endpoint=False)
        self._points = radius * np.column_stack([np.cos(theta), np.sin(theta)])
        self._normals = np.column_stack([np.cos(theta), np.sin(theta)])
        self._weights = np.ones(n) * 2 * np.pi * radius / n
        self._curvature = np.ones(n) / radius

    @property
    def points(self):
        return self._points

    @property
    def normals(self):
        return self._normals

    @property
    def weights(self):
        return self._weights

    @property
    def curvature(self):
        return self._curvature


class TestDoubleLayerLaplace:
    """Tests for the DoubleLayerLaplace solver."""

    @pytest.fixture
    def circle_geometry(self):
        """Create a unit circle discretization for testing."""
        return SimpleGeometry(n=128)  # More points for accuracy

    def test_constant_boundary_data(self, circle_geometry):
        """Constant boundary data should give constant solution."""
        from panel_bie.laplace import DoubleLayerLaplace

        solver = DoubleLayerLaplace(circle_geometry)

        # u = 1 on boundary
        g = np.ones(len(circle_geometry.points))
        mu = solver.solve(g)

        # Evaluate at origin
        u_origin = solver.evaluate(mu, np.array([0.0, 0.0]))

        # Should be approximately 1 (trapezoidal quadrature has ~2% error)
        assert u_origin == pytest.approx(1.0, rel=0.02)

    def test_linear_boundary_data(self, circle_geometry):
        """Linear boundary data u=x should give u=x inside."""
        from panel_bie.laplace import DoubleLayerLaplace

        solver = DoubleLayerLaplace(circle_geometry)

        # u = x on boundary
        g = circle_geometry.points[:, 0]
        mu = solver.solve(g)

        # Test at several interior points (looser tolerance for trap rule)
        test_points = np.array([
            [0.0, 0.0],
            [0.3, 0.0],
            [0.0, 0.4],
        ])

        for p in test_points:
            u = solver.evaluate(mu, p)
            expected = p[0]  # u = x
            assert u == pytest.approx(expected, abs=0.02)

    def test_quadratic_boundary_data(self, circle_geometry):
        """Test with u = x² - y² (harmonic)."""
        from panel_bie.laplace import DoubleLayerLaplace

        solver = DoubleLayerLaplace(circle_geometry)

        # u = x² - y² on boundary
        pts = circle_geometry.points
        g = pts[:, 0] ** 2 - pts[:, 1] ** 2
        mu = solver.solve(g)

        # Test at interior points
        test_points = np.array([
            [0.0, 0.0],
            [0.3, 0.2],
        ])

        for p in test_points:
            u = solver.evaluate(mu, p)
            expected = p[0] ** 2 - p[1] ** 2
            assert u == pytest.approx(expected, abs=0.02)

    def test_solver_callable_boundary(self, circle_geometry):
        """Solver should accept callable for boundary data."""
        from panel_bie.laplace import DoubleLayerLaplace

        solver = DoubleLayerLaplace(circle_geometry)

        def boundary_func(pt):
            return pt[0] + 2 * pt[1]

        mu = solver.solve(boundary_func)

        # Should work and give reasonable result
        u = solver.evaluate(mu, np.array([0.0, 0.0]))
        assert u == pytest.approx(0.0, abs=0.02)

        u = solver.evaluate(mu, np.array([0.25, 0.25]))
        assert u == pytest.approx(0.75, abs=0.05)

    def test_assemble_matrix_shape(self, circle_geometry):
        """Assembled matrix should be square."""
        from panel_bie.laplace import DoubleLayerLaplace

        solver = DoubleLayerLaplace(circle_geometry)
        A = solver.assemble()

        n = len(circle_geometry.points)
        assert A.shape == (n, n)

    def test_assemble_caches_matrix(self, circle_geometry):
        """Matrix assembly should be cached."""
        from panel_bie.laplace import DoubleLayerLaplace

        solver = DoubleLayerLaplace(circle_geometry)
        A1 = solver.assemble()
        A2 = solver.assemble()

        # Should be the same object
        assert A1 is A2

    def test_n_nodes_property(self, circle_geometry):
        """n_nodes should return number of boundary points."""
        from panel_bie.laplace import DoubleLayerLaplace

        solver = DoubleLayerLaplace(circle_geometry)
        assert solver.n_nodes == len(circle_geometry.points)

    def test_evaluate_grid(self, circle_geometry):
        """Test grid evaluation."""
        from panel_bie.laplace import DoubleLayerLaplace

        solver = DoubleLayerLaplace(circle_geometry)

        # u = x on boundary
        g = circle_geometry.points[:, 0]
        mu = solver.solve(g)

        # Create a small grid
        grid = np.array([
            [0.0, 0.0],
            [0.3, 0.0],
            [0.0, 0.4],
            [0.0, 0.0],  # Duplicate to test
        ])

        u = solver.evaluate_grid(mu, grid)

        assert u.shape == (4,)
        assert u[0] == pytest.approx(0.0, abs=0.02)
        assert u[1] == pytest.approx(0.3, abs=0.02)
        assert u[2] == pytest.approx(0.0, abs=0.02)

    def test_evaluate_grid_with_mask(self, circle_geometry):
        """Test grid evaluation with mask."""
        from panel_bie.laplace import DoubleLayerLaplace

        solver = DoubleLayerLaplace(circle_geometry)

        g = circle_geometry.points[:, 0]
        mu = solver.solve(g)

        grid = np.array([
            [0.0, 0.0],
            [0.3, 0.0],
            [2.0, 0.0],  # Outside, should be masked
        ])

        mask = np.array([True, True, False])
        u = solver.evaluate_grid(mu, grid, mask=mask)

        assert u.shape == (3,)
        assert not np.isnan(u[0])
        assert not np.isnan(u[1])
        assert np.isnan(u[2])  # Masked point

    def test_larger_radius(self):
        """Test with larger circle radius."""
        from panel_bie.laplace import DoubleLayerLaplace

        geom = SimpleGeometry(n=128, radius=2.0)
        solver = DoubleLayerLaplace(geom)

        # u = 1 on boundary
        g = np.ones(len(geom.points))
        mu = solver.solve(g)

        u = solver.evaluate(mu, np.array([0.0, 0.0]))
        assert u == pytest.approx(1.0, rel=0.02)


class TestSingleLayerLaplace:
    """Tests for SingleLayerLaplace solver."""

    @pytest.fixture
    def circle_geometry(self):
        return SimpleGeometry(n=64)

    def test_assemble_shape(self, circle_geometry):
        """Assembled matrix should be square."""
        from panel_bie.laplace import SingleLayerLaplace

        solver = SingleLayerLaplace(circle_geometry)
        A = solver.assemble()

        n = len(circle_geometry.points)
        assert A.shape == (n, n)

    def test_n_nodes_property(self, circle_geometry):
        """n_nodes should return number of boundary points."""
        from panel_bie.laplace import SingleLayerLaplace

        solver = SingleLayerLaplace(circle_geometry)
        assert solver.n_nodes == len(circle_geometry.points)

    def test_evaluate_single_layer_potential(self, circle_geometry):
        """Test evaluation of single-layer potential."""
        from panel_bie.laplace import SingleLayerLaplace

        solver = SingleLayerLaplace(circle_geometry)

        # Constant density
        sigma = np.ones(len(circle_geometry.points))

        # Evaluate at origin
        u = solver.evaluate(sigma, np.array([0.0, 0.0]))

        # Should be finite
        assert np.isfinite(u)
