"""
Define the unit tests for the :mod:`colour.algebra.interpolation` module.

References
----------
-   :cite:`Fairman1985b` : Fairman, H. S. (1985). The calculation of weight
    factors for tristimulus integration. Color Research & Application, 10(4),
    199-203. doi:10.1002/col.5080100407
"""

from __future__ import annotations

import os
from itertools import product

import numpy as np
import pytest

from colour.algebra import (
    CubicSplineInterpolator,
    KernelInterpolator,
    LinearInterpolator,
    NearestNeighbourInterpolator,
    NullInterpolator,
    PchipInterpolator,
    SpragueInterpolator,
    kernel_cardinal_spline,
    kernel_lanczos,
    kernel_linear,
    kernel_nearest_neighbour,
    kernel_sinc,
    lagrange_coefficients,
    random_triplet_generator,
    table_interpolation,
    table_interpolation_tetrahedral,
    table_interpolation_trilinear,
)
from colour.constants import TOLERANCE_ABSOLUTE_TESTS
from colour.hints import NDArrayFloat, cast
from colour.io import LUT3D, read_LUT
from colour.utilities import ignore_numpy_errors, is_scipy_installed

__author__ = "Colour Developers"
__copyright__ = "Copyright 2013 Colour Developers"
__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"

__all__ = [
    "DATA_POINTS_A",
    "DATA_POINTS_A_LINEAR_INTERPOLATED_10_SAMPLES",
    "DATA_POINTS_A_SPRAGUE_INTERPOLATED_10_SAMPLES",
    "DATA_POINTS_A_CUBIC_SPLINE_INTERPOLATED_X2_SAMPLES",
    "LAGRANGE_COEFFICIENTS_A",
    "LAGRANGE_COEFFICIENTS_B",
    "LUT_TABLE",
    "TestKernelNearestNeighbour",
    "TestKernelLinear",
    "TestKernelSinc",
    "TestKernelLanczos",
    "TestKernelCardinalSpline",
    "TestKernelInterpolator",
    "TestLinearInterpolator",
    "TestSpragueInterpolator",
    "TestCubicSplineInterpolator",
    "TestPchipInterpolator",
    "TestNullInterpolator",
    "TestLagrangeCoefficients",
    "TestTableInterpolationTrilinear",
    "TestTableInterpolationTetrahedral",
]

DATA_POINTS_A: tuple = (
    9.3700,
    12.3200,
    12.4600,
    9.5100,
    5.9200,
    4.3300,
    4.2900,
    3.8800,
    4.5100,
    10.9200,
    27.5000,
    49.6700,
    69.5900,
    81.7300,
    88.1900,
    86.0500,
)

DATA_POINTS_A_LINEAR_INTERPOLATED_10_SAMPLES: tuple = (
    9.370,
    9.665,
    9.960,
    10.255,
    10.550,
    10.845,
    11.140,
    11.435,
    11.730,
    12.025,
    12.320,
    12.334,
    12.348,
    12.362,
    12.376,
    12.390,
    12.404,
    12.418,
    12.432,
    12.446,
    12.460,
    12.165,
    11.870,
    11.575,
    11.280,
    10.985,
    10.690,
    10.395,
    10.100,
    9.805,
    9.510,
    9.151,
    8.792,
    8.433,
    8.074,
    7.715,
    7.356,
    6.997,
    6.638,
    6.279,
    5.920,
    5.761,
    5.602,
    5.443,
    5.284,
    5.125,
    4.966,
    4.807,
    4.648,
    4.489,
    4.330,
    4.326,
    4.322,
    4.318,
    4.314,
    4.310,
    4.306,
    4.302,
    4.298,
    4.294,
    4.290,
    4.249,
    4.208,
    4.167,
    4.126,
    4.085,
    4.044,
    4.003,
    3.962,
    3.921,
    3.880,
    3.943,
    4.006,
    4.069,
    4.132,
    4.195,
    4.258,
    4.321,
    4.384,
    4.447,
    4.510,
    5.151,
    5.792,
    6.433,
    7.074,
    7.715,
    8.356,
    8.997,
    9.638,
    10.279,
    10.920,
    12.578,
    14.236,
    15.894,
    17.552,
    19.210,
    20.868,
    22.526,
    24.184,
    25.842,
    27.500,
    29.717,
    31.934,
    34.151,
    36.368,
    38.585,
    40.802,
    43.019,
    45.236,
    47.453,
    49.670,
    51.662,
    53.654,
    55.646,
    57.638,
    59.630,
    61.622,
    63.614,
    65.606,
    67.598,
    69.590,
    70.804,
    72.018,
    73.232,
    74.446,
    75.660,
    76.874,
    78.088,
    79.302,
    80.516,
    81.730,
    82.376,
    83.022,
    83.668,
    84.314,
    84.960,
    85.606,
    86.252,
    86.898,
    87.544,
    88.190,
    87.976,
    87.762,
    87.548,
    87.334,
    87.120,
    86.906,
    86.692,
    86.478,
    86.264,
    86.050,
)

