# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

import os
from unittest import SkipTest
import warnings
import typing as tp
import pytest
import numpy as np
from nevergrad.common import testing
from . import core
from . import photonics


@testing.parametrized(
    bragg=("bragg", [2.93, 2.18, 2.35, 2.12, 45.77, 37.99, 143.34, 126.55]),
    morpho=("morpho", [280.36, 52.96, 208.16, 72.69, 89.92, 60.37, 226.69, 193.11]),
    chirped=("chirped", [170.18, 56.48, 82.04, 48.17, 45.77, 37.99, 143.34, 126.55]),
)
def test_photonics_bounding_methods(pb: str, expected: tp.List[float]) -> None:
    func = core.Photonics(pb, 8, bounding_method="tanh")
    np.random.seed(24)
    x = np.random.normal(0, 1, size=8)
    output = func.parametrization.spawn_child().set_standardized_data(x).value.ravel()
    np.testing.assert_almost_equal(output, expected, decimal=2)


@testing.parametrized(
    # bragg domain (n=60): [2,3]^30 x [30,180]^30
    bragg=("bragg", [2.5, 2.5, 2.5, 2.5, 105.0, 105.0, 105.0, 105.0]),
    # chirped domain (n=60): [30,170]^60
    chirped=("chirped", [105.0, 105.0, 105.0, 105.0, 105.0, 105.0, 105.0, 105.0]),
    # morpho domain (n=60): [0,300]^15 x [0,600]^15 x [30,600]^15 x [0,300]^15
    morpho=("morpho", [150.0, 150.0, 300.0, 300.0, 315.0, 315.0, 150.0, 150.0]),
)
def test_photonics_mean(pb: str, expected: tp.List[float]) -> None:
    func = core.Photonics(pb, 8)
    all_x = func.parametrization.value
    output = all_x.ravel()
    np.testing.assert_almost_equal(output, expected, decimal=2)


def test_morpho_bounding_method_constraints() -> None:
    func = core.Photonics("morpho", 60, bounding_method="arctan")
    x = np.random.normal(0, 5, size=60)  # std 5 to play with boundaries
    output1 = func.parametrization.spawn_child().set_standardized_data(x)
    output2 = output1.sample()
    for output in (output1, output2):
        assert np.all(output.value >= 0)
        assert np.all(output.value[[0, 3], :] <= 300)
        assert np.all(output.value[[1, 2], :] <= 600)
        assert np.all(output.value[2, :] >= 30)


def test_photonics_bragg_recombination() -> None:
    func = core.Photonics("bragg", 8)
    func.parametrization.random_state.seed(24)
    arrays = [func.parametrization.spawn_child() for _ in range(2)]
    arrays[0].value = [[2, 2, 2, 2], [35, 35, 35, 35]]
    arrays[1].value = [[3, 3, 3, 3], [45, 45, 45, 45]]
    arrays[0].recombine(arrays[1])
    expected = [[3, 2, 2, 2], [45, 35, 35, 35]]
    np.testing.assert_array_equal(arrays[0].value, expected)


def test_photonics_custom_mutation() -> None:
    if os.environ.get("CIRCLECI", False):
        raise SkipTest("Skipping in CI because way too slow on their machine (weird)")
    func = core.Photonics("morpho", 16, rolling=True)
    param = func.parametrization.spawn_child()
    for _ in range(10):
        param.mutate()
    # as tuple
    func = core.Photonics("morpho", 16, rolling=True, as_tuple=True)
    func.evaluation_function(func.parametrization)


def test_photonics_error() -> None:
    # check error
    photo = core.Photonics("bragg", 16)
    np.testing.assert_raises(AssertionError, photo, np.zeros(12))
    with warnings.catch_warnings(record=True) as ws:
        output = photo(np.zeros(16))
        # one warning on Ubuntu, two warnings with Windows
        assert any(isinstance(w.message, RuntimeWarning) for w in ws)
    np.testing.assert_almost_equal(output, float("inf"))


