"""
 pyfuzzylite (TM), a fuzzy logic control library in Python.
 Copyright (C) 2010-2017 FuzzyLite Limited. All rights reserved.
 Author: Juan Rada-Vilela, Ph.D. <jcrada@fuzzylite.com>

 This file is part of pyfuzzylite.

 pyfuzzylite is free software: you can redistribute it and/or modify it under
 the terms of the FuzzyLite License included with the software.

 You should have received a copy of the FuzzyLite License along with
 pyfuzzylite. If not, see <http://www.fuzzylite.com/license/>.

 pyfuzzylite is a trademark of FuzzyLite Limited
 fuzzylite is a registered trademark of FuzzyLite Limited.
"""

import copy
import math
import operator
import platform
import re
import unittest
from typing import Callable, Dict, NoReturn, Optional, Sequence, Type

import fuzzylite as fl
from tests.assert_component import BaseAssert


class TermAssert(BaseAssert[fl.Term]):

    def has_name(self, name: str, height: float = 1.0) -> 'TermAssert':
        self.test.assertEqual(self.actual.name, name)
        self.test.assertEqual(self.actual.height, height)
        return self

    def takes_parameters(self, parameters: int) -> 'TermAssert':
        with self.test.assertRaisesRegex(ValueError, re.escape("not enough values to unpack "
                                                               f"(expected {parameters}, got 0)")):
            self.actual.__class__().configure("")
        return self

    def is_monotonic(self, monotonic: bool = True) -> 'TermAssert':
        self.test.assertEqual(monotonic, self.actual.is_monotonic())
        return self

    def is_not_monotonic(self) -> 'TermAssert':
        self.test.assertEqual(False, self.actual.is_monotonic())
        return self

    def configured_as(self, parameters: str) -> 'TermAssert':
        self.actual.configure(parameters)
        return self

    def has_membership(self, x: float, mf: float) -> 'TermAssert':
        message = "\n".join([f"{str(self.actual)}",
                             f"expected: \u03BC(x={x:.3f})={mf}, but"])
        if math.isnan(mf):
            self.test.assertEqual(str(fl.nan), str(self.actual.membership(x)), message)
            return self

        # TODO: Find out why we get different values in different platforms
        # compare against exact values on Mac OSX
        if platform.system() == 'Darwin':
            self.test.assertEqual(mf, self.actual.membership(x), message)
        else:  # use approximate values in other platforms
            self.test.assertAlmostEqual(mf, self.actual.membership(x), places=15, msg=message)
        return self

    def has_memberships(self, x_mf: Dict[float, float], height: float = 1.0) -> 'TermAssert':
        for x in x_mf.keys():
            self.has_membership(x, height * x_mf[x])
        return self

    def membership_fails(self, x: float, exception: Type[Exception],
                         regex: str) -> 'TermAssert':
        with self.test.assertRaisesRegex(exception, regex, msg=f"when x={x:.3f}"):
            self.actual.membership(x)
        return self

    def memberships_fail(self, x_mf: Dict[float, float], exception: Type[Exception],
                         regex: str) -> 'TermAssert':
        for x, _ in x_mf.items():
            self.membership_fails(x, exception, regex)
        return self

    def has_tsukamoto(self, x: float, mf: float, minimum: float = -1.0,
                      maximum: float = 1.0) -> 'TermAssert':
        self.test.assertEqual(True, self.actual.is_monotonic())
        if math.isnan(mf):
            self.test.assertEqual(True, math.isnan(self.actual.tsukamoto(x, minimum, maximum)),
                                  f"{str(self.actual)}\nwhen x={x:.3f}")
        else:
            self.test.assertEqual(mf, self.actual.tsukamoto(x, minimum, maximum),
                                  f"{str(self.actual)}\nwhen x={x:.3f}")
        return self

    def has_tsukamotos(self, x_mf: Dict[float, float], minimum: float = -1.0,
                       maximum: float = 1.0) -> 'TermAssert':
        for x in x_mf.keys():
            self.has_tsukamoto(x, x_mf[x], minimum, maximum)
        return self

    def apply(self, func: Callable[..., None], args: Sequence[str] = (),
              **keywords: Dict[str, object]) -> 'TermAssert':
        func(self.actual, *args, **keywords)
        return self


