"""Tests for BIE kernel functions."""

import numpy as np
import pytest

from panel_bie.kernels import (
    laplace_double_layer_kernel,
    laplace_double_layer_diagonal,
    laplace_double_layer_matrix,
)


class TestDoublLayerKernel:
    """Tests for double-layer kernel evaluation."""

    def test_kernel_basic_value(self):
        """Test kernel at a specific point."""
        x = np.array([1.0, 0.0])
        y = np.array([0.0, 1.0])
        n_y = np.array([0.0, 1.0])  # Normal pointing in +y

        k = laplace_double_layer_kernel(x, y, n_y)

        # K(x,y) = (1/2π) * (x-y)·n_y / |x-y|²
        diff = x - y  # [1, -1]
        r_sq = 2.0
        expected = (1 / (2 * np.pi)) * np.dot(diff, n_y) / r_sq
        # dot = 1*0 + (-1)*1 = -1
        expected = -1 / (2 * np.pi * 2)

        assert k == pytest.approx(expected)

    def test_kernel_zero_when_perpendicular(self):
        """Kernel is zero when (x-y) perpendicular to normal."""
        x = np.array([1.0, 0.0])
        y = np.array([0.0, 0.0])
        n_y = np.array([0.0, 1.0])  # Perpendicular to x-y

        k = laplace_double_layer_kernel(x, y, n_y)
        assert k == pytest.approx(0.0)

    def test_kernel_symmetry(self):
        """Test kernel behavior under reflection."""
        x = np.array([2.0, 1.0])
        y = np.array([1.0, 0.0])
        n_y = np.array([1.0, 0.0])

        k1 = laplace_double_layer_kernel(x, y, n_y)

        # Reflect across x-axis
        x_ref = np.array([2.0, -1.0])
        y_ref = np.array([1.0, 0.0])
        n_y_ref = np.array([1.0, 0.0])

        k2 = laplace_double_layer_kernel(x_ref, y_ref, n_y_ref)

        # Should be equal by symmetry
        assert k1 == pytest.approx(k2)


class TestDiagonalCorrection:
    """Tests for diagonal (self-interaction) correction."""

    def test_diagonal_flat(self):
        """Zero curvature should give zero diagonal."""
        kappa = 0.0
        diag = laplace_double_layer_diagonal(kappa)
        assert diag == 0.0

    def test_diagonal_circle(self):
        """Unit circle has κ=1, diagonal should be 1/(4π)."""
        kappa = 1.0
        diag = laplace_double_layer_diagonal(kappa)
        assert diag == pytest.approx(1 / (4 * np.pi))

    def test_diagonal_proportional_to_curvature(self):
        """Diagonal should be proportional to curvature."""
        kappa1, kappa2 = 2.0, 4.0
        d1 = laplace_double_layer_diagonal(kappa1)
        d2 = laplace_double_layer_diagonal(kappa2)

        assert d2 / d1 == pytest.approx(kappa2 / kappa1)


class TestDoublLayerMatrix:
    """Tests for full matrix assembly."""

    def test_matrix_shape(self):
        """Matrix should be square with size = number of nodes."""
        n = 32
        targets = np.random.randn(n, 2)
        sources = targets.copy()
        normals = np.column_stack([
            np.cos(np.linspace(0, 2*np.pi, n)),
            np.sin(np.linspace(0, 2*np.pi, n)),
        ])
        weights = np.ones(n) * 2 * np.pi / n
        curvature = np.ones(n)

        K = laplace_double_layer_matrix(
            targets, sources, normals, weights,
            curvature=curvature, same_points=True
        )

        assert K.shape == (n, n)

    def test_matrix_diagonal_uses_curvature(self):
        """When same_points=True, diagonal should use curvature correction."""
        n = 16
        # Circle
        theta = np.linspace(0, 2*np.pi, n, endpoint=False)
        sources = np.column_stack([np.cos(theta), np.sin(theta)])
        normals = sources.copy()  # Outward normals for unit circle
        weights = np.ones(n) * 2 * np.pi / n
        curvature = np.ones(n)  # κ = 1 for unit circle

        K = laplace_double_layer_matrix(
            sources, sources, normals, weights,
            curvature=curvature, same_points=True
        )

        # Diagonal should be κ/(4π) * weight = 1/(4π) * (2π/n) = 1/(2n)
        expected_diag = curvature / (4 * np.pi) * weights
        np.testing.assert_allclose(np.diag(K), expected_diag, rtol=1e-10)