DATA_POINTS_A_SPRAGUE_INTERPOLATED_10_SAMPLES: tuple = (
    9.37000000,
    9.72075073,
    10.06936191,
    10.41147570,
    10.74302270,
    11.06022653,
    11.35960827,
    11.63799100,
    11.89250427,
    12.12058860,
    12.32000000,
    12.48873542,
    12.62489669,
    12.72706530,
    12.79433478,
    12.82623598,
    12.82266243,
    12.78379557,
    12.71003009,
    12.60189921,
    12.46000000,
    12.28440225,
    12.07404800,
    11.82976500,
    11.55443200,
    11.25234375,
    10.92857600,
    10.58835050,
    10.23640000,
    9.87633325,
    9.51000000,
    9.13692962,
    8.75620800,
    8.36954763,
    7.98097600,
    7.59601562,
    7.22086400,
    6.86157362,
    6.52323200,
    6.20914162,
    5.92000000,
    5.65460200,
    5.41449600,
    5.20073875,
    5.01294400,
    4.84968750,
    4.70891200,
    4.58833225,
    4.48584000,
    4.39990900,
    4.33000000,
    4.27757887,
    4.24595200,
    4.23497388,
    4.24099200,
    4.25804688,
    4.27907200,
    4.29709387,
    4.30643200,
    4.30389887,
    4.29000000,
    4.26848387,
    4.24043200,
    4.20608887,
    4.16603200,
    4.12117188,
    4.07275200,
    4.02234887,
    3.97187200,
    3.92356387,
    3.88000000,
    3.84319188,
    3.81318400,
    3.79258487,
    3.78691200,
    3.80367187,
    3.85144000,
    3.93894087,
    4.07412800,
    4.26326387,
    4.51000000,
    4.81362075,
    5.17028800,
    5.58225150,
    6.05776000,
    6.60890625,
    7.24947200,
    7.99277300,
    8.84950400,
    9.82558375,
    10.92000000,
    12.12700944,
    13.44892800,
    14.88581406,
    16.43283200,
    18.08167969,
    19.82201600,
    21.64288831,
    23.53416000,
    25.48793794,
    27.50000000,
    29.57061744,
    31.69964800,
    33.88185481,
    36.10777600,
    38.36511719,
    40.64014400,
    42.91907456,
    45.18947200,
    47.44163694,
    49.67000000,
    51.87389638,
    54.05273600,
    56.20157688,
    58.31198400,
    60.37335938,
    62.37427200,
    64.30378787,
    66.15280000,
    67.91535838,
    69.59000000,
    71.17616669,
    72.66283200,
    74.04610481,
    75.33171200,
    76.53183594,
    77.66195200,
    78.73766606,
    79.77155200,
    80.76998919,
    81.73000000,
    82.64375688,
    83.51935227,
    84.35919976,
    85.15567334,
    85.89451368,
    86.55823441,
    87.12952842,
    87.59467414,
    87.94694187,
    88.19000000,
    88.33345751,
    88.37111372,
    88.30221714,
    88.13600972,
    87.88846516,
    87.57902706,
    87.22734720,
    86.85002373,
    86.45733945,
    86.05000000,
)

DATA_POINTS_A_CUBIC_SPLINE_INTERPOLATED_X2_SAMPLES: tuple = (
    9.37000000,
    11.08838189,
    12.26359953,
    12.78808025,
    12.55425139,
    11.50391691,
    9.87473603,
    8.01707329,
    6.30369624,
    5.08664365,
    4.43550284,
    4.25438019,
    4.29206798,
    4.21753374,
    3.98875865,
    3.79691327,
    4.02534907,
    5.23223510,
    8.08816250,
    13.36306794,
    21.19519815,
    30.89350026,
    41.64531611,
    52.53540869,
    62.65180882,
    71.10713687,
    77.46889540,
    82.31355134,
    86.05208477,
    88.28078752,
    88.45998434,
    86.05000000,
)

LAGRANGE_COEFFICIENTS_A: NDArrayFloat = np.array(
    [
        [0.92625, 0.09750, -0.02375],
        [0.85500, 0.19000, -0.04500],
        [0.78625, 0.27750, -0.06375],
        [0.72000, 0.36000, -0.08000],
        [0.65625, 0.43750, -0.09375],
        [0.59500, 0.51000, -0.10500],
        [0.53625, 0.57750, -0.11375],
        [0.48000, 0.64000, -0.12000],
        [0.42625, 0.69750, -0.12375],
        [0.37500, 0.75000, -0.12500],
        [0.32625, 0.79750, -0.12375],
        [0.28000, 0.84000, -0.12000],
        [0.23625, 0.87750, -0.11375],
        [0.19500, 0.91000, -0.10500],
        [0.15625, 0.93750, -0.09375],
        [0.12000, 0.96000, -0.08000],
        [0.08625, 0.97750, -0.06375],
        [0.05500, 0.99000, -0.04500],
        [0.02625, 0.99750, -0.02375],
    ]
)

LAGRANGE_COEFFICIENTS_B: NDArrayFloat = np.array(
    [
        [-0.0154375, 0.9725625, 0.0511875, -0.0083125],
        [-0.0285000, 0.9405000, 0.1045000, -0.0165000],
        [-0.0393125, 0.9041875, 0.1595625, -0.0244375],
        [-0.0480000, 0.8640000, 0.2160000, -0.0320000],
        [-0.0546875, 0.8203125, 0.2734375, -0.0390625],
        [-0.0595000, 0.7735000, 0.3315000, -0.0455000],
        [-0.0625625, 0.7239375, 0.3898125, -0.0511875],
        [-0.0640000, 0.6720000, 0.4480000, -0.0560000],
        [-0.0639375, 0.6180625, 0.5056875, -0.0598125],
        [-0.0625000, 0.5625000, 0.5625000, -0.0625000],
        [-0.0598125, 0.5056875, 0.6180625, -0.0639375],
        [-0.0560000, 0.4480000, 0.6720000, -0.0640000],
        [-0.0511875, 0.3898125, 0.7239375, -0.0625625],
        [-0.0455000, 0.3315000, 0.7735000, -0.0595000],
        [-0.0390625, 0.2734375, 0.8203125, -0.0546875],
        [-0.0320000, 0.2160000, 0.8640000, -0.0480000],
        [-0.0244375, 0.1595625, 0.9041875, -0.0393125],
        [-0.0165000, 0.1045000, 0.9405000, -0.0285000],
        [-0.0083125, 0.0511875, 0.9725625, -0.0154375],
    ]
)