class TestTerm(unittest.TestCase):

    def test_term(self) -> None:
        self.assertEqual(fl.Term().name, "")
        self.assertEqual(fl.Term("X").name, "X")
        self.assertEqual(fl.Term("X").height, 1.0)
        self.assertEqual(fl.Term("X", .5).height, .5)

        self.assertEqual(str(fl.Term("xxx", 0.5)), "term: xxx Term 0.500")
        self.assertEqual(fl.Term().is_monotonic(), False)

        with self.assertRaisesRegex(NotImplementedError, ""):
            fl.Term().membership(math.nan)
        with self.assertRaisesRegex(NotImplementedError, ""):
            fl.Term().tsukamoto(math.nan, math.nan, math.nan)

        # does nothing, for test coverage
        fl.Term().update_reference(None)

        discrete_triangle = fl.Triangle("triangle", -1.0, 0.0, 1.0).discretize(-1, 1, 10)
        self.assertEqual(fl.Discrete.dict_from(discrete_triangle.xy),
                         {-1.0: 0.0,
                          -0.8: 0.19999999999999996,
                          -0.6: 0.4,
                          -0.3999999999999999: 0.6000000000000001,
                          -0.19999999999999996: 0.8,
                          0.0: 1.0,
                          0.20000000000000018: 0.7999999999999998,
                          0.40000000000000013: 0.5999999999999999,
                          0.6000000000000001: 0.3999999999999999,
                          0.8: 0.19999999999999996,
                          1.0: 0.0})

    def test_activated(self) -> None:
        TermAssert(self,
                   fl.Activated(
                       fl.Triangle("triangle", -0.400, 0.000, 0.400), 1.0,
                       fl.AlgebraicProduct())) \
            .exports_fll("term: _ Activated AlgebraicProduct(1.000,triangle)") \
            .is_not_monotonic() \
            .has_memberships({-0.5: 0.000,
                              -0.4: 0.000,
                              -0.25: 0.37500000000000006,
                              -0.1: 0.7500000000000001,
                              0.0: 1.000,
                              0.1: 0.7500000000000001,
                              0.25: 0.37500000000000006,
                              0.4: 0.000,
                              0.5: 0.000,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0})

        TermAssert(self,
                   fl.Activated(
                       fl.Triangle("triangle", -0.400, 0.000, 0.400), 0.5,
                       fl.AlgebraicProduct())) \
            .exports_fll("term: _ Activated AlgebraicProduct(0.500,triangle)") \
            .is_not_monotonic() \
            .has_memberships({-0.5: 0.000,
                              -0.4: 0.000,
                              -0.25: 0.18750000000000003,
                              -0.1: 0.37500000000000006,
                              0.0: 0.5,
                              0.1: 0.37500000000000006,
                              0.25: 0.18750000000000003,
                              0.4: 0.000,
                              0.5: 0.000,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0})

        activated = fl.Activated(None, 1.0)  # type: ignore
        self.assertEqual("term: _ Activated (1.000*none)", str(activated))
        self.assertEqual(math.isnan(activated.membership(math.nan)), True, f"when x={math.nan}")
        activated.configure("")

        with self.assertRaisesRegex(ValueError, "expected a term to activate, but none found"):
            activated.membership(0.0)

        activated = fl.Activated(fl.Triangle("x", 0, 1), degree=1.0)
        with self.assertRaisesRegex(ValueError, "expected an implication operator, but none found"):
            activated.membership(0.0)

    def test_aggregated(self) -> None:
        aggregated = fl.Aggregated("fuzzy_output", -1.0, 1.0, fl.Maximum())
        low = fl.Triangle("LOW", -1.000, -0.500, 0.000)
        medium = fl.Triangle("MEDIUM", -0.500, 0.000, 0.500)
        aggregated.terms.extend(
            [fl.Activated(low, 0.6, fl.Minimum()), fl.Activated(medium, 0.4, fl.Minimum())])

        TermAssert(self, aggregated) \
            .exports_fll(
            "term: fuzzy_output Aggregated Maximum[Minimum(0.600,LOW),Minimum(0.400,MEDIUM)]") \
            .is_not_monotonic() \
            .has_memberships({-0.5: 0.6,
                              -0.4: 0.6,
                              -0.25: 0.5,
                              -0.1: 0.4,
                              0.0: 0.4,
                              0.1: 0.4,
                              0.25: 0.4,
                              0.4: 0.19999999999999996,
                              0.5: 0.0,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0})

        self.assertEqual(aggregated.activation_degree(low), 0.6)
        self.assertEqual(aggregated.activation_degree(medium), 0.4)

        self.assertEqual(aggregated.highest_activated_term().term, low)  # type: ignore

        aggregated.terms.append(fl.Activated(low, 0.4))
        aggregated.aggregation = fl.UnboundedSum()
        self.assertEqual(aggregated.activation_degree(low), 0.6 + 0.4)

        aggregated.aggregation = None
        TermAssert(self, aggregated) \
            .exports_fll(
            "term: fuzzy_output Aggregated [Minimum(0.600,LOW)+Minimum(0.400,MEDIUM)+(0.400*LOW)]")

        with self.assertRaisesRegex(ValueError, "expected an aggregation operator, but none found"):
            aggregated.membership(0.0)

        self.assertEqual(aggregated.range(), 2.0)

    def test_bell(self) -> None:
        TermAssert(self, fl.Bell("bell")) \
            .exports_fll("term: bell Bell nan nan nan") \
            .takes_parameters(3) \
            .is_not_monotonic() \
            .configured_as("0 0.25 3.0") \
            .exports_fll("term: bell Bell 0.000 0.250 3.000") \
            .has_memberships({-0.5: 0.015384615384615385,
                              -0.4: 0.05625177755617076,
                              -0.25: 0.5,
                              -0.1: 0.9959207087768499,
                              0.0: 1.0,
                              0.1: 0.9959207087768499,
                              0.25: 0.5,
                              0.4: 0.05625177755617076,
                              0.5: 0.015384615384615385,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}) \
            .configured_as("0 0.25 3.0 0.5") \
            .exports_fll("term: bell Bell 0.000 0.250 3.000 0.500") \
            .has_memberships({-0.5: 0.015384615384615385,
                              -0.4: 0.05625177755617076,
                              -0.25: 0.5,
                              -0.1: 0.9959207087768499,
                              0.0: 1.0,
                              0.1: 0.9959207087768499,
                              0.25: 0.5,
                              0.4: 0.05625177755617076,
                              0.5: 0.015384615384615385,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}, height=0.5)

    def test_binary(self) -> None:
        TermAssert(self, fl.Binary("binary")) \
            .exports_fll("term: binary Binary nan nan") \
            .takes_parameters(2) \
            .is_not_monotonic() \
            .configured_as("0 inf") \
            .exports_fll("term: binary Binary 0.000 inf") \
            .has_memberships({-0.5: 0.0,
                              -0.4: 0.0,
                              -0.25: 0.0,
                              -0.1: 0.0,
                              0.0: 1.0,
                              0.1: 1.0,
                              0.25: 1.0,
                              0.4: 1.0,
                              0.5: 1.0,
                              math.nan: math.nan,
                              math.inf: 1.0,
                              -math.inf: 0.0}) \
            .configured_as("0 -inf 0.5") \
            .exports_fll("term: binary Binary 0.000 -inf 0.500") \
            .has_memberships({-0.5: 0.5,
                              -0.4: 0.5,
                              -0.25: 0.5,
                              -0.1: 0.5,
                              0.0: 0.5,
                              0.1: 0.0,
                              0.25: 0.0,
                              0.4: 0.0,
                              0.5: 0.0,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.5})

    def test_concave(self) -> None:
        TermAssert(self, fl.Concave("concave")) \
            .exports_fll("term: concave Concave nan nan") \
            .takes_parameters(2) \
            .is_monotonic() \
            .configured_as("0.00 0.50") \
            .exports_fll("term: concave Concave 0.000 0.500") \
            .has_memberships({-0.5: 0.3333333333333333,
                              -0.4: 0.35714285714285715,
                              -0.25: 0.4,
                              -0.1: 0.45454545454545453,
                              0.0: 0.5,
                              0.1: 0.5555555555555556,
                              0.25: 0.6666666666666666,
                              0.4: 0.8333333333333334,
                              0.5: 1.0,
                              math.nan: math.nan,
                              math.inf: 1.0,
                              -math.inf: 0.0}) \
            .configured_as("0.00 -0.500 0.5") \
            .exports_fll("term: concave Concave 0.000 -0.500 0.500") \
            .has_memberships({-0.5: 0.5,
                              -0.4: 0.4166666666666667,
                              -0.25: 0.3333333333333333,
                              -0.1: 0.2777777777777778,
                              0.0: 0.25,
                              0.1: 0.22727272727272727,
                              0.25: 0.2,
                              0.4: 0.17857142857142858,
                              0.5: 0.16666666666666666,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.5})

    def test_constant(self) -> None:
        TermAssert(self, fl.Constant("constant")) \
            .exports_fll("term: constant Constant nan") \
            .takes_parameters(1) \
            .is_not_monotonic() \
            .configured_as("0.5") \
            .exports_fll("term: constant Constant 0.500") \
            .has_memberships({-0.5: 0.5,
                              -0.4: 0.5,
                              -0.25: 0.5,
                              -0.1: 0.5,
                              0.0: 0.5,
                              0.1: 0.5,
                              0.25: 0.5,
                              0.4: 0.5,
                              0.5: 0.5,
                              math.nan: 0.5,
                              math.inf: 0.5,
                              -math.inf: 0.5}) \
            .configured_as("-0.500 0.5") \
            .exports_fll("term: constant Constant -0.500") \
            .has_memberships({-0.5: -0.5,
                              -0.4: -0.5,
                              -0.25: -0.5,
                              -0.1: -0.5,
                              0.0: -0.5,
                              0.1: -0.5,
                              0.25: -0.5,
                              0.4: -0.5,
                              0.5: -0.5,
                              math.nan: -0.5,
                              math.inf: -0.5,
                              -math.inf: -0.5})

    def test_cosine(self) -> None:
        TermAssert(self, fl.Cosine("cosine")) \
            .exports_fll("term: cosine Cosine nan nan") \
            .takes_parameters(2) \
            .is_not_monotonic() \
            .configured_as("0.0 1") \
            .exports_fll("term: cosine Cosine 0.000 1.000") \
            .has_memberships({-0.5: 0.0,
                              -0.4: 0.09549150281252633,
                              -0.25: 0.5,
                              -0.1: 0.9045084971874737,
                              0.0: 1.0,
                              0.1: 0.9045084971874737,
                              0.25: 0.5,
                              0.4: 0.09549150281252633,
                              0.5: 0.0,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}) \
            .configured_as("0.0 1.0 0.5") \
            .exports_fll("term: cosine Cosine 0.000 1.000 0.500") \
            .has_memberships({-0.5: 0.0,
                              -0.4: 0.09549150281252633,
                              -0.25: 0.5,
                              -0.1: 0.9045084971874737,
                              0.0: 1.0,
                              0.1: 0.9045084971874737,
                              0.25: 0.5,
                              0.4: 0.09549150281252633,
                              0.5: 0.0,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}, height=0.5)

    def test_discrete(self) -> None:
        TermAssert(self, fl.Discrete("discrete")) \
            .exports_fll("term: discrete Discrete") \
            .is_not_monotonic() \
            .configured_as("0 1 8 9 4 5 2 3 6 7") \
            .exports_fll("term: discrete Discrete "
                         "0.000 1.000 8.000 9.000 4.000 5.000 2.000 3.000 6.000 7.000") \
            .apply(fl.Discrete.sort) \
            .exports_fll("term: discrete Discrete "
                         "0.000 1.000 2.000 3.000 4.000 5.000 6.000 7.000 8.000 9.000") \
            .configured_as("0 1 8 9 4 5 2 3 6 7 0.5") \
            .apply(fl.Discrete.sort) \
            .exports_fll("term: discrete Discrete "
                         "0.000 1.000 2.000 3.000 4.000 5.000 6.000 7.000 8.000 9.000 0.500") \
            .configured_as(" -0.500 0.000 -0.250 1.000 0.000 0.500 0.250 1.000 0.500 0.000") \
            .exports_fll("term: discrete Discrete "
                         "-0.500 0.000 -0.250 1.000 0.000 0.500 0.250 1.000 0.500 0.000") \
            .has_memberships({-0.5: 0.0,
                              -0.4: 0.3999999999999999,
                              -0.25: 1.0,
                              -0.1: 0.7,
                              0.0: 0.5,
                              0.1: 0.7,
                              0.25: 1.0,
                              0.4: 0.3999999999999999,
                              0.5: 0.0,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}) \
            .configured_as(" -0.500 0.000 -0.250 1.000 0.000 0.500 0.250 1.000 0.500 0.000 0.5") \
            .exports_fll("term: discrete Discrete "
                         "-0.500 0.000 -0.250 1.000 0.000 0.500 0.250 1.000 0.500 0.000 0.500") \
            .has_memberships({-0.5: 0.0,
                              -0.4: 0.3999999999999999,
                              -0.25: 1.0,
                              -0.1: 0.7,
                              0.0: 0.5,
                              0.1: 0.7,
                              0.25: 1.0,
                              0.4: 0.3999999999999999,
                              0.5: 0.0,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}, height=0.5)

        xy = fl.Discrete("name", ("0 1 2 3 4 5 6 7".split()))
        self.assertSequenceEqual(tuple(xy.x()), (0, 2, 4, 6))
        self.assertSequenceEqual(tuple(xy.y()), (1, 3, 5, 7))
        self.assertEqual(3, xy.membership(2))

        # Test iterators
        it = iter(xy)
        self.assertEqual(next(it), (0, 1))
        self.assertEqual(next(it), (2, 3))
        self.assertEqual(next(it), (4, 5))
        self.assertEqual(next(it), (6, 7))
        with self.assertRaisesRegex(StopIteration, ""):
            next(it)

        self.assertEqual(fl.Discrete.pairs_from([]), [])
        self.assertEqual(fl.Discrete.values_from(
            fl.Discrete.pairs_from([1, 2, 3, 4])), [1, 2, 3, 4])
        self.assertEqual(fl.Discrete.values_from(
            fl.Discrete.pairs_from({1: 2, 3: 4})), [1, 2, 3, 4])

        with self.assertRaisesRegex(ValueError, re.escape("not enough values to unpack "
                                                          "(expected an even number, but got 3)")):
            fl.Discrete.pairs_from([1, 2, 3])

        term = fl.Discrete()
        with self.assertRaisesRegex(ValueError, re.escape(
                "expected a list of (x,y)-pairs, but found none")):
            term.membership(0.0)
        with self.assertRaisesRegex(ValueError, re.escape(
                "expected a list of (x,y)-pairs, but found none")):
            term.xy = []
            term.membership(0.0)

    def test_discrete_pairs(self) -> None:
        pairs = [fl.Discrete.Pair(*pair) for pair in [(1, 0), (3, 0), (5, 0), (2, 0), (4, 0)]]
        self.assertListEqual([(1, 0), (2, 0), (3, 0), (4, 0), (5, 0)],
                             [pair.values for pair in sorted(pairs)])

        # Comparison between Pairs
        self.assertTrue(fl.Discrete.Pair(0.1, 0.5) == fl.Discrete.Pair(0.1, 0.5))
        self.assertTrue(fl.Discrete.Pair(0.5, 0.1) == fl.Discrete.Pair(0.5, 0.1))
        self.assertTrue(fl.Discrete.Pair(0.1, 0.5) != fl.Discrete.Pair(0.5, 0.1))
        self.assertTrue(fl.Discrete.Pair(0.1, 0.5) != fl.Discrete.Pair(0.1, 0.55))

        self.assertTrue(fl.Discrete.Pair(0.1, 0.5) < fl.Discrete.Pair(0.1, 0.55))
        self.assertTrue(fl.Discrete.Pair(0.1, 0.5) < fl.Discrete.Pair(0.11, 0.5))
        self.assertTrue(fl.Discrete.Pair(0.1, 0.5) <= fl.Discrete.Pair(0.1, 0.5))
        self.assertTrue(fl.Discrete.Pair(0.1, 0.5) <= fl.Discrete.Pair(0.1, 0.51))

        self.assertTrue(fl.Discrete.Pair(0.1, 0.5) > fl.Discrete.Pair(0.1, 0.49))
        self.assertTrue(fl.Discrete.Pair(0.1, 0.5) > fl.Discrete.Pair(0.09, 0.5))
        self.assertTrue(fl.Discrete.Pair(0.1, 0.5) >= fl.Discrete.Pair(0.1, 0.5))
        self.assertTrue(fl.Discrete.Pair(0.1, 0.5) >= fl.Discrete.Pair(0.1, 0.49))

        # Comparison of tuples
        self.assertTrue(fl.Discrete.Pair(0.1, 0.5) == (0.1, 0.5))
        self.assertTrue(fl.Discrete.Pair(0.5, 0.1) == (0.5, 0.1))
        self.assertTrue(fl.Discrete.Pair(0.1, 0.5) != (0.5, 0.1))
        self.assertTrue(fl.Discrete.Pair(0.1, 0.5) != (0.1, 0.55))

        self.assertTrue(fl.Discrete.Pair(0.1, 0.5) < (0.1, 0.55))
        self.assertTrue(fl.Discrete.Pair(0.1, 0.5) < (0.11, 0.5))
        self.assertTrue(fl.Discrete.Pair(0.1, 0.5) <= (0.1, 0.5))
        self.assertTrue(fl.Discrete.Pair(0.1, 0.5) <= (0.1, 0.51))

        self.assertTrue(fl.Discrete.Pair(0.1, 0.5) > (0.1, 0.49))
        self.assertTrue(fl.Discrete.Pair(0.1, 0.5) > (0.09, 0.5))
        self.assertTrue(fl.Discrete.Pair(0.1, 0.5) >= (0.1, 0.5))
        self.assertTrue(fl.Discrete.Pair(0.1, 0.5) >= (0.1, 0.49))

        # Comparison of floats
        base_pair = fl.Discrete.Pair()
        self.assertEqual("(nan, nan)", str(base_pair))
        base_pair.values = (0.1, 0.5)
        self.assertEqual("(0.1, 0.5)", str(base_pair))
        self.assertFalse(base_pair == 0.1)
        self.assertTrue(base_pair != 0.1)

        for value in [fl.nan, fl.inf, -fl.inf, -1.0, -0.5, 0.0, 0.5, 1.0]:
            for compare in [fl.Discrete.Pair.__lt__, fl.Discrete.Pair.__gt__,
                            fl.Discrete.Pair.__le__, fl.Discrete.Pair.__ge__]:
                with self.assertRaisesRegex(ValueError, re.escape(
                        "expected Union[Tuple[float, float], 'Discrete.Pair'], "
                        "but found <class 'float'>")):
                    compare(base_pair, value)  # type: ignore

    def test_gaussian(self) -> None:
        TermAssert(self, fl.Gaussian("gaussian")) \
            .exports_fll("term: gaussian Gaussian nan nan") \
            .takes_parameters(2) \
            .is_not_monotonic() \
            .configured_as("0.0 0.25") \
            .exports_fll("term: gaussian Gaussian 0.000 0.250") \
            .has_memberships({-0.5: 0.1353352832366127,
                              -0.4: 0.2780373004531941,
                              -0.25: 0.6065306597126334,
                              -0.1: 0.9231163463866358,
                              0.0: 1.0,
                              0.1: 0.9231163463866358,
                              0.25: 0.6065306597126334,
                              0.4: 0.2780373004531941,
                              0.5: 0.1353352832366127,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}) \
            .configured_as("0.0 0.25 0.5") \
            .exports_fll("term: gaussian Gaussian 0.000 0.250 0.500") \
            .has_memberships({-0.5: 0.1353352832366127,
                              -0.4: 0.2780373004531941,
                              -0.25: 0.6065306597126334,
                              -0.1: 0.9231163463866358,
                              0.0: 1.0,
                              0.1: 0.9231163463866358,
                              0.25: 0.6065306597126334,
                              0.4: 0.2780373004531941,
                              0.5: 0.1353352832366127,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}, height=0.5)

    def test_gaussian_product(self) -> None:
        TermAssert(self, fl.GaussianProduct("gaussian_product")) \
            .exports_fll(
            "term: gaussian_product GaussianProduct nan nan nan nan") \
            .takes_parameters(4) \
            .is_not_monotonic() \
            .configured_as("0.0 0.25 0.1 0.5") \
            .exports_fll("term: gaussian_product GaussianProduct 0.000 0.250 0.100 0.500") \
            .has_memberships({-0.5: 0.1353352832366127,
                              -0.4: 0.2780373004531941,
                              -0.25: 0.6065306597126334,
                              -0.1: 0.9231163463866358,
                              0.0: 1.0,
                              0.1: 1.0,
                              0.25: 0.9559974818331,
                              0.4: 0.835270211411272,
                              0.5: 0.7261490370736908,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}) \
            .configured_as("0.0 0.25 0.1 0.5 0.5") \
            .exports_fll("term: gaussian_product GaussianProduct 0.000 0.250 0.100 0.500 0.500") \
            .has_memberships({-0.5: 0.1353352832366127,
                              -0.4: 0.2780373004531941,
                              -0.25: 0.6065306597126334,
                              -0.1: 0.9231163463866358,
                              0.0: 1.0,
                              0.1: 1.0,
                              0.25: 0.9559974818331,
                              0.4: 0.835270211411272,
                              0.5: 0.7261490370736908,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}, height=0.5)

    def test_linear(self) -> None:
        engine = fl.Engine(
            input_variables=[fl.InputVariable("A"), fl.InputVariable("B"), fl.InputVariable("C")])
        engine.input_variables[0].value = 0
        engine.input_variables[1].value = 1
        engine.input_variables[2].value = 2

        with self.assertRaisesRegex(ValueError,
                                    "expected the reference to an engine, but found none"):
            fl.Linear().membership(math.nan)

        linear = fl.Linear("linear", [1.0, 2.0])
        self.assertEqual(linear.engine, None)
        linear.update_reference(engine)
        self.assertEqual(linear.engine, engine)

        TermAssert(self, linear) \
            .exports_fll("term: linear Linear 1.000 2.000") \
            .is_not_monotonic() \
            .configured_as("1.0 2.0 3") \
            .exports_fll("term: linear Linear 1.000 2.000 3.000") \
            .has_memberships({-0.5: 1 * 0 + 2 * 1 + 3 * 2,  # = 8
                              -0.4: 8,
                              -0.25: 8,
                              -0.1: 8,
                              0.0: 8,
                              0.1: 8,
                              0.25: 8,
                              0.4: 8,
                              0.5: 8,
                              math.nan: 8,
                              math.inf: 8,
                              -math.inf: 8}) \
            .configured_as("1 2 3 5") \
            .exports_fll("term: linear Linear 1.000 2.000 3.000 5.000") \
            .has_memberships({-0.5: 1 * 0 + 2 * 1 + 3 * 2 + 5,  # = 13
                              -0.4: 13,
                              -0.25: 13,
                              -0.1: 13,
                              0.0: 13,
                              0.1: 13,
                              0.25: 13,
                              0.4: 13,
                              0.5: 13,
                              math.nan: 13,
                              math.inf: 13,
                              -math.inf: 13}) \
            .configured_as("1 2 3 5 8") \
            .exports_fll("term: linear Linear 1.000 2.000 3.000 5.000 8.000") \
            .has_memberships({-0.5: 1 * 0 + 2 * 1 + 3 * 2 + 5,  # = 13
                              -0.4: 13,
                              -0.25: 13,
                              -0.1: 13,
                              0.0: 13,
                              0.1: 13,
                              0.25: 13,
                              0.4: 13,
                              0.5: 13,
                              math.nan: 13,
                              math.inf: 13,
                              -math.inf: 13})

    def test_pi_shape(self) -> None:
        TermAssert(self, fl.PiShape("pi_shape")) \
            .exports_fll("term: pi_shape PiShape nan nan nan nan") \
            .takes_parameters(4) \
            .is_not_monotonic() \
            .configured_as("-.9 -.1 .1 1") \
            .exports_fll("term: pi_shape PiShape -0.900 -0.100 0.100 1.000") \
            .has_memberships({-0.5: 0.5,
                              -0.4: 0.71875,
                              -0.25: 0.9296875,
                              -0.1: 1.0,
                              0.0: 1.0,
                              0.1: 1.0,
                              0.25: 0.9444444444444444,
                              0.4: 0.7777777777777777,
                              0.5: 0.6049382716049383,
                              0.95: 0.00617283950617285,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}) \
            .configured_as("-.9 -.1 .1 1 .5") \
            .exports_fll("term: pi_shape PiShape -0.900 -0.100 0.100 1.000 0.500") \
            .has_memberships({-0.5: 0.5,
                              -0.4: 0.71875,
                              -0.25: 0.9296875,
                              -0.1: 1.0,
                              0.0: 1.0,
                              0.1: 1.0,
                              0.25: 0.9444444444444444,
                              0.4: 0.7777777777777777,
                              0.5: 0.6049382716049383,
                              0.95: 0.00617283950617285,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}, height=0.5)

    def test_ramp(self) -> None:
        TermAssert(self, fl.Ramp("ramp")) \
            .exports_fll("term: ramp Ramp nan nan") \
            .takes_parameters(2) \
            .is_monotonic() \
            .configured_as("1 1") \
            .exports_fll("term: ramp Ramp 1.000 1.000") \
            .has_memberships({-0.5: 0.0,
                              -0.4: 0.0,
                              -0.25: 0.0,
                              -0.1: 0.0,
                              0.0: 0.0,
                              0.1: 0.0,
                              0.25: 0.0,
                              0.4: 0.0,
                              0.5: 0.0,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}) \
            .configured_as("-0.250 0.750") \
            .exports_fll("term: ramp Ramp -0.250 0.750") \
            .has_memberships({-0.5: 0.0,
                              -0.4: 0.0,
                              -0.25: 0.0,
                              -0.1: 0.150,
                              0.0: 0.250,
                              0.1: 0.350,
                              0.25: 0.500,
                              0.4: 0.650,
                              0.5: 0.750,
                              math.nan: math.nan,
                              math.inf: 1.0,
                              -math.inf: 0.0}) \
            .has_tsukamotos({0.0: -0.250,
                             0.1: -0.150,
                             0.25: 0.0,
                             0.4: 0.15000000000000002,
                             0.5: 0.25,
                             0.6: 0.35,
                             0.75: 0.5,
                             0.9: 0.65,
                             1.0: 0.75,
                             math.nan: math.nan,
                             math.inf: math.inf,
                             -math.inf: -math.inf
                             }) \
            .configured_as("0.250 -0.750 0.5") \
            .exports_fll("term: ramp Ramp 0.250 -0.750 0.500") \
            .has_memberships({-0.5: 0.750,
                              -0.4: 0.650,
                              -0.25: 0.500,
                              -0.1: 0.350,
                              0.0: 0.250,
                              0.1: 0.150,
                              0.25: 0.0,
                              0.4: 0.0,
                              0.5: 0.0,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 1.0}, height=0.5) \
            .has_tsukamotos({0.0: 0.250,
                             0.1: 0.04999999999999999,
                             0.25: -0.25,
                             0.4: -0.550,
                             0.5: -0.75,  # maximum \mu(x)=0.5.
                             # 0.6: -0.75,
                             # 0.75: -0.75,
                             # 0.9: -0.75,
                             # 1.0: -0.75,
                             math.nan: math.nan,
                             math.inf: -math.inf,
                             -math.inf: math.inf
                             })

    def test_rectangle(self) -> None:
        TermAssert(self, fl.Rectangle("rectangle")) \
            .exports_fll("term: rectangle Rectangle nan nan") \
            .takes_parameters(2) \
            .is_not_monotonic() \
            .configured_as("-0.4 0.4") \
            .exports_fll("term: rectangle Rectangle -0.400 0.400") \
            .has_memberships({-0.5: 0.0,
                              -0.4: 1.0,
                              -0.25: 1.0,
                              -0.1: 1.0,
                              0.0: 1.0,
                              0.1: 1.0,
                              0.25: 1.0,
                              0.4: 1.0,
                              0.5: 0.0,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}) \
            .configured_as("-0.4 0.4 0.5") \
            .exports_fll("term: rectangle Rectangle -0.400 0.400 0.500") \
            .has_memberships({-0.5: 0.0,
                              -0.4: 1.0,
                              -0.25: 1.0,
                              -0.1: 1.0,
                              0.0: 1.0,
                              0.1: 1.0,
                              0.25: 1.0,
                              0.4: 1.0,
                              0.5: 0.0,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}, height=0.5)

    def test_s_shape(self) -> None:
        TermAssert(self, fl.SShape("s_shape")) \
            .exports_fll("term: s_shape SShape nan nan") \
            .takes_parameters(2) \
            .is_monotonic() \
            .configured_as("-0.4 0.4") \
            .exports_fll("term: s_shape SShape -0.400 0.400") \
            .has_memberships({-0.5: 0.0,
                              -0.4: 0.0,
                              -0.25: 0.07031250000000001,
                              -0.1: 0.28125000000000006,
                              0.0: 0.5,
                              0.1: 0.71875,
                              0.25: 0.9296875,
                              0.4: 1.0,
                              0.5: 1.0,
                              math.nan: math.nan,
                              math.inf: 1.0,
                              -math.inf: 0.0}) \
            .configured_as("-0.4 0.4 0.5") \
            .exports_fll("term: s_shape SShape -0.400 0.400 0.500") \
            .has_memberships({-0.5: 0.0,
                              -0.4: 0.0,
                              -0.25: 0.07031250000000001,
                              -0.1: 0.28125000000000006,
                              0.0: 0.5,
                              0.1: 0.71875,
                              0.25: 0.9296875,
                              0.4: 1.0,
                              0.5: 1.0,
                              math.nan: math.nan,
                              math.inf: 1.0,
                              -math.inf: 0.0}, height=0.5)

    def test_sigmoid(self) -> None:
        TermAssert(self, fl.Sigmoid("sigmoid")) \
            .exports_fll("term: sigmoid Sigmoid nan nan") \
            .takes_parameters(2) \
            .is_monotonic() \
            .configured_as("0 10") \
            .exports_fll("term: sigmoid Sigmoid 0.000 10.000") \
            .has_memberships({-0.5: 0.0066928509242848554,
                              -0.4: 0.01798620996209156,
                              -0.25: 0.07585818002124355,
                              -0.1: 0.2689414213699951,
                              0.0: 0.5,
                              0.1: 0.7310585786300049,
                              0.25: 0.9241418199787566,
                              0.4: 0.9820137900379085,
                              0.5: 0.9933071490757153,
                              math.nan: math.nan,
                              math.inf: 1.0,
                              -math.inf: 0.0}) \
            .configured_as("0 10 .5") \
            .exports_fll("term: sigmoid Sigmoid 0.000 10.000 0.500") \
            .has_memberships({-0.5: 0.0066928509242848554,
                              -0.4: 0.01798620996209156,
                              -0.25: 0.07585818002124355,
                              -0.1: 0.2689414213699951,
                              0.0: 0.5,
                              0.1: 0.7310585786300049,
                              0.25: 0.9241418199787566,
                              0.4: 0.9820137900379085,
                              0.5: 0.9933071490757153,
                              math.nan: math.nan,
                              math.inf: 1.0,
                              -math.inf: 0.0}, height=0.5)

    def test_sigmoid_difference(self) -> None:
        TermAssert(self, fl.SigmoidDifference("sigmoid_difference")) \
            .exports_fll("term: sigmoid_difference SigmoidDifference nan nan nan nan") \
            .takes_parameters(4) \
            .is_not_monotonic() \
            .configured_as("-0.25 25.00 50.00 0.25") \
            .exports_fll("term: sigmoid_difference SigmoidDifference -0.250 25.000 50.000 0.250") \
            .has_memberships({-0.5: 0.0019267346633274238,
                              -0.4: 0.022977369910017923,
                              -0.25: 0.49999999998611205,
                              -0.1: 0.9770226049799834,
                              0.0: 0.9980695386973883,
                              0.1: 0.9992887851439739,
                              0.25: 0.49999627336071584,
                              0.4: 0.000552690994449101,
                              0.5: 3.7194451510957904e-06,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}) \
            .configured_as("-0.25 25.00 50.00 0.25 0.5") \
            .exports_fll(
            "term: sigmoid_difference SigmoidDifference -0.250 25.000 50.000 0.250 0.500") \
            .has_memberships({-0.5: 0.0019267346633274238,
                              -0.4: 0.022977369910017923,
                              -0.25: 0.49999999998611205,
                              -0.1: 0.9770226049799834,
                              0.0: 0.9980695386973883,
                              0.1: 0.9992887851439739,
                              0.25: 0.49999627336071584,
                              0.4: 0.000552690994449101,
                              0.5: 3.7194451510957904e-06,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}, height=0.5)

    def test_sigmoid_product(self) -> None:
        TermAssert(self, fl.SigmoidProduct("sigmoid_product")) \
            .exports_fll("term: sigmoid_product SigmoidProduct nan nan nan nan") \
            .takes_parameters(4) \
            .is_not_monotonic() \
            .configured_as("-0.250 20.000 -20.000 0.250") \
            .exports_fll("term: sigmoid_product SigmoidProduct -0.250 20.000 -20.000 0.250") \
            .has_memberships({-0.5: 0.006692848876926853,
                              -0.4: 0.04742576597971327,
                              -0.25: 0.4999773010656488,
                              -0.1: 0.9517062830264366,
                              0.0: 0.9866590924049252,
                              0.1: 0.9517062830264366,
                              0.25: 0.4999773010656488,
                              0.4: 0.04742576597971327,
                              0.5: 0.006692848876926853,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}) \
            .configured_as("-0.250 20.000 -20.000 0.250 0.5") \
            .exports_fll("term: sigmoid_product SigmoidProduct -0.250 20.000 -20.000 0.250 0.500") \
            .has_memberships({-0.5: 0.006692848876926853,
                              -0.4: 0.04742576597971327,
                              -0.25: 0.4999773010656488,
                              -0.1: 0.9517062830264366,
                              0.0: 0.9866590924049252,
                              0.1: 0.9517062830264366,
                              0.25: 0.4999773010656488,
                              0.4: 0.04742576597971327,
                              0.5: 0.006692848876926853,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}, height=0.5)

    def test_spike(self) -> None:
        TermAssert(self, fl.Spike("spike")) \
            .exports_fll("term: spike Spike nan nan") \
            .takes_parameters(2) \
            .is_not_monotonic() \
            .configured_as("0 1.0") \
            .exports_fll("term: spike Spike 0.000 1.000") \
            .has_memberships({-0.5: 0.006737946999085467,
                              -0.4: 0.01831563888873418,
                              -0.25: 0.0820849986238988,
                              -0.1: 0.36787944117144233,
                              0.0: 1.0,
                              0.1: 0.36787944117144233,
                              0.25: 0.0820849986238988,
                              0.4: 0.01831563888873418,
                              0.5: 0.006737946999085467,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}) \
            .configured_as("0 1.0 .5") \
            .exports_fll("term: spike Spike 0.000 1.000 0.500") \
            .has_memberships({-0.5: 0.006737946999085467,
                              -0.4: 0.01831563888873418,
                              -0.25: 0.0820849986238988,
                              -0.1: 0.36787944117144233,
                              0.0: 1.0,
                              0.1: 0.36787944117144233,
                              0.25: 0.0820849986238988,
                              0.4: 0.01831563888873418,
                              0.5: 0.006737946999085467,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}, height=0.5)

    def test_trapezoid(self) -> None:
        TermAssert(self, fl.Trapezoid("trapezoid", 0.0, 1.0)).exports_fll(
            "term: trapezoid Trapezoid 0.000 0.200 0.800 1.000")

        TermAssert(self, fl.Trapezoid("trapezoid")) \
            .exports_fll("term: trapezoid Trapezoid nan nan nan nan") \
            .takes_parameters(4) \
            .is_not_monotonic() \
            .configured_as("-0.400 -0.100 0.100 0.400") \
            .exports_fll("term: trapezoid Trapezoid -0.400 -0.100 0.100 0.400") \
            .has_memberships({-0.5: 0.000,
                              -0.4: 0.000,
                              -0.25: 0.500,
                              -0.1: 1.000,
                              0.0: 1.000,
                              0.1: 1.000,
                              0.25: 0.500,
                              0.4: 0.000,
                              0.5: 0.000,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}) \
            .configured_as("-0.400 -0.100 0.100 0.400 .5") \
            .exports_fll("term: trapezoid Trapezoid -0.400 -0.100 0.100 0.400 0.500") \
            .has_memberships({-0.5: 0.000,
                              -0.4: 0.000,
                              -0.25: 0.500,
                              -0.1: 1.000,
                              0.0: 1.000,
                              0.1: 1.000,
                              0.25: 0.500,
                              0.4: 0.000,
                              0.5: 0.000,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}, height=0.5) \
            .configured_as("-0.400 -0.400 0.100 0.400") \
            .exports_fll("term: trapezoid Trapezoid -0.400 -0.400 0.100 0.400") \
            .has_memberships({-0.5: 0.000,
                              -0.4: 1.000,
                              -0.25: 1.000,
                              -0.1: 1.000,
                              0.0: 1.000,
                              0.1: 1.000,
                              0.25: 0.500,
                              0.4: 0.000,
                              0.5: 0.000,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}) \
            .configured_as("-0.400 -0.100 0.400 0.400") \
            .exports_fll("term: trapezoid Trapezoid -0.400 -0.100 0.400 0.400") \
            .has_memberships({-0.5: 0.000,
                              -0.4: 0.000,
                              -0.25: 0.5,
                              -0.1: 1.000,
                              0.0: 1.000,
                              0.1: 1.000,
                              0.25: 1.000,
                              0.4: 1.000,
                              0.5: 0.000,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}) \
            .configured_as("-inf -0.100 0.100 .4") \
            .exports_fll("term: trapezoid Trapezoid -inf -0.100 0.100 0.400") \
            .has_memberships({-0.5: 1.000,
                              -0.4: 1.000,
                              -0.25: 1.000,
                              -0.1: 1.000,
                              0.0: 1.000,
                              0.1: 1.000,
                              0.25: 0.500,
                              0.4: 0.000,
                              0.5: 0.000,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 1.0}) \
            .configured_as("-.4 -0.100 0.100 inf .5") \
            .exports_fll("term: trapezoid Trapezoid -0.400 -0.100 0.100 inf 0.500") \
            .has_memberships({-0.5: 0.000,
                              -0.4: 0.000,
                              -0.25: 0.500,
                              -0.1: 1.000,
                              0.0: 1.000,
                              0.1: 1.000,
                              0.25: 1.000,
                              0.4: 1.000,
                              0.5: 1.000,
                              math.nan: math.nan,
                              math.inf: 1.0,
                              -math.inf: 0.0}, height=0.5)

    def test_triangle(self) -> None:
        TermAssert(self, fl.Triangle("triangle", 0.0, 1.0)).exports_fll(
            "term: triangle Triangle 0.000 0.500 1.000")

        TermAssert(self, fl.Triangle("triangle")) \
            .exports_fll("term: triangle Triangle nan nan nan") \
            .takes_parameters(3) \
            .is_not_monotonic() \
            .configured_as("-0.400 0.000 0.400") \
            .exports_fll("term: triangle Triangle -0.400 0.000 0.400") \
            .has_memberships({-0.5: 0.000,
                              -0.4: 0.000,
                              -0.25: 0.37500000000000006,
                              -0.1: 0.7500000000000001,
                              0.0: 1.000,
                              0.1: 0.7500000000000001,
                              0.25: 0.37500000000000006,
                              0.4: 0.000,
                              0.5: 0.000,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}) \
            .configured_as("-0.400 0.000 0.400 .5") \
            .exports_fll("term: triangle Triangle -0.400 0.000 0.400 0.500") \
            .has_memberships({-0.5: 0.000,
                              -0.4: 0.000,
                              -0.25: 0.37500000000000006,
                              -0.1: 0.7500000000000001,
                              0.0: 1.000,
                              0.1: 0.7500000000000001,
                              0.25: 0.37500000000000006,
                              0.4: 0.000,
                              0.5: 0.000,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}, height=0.5) \
            .configured_as("-0.500 0.000 0.500") \
            .exports_fll("term: triangle Triangle -0.500 0.000 0.500") \
            .has_memberships({-0.5: 0.000,
                              -0.4: 0.19999999999999996,
                              -0.25: 0.5,
                              -0.1: 0.8,
                              0.0: 1.000,
                              0.1: 0.8,
                              0.25: 0.5,
                              0.4: 0.19999999999999996,
                              0.5: 0.000,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}) \
            .configured_as("-0.500 -0.500 0.500") \
            .exports_fll("term: triangle Triangle -0.500 -0.500 0.500") \
            .has_memberships({-0.5: 1.000,
                              -0.4: 0.900,
                              -0.25: 0.75,
                              -0.1: 0.6,
                              0.0: 0.5,
                              0.1: 0.4,
                              0.25: 0.25,
                              0.4: 0.09999999999999998,
                              0.5: 0.000,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}) \
            .configured_as("-0.500 0.500 0.500") \
            .exports_fll("term: triangle Triangle -0.500 0.500 0.500") \
            .has_memberships({-0.5: 0.000,
                              -0.4: 0.09999999999999998,
                              -0.25: 0.25,
                              -0.1: 0.4,
                              0.0: 0.5,
                              0.1: 0.6,
                              0.25: 0.75,
                              0.4: 0.900,
                              0.5: 1.000,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 0.0}) \
            .configured_as("-inf 0.000 0.400") \
            .exports_fll("term: triangle Triangle -inf 0.000 0.400") \
            .has_memberships({-0.5: 1.000,
                              -0.4: 1.000,
                              -0.25: 1.000,
                              -0.1: 1.000,
                              0.0: 1.000,
                              0.1: 0.7500000000000001,
                              0.25: 0.37500000000000006,
                              0.4: 0.000,
                              0.5: 0.000,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 1.000}) \
            .configured_as("-0.400 0.000 inf .5") \
            .exports_fll("term: triangle Triangle -0.400 0.000 inf 0.500") \
            .has_memberships({-0.5: 0.000,
                              -0.4: 0.000,
                              -0.25: 0.37500000000000006,
                              -0.1: 0.7500000000000001,
                              0.0: 1.000,
                              0.1: 1.000,
                              0.25: 1.000,
                              0.4: 1.000,
                              0.5: 1.000,
                              math.nan: math.nan,
                              math.inf: 1.000,
                              -math.inf: 0.0}, height=0.5)

    def test_z_shape(self) -> None:
        TermAssert(self, fl.ZShape("z_shape")) \
            .exports_fll("term: z_shape ZShape nan nan") \
            .takes_parameters(2) \
            .is_monotonic() \
            .configured_as("-0.4 0.4") \
            .exports_fll("term: z_shape ZShape -0.400 0.400") \
            .has_memberships({-0.5: 1.0,
                              -0.4: 1.0,
                              -0.25: 0.9296875,
                              -0.1: 0.71875,
                              0.0: 0.5,
                              0.1: 0.28125000000000006,
                              0.25: 0.07031250000000001,
                              0.4: 0.0,
                              0.5: 0.0,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 1.0}) \
            .configured_as("-0.4 0.4 0.5") \
            .exports_fll("term: z_shape ZShape -0.400 0.400 0.500") \
            .has_memberships({-0.5: 1.0,
                              -0.4: 1.0,
                              -0.25: 0.9296875,
                              -0.1: 0.71875,
                              0.0: 0.5,
                              0.1: 0.28125000000000006,
                              0.25: 0.07031250000000001,
                              0.4: 0.0,
                              0.5: 0.0,
                              math.nan: math.nan,
                              math.inf: 0.0,
                              -math.inf: 1.0}, height=0.5)

    # @unittest.skip("division by zero not handled well by Python")
    def test_division_by_zero_fails_with_float(self) -> None:
        self.assertEqual(fl.lib.floating_point_type, float)

        TermAssert(self, fl.Function.create("dbz", "0.0/x")) \
            .membership_fails(0.0, ZeroDivisionError, re.escape("float division by zero")) \
            .has_memberships({fl.inf: 0.0, -fl.inf: -0.0, fl.nan: fl.nan})

        TermAssert(self, fl.Function.create("dbz", "inf/x")) \
            .membership_fails(0.0, ZeroDivisionError, re.escape("float division by zero")) \
            .has_memberships({fl.inf: fl.nan, -fl.inf: fl.nan, fl.nan: fl.nan})

        TermAssert(self, fl.Function.create("dbz", ".-inf/x")) \
            .membership_fails(0.0, ZeroDivisionError, re.escape("float division by zero")) \
            .has_memberships({fl.inf: fl.nan, -fl.inf: fl.nan, -fl.nan: fl.nan})

        TermAssert(self, fl.Function.create("dbz", "nan/x")) \
            .membership_fails(0.0, ZeroDivisionError, re.escape("float division by zero")) \
            .has_memberships({fl.inf: fl.nan, -fl.inf: fl.nan, -fl.nan: fl.nan})

    def test_division_by_zero_does_not_fail_with_numpy_float(self) -> None:
        import numpy as np  # type: ignore
        fl.lib.floating_point_type = np.float_
        np.seterr('ignore')  # ignore "errors", (e.g., division by zero)
        try:
            TermAssert(self, fl.Function.create("dbz", "0.0/x")) \
                .has_memberships({0.0: fl.nan, fl.inf: 0.0, -fl.inf: 0.0, fl.nan: fl.nan})

            TermAssert(self, fl.Function.create("dbz", "inf/x")) \
                .has_memberships({0.0: fl.inf, fl.inf: fl.nan, -fl.inf: fl.nan, -fl.nan: fl.nan})

            TermAssert(self, fl.Function.create("dbz", "~inf/x")) \
                .has_memberships({0.0: -fl.inf, fl.inf: fl.nan, -fl.inf: fl.nan, -fl.nan: fl.nan})

            TermAssert(self, fl.Function.create("dbz", "nan/x")) \
                .has_memberships({0.0: fl.nan, fl.inf: fl.nan, -fl.inf: fl.nan, -fl.nan: fl.nan})
        except Exception:
            fl.lib.floating_point_type = float
            raise

        fl.lib.floating_point_type = float
        self.assertEqual(fl.lib.floating_point_type, float)

    @unittest.skip("Testing of Tsukamoto")
    def test_tsukamoto(self) -> None:
        pass


