#!/usr/bin/env python3
""" Tests for Faceswap Initializers.

Adapted from Keras tests.
"""
import pytest

import numpy as np
from numpy.testing import assert_allclose

from lib.model import optimizers
from lib.utils import get_backend

from tests.utils import generate_test_data, to_categorical

if get_backend() == "amd":
    from keras import optimizers as k_optimizers
    from keras.layers import Dense, Activation
    from keras.models import Sequential
else:
    # Ignore linting errors from Tensorflow's thoroughly broken import system
    from tensorflow.keras import optimizers as k_optimizers  # pylint:disable=import-error
    from tensorflow.keras.layers import Dense, Activation  # noqa pylint:disable=import-error,no-name-in-module
    from tensorflow.keras.models import Sequential  # pylint:disable=import-error,no-name-in-module


def get_test_data():
    """ Obtain randomized test data for training """
    np.random.seed(1337)
    (x_train, y_train), _ = generate_test_data(num_train=1000,
                                               num_test=200,
                                               input_shape=(10,),
                                               classification=True,
                                               num_classes=2)
    y_train = to_categorical(y_train)
    return x_train, y_train


def _test_optimizer(optimizer, target=0.75):
    x_train, y_train = get_test_data()

    model = Sequential()
    model.add(Dense(10, input_shape=(x_train.shape[1],)))
    model.add(Activation("relu"))
    model.add(Dense(y_train.shape[1]))
    model.add(Activation("softmax"))
    model.compile(loss="categorical_crossentropy",
                  optimizer=optimizer,
                  metrics=["accuracy"])

    history = model.fit(x_train, y_train, epochs=2, batch_size=16, verbose=0)
    accuracy = "acc" if get_backend() == "amd" else "accuracy"
    assert history.history[accuracy][-1] >= target
    config = k_optimizers.serialize(optimizer)
    optim = k_optimizers.deserialize(config)
    new_config = k_optimizers.serialize(optim)
    config["class_name"] = config["class_name"].lower()
    new_config["class_name"] = new_config["class_name"].lower()
    assert config == new_config

    # Test constraints.
    if get_backend() == "amd":
        # NB: PlaidML does not support constraints, so this test skipped for AMD backends
        return
    model = Sequential()
    dense = Dense(10,
                  input_shape=(x_train.shape[1],),
                  kernel_constraint=lambda x: 0. * x + 1.,
                  bias_constraint=lambda x: 0. * x + 2.,)
    model.add(dense)
    model.add(Activation("relu"))
    model.add(Dense(y_train.shape[1]))
    model.add(Activation("softmax"))
    model.compile(loss="categorical_crossentropy",
                  optimizer=optimizer,
                  metrics=["accuracy"])
    model.train_on_batch(x_train[:10], y_train[:10])
    kernel, bias = dense.get_weights()
    assert_allclose(kernel, 1.)
    assert_allclose(bias, 2.)


@pytest.mark.parametrize("dummy", [None], ids=[get_backend().upper()])
def test_adam(dummy):  # pylint:disable=unused-argument
    """ Test for custom Adam optimizer """
    _test_optimizer(k_optimizers.Adam(), target=0.45)
    _test_optimizer(k_optimizers.Adam(decay=1e-3), target=0.45)


@pytest.mark.parametrize("dummy", [None], ids=[get_backend().upper()])
def test_adabelief(dummy):  # pylint:disable=unused-argument
    """ Test for custom Adam optimizer """
    _test_optimizer(optimizers.AdaBelief(), target=0.20)