LUT_TABLE: NDArrayFloat = cast(
    "LUT3D",
    read_LUT(
        os.path.join(
            os.path.dirname(__file__),
            "..",
            "..",
            "io",
            "luts",
            "tests",
            "resources",
            "iridas_cube",
            "Colour_Correct.cube",
        )
    ),
).table


class TestKernelNearestNeighbour:
    """
    Define :func:`colour.algebra.interpolation.kernel_nearest_neighbour`
    definition unit tests methods.
    """

    def test_kernel_nearest(self) -> None:
        """
        Test :func:`colour.algebra.interpolation.kernel_nearest_neighbour`
        definition.
        """

        np.testing.assert_allclose(
            kernel_nearest_neighbour(np.linspace(-5, 5, 25)),
            np.array(
                [
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    1,
                    1,
                    1,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                ]
            ),
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )


class TestKernelLinear:
    """
    Define :func:`colour.algebra.interpolation.kernel_linear` definition
    unit tests methods.
    """

    def test_kernel_linear(self) -> None:
        """Test :func:`colour.algebra.interpolation.kernel_linear` definition."""

        np.testing.assert_allclose(
            kernel_linear(np.linspace(-5, 5, 25)),
            np.array(
                [
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.16666667,
                    0.58333333,
                    1.00000000,
                    0.58333333,
                    0.16666667,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                ]
            ),
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )


class TestKernelSinc:
    """
    Define :func:`colour.algebra.interpolation.kernel_sinc` definition
    unit tests methods.
    """

    def test_kernel_sinc(self) -> None:
        """Test :func:`colour.algebra.interpolation.kernel_sinc` definition."""

        np.testing.assert_allclose(
            kernel_sinc(np.linspace(-5, 5, 25)),
            np.array(
                [
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.02824617,
                    0.12732395,
                    0.03954464,
                    -0.16539867,
                    -0.18006326,
                    0.19098593,
                    0.73791298,
                    1.00000000,
                    0.73791298,
                    0.19098593,
                    -0.18006326,
                    -0.16539867,
                    0.03954464,
                    0.12732395,
                    0.02824617,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                ]
            ),
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )

        np.testing.assert_allclose(
            kernel_sinc(np.linspace(-5, 5, 25), 1),
            np.array(
                [
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.19098593,
                    0.73791298,
                    1.00000000,
                    0.73791298,
                    0.19098593,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                ]
            ),
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )


class TestKernelLanczos:
    """
    Define :func:`colour.algebra.interpolation.kernel_lanczos` definition
    unit tests methods.
    """

    def test_kernel_lanczos(self) -> None:
        """Test :func:`colour.algebra.interpolation.kernel_lanczos` definition."""

        np.testing.assert_allclose(
            kernel_lanczos(np.linspace(-5, 5, 25)),
            np.array(
                [
                    0.00000000e00,
                    0.00000000e00,
                    0.00000000e00,
                    0.00000000e00,
                    0.00000000e00,
                    8.06009483e-04,
                    2.43170841e-02,
                    1.48478897e-02,
                    -9.33267411e-02,
                    -1.32871018e-01,
                    1.67651704e-01,
                    7.14720157e-01,
                    1.00000000e00,
                    7.14720157e-01,
                    1.67651704e-01,
                    -1.32871018e-01,
                    -9.33267411e-02,
                    1.48478897e-02,
                    2.43170841e-02,
                    8.06009483e-04,
                    0.00000000e00,
                    0.00000000e00,
                    0.00000000e00,
                    0.00000000e00,
                    0.00000000e00,
                ]
            ),
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )

        np.testing.assert_allclose(
            kernel_lanczos(np.linspace(-5, 5, 25), 1),
            np.array(
                [
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.03647563,
                    0.54451556,
                    1.00000000,
                    0.54451556,
                    0.03647563,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                ]
            ),
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )


class TestKernelCardinalSpline:
    """
    Define :func:`colour.algebra.interpolation.kernel_cardinal_spline`
    definition unit tests methods.
    """

    def test_kernel_cardinal_spline(self) -> None:
        """
        Test :func:`colour.algebra.interpolation.kernel_cardinal_spline`
        definition.
        """

        np.testing.assert_allclose(
            kernel_cardinal_spline(np.linspace(-5, 5, 25)),
            np.array(
                [
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    -0.03703704,
                    -0.0703125,
                    0.13194444,
                    0.67447917,
                    1.00000000,
                    0.67447917,
                    0.13194444,
                    -0.0703125,
                    -0.03703704,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                ]
            ),
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )

        np.testing.assert_allclose(
            kernel_cardinal_spline(np.linspace(-5, 5, 25), 0, 1),
            np.array(
                [
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00617284,
                    0.0703125,
                    0.26157407,
                    0.52922454,
                    0.66666667,
                    0.52922454,
                    0.26157407,
                    0.0703125,
                    0.00617284,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                    0.00000000,
                ]
            ),
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )


