"""Define the unit tests for the :mod:`colour.io.luts.sequence` module."""

from __future__ import annotations

import textwrap
import typing

import numpy as np

from colour.constants import TOLERANCE_ABSOLUTE_TESTS

if typing.TYPE_CHECKING:
    from colour.hints import Any, ArrayLike, NDArrayFloat

from colour.io.luts import (
    LUT1D,
    LUT3D,
    AbstractLUTSequenceOperator,
    LUT3x1D,
    LUTSequence,
)
from colour.models import gamma_function
from colour.utilities import as_float_array, tstack

__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__ = [
    "TestLUTSequence",
]


class TestLUTSequence:
    """
    Define :class:`colour.io.luts.sequence.LUTSequence` class unit tests
    methods.
    """

    def setup_method(self) -> None:
        """Initialise the common tests attributes."""

        self._LUT_1 = LUT1D(LUT1D.linear_table(16) + 0.125, "Nemo 1D")
        self._LUT_2 = LUT3D(LUT3D.linear_table(16) ** (1 / 2.2), "Nemo 3D")
        self._LUT_3 = LUT3x1D(LUT3x1D.linear_table(16) * 0.750, "Nemo 3x1D")
        self._LUT_sequence = LUTSequence(self._LUT_1, self._LUT_2, self._LUT_3)

        samples = np.linspace(0, 1, 5)

        self._RGB = tstack([samples, samples, samples])

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

        required_attributes = ("sequence",)

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

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

        required_methods = (
            "__init__",
            "__getitem__",
            "__setitem__",
            "__delitem__",
            "__len__",
            "__str__",
            "__repr__",
            "__eq__",
            "__ne__",
            "insert",
            "apply",
            "copy",
        )

        for method in required_methods:
            assert method in dir(LUTSequence)

    def test_sequence(self) -> None:
        """Test :class:`colour.io.luts.sequence.LUTSequence.sequence` property."""

        sequence = [self._LUT_1, self._LUT_2, self._LUT_3]
        LUT_sequence = LUTSequence()
        LUT_sequence.sequence = sequence
        assert self._LUT_sequence.sequence == sequence

    def test__init__(self) -> None:
        """Test :class:`colour.io.luts.sequence.LUTSequence.__init__` method."""

        assert LUTSequence(self._LUT_1, self._LUT_2, self._LUT_3) == self._LUT_sequence

    def test__getitem__(self) -> None:
        """Test :class:`colour.io.luts.sequence.LUTSequence.__getitem__` method."""

        assert self._LUT_sequence[0] == self._LUT_1
        assert self._LUT_sequence[1] == self._LUT_2
        assert self._LUT_sequence[2] == self._LUT_3

    def test__setitem__(self) -> None:
        """Test :class:`colour.io.luts.sequence.LUTSequence.__setitem__` method."""

        LUT_sequence = self._LUT_sequence.copy()
        LUT_sequence[0] = self._LUT_3
        LUT_sequence[1] = self._LUT_1
        LUT_sequence[2] = self._LUT_2

        assert LUT_sequence[1] == self._LUT_1
        assert LUT_sequence[2] == self._LUT_2
        assert LUT_sequence[0] == self._LUT_3

    def test__delitem__(self) -> None:
        """Test :class:`colour.io.luts.sequence.LUTSequence.__delitem__` method."""

        LUT_sequence = self._LUT_sequence.copy()

        del LUT_sequence[0]
        del LUT_sequence[0]

        assert LUT_sequence[0] == self._LUT_3

    def test__len__(self) -> None:
        """Test :class:`colour.io.luts.sequence.LUTSequence.__len__` method."""

        assert len(self._LUT_sequence) == 3

    def test__str__(self) -> None:
        """Test :class:`colour.io.luts.sequence.LUTSequence.__str__` method."""

        assert str(self._LUT_sequence) == (
            textwrap.dedent(
                """
            LUT Sequence
            ------------

            Overview

                LUT1D --> LUT3D --> LUT3x1D

            Operations

                LUT1D - Nemo 1D
                ---------------

                Dimensions : 1
                Domain     : [ 0.  1.]
                Size       : (16,)

                LUT3D - Nemo 3D
                ---------------

                Dimensions : 3
                Domain     : [[ 0.  0.  0.]
                              [ 1.  1.  1.]]
                Size       : (16, 16, 16, 3)

                LUT3x1D - Nemo 3x1D
                -------------------

                Dimensions : 2
                Domain     : [[ 0.  0.  0.]
                              [ 1.  1.  1.]]
                Size       : (16, 3)
            """
            ).strip()
        )

    def test__repr__(self) -> None:
        """Test :class:`colour.io.luts.sequence.LUTSequence.__repr__` method."""

        LUT_sequence = self._LUT_sequence.copy()
        LUT_sequence[1].table = LUT3D.linear_table(5)

        assert repr(LUT_sequence) == (
            textwrap.dedent(
                """
LUTSequence(
    LUT1D([ 0.125     ,  0.19166667,  0.25833333,  0.325     ,  0.39166667,
            0.45833333,  0.525     ,  0.59166667,  0.65833333,  0.725     ,
            0.79166667,  0.85833333,  0.925     ,  0.99166667,  1.05833333,
            1.125     ],
          'Nemo 1D',
          [ 0.,  1.],
          16),
    LUT3D([[[[ 0.  ,  0.  ,  0.  ],
             [ 0.  ,  0.  ,  0.25],
             [ 0.  ,  0.  ,  0.5 ],
             [ 0.  ,  0.  ,  0.75],
             [ 0.  ,  0.  ,  1.  ]],

            [[ 0.  ,  0.25,  0.  ],
             [ 0.  ,  0.25,  0.25],
             [ 0.  ,  0.25,  0.5 ],
             [ 0.  ,  0.25,  0.75],
             [ 0.  ,  0.25,  1.  ]],

            [[ 0.  ,  0.5 ,  0.  ],
             [ 0.  ,  0.5 ,  0.25],
             [ 0.  ,  0.5 ,  0.5 ],
             [ 0.  ,  0.5 ,  0.75],
             [ 0.  ,  0.5 ,  1.  ]],

            [[ 0.  ,  0.75,  0.  ],
             [ 0.  ,  0.75,  0.25],
             [ 0.  ,  0.75,  0.5 ],
             [ 0.  ,  0.75,  0.75],
             [ 0.  ,  0.75,  1.  ]],

            [[ 0.  ,  1.  ,  0.  ],
             [ 0.  ,  1.  ,  0.25],
             [ 0.  ,  1.  ,  0.5 ],
             [ 0.  ,  1.  ,  0.75],
             [ 0.  ,  1.  ,  1.  ]]],

           [[[ 0.25,  0.  ,  0.  ],
             [ 0.25,  0.  ,  0.25],
             [ 0.25,  0.  ,  0.5 ],
             [ 0.25,  0.  ,  0.75],
             [ 0.25,  0.  ,  1.  ]],

            [[ 0.25,  0.25,  0.  ],
             [ 0.25,  0.25,  0.25],
             [ 0.25,  0.25,  0.5 ],
             [ 0.25,  0.25,  0.75],
             [ 0.25,  0.25,  1.  ]],

            [[ 0.25,  0.5 ,  0.  ],
             [ 0.25,  0.5 ,  0.25],
             [ 0.25,  0.5 ,  0.5 ],
             [ 0.25,  0.5 ,  0.75],
             [ 0.25,  0.5 ,  1.  ]],

            [[ 0.25,  0.75,  0.  ],
             [ 0.25,  0.75,  0.25],
             [ 0.25,  0.75,  0.5 ],
             [ 0.25,  0.75,  0.75],
             [ 0.25,  0.75,  1.  ]],

            [[ 0.25,  1.  ,  0.  ],
             [ 0.25,  1.  ,  0.25],
             [ 0.25,  1.  ,  0.5 ],
             [ 0.25,  1.  ,  0.75],
             [ 0.25,  1.  ,  1.  ]]],

           [[[ 0.5 ,  0.  ,  0.  ],
             [ 0.5 ,  0.  ,  0.25],
             [ 0.5 ,  0.  ,  0.5 ],
             [ 0.5 ,  0.  ,  0.75],
             [ 0.5 ,  0.  ,  1.  ]],

            [[ 0.5 ,  0.25,  0.  ],
             [ 0.5 ,  0.25,  0.25],
             [ 0.5 ,  0.25,  0.5 ],
             [ 0.5 ,  0.25,  0.75],
             [ 0.5 ,  0.25,  1.  ]],

            [[ 0.5 ,  0.5 ,  0.  ],
             [ 0.5 ,  0.5 ,  0.25],
             [ 0.5 ,  0.5 ,  0.5 ],
             [ 0.5 ,  0.5 ,  0.75],
             [ 0.5 ,  0.5 ,  1.  ]],

            [[ 0.5 ,  0.75,  0.  ],
             [ 0.5 ,  0.75,  0.25],
             [ 0.5 ,  0.75,  0.5 ],
             [ 0.5 ,  0.75,  0.75],
             [ 0.5 ,  0.75,  1.  ]],

            [[ 0.5 ,  1.  ,  0.  ],
             [ 0.5 ,  1.  ,  0.25],
             [ 0.5 ,  1.  ,  0.5 ],
             [ 0.5 ,  1.  ,  0.75],
             [ 0.5 ,  1.  ,  1.  ]]],

           [[[ 0.75,  0.  ,  0.  ],
             [ 0.75,  0.  ,  0.25],
             [ 0.75,  0.  ,  0.5 ],
             [ 0.75,  0.  ,  0.75],
             [ 0.75,  0.  ,  1.  ]],

            [[ 0.75,  0.25,  0.  ],
             [ 0.75,  0.25,  0.25],
             [ 0.75,  0.25,  0.5 ],
             [ 0.75,  0.25,  0.75],
             [ 0.75,  0.25,  1.  ]],

            [[ 0.75,  0.5 ,  0.  ],
             [ 0.75,  0.5 ,  0.25],
             [ 0.75,  0.5 ,  0.5 ],
             [ 0.75,  0.5 ,  0.75],
             [ 0.75,  0.5 ,  1.  ]],

            [[ 0.75,  0.75,  0.  ],
             [ 0.75,  0.75,  0.25],
             [ 0.75,  0.75,  0.5 ],
             [ 0.75,  0.75,  0.75],
             [ 0.75,  0.75,  1.  ]],

            [[ 0.75,  1.  ,  0.  ],
             [ 0.75,  1.  ,  0.25],
             [ 0.75,  1.  ,  0.5 ],
             [ 0.75,  1.  ,  0.75],
             [ 0.75,  1.  ,  1.  ]]],

           [[[ 1.  ,  0.  ,  0.  ],
             [ 1.  ,  0.  ,  0.25],
             [ 1.  ,  0.  ,  0.5 ],
             [ 1.  ,  0.  ,  0.75],
             [ 1.  ,  0.  ,  1.  ]],

            [[ 1.  ,  0.25,  0.  ],
             [ 1.  ,  0.25,  0.25],
             [ 1.  ,  0.25,  0.5 ],
             [ 1.  ,  0.25,  0.75],
             [ 1.  ,  0.25,  1.  ]],

            [[ 1.  ,  0.5 ,  0.  ],
             [ 1.  ,  0.5 ,  0.25],
             [ 1.  ,  0.5 ,  0.5 ],
             [ 1.  ,  0.5 ,  0.75],
             [ 1.  ,  0.5 ,  1.  ]],

            [[ 1.  ,  0.75,  0.  ],
             [ 1.  ,  0.75,  0.25],
             [ 1.  ,  0.75,  0.5 ],
             [ 1.  ,  0.75,  0.75],
             [ 1.  ,  0.75,  1.  ]],

            [[ 1.  ,  1.  ,  0.  ],
             [ 1.  ,  1.  ,  0.25],
             [ 1.  ,  1.  ,  0.5 ],
             [ 1.  ,  1.  ,  0.75],
             [ 1.  ,  1.  ,  1.  ]]]],
          'Nemo 3D',
          [[ 0.,  0.,  0.],
           [ 1.,  1.,  1.]],
          5),
    LUT3x1D([[ 0.  ,  0.  ,  0.  ],
             [ 0.05,  0.05,  0.05],
             [ 0.1 ,  0.1 ,  0.1 ],
             [ 0.15,  0.15,  0.15],
             [ 0.2 ,  0.2 ,  0.2 ],
             [ 0.25,  0.25,  0.25],
             [ 0.3 ,  0.3 ,  0.3 ],
             [ 0.35,  0.35,  0.35],
             [ 0.4 ,  0.4 ,  0.4 ],
             [ 0.45,  0.45,  0.45],
             [ 0.5 ,  0.5 ,  0.5 ],
             [ 0.55,  0.55,  0.55],
             [ 0.6 ,  0.6 ,  0.6 ],
             [ 0.65,  0.65,  0.65],
             [ 0.7 ,  0.7 ,  0.7 ],
             [ 0.75,  0.75,  0.75]],
            'Nemo 3x1D',
            [[ 0.,  0.,  0.],
             [ 1.,  1.,  1.]],
            16)
)
""".strip()
            )
        )

    def test__eq__(self) -> None:
        """Test :class:`colour.io.luts.sequence.LUTSequence.__eq__` method."""

        LUT_sequence_1 = LUTSequence(self._LUT_1, self._LUT_2, self._LUT_3)
        LUT_sequence_2 = LUTSequence(self._LUT_1, self._LUT_2)

        assert self._LUT_sequence == LUT_sequence_1

        assert self._LUT_sequence != self._LUT_sequence[0]

        assert LUT_sequence_1 != LUT_sequence_2

    def test__neq__(self) -> None:
        """Test :class:`colour.io.luts.sequence.LUTSequence.__neq__` method."""

        assert self._LUT_sequence != LUTSequence(
            self._LUT_1, self._LUT_2.copy() * 0.75, self._LUT_3
        )

    def test_insert(self) -> None:
        """Test :class:`colour.io.luts.sequence.LUTSequence.insert` method."""

        LUT_sequence = self._LUT_sequence.copy()

        LUT_sequence.insert(1, self._LUT_2.copy())

        assert LUT_sequence == LUTSequence(
            self._LUT_1,
            self._LUT_2,
            self._LUT_2,
            self._LUT_3,
        )

    def test_apply(self) -> None:
        """Test :class:`colour.io.luts.sequence.LUTSequence.apply` method."""

        class GammaOperator(AbstractLUTSequenceOperator):
            """
            Gamma operator for unit tests.

            Parameters
            ----------
            gamma
                Gamma value.
            """

            def __init__(self, gamma: ArrayLike = 1) -> None:
                self._gamma = as_float_array(gamma)

            def apply(
                self,
                RGB: ArrayLike,
                *args: Any,  # noqa: ARG002
                **kwargs: Any,
            ) -> NDArrayFloat:
                """
                Apply the *LUT* sequence operator to the specified *RGB* colourspace
                array.

                Parameters
                ----------
                RGB
                    *RGB* colourspace array to apply the *LUT* sequence
                    operator onto.

                Returns
                -------
                :class:`numpy.ndarray`
                    Processed *RGB* colourspace array.
                """

                direction = kwargs.get("direction", "Forward")

                gamma = self._gamma if direction == "Forward" else 1.0 / self._gamma

                return as_float_array(gamma_function(RGB, gamma))

        LUT_sequence = self._LUT_sequence.copy()
        LUT_sequence.insert(1, GammaOperator(1 / 2.2))
        samples = np.linspace(0, 1, 5)
        RGB = tstack([samples, samples, samples])

        np.testing.assert_allclose(
            LUT_sequence.apply(RGB, GammaOperator={"direction": "Inverse"}),
            np.array(
                [
                    [0.03386629, 0.03386629, 0.03386629],
                    [0.27852298, 0.27852298, 0.27852298],
                    [0.46830881, 0.46830881, 0.46830881],
                    [0.65615595, 0.65615595, 0.65615595],
                    [0.75000000, 0.75000000, 0.75000000],
                ]
            ),
            atol=TOLERANCE_ABSOLUTE_TESTS,
        )
