"""
This module contains tests of the main root finding algorithms using
polynomials with roots generated by the `hypothesis` package.

Authors:\n
- Luca Wasmuth\n
"""

from datetime import timedelta
from typing import List

import numpy as np
import pytest
from hypothesis import Phase, given, settings, strategies
from numpy.polynomial import Polynomial

from pyzeal.pyzeal_types.algorithm_types import AlgorithmTypes
from pyzeal.pyzeal_types.container_types import ContainerTypes
from pyzeal.pyzeal_types.estimator_types import EstimatorTypes
from pyzeal.pyzeal_types.filter_types import FilterTypes
from pyzeal.pyzeal_types.root_types import tHoloFunc
from pyzeal.rootfinders import RootFinder
from pyzeal.rootfinders.parallel_finder import ParallelRootFinder
from pyzeal.settings.ram_settings_service import RAMSettingsService
from pyzeal.settings.settings_service import SettingsService
from pyzeal.tests.resources.utils import rootsMatchClosely
from pyzeal.utils.service_locator import ServiceLocator

settingsService = RAMSettingsService(verbose=False)
ServiceLocator.registerAsSingleton(SettingsService, settingsService)


@pytest.mark.slow
@pytest.mark.parametrize(
    "algorithm",
    [
        AlgorithmTypes.NEWTON_GRID,
        AlgorithmTypes.SIMPLE_ARGUMENT,
        AlgorithmTypes.SIMPLE_ARGUMENT_NEWTON,
    ],
)
@pytest.mark.parametrize(
    "estimator",
    [EstimatorTypes.SUMMATION_ESTIMATOR, EstimatorTypes.QUADRATURE_ESTIMATOR],
)
@pytest.mark.parametrize("parallel", [True, False])
@given(
    strategies.lists(
        strategies.complex_numbers(max_magnitude=5), min_size=2, max_size=5
    )
)
@settings(
    deadline=(timedelta(seconds=9)),
    max_examples=5,
    phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.explain],
)
def testRootFinderHypothesis(
    algorithm: AlgorithmTypes,
    estimator: EstimatorTypes,
    parallel: bool,
    roots: List[complex],
) -> None:
    """
    Test the root finder algorithm based on a simple partial integration of the
    classical argument principle combined with a Newton algorithm upon
    sufficient refinement of the subdivision into rectangles. The testfunctions
    are polynomials whose roots are generated automatically using the
    hypothesis package.

    :param algorithm: The type of algorithm to test
    :param estimator: The type of estimator to test
    :param parallel: Whether the tested finder works in parallel
    :param roots: Roots of the test polynomial
    """
    # We only find a higher-order zero once, so we have to remove duplicates
    uniqueRoots = list(set(np.round(roots, 2)))
    polynomial = Polynomial.fromroots(uniqueRoots)
    f: tHoloFunc = polynomial
    df: tHoloFunc = polynomial.deriv()
    Finder = RootFinder if parallel else ParallelRootFinder
    hrf = Finder(
        f,
        df,
        numSamplePoints=30,
        algorithmType=algorithm,
        containerType=ContainerTypes.ROUNDING_CONTAINER,
        estimatorType=estimator,
        verbose=False,
    )

    hrf.setRootFilter(filterType=FilterTypes.FUNCTION_VALUE_ZERO)
    hrf.setRootFilter(filterType=FilterTypes.ZERO_IN_BOUNDS)
    hrf.calculateRoots((-10.1, 10.2), (-10.3, 10.4), precision=(3, 3))

    foundRoots = np.sort_complex(hrf.roots)
    expectedRoots = np.sort_complex(np.array(uniqueRoots))

    assert rootsMatchClosely(
        foundRoots, expectedRoots, precision=(3, 3), allowSubset=True
    )