class TestKernelInterpolator:
    """
    Define :class:`colour.algebra.interpolation.KernelInterpolator` class unit
    tests methods.
    """

    def test_required_attributes(self) -> None:
        """Test the presence of required attributes."""

        if not is_scipy_installed():  # pragma: no cover
            return

        required_attributes = (
            "x",
            "y",
            "window",
            "kernel",
            "kernel_kwargs",
            "padding_kwargs",
        )

        for attribute in required_attributes:
            assert attribute in dir(KernelInterpolator)

    def test_required_methods(self) -> None:
        """Test the presence of required methods."""

        required_methods = ("__init__", "__call__")

        for method in required_methods:  # pragma: no cover
            assert method in dir(KernelInterpolator)

    def test_x(self) -> None:
        """
        Test :attr:`colour.algebra.interpolation.KernelInterpolator.x`
        property.
        """

        x = y = np.linspace(0, 1, 10)
        kernel_interpolator = KernelInterpolator(x, y)

        np.testing.assert_equal(kernel_interpolator.x, x)

    def test_y(self) -> None:
        """
        Test :attr:`colour.algebra.interpolation.KernelInterpolator.y`
        property.
        """

        if not is_scipy_installed():  # pragma: no cover
            return

        x = y = np.linspace(0, 1, 10)
        kernel_interpolator = KernelInterpolator(x, y)

        np.testing.assert_equal(kernel_interpolator.y, y)

    def test_window(self) -> None:
        """
        Test :attr:`colour.algebra.interpolation.KernelInterpolator.window`
        property.
        """

        x = y = np.linspace(0, 1, 10)
        kernel_interpolator = KernelInterpolator(x, y, window=3)

        assert kernel_interpolator.window == 3

    def test_kernel(self) -> None:
        """
        Test :attr:`colour.algebra.interpolation.KernelInterpolator.kernel`
        property.
        """

        x = y = np.linspace(0, 1, 10)
        kernel_interpolator = KernelInterpolator(x, y, kernel=kernel_linear)

        assert kernel_interpolator.kernel is kernel_linear

    def test_kernel_kwargs(self) -> None:
        """
        Test :attr:`colour.algebra.interpolation.KernelInterpolator.\
kernel_kwargs` property.
        """

        x = y = np.linspace(0, 1, 10)
        kernel_kwargs = {"a": 1}
        kernel_interpolator = KernelInterpolator(x, y, kernel_kwargs=kernel_kwargs)

        assert kernel_interpolator.kernel_kwargs == kernel_kwargs

    def test_padding_kwargs(self) -> None:
        """
        Test :attr:`colour.algebra.interpolation.KernelInterpolator.\
padding_kwargs` property.
        """

        x = y = np.linspace(0, 1, 10)
        padding_kwargs = {"pad_width": (3, 3), "mode": "mean"}
        kernel_interpolator = KernelInterpolator(x, y, padding_kwargs=padding_kwargs)

        assert kernel_interpolator.padding_kwargs == padding_kwargs

    def test_raise_exception___init__(self) -> None:
        """
        Test :meth:`colour.algebra.interpolation.KernelInterpolator.__init__`
        method raised exception.
        """

        pytest.raises(
            ValueError,
            KernelInterpolator,
            np.linspace(0, 1, 10),
            np.linspace(0, 1, 15),
        )

    def test__call__(self) -> None:
        """
        Test :meth:`colour.algebra.interpolation.KernelInterpolator.__call__`
        method.
        """

        if not is_scipy_installed():  # pragma: no cover
            return

        x = np.arange(11, 26, 1)
        y = np.sin(x / len(x) * np.pi * 6) / (x / len(x)) + np.pi
        x_i = np.linspace(11, 25, 25)

        kernel_interpolator = KernelInterpolator(x, y)
        np.testing.assert_allclose(
            kernel_interpolator(x_i),
            np.array(
                [
                    4.43848790,
                    4.26286480,
                    3.64640076,
                    2.77982023,
                    2.13474499,
                    2.08206794,
                    2.50585862,
                    3.24992692,
                    3.84593162,
                    4.06289704,
                    3.80825633,
                    3.21068994,
                    2.65177161,
                    2.32137382,
                    2.45995375,
                    2.88799997,
                    3.43843598,
                    3.79504892,
                    3.79937086,
                    3.47673343,
                    2.99303182,
                    2.59305006,
                    2.47805594,
                    2.82957843,
                    3.14159265,
                ]
            ),
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )

        kernel_interpolator = KernelInterpolator(x, y, kernel=kernel_sinc)
        np.testing.assert_allclose(
            kernel_interpolator(x_i),
            np.array(
                [
                    4.43848790,
                    4.47570010,
                    3.84353906,
                    3.05959493,
                    2.53514958,
                    2.19916874,
                    2.93225625,
                    3.32187855,
                    4.09458791,
                    4.23088094,
                    3.92591447,
                    3.53263071,
                    2.65177161,
                    2.73541557,
                    2.65740315,
                    3.17077616,
                    3.69624479,
                    3.87159620,
                    4.06433758,
                    3.56283868,
                    3.28312289,
                    2.79652091,
                    2.62481419,
                    3.22117115,
                    3.14159265,
                ]
            ),
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )

        kernel_interpolator = KernelInterpolator(x, y, window=1)
        np.testing.assert_allclose(
            kernel_interpolator(x_i),
            np.array(
                [
                    4.43848790,
                    4.96712277,
                    4.09584229,
                    3.23991575,
                    2.80418924,
                    2.28470276,
                    3.20024753,
                    3.41120944,
                    4.46416970,
                    4.57878168,
                    4.15371498,
                    3.92841633,
                    2.65177161,
                    3.02110187,
                    2.79812654,
                    3.44218674,
                    4.00032377,
                    4.01356870,
                    4.47633386,
                    3.70912627,
                    3.58365067,
                    3.14325415,
                    2.88247572,
                    3.37531662,
                    3.14159265,
                ]
            ),
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )

        kernel_interpolator = KernelInterpolator(x, y, window=1, kernel_kwargs={"a": 1})
        np.testing.assert_allclose(
            kernel_interpolator(x_i),
            np.array(
                [
                    4.43848790,
                    3.34379320,
                    3.62463711,
                    2.34585418,
                    2.04767083,
                    2.09444849,
                    2.13349835,
                    3.10304927,
                    3.29553153,
                    3.59884738,
                    3.48484031,
                    2.72974983,
                    2.65177161,
                    2.03850468,
                    2.29470194,
                    2.76179863,
                    2.80189050,
                    3.75979450,
                    2.98422257,
                    3.48444099,
                    2.49208997,
                    2.46516442,
                    2.42336082,
                    2.25975903,
                    3.14159265,
                ]
            ),
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )

        kernel_interpolator = KernelInterpolator(
            x, y, padding_kwargs={"pad_width": (3, 3), "mode": "mean"}
        )
        np.testing.assert_allclose(
            kernel_interpolator(x_i),
            np.array(
                [
                    4.4384879,
                    4.35723245,
                    3.62918155,
                    2.77471295,
                    2.13474499,
                    2.08206794,
                    2.50585862,
                    3.24992692,
                    3.84593162,
                    4.06289704,
                    3.80825633,
                    3.21068994,
                    2.65177161,
                    2.32137382,
                    2.45995375,
                    2.88799997,
                    3.43843598,
                    3.79504892,
                    3.79937086,
                    3.47673343,
                    2.99303182,
                    2.59771985,
                    2.49380017,
                    2.76339043,
                    3.14159265,
                ]
            ),
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )

        x_1 = np.arange(1, 10, 1)
        x_2 = x_1 * 10
        x_3 = x_1 / 10
        y = np.sin(x_1 / len(x_1) * np.pi * 6) / (x_1 / len(x_1))
        x_i = np.linspace(1, 9, 25)

        np.testing.assert_allclose(
            KernelInterpolator(x_1, y)(x_i),
            KernelInterpolator(x_2, y)(x_i * 10),
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )

        np.testing.assert_allclose(
            KernelInterpolator(x_1, y)(x_i),
            KernelInterpolator(x_3, y)(x_i / 10),
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )

    def test_raise_exception___call__(self) -> None:
        """
        Test :meth:`colour.algebra.interpolation.KernelInterpolator.__call__`
        method raised exception.
        """

        x = y = np.linspace(0, 1, 10)
        kernel_interpolator = KernelInterpolator(x, y)

        pytest.raises(ValueError, kernel_interpolator, -1)

        pytest.raises(ValueError, kernel_interpolator, 11)

    @ignore_numpy_errors
    def test_nan__call__(self) -> None:
        """
        Test :meth:`colour.algebra.interpolation.KernelInterpolator.__call__`
        method nan support.
        """

        # NOTE: As the "x" independent variable must be uniform, it cannot
        # contain NaNs.
        # TODO: Revisit if the interpolator can be applied on non-uniform "x"
        # independent variable.