class FunctionNodeAssert(BaseAssert[fl.Function.Node]):

    def prefix_is(self, prefix: str) -> 'FunctionNodeAssert':
        self.test.assertEqual(prefix, self.actual.prefix())
        return self

    def infix_is(self, infix: str) -> 'FunctionNodeAssert':
        self.test.assertEqual(infix, self.actual.infix())
        return self

    def postfix_is(self, postfix: str) -> 'FunctionNodeAssert':
        self.test.assertEqual(postfix, self.actual.postfix())
        return self

    def value_is(self, expected: str) -> 'FunctionNodeAssert':
        self.test.assertEqual(expected, self.actual.value())
        return self

    def evaluates_to(self, value: float,
                     variables: Optional[
                         Dict[str, float]] = None) -> 'FunctionNodeAssert':
        self.test.assertAlmostEqual(value, self.actual.evaluate(variables), places=15,
                                    msg=f"when value is {value:.3f}")
        return self

    def fails_to_evaluate(self, exception: Type[Exception],
                          message: str) -> 'FunctionNodeAssert':
        with self.test.assertRaisesRegex(exception, message):
            self.actual.evaluate()
        return self


class TestFunction(unittest.TestCase):

    def test_function(self) -> None:
        with self.assertRaisesRegex(RuntimeError, re.escape("function 'f(x)=2x+1' is not loaded")):
            fl.Function("f(x)", "f(x)=2x+1").membership(math.nan)

        TermAssert(self, fl.Function("function", "", variables={"y": 1.5})) \
            .exports_fll("term: function Function") \
            .configured_as("2*x**3 +2*y - 3") \
            .exports_fll("term: function Function 2*x**3 +2*y - 3") \
            .has_memberships({-0.5: -0.25,
                              -0.4: -0.1280000000000001,
                              -0.25: -0.03125,
                              -0.1: -0.0019999999999997797,
                              0.0: 0.0,
                              0.1: 0.0019999999999997797,
                              0.25: 0.03125,
                              0.4: 0.1280000000000001,
                              0.5: 0.25,
                              math.nan: math.nan,
                              math.inf: math.inf,
                              -math.inf: -math.inf})

        input_a = fl.InputVariable("i_A")
        output_a = fl.OutputVariable("o_A")
        engine_a = fl.Engine("A", "Engine A", [input_a], [output_a])
        with self.assertRaisesRegex(ValueError, re.escape(
                "expected a map of variables containing the value for 'i_A', "
                "but the map contains: {'x': 0.0}")):
            fl.Function.create("engine_a", "2*i_A + o_A + x").membership(0.0)

        function_a = fl.Function.create("f", "2*i_A + o_A + x", engine_a)
        assert_that = TermAssert(self, function_a)
        assert_that.exports_fll("term: f Function 2*i_A + o_A + x").has_membership(0.0, math.nan)
        input_a.value = 3.0
        output_a.value = 1.0
        assert_that.has_memberships({
            -1.0: 6.0,
            -0.5: 6.5,
            0.0: 7.0,
            0.5: 7.5,
            1.0: 8.0,
            math.nan: math.nan,
            math.inf: math.inf,
            -math.inf: -math.inf
        })

        function_a.variables = {"x": math.nan}
        with self.assertRaisesRegex(ValueError, re.escape(
                "variable 'x' is reserved for internal use of Function term, "
                "please remove it from the map of variables: {'x': nan}")):
            function_a.membership(0.0)
        del function_a.variables["x"]

        input_a.name = "x"
        with self.assertRaisesRegex(ValueError, re.escape(
                "variable 'x' is reserved for internal use of Function term, "
                f"please rename the engine variable: {str(input_a)}")):
            function_a.membership(0.0)

        input_b = fl.InputVariable("i_B")
        output_b = fl.OutputVariable("o_B")
        engine_b = fl.Engine("B", "Engine B", [input_b], [output_b])
        self.assertEqual(engine_a, function_a.engine)
        self.assertTrue(function_a.is_loaded())
        with self.assertRaisesRegex(ValueError, re.escape(
                "expected a map of variables containing the value for 'i_A', "
                "but the map contains: {'i_B': nan, 'o_B': nan, 'x': 0.0}")):
            function_a.update_reference(engine_b)
            function_a.membership(0.0)

    def test_element(self) -> None:
        element = fl.Function.Element("function", "math function()",  # type: ignore
                                      fl.Function.Element.Type.Function, None, 0, 0,
                                      -1)
        self.assertEqual(str(element), "Element: name='function', description='math function()', "
                                       "element_type='Type.Function', method='None', arity=0, "
                                       "precedence=0, associativity=-1")

        element = fl.Function.Element("operator", "math operator",
                                      fl.Function.Element.Type.Operator, operator.add, 2, 10, 1)
        self.assertEqual(str(element), "Element: name='operator', description='math operator', "
                                       "element_type='Type.Operator', "
                                       "method='<built-in function add>', arity=2, "
                                       "precedence=10, associativity=1")

        self.assertEqual(str(element), str(copy.deepcopy(element)))

    def test_node_evaluation(self) -> None:
        type_function = fl.Function.Element.Type.Function
        FunctionNodeAssert(self, fl.Function.Node(
            element=fl.Function.Element("undefined", "undefined method",  # type: ignore
                                        type_function, None))
                           ).fails_to_evaluate(ValueError,
                                               "expected a method reference, but found none")

        functions = fl.FunctionFactory()
        node_pow = fl.Function.Node(
            element=functions.copy("**"),
            left=fl.Function.Node(constant=3.0),
            right=fl.Function.Node(constant=4.0)
        )
        FunctionNodeAssert(self, node_pow) \
            .postfix_is("3.000 4.000 **") \
            .prefix_is("** 3.000 4.000") \
            .infix_is("3.000 ** 4.000") \
            .evaluates_to(81.0)

        node_sin = fl.Function.Node(
            element=functions.copy("sin"),
            right=node_pow
        )
        FunctionNodeAssert(self, node_sin) \
            .postfix_is("3.000 4.000 ** sin") \
            .prefix_is("sin ** 3.000 4.000") \
            .infix_is("sin ( 3.000 ** 4.000 )") \
            .evaluates_to(-0.629887994274454)

        node_pow = fl.Function.Node(
            element=functions.copy("pow"),
            left=node_sin,
            right=fl.Function.Node(variable="two")
        )

        FunctionNodeAssert(self, node_pow) \
            .postfix_is("3.000 4.000 ** sin two pow") \
            .prefix_is("pow sin ** 3.000 4.000 two") \
            .infix_is("pow ( sin ( 3.000 ** 4.000 ) two )") \
            .fails_to_evaluate(ValueError,
                               "expected a map of variables containing the value for 'two', "
                               "but the map contains: None") \
            .evaluates_to(0.39675888533109455, {'two': 2})

        node_sum = fl.Function.Node(
            element=functions.copy("+"),
            left=node_pow,
            right=node_pow
        )

        FunctionNodeAssert(self, node_sum) \
            .postfix_is("3.000 4.000 ** sin two pow 3.000 4.000 ** sin two pow +") \
            .prefix_is("+ pow sin ** 3.000 4.000 two pow sin ** 3.000 4.000 two") \
            .infix_is("pow ( sin ( 3.000 ** 4.000 ) two ) + pow ( sin ( 3.000 ** 4.000 ) two )") \
            .evaluates_to(0.7935177706621891, {'two': 2})

        FunctionNodeAssert(self, fl.Function.Node(element=functions.copy("cos"), right=None)) \
            .fails_to_evaluate(ValueError, re.escape("expected a right node, but found none"))

        FunctionNodeAssert(self, fl.Function.Node(element=functions.copy("cos"),
                                                  right=fl.Function.Node(constant=math.pi),
                                                  left=None)).evaluates_to(-1)

        FunctionNodeAssert(self, fl.Function.Node(element=functions.copy("pow"),
                                                  left=None, right=None)) \
            .fails_to_evaluate(ValueError, re.escape("expected a right node, but found none"))
        FunctionNodeAssert(self, fl.Function.Node(element=functions.copy("pow"),
                                                  left=None,
                                                  right=fl.Function.Node(constant=2.0))) \
            .fails_to_evaluate(ValueError, re.escape("expected a left node, but found none"))
        FunctionNodeAssert(self, fl.Function.Node(element=functions.copy("pow"),
                                                  left=fl.Function.Node(constant=2.0),
                                                  right=None)) \
            .fails_to_evaluate(ValueError, re.escape("expected a right node, but found none"))

        def raise_exception() -> NoReturn:
            raise ValueError("mocking testing exception")

        FunctionNodeAssert(self,
                           fl.Function.Node(element=functions.copy("pow"),
                                            left=fl.Function.Node(constant=2.0),
                                            right=fl.Function.Node(
                                                element=fl.Function.Element("raise", "exception",
                                                                            type_function,
                                                                            raise_exception)))) \
            .fails_to_evaluate(ValueError, re.escape("mocking testing exception"))

    def test_node_deep_copy(self) -> None:
        node_mult = fl.Function.Node(
            element=fl.Function.Element("*", "multiplication", fl.Function.Element.Type.Operator,
                                        operator.mul, 2, 80),
            left=fl.Function.Node(constant=3.0),
            right=fl.Function.Node(constant=4.0)
        )
        node_sin = fl.Function.Node(
            element=fl.Function.Element("sin", "sine", fl.Function.Element.Type.Function,
                                        math.sin, 1),
            right=node_mult
        )
        FunctionNodeAssert(self, node_sin) \
            .infix_is("sin ( 3.000 * 4.000 )") \
            .evaluates_to(-0.5365729180004349)

        node_copy = copy.deepcopy(node_sin)

        FunctionNodeAssert(self, node_copy) \
            .infix_is("sin ( 3.000 * 4.000 )") \
            .evaluates_to(-0.5365729180004349)

        # if we change the original object
        node_sin.right.element.name = "?"  # type: ignore
        # the copy cannot be affected
        FunctionNodeAssert(self, node_copy) \
            .infix_is("sin ( 3.000 * 4.000 )") \
            .evaluates_to(-0.5365729180004349)

    def test_node_str(self) -> None:
        some_type = fl.Function.Element.Type.Operator
        FunctionNodeAssert(self, fl.Function.Node(
            element=fl.Function.Element("+", "sum", some_type, sum))) \
            .value_is("+")
        FunctionNodeAssert(self, fl.Function.Node(
            element=fl.Function.Element("+", "sum", some_type, sum), variable="x")) \
            .value_is("+")
        FunctionNodeAssert(self, fl.Function.Node(
            element=fl.Function.Element("+", "sum", some_type, sum), variable="x", constant=1)) \
            .value_is("+")

        FunctionNodeAssert(self, fl.Function.Node(variable="x")) \
            .value_is("x")
        FunctionNodeAssert(self, fl.Function.Node(variable="x", constant=1.0)) \
            .value_is("x")

        FunctionNodeAssert(self, fl.Function.Node(constant=1)) \
            .value_is("1")

    def test_function_format_infix(self) -> None:
        self.assertEqual("a + b * 1 ( True or True ) / ( False and False )",
                         fl.Function.format_infix(
                             f"a+b*1(True {fl.Rule.OR} True)/(False {fl.Rule.AND} False)"))
        self.assertEqual("sqrt ( a + b * 1 + sin ( pi / 2 ) - ~ 3 )",
                         fl.Function.format_infix(
                             f"sqrt(a+b*1+sin(pi/2)-~3)"))

    def test_function_postfix(self) -> None:
        infix_postfix = {
            "a+b": "a b +",
            "a+b*2": "a b 2 * +",
            "a+b*2^3": "a b 2 3 ^ * +",
            "a+b*2^3/(4 - 2)": "a b 2 3 ^ * 4 2 - / +",
            "a+b*2^3/(4 - 2)*sin(pi/4)":
                "a b 2 3 ^ * 4 2 - / pi 4 / sin * +",
            ".-.-a + .+.+b": "a .- .- b .+ .+ +",
            "a*.-b**3": "a b 3 ** .- *",
            ".-(a)**.-b": "a b .- ** .-",
            ".+a**.-b": "a b .- ** .+",
            ".-a**b + .+a**.-b - .-a ** .-b + .-(a**b) - .-(a)**.-b":
                "a b ** .- a b .- ** .+ + a b .- ** .- - a b ** .- + a b .- ** .- -",
            "a+~b": "a b ~ +",
            "~a*~b": "a ~ b ~ *",
            "(sin(pi()/4) + cos(pi/4)) / (~sin(pi()/4) - ~cos(pi/4))":
                "pi 4 / sin pi 4 / cos + pi 4 / sin ~ pi 4 / cos ~ - /"
        }
        for infix, postfix in infix_postfix.items():
            self.assertEqual(postfix, fl.Function.infix_to_postfix(infix))

    def test_function_parse(self) -> None:
        infix_postfix = {
            "a+b": "a b +",
            "a+b*2": "a b 2.000 * +",
            "a+b*2^3": "a b 2.000 3.000 ^ * +",
            "a+b*2^3/(4 - 2)": "a b 2.000 3.000 ^ * 4.000 2.000 - / +",
            "a+b*2^3/(4 - 2)*sin(pi/4)":
                "a b 2.000 3.000 ^ * 4.000 2.000 - / pi 4.000 / sin * +",
            "a+~b": "a b ~ +",
            "~a*~b": "a ~ b ~ *",
            "(sin(pi()/4) + cos(pi/4)) / (~sin(pi()/4) - ~cos(pi/4))":
                "pi 4.000 / sin pi 4.000 / cos + "
                "pi 4.000 / sin ~ pi 4.000 / cos ~ - /"
        }
        for infix, postfix in infix_postfix.items():
            self.assertEqual(postfix, fl.Function.parse(infix).postfix())


if __name__ == '__main__':
    unittest.main()