@pytest.mark.parametrize("method", ["clipping", "arctan", "tanh", "constraint"])  # type: ignore
@pytest.mark.parametrize("name", ["bragg", "morpho", "chirped"])  # type: ignore
def test_no_warning(name: str, method: str) -> None:
    with warnings.catch_warnings(record=True) as w:
        core.Photonics(name, 24, bounding_method=method)
        assert not w, f"Got a warning at initialization: {w[0]}"


@testing.parametrized(
    # morpho=("morpho", 100, 1.1647),  # too slow
    chirped=("chirped", 150, 0.94439),
    bragg=("bragg", 2.5, 0.93216),
)
def test_photonics_values(name: str, value: float, expected: float) -> None:
    if name == "morpho" and os.environ.get("CIRCLECI", False):
        raise SkipTest("Too slow in CircleCI")
    photo = core.Photonics(name, 16)
    np.testing.assert_almost_equal(photo(value * np.ones(16)), expected, decimal=4)


GOOD_CHIRPED = [
    89.04887416,
    109.54188095,
    89.74520725,
    121.81700431,
    179.99830918,
    124.38222473,
    95.31017129,
    116.0239629,
    92.92345776,
    118.06108198,
    179.99965859,
    116.89288181,
    88.90191494,
    110.30816229,
    93.11974992,
    137.42629858,
    118.81810084,
    110.74139708,
    85.15270955,
    100.9382438,
    81.44070951,
    100.6382896,
    84.97336252,
    110.59252719,
    134.89164276,
    121.84205195,
    89.28450356,
    106.72776991,
    85.77168797,
    102.33562547,
]


EPS_AND_D = [  # photosic realistic
    2.0000,
    3.0000,
    2.1076,
    2.0000,
    3.0000,
    2.5783,
    2.0000,
    3.0000,
    2.0000,
    3.0000,
    90.0231,
    78.9789,
    72.8369,
    99.9577,
    82.7487,
    62.7583,
    104.1682,
    139.9002,
    93.3356,
    75.6039,
]


@testing.parametrized(
    morpho=("morpho", 1.127904, None),
    chirped=("chirped", 0.594587, None),
    good_chirped=("chirped", 0.275923, np.array([GOOD_CHIRPED])),  # supposed to be better
    bragg=("bragg", 0.96776, None),
    photosic_realistic=("cf_photosic_realistic", 0.0860257, np.array(EPS_AND_D).reshape((2, -1))),
    photosic_reference=("cf_photosic_reference", 0.431072, None),
)
def test_photonics_values_random(name: str, expected: float, data: tp.Optional[np.ndarray]) -> None:
    if name == "morpho" and os.environ.get("CIRCLECI", False):
        raise SkipTest("Too slow in CircleCI")
    size = data.size if data is not None else (16 if name != "morpho" else 4)
    photo = core.Photonics(name, size)
    if data is None:
        x = np.random.RandomState(12).normal(0, 1, size=size)
        candidate = photo.parametrization.spawn_child().set_standardized_data(x)
    else:
        candidate = photo.parametrization.spawn_child(new_value=data)
    np.testing.assert_almost_equal(photo(candidate.value), expected, decimal=4)
    np.testing.assert_almost_equal(photo.evaluation_function(candidate), expected, decimal=4)


def test_photosic_reference() -> None:
    debut = np.array([79, 102])
    fin = np.array([100, 70])
    dbr = np.tile(np.array([147, 120]), 3)
    X = np.concatenate([debut, dbr, fin])
    cf_test = photonics.cf_photosic_reference(X)
    np.testing.assert_almost_equal(cf_test, 0.09069397)


def test_photosic_realist() -> None:
    cf_test = photonics.cf_photosic_realistic(np.array(EPS_AND_D))
    np.testing.assert_almost_equal(cf_test, 0.08602574254532869)