class TestNearestNeighbourInterpolator:
    """
    Define :class:`colour.algebra.interpolation.NearestNeighbourInterpolator`
    class unit tests methods.
    """

    def test_required_attributes(self) -> None:
        """Test the presence of required attributes."""

        if not is_scipy_installed():  # pragma: no cover
            return

        required_attributes = ()

        for attribute in required_attributes:  # pragma: no cover
            assert attribute in dir(NearestNeighbourInterpolator)

    def test_required_methods(self) -> None:
        """Test the presence of required methods."""

        required_methods = ("__init__",)

        for method in required_methods:  # pragma: no cover
            assert method in dir(NearestNeighbourInterpolator)

    def test___init__(self) -> None:
        """
        Test :meth:`colour.algebra.interpolation.KernelInterpolator.__init__`
        method.
        """

        x = y = np.linspace(0, 1, 10)
        nearest_neighbour_interpolator = NearestNeighbourInterpolator(
            x, y, kernel_kwargs={"a": 1}
        )

        assert nearest_neighbour_interpolator.kernel_kwargs == {}


class TestLinearInterpolator:
    """
    Define :class:`colour.algebra.interpolation.LinearInterpolator` class unit
    tests methods.
    """

    def test_required_attributes(self) -> None:
        """Test the presence of required attributes."""

        if not is_scipy_installed():  # pragma: no cover
            return

        required_attributes = ("x", "y")

        for attribute in required_attributes:
            assert attribute in dir(LinearInterpolator)

    def test_required_methods(self) -> None:
        """Test the presence of required methods."""

        required_methods = ("__init__", "__call__")

        for method in required_methods:  # pragma: no cover
            assert method in dir(LinearInterpolator)

    def test_raise_exception___init__(self) -> None:
        """
        Test :meth:`colour.algebra.interpolation.LinearInterpolator.__init__`
        method raised exception.
        """

        x, y = np.linspace(0, 1, 10), np.linspace(0, 1, 15)
        pytest.raises(ValueError, LinearInterpolator, x, y)

    def test__call__(self) -> None:
        """
        Test :meth:`colour.algebra.interpolation.LinearInterpolator.__call__`
        method.
        """

        if not is_scipy_installed():  # pragma: no cover
            return

        interval = 0.1
        x = np.arange(len(DATA_POINTS_A))
        linear_interpolator = LinearInterpolator(x, DATA_POINTS_A)

        for i, value in enumerate(
            np.arange(0, len(DATA_POINTS_A) - 1 + interval, interval)
        ):
            np.testing.assert_allclose(
                DATA_POINTS_A_LINEAR_INTERPOLATED_10_SAMPLES[i],
                linear_interpolator(value),
                atol=TOLERANCE_ABSOLUTE_TESTS,
            )

        np.testing.assert_allclose(
            linear_interpolator(
                np.arange(0, len(DATA_POINTS_A) - 1 + interval, interval)
            ),
            DATA_POINTS_A_LINEAR_INTERPOLATED_10_SAMPLES,
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )

    def test_raise_exception___call__(self) -> None:
        """
        Test :meth:`colour.algebra.interpolation.LinearInterpolator.__call__`
        method raised exception.
        """

        x = y = np.linspace(0, 1, 10)
        linear_interpolator = LinearInterpolator(x, y)

        pytest.raises(ValueError, linear_interpolator, -1)

        pytest.raises(ValueError, linear_interpolator, 11)

    @ignore_numpy_errors
    def test_nan__call__(self) -> None:
        """
        Test :meth:`colour.algebra.interpolation.LinearInterpolator.__call__`
        method nan support.
        """

        cases = [-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan]
        cases = np.array(list(set(product(cases, repeat=3))))
        for case in cases:
            try:
                linear_interpolator = LinearInterpolator(case, case)
                linear_interpolator(case[0])
            except ValueError:
                pass


class TestSpragueInterpolator:
    """
    Define :class:`colour.algebra.interpolation.SpragueInterpolator` class
    unit tests methods.
    """

    def test_required_attributes(self) -> None:
        """Test the presence of required attributes."""

        if not is_scipy_installed():  # pragma: no cover
            return

        required_attributes = ("x", "y")

        for attribute in required_attributes:
            assert attribute in dir(SpragueInterpolator)

    def test_required_methods(self) -> None:
        """Test the presence of required methods."""

        required_methods = ("__init__", "__call__")

        for method in required_methods:  # pragma: no cover
            assert method in dir(SpragueInterpolator)

    def test_raise_exception___init__(self) -> None:
        """
        Test :meth:`colour.algebra.interpolation.SpragueInterpolator.__init__`
        method raised exception.
        """

        x, y = np.linspace(0, 1, 10), np.linspace(0, 1, 15)
        pytest.raises(ValueError, SpragueInterpolator, x, y)

    def test__call__(self) -> None:
        """
        Test :meth:`colour.algebra.interpolation.SpragueInterpolator.__call__`
        method.
        """

        if not is_scipy_installed():  # pragma: no cover
            return

        interval = 0.1
        x = np.arange(len(DATA_POINTS_A))
        sprague_interpolator = SpragueInterpolator(x, DATA_POINTS_A)

        for i, value in enumerate(
            np.arange(0, len(DATA_POINTS_A) - 1 + interval, interval)
        ):
            np.testing.assert_allclose(
                DATA_POINTS_A_SPRAGUE_INTERPOLATED_10_SAMPLES[i],
                sprague_interpolator(value),
                atol=TOLERANCE_ABSOLUTE_TESTS,
            )

        np.testing.assert_allclose(
            sprague_interpolator(
                np.arange(0, len(DATA_POINTS_A) - 1 + interval, interval)
            ),
            DATA_POINTS_A_SPRAGUE_INTERPOLATED_10_SAMPLES,
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )

    def test_raise_exception___call__(self) -> None:
        """
        Test :meth:`colour.algebra.interpolation.SpragueInterpolator.__call__`
        method raised exception.
        """

        x = y = np.linspace(0, 1, 10)
        sprague_interpolator = SpragueInterpolator(x, y)

        pytest.raises(ValueError, sprague_interpolator, -1)

        pytest.raises(ValueError, sprague_interpolator, 11)

    @ignore_numpy_errors
    def test_nan__call__(self) -> None:
        """
        Test :meth:`colour.algebra.interpolation.SpragueInterpolator.__call__`
        method nan support.
        """

        cases = [-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan]
        cases = np.array(list(set(product(cases, repeat=3))))
        for case in cases:
            try:
                sprague_interpolator = SpragueInterpolator(case, case)
                sprague_interpolator(case[0])  # pragma: no cover
            except AssertionError:
                pass


class TestCubicSplineInterpolator:
    """
    Define :class:`colour.algebra.interpolation.CubicSplineInterpolator` class
    unit tests methods.
    """

    def test__call__(self) -> None:
        """
        Test :meth:`colour.algebra.interpolation.CubicSplineInterpolator.\
__call__` method.

        Notes
        -----
        -   This class is a wrapper around *scipy.interpolate.interp1d* class
            and is assumed to be unit tested thoroughly.
        """

        if not is_scipy_installed():  # pragma: no cover
            return

        np.testing.assert_allclose(
            CubicSplineInterpolator(
                np.linspace(0, 1, len(DATA_POINTS_A)), DATA_POINTS_A
            )(np.linspace(0, 1, len(DATA_POINTS_A) * 2)),
            DATA_POINTS_A_CUBIC_SPLINE_INTERPOLATED_X2_SAMPLES,
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )


class TestPchipInterpolator:
    """
    Define :class:`colour.algebra.interpolation.PchipInterpolator` class
    unit tests methods.
    """

    def test_required_attributes(self) -> None:
        """Test the presence of required attributes."""

        if not is_scipy_installed():  # pragma: no cover
            return

        required_attributes = ("x", "y")

        for attribute in required_attributes:
            assert attribute in dir(PchipInterpolator)

    def test_required_methods(self) -> None:
        """Test the presence of required methods."""

        required_methods = ("__init__",)

        for method in required_methods:  # pragma: no cover
            assert method in dir(PchipInterpolator)

    def test_y(self) -> None:
        """
        Test :attr:`colour.algebra.interpolation.PchipInterpolator.y` property.
        """

        if not is_scipy_installed():  # pragma: no cover
            return

        interpolator = PchipInterpolator(np.linspace(0, 1, 10), np.linspace(0, 1, 10))

        interpolator.y = np.linspace(0, 1, 10)

        assert interpolator(np.array(5)) == 5


class TestNullInterpolator:
    """
    Define :class:`colour.algebra.interpolation.NullInterpolator` class
    unit tests methods.
    """

    def test_required_attributes(self) -> None:
        """Test the presence of required attributes."""

        if not is_scipy_installed():  # pragma: no cover
            return

        required_attributes = ("x", "y")

        for attribute in required_attributes:
            assert attribute in dir(NullInterpolator)

    def test_required_methods(self) -> None:
        """Test the presence of required methods."""

        required_methods = ("__init__", "__call__")

        for method in required_methods:  # pragma: no cover
            assert method in dir(NullInterpolator)

    def test_x(self) -> None:
        """
        Test :attr:`colour.algebra.interpolation.NullInterpolator.x`
        property.
        """

        x = y = np.linspace(0, 1, 10)
        null_interpolator = NullInterpolator(x, y)

        np.testing.assert_equal(null_interpolator.x, x)

    def test_y(self) -> None:
        """
        Test :attr:`colour.algebra.interpolation.NullInterpolator.y`
        property.
        """

        if not is_scipy_installed():  # pragma: no cover
            return

        x = y = np.linspace(0, 1, 10)
        null_interpolator = NullInterpolator(x, y)

        np.testing.assert_equal(null_interpolator.y, y)

    def test_absolute_tolerance(self) -> None:
        """
        Test :attr:`colour.algebra.interpolation.NullInterpolator.\
absolute_tolerance` property.
        """

        x = y = np.linspace(0, 1, 10)
        null_interpolator = NullInterpolator(x, y, absolute_tolerance=0.1)

        np.testing.assert_equal(null_interpolator.absolute_tolerance, 0.1)

    def test_relative_tolerance(self) -> None:
        """
        Test :attr:`colour.algebra.interpolation.NullInterpolator.\
relative_tolerance` property.
        """

        x = y = np.linspace(0, 1, 10)
        null_interpolator = NullInterpolator(x, y, relative_tolerance=0.1)

        np.testing.assert_equal(null_interpolator.relative_tolerance, 0.1)

    def test_default(self) -> None:
        """
        Test :attr:`colour.algebra.interpolation.NullInterpolator.\
default` property.
        """

        x = y = np.linspace(0, 1, 10)
        null_interpolator = NullInterpolator(x, y, default=0)

        np.testing.assert_equal(null_interpolator.default, 0)

    def test_raise_exception___init__(self) -> None:
        """
        Test :meth:`colour.algebra.interpolation.NullInterpolator.__init__`
        method raised exception.
        """

        x, y = np.linspace(0, 1, 10), np.linspace(0, 1, 15)
        pytest.raises(ValueError, NullInterpolator, x, y)

    def test__call__(self) -> None:
        """
        Test :meth:`colour.algebra.interpolation.NullInterpolator.__call__`
        method.
        """

        if not is_scipy_installed():  # pragma: no cover
            return

        x = np.arange(len(DATA_POINTS_A))
        null_interpolator = NullInterpolator(x, DATA_POINTS_A)
        np.testing.assert_allclose(
            null_interpolator(np.array([0.75, 2.0, 3.0, 4.75])),
            np.array([np.nan, 12.46, 9.51, np.nan]),
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )

        null_interpolator = NullInterpolator(x, DATA_POINTS_A, 0.25, 0.25)
        np.testing.assert_allclose(
            null_interpolator(np.array([0.75, 2.0, 3.0, 4.75])),
            np.array([12.32, 12.46, 9.51, 4.33]),
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )

    def test_raise_exception___call__(self) -> None:
        """
        Test :meth:`colour.algebra.interpolation.NullInterpolator.__call__`
        method raised exception.
        """

        x = y = np.linspace(0, 1, 10)
        null_interpolator = NullInterpolator(x, y)

        pytest.raises(ValueError, null_interpolator, -1)

        pytest.raises(ValueError, null_interpolator, 11)

    @ignore_numpy_errors
    def test_nan__call__(self) -> None:
        """
        Test :meth:`colour.algebra.interpolation.NullInterpolator.__call__`
        method nan support.
        """

        cases = [-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan]
        cases = np.array(list(set(product(cases, repeat=3))))
        for case in cases:
            try:
                null_interpolator = NullInterpolator(case, case)
                null_interpolator(case[0])
            except ValueError:
                pass


class TestLagrangeCoefficients:
    """
    Define :func:`colour.algebra.interpolation.lagrange_coefficients`
    definition unit tests methods.
    """

    def test_lagrange_coefficients(self) -> None:
        """
        Test :func:`colour.algebra.interpolation.lagrange_coefficients`
        definition.

        Notes
        -----
        -   :attr:`LAGRANGE_COEFFICIENTS_A` and :attr:`LAGRANGE_COEFFICIENTS_B`
            attributes data is matching :cite:`Fairman1985b`.

        References
        ----------
        :cite:`Fairman1985b`
        """

        lc = [lagrange_coefficients(i, 3) for i in np.linspace(0.05, 0.95, 19)]
        np.testing.assert_allclose(
            lc, LAGRANGE_COEFFICIENTS_A, atol=TOLERANCE_ABSOLUTE_TESTS
        )

        lc = [lagrange_coefficients(i, 4) for i in np.linspace(1.05, 1.95, 19)]
        np.testing.assert_allclose(
            lc, LAGRANGE_COEFFICIENTS_B, atol=TOLERANCE_ABSOLUTE_TESTS
        )


class TestTableInterpolationTrilinear:
    """
    Define :func:`colour.algebra.interpolation.\
table_interpolation_trilinear` definition unit tests methods.
    """

    def test_interpolation_trilinear(self) -> None:
        """
        Test :func:`colour.algebra.interpolation.\
table_interpolation_trilinear` definition.
        """

        prng = np.random.RandomState(4)

        V_xyz = random_triplet_generator(16, random_state=prng)

        np.testing.assert_allclose(
            table_interpolation_trilinear(V_xyz, LUT_TABLE),
            np.array(
                [
                    [1.07937594, -0.02773926, 0.55498254],
                    [0.53983424, 0.37099516, 0.13994561],
                    [1.13449122, -0.00305380, 0.13792909],
                    [0.73411897, 1.00141020, 0.59348239],
                    [0.74066176, 0.44679540, 0.55030394],
                    [0.20634750, 0.84797880, 0.55905579],
                    [0.92348649, 0.73112515, 0.42362820],
                    [0.03639248, 0.70357649, 0.52375041],
                    [0.29215488, 0.19697840, 0.44603879],
                    [0.47793470, 0.08696360, 0.70288463],
                    [0.88883354, 0.68680856, 0.87404642],
                    [0.21430977, 0.16796653, 0.19634247],
                    [0.82118989, 0.69239283, 0.39932389],
                    [1.06679072, 0.37974319, 0.49759377],
                    [0.17856230, 0.44755467, 0.62045271],
                    [0.59220355, 0.93136492, 0.30063692],
                ]
            ),
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )


class TestTableInterpolationTetrahedral:
    """
    Define :func:`colour.algebra.interpolation.\
table_interpolation_tetrahedral` definition unit tests methods.
    """

    def test_interpolation_tetrahedral(self) -> None:
        """
        Test :func:`colour.algebra.interpolation.\
table_interpolation_tetrahedral` definition.
        """

        prng = np.random.RandomState(4)

        V_xyz = random_triplet_generator(16, random_state=prng)

        np.testing.assert_allclose(
            table_interpolation_tetrahedral(V_xyz, LUT_TABLE),
            np.array(
                [
                    [1.08039215, -0.02840092, 0.55855303],
                    [0.52208945, 0.35297753, 0.13599555],
                    [1.14373467, -0.00422138, 0.13413290],
                    [0.71384967, 0.98420883, 0.57982724],
                    [0.76771576, 0.46280975, 0.55106736],
                    [0.20861663, 0.85077712, 0.57102264],
                    [0.90398698, 0.72351675, 0.41151955],
                    [0.03749453, 0.70226823, 0.52614254],
                    [0.29626758, 0.21645072, 0.47615873],
                    [0.46729624, 0.07494851, 0.68892548],
                    [0.85907681, 0.67744258, 0.84410486],
                    [0.24335535, 0.20896545, 0.21996717],
                    [0.79244027, 0.66930773, 0.39213595],
                    [1.08383608, 0.37985897, 0.49011919],
                    [0.14683649, 0.43624903, 0.58706947],
                    [0.61272658, 0.92799297, 0.29650424],
                ]
            ),
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )


class TestTableInterpolation:
    """
    Define :func:`colour.algebra.interpolation.table_interpolation`
    wrapper definition unit tests methods.
    """

    def test_table_interpolation(self) -> None:
        """
        Test :func:`colour.algebra.interpolation.table_interpolation`
        wrapper definition.
        """

        prng = np.random.RandomState(4)
        V_xyz = prng.random_sample((10, 3))

        # Test with "Trilinear" method
        np.testing.assert_allclose(
            table_interpolation(V_xyz, LUT_TABLE, method="Trilinear"),
            table_interpolation_trilinear(V_xyz, LUT_TABLE),
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )

        # Test with "Tetrahedral" method
        np.testing.assert_allclose(
            table_interpolation(V_xyz, LUT_TABLE, method="Tetrahedral"),
            table_interpolation_tetrahedral(V_xyz, LUT_TABLE),
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )
