"""
Test modèles évaluations avec poids BUT
et calcul moyennes modules
"""
import datetime
import numpy as np
import pandas as pd

from tests.unit import setup
from app import db
from app.models import Evaluation, EvaluationUEPoids, Module, ModuleImpl, UniteEns
from app.comp import moy_mod
from app.comp import moy_ue
from app.scodoc import sco_saisie_notes
from app.scodoc.sco_utils import (
    NOTES_ATTENTE,
    NOTES_NEUTRALISE,
    NOTES_SUPPRESS,
    NOTES_PRECISION,
)

"""
mapp.set_sco_dept("RT")
from app.auth.models import get_super_admin
admin_user = get_super_admin()
ctx.push()
login_user(admin_user)
"""


def same_note(x, y):
    return abs(x - y) < NOTES_PRECISION


def test_evaluation_poids(test_client):
    """Association de poids vers les UE"""
    G, formation_id, (ue1_id, ue2_id, ue3_id), module_ids = setup.build_formation_test()
    formsemestre_id = G.create_formsemestre(
        formation_id=formation_id,
        semestre_id=1,
        date_debut="01/01/2021",
        date_fin="30/06/2021",
    )
    moduleimpl_id = G.create_moduleimpl(
        module_id=module_ids[0],
        formsemestre_id=formsemestre_id,
    )
    _e1 = G.create_evaluation(
        moduleimpl_id=moduleimpl_id,
        date_debut=datetime.datetime(2021, 1, 1),
        description="evaluation 1",
        coefficient=0,
    )
    evaluation_id = _e1["evaluation_id"]  # evaluation_id=25246
    # ue1_id=1684
    # formation_id=199
    #
    e1 = db.session.get(Evaluation, evaluation_id)
    ue1 = db.session.get(UniteEns, ue1_id)
    assert e1.ue_poids == []
    p1 = 3.14
    e1.set_ue_poids(ue1, p1)
    db.session.commit()
    assert e1.get_ue_poids_dict()[ue1_id] == p1
    ues = UniteEns.query.filter_by(formation_id=formation_id, semestre_idx=2).all()
    poids = [1.0, 2.0, 3.0]
    for ue, p in zip(ues, poids):
        e1.set_ue_poids(ue, p)
    assert len(e1.ue_poids) == len(ues)
    assert e1.get_ue_poids_dict()[ues[1].id] == poids[1]
    e1.set_ue_poids(ue1, p1)
    db.session.commit()
    poids2 = [10, 20]
    e1.update_ue_poids_dict({ue.id: p for (ue, p) in zip(ues[:-1], poids2)})
    assert e1.get_ue_poids_dict()[ues[0].id] == poids2[0]
    assert e1.get_ue_poids_dict()[ues[1].id] == poids2[1]
    assert e1.get_ue_poids_dict()[ues[2].id] == poids[2]
    # Delete UE
    db.session.delete(ues[2])
    db.session.commit()
    # Delete eval
    db.session.delete(e1)
    db.session.commit()
    assert len(EvaluationUEPoids.query.all()) == 0


def test_modules_coefs(test_client):
    """Coefs vers les UE (BUT)"""
    G, formation_id, (ue1_id, ue2_id, ue3_id), module_ids = setup.build_formation_test()
    ue1 = db.session.get(UniteEns, ue1_id)
    ue2 = db.session.get(UniteEns, ue2_id)
    mod = db.session.get(Module, module_ids[0])
    coef = 2.5
    mod.set_ue_coef(ue1, coef)
    db.session.commit()
    assert mod.ue_coefs[0].coef == coef
    mod.set_ue_coef(ue2, 2 * coef)
    db.session.commit()
    assert set(mod.get_ue_coef_dict().values()) == {coef, 2 * coef}
    assert set(mod.get_ue_coef_dict().keys()) == {ue1_id, ue2_id}
    mod.delete_ue_coef(ue1)
    db.session.commit()
    assert len(mod.ue_coefs) == 1
    # Gestion des coefs nuls:
    mod.set_ue_coef(ue2, 0.0)
    db.session.commit()
    assert len(mod.ue_coefs) == 0


def test_module_conformity(test_client):
    """Vérification coefficients module<->UE vs poids des évaluations"""
    (
        _,
        formation_id,
        _,
        evaluation_ids,
        ue1,
        ue2,
        ue3,
    ) = setup.build_modules_with_evaluations()
    semestre_idx = 2
    nb_ues = 3  # 3 UEs dans ce test
    nb_mods = 1  # 1 seul module
    nb_evals = 1  # 1 seule evaluation pour l'instant
    p1, p2, p3 = 1.0, 2.0, 0.0  # poids de l'éval vers les UE 1, 2 et 3
    evaluation = db.session.get(Evaluation, evaluation_ids[0])
    evaluation.set_ue_poids_dict({ue1.id: p1, ue2.id: p2, ue3.id: p3})
    assert evaluation.get_ue_poids_dict() == {ue1.id: p1, ue2.id: p2, ue3.id: p3}
    # On n'est pas conforme car p3 est nul alors que c3 est non nul
    modules_coefficients, _ues, _modules = moy_ue.df_load_module_coefs(
        formation_id, semestre_idx
    )
    assert isinstance(modules_coefficients, pd.DataFrame)
    assert modules_coefficients.shape == (nb_ues, nb_mods)
    evals_poids, ues = moy_mod.load_evaluations_poids(evaluation.moduleimpl_id)
    assert isinstance(evals_poids, pd.DataFrame)
    assert len(ues) == nb_ues
    assert all(evals_poids.dtypes == np.float64)
    assert evals_poids.shape == (nb_evals, nb_ues)
    assert not moy_mod.moduleimpl_is_conforme(
        evaluation.moduleimpl, evals_poids, modules_coefficients
    )


# En ScoDoc 9.2  test ne peut plus exister car compute_module_moy
# est maintenant incorporé dans la classe ModuleImplResultsAPC
# def test_module_moy_elem(test_client):
#     """Vérification calcul moyenne d'un module
#     (notes entrées dans un DataFrame sans passer par ScoDoc)
#     """
#     # Création de deux évaluations:
#     e1 = Evaluation(note_max=20.0, coefficient=1.0)
#     e2 = Evaluation(note_max=20.0, coefficient=1.0)
#     db.session.add(e1)
#     db.session.add(e2)
#     db.session.flush()
#     # Repris du notebook CalculNotesBUT.ipynb
#     data = [  # Les notes de chaque étudiant dans les 2 evals:
#         {
#             e1.id: 11.0,
#             e2.id: 16.0,
#         },
#         {
#             e1.id: None,  # une absence
#             e2.id: 17.0,
#         },
#         {
#             e1.id: 13.0,
#             e2.id: NOTES_NEUTRALISE,  # une abs EXC
#         },
#         {
#             e1.id: 14.0,
#             e2.id: 19.0,
#         },
#         {
#             e1.id: NOTES_ATTENTE,  # une ATT (traitée comme EXC)
#             e2.id: None,  # et une ABS
#         },
#     ]
#     evals_notes_df = pd.DataFrame(
#         data, index=["etud1", "etud2", "etud3", "etud4", "etud5"]
#     )
#     # Poids des évaluations (1 ligne / évaluation)
#     data = [
#         {"UE1": 1, "UE2": 0, "UE3": 0},
#         {"UE1": 2, "UE2": 5, "UE3": 0},
#     ]
#     evals_poids_df = pd.DataFrame(data, index=[e1.id, e2.id], dtype=float)
#     evaluations = [e1, e2]
#     etuds_moy_module_df = moy_mod.compute_module_moy(
#         evals_notes_df.fillna(0.0), evals_poids_df, evaluations, [True, True]
#     )
#     NAN = 666.0  # pour pouvoir comparer NaN et NaN (car NaN != NaN)
#     r = etuds_moy_module_df.fillna(NAN)
#     assert tuple(r.loc["etud1"]) == (14 + 1 / 3, 16.0, NAN)
#     assert tuple(r.loc["etud2"]) == (11 + 1 / 3, 17.0, NAN)
#     assert tuple(r.loc["etud3"]) == (13, NAN, NAN)
#     assert tuple(r.loc["etud4"]) == (17 + 1 / 3, 19, NAN)
#     assert tuple(r.loc["etud5"]) == (0.0, 0.0, NAN)
#     # note: les notes UE3 sont toutes NAN car les poids vers l'UE3 sont nuls


def test_module_moy(test_client):
    """Test calcul moyenne module avec saisie des notes via ScoDoc"""
    coef_e1, coef_e2 = 7.0, 11.0  # coefficients des évaluations
    (
        G,
        formation_id,
        formsemestre_id,
        evaluation1_ids,
        ue1,
        ue2,
        ue3,
    ) = setup.build_modules_with_evaluations()
    etud = G.create_etud(nom="test")
    G.inscrit_etudiant(formsemestre_id, etud)
    etudid = etud["etudid"]
    evaluation1 = db.session.get(Evaluation, evaluation1_ids[0])
    # Crée une deuxième évaluation dans le même moduleimpl:
    evaluation2_id = G.create_evaluation(
        moduleimpl_id=evaluation1.moduleimpl_id,
        date_debut=datetime.datetime(2021, 1, 2),
        description="evaluation 2",
        coefficient=coef_e2,
    )["evaluation_id"]
    evaluation2 = db.session.get(Evaluation, evaluation2_id)
    # Coefficients de l'eval 1
    evaluation1.coefficient = coef_e1
    # Poids des évaluations:
    e1p1, e1p2, e1p3 = 1.0, 2.0, 0.0  # poids de l'éval 1 vers les UE 1, 2 et 3
    e2p1, e2p2, e2p3 = 0.0, 1.0, 0.0  # poids de l'éval 2 vers les UE
    evaluation1.set_ue_poids_dict({ue1.id: e1p1, ue2.id: e1p2, ue3.id: e1p3})
    evaluation2.set_ue_poids_dict({ue1.id: e2p1, ue2.id: e2p2, ue3.id: e2p3})
    # Vérifications
    moduleimpl_id = evaluation1.moduleimpl_id
    nb_evals = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).count()
    assert nb_evals == 2
    nb_ues = 3
    modimpl = db.session.get(ModuleImpl, moduleimpl_id)

    # --- Change les notes et recalcule les moyennes du module
    # (rappel: on a deux évaluations: evaluation1, evaluation2, et un seul étudiant)
    def change_notes(n1, n2):
        # Saisie d'une note dans chaque éval
        _ = sco_saisie_notes.notes_add(G.default_user, evaluation1.id, [(etudid, n1)])
        _ = sco_saisie_notes.notes_add(G.default_user, evaluation2.id, [(etudid, n2)])
        # Calcul de la moyenne du module
        evals_poids, ues = moy_mod.load_evaluations_poids(moduleimpl_id)
        assert evals_poids.shape == (nb_evals, nb_ues)

        mod_results = moy_mod.ModuleImplResultsAPC(modimpl)
        evals_notes = mod_results.evals_notes
        assert evals_notes[evaluation1.id].dtype == np.float64

        etuds_moy_module = mod_results.compute_module_moy(evals_poids)
        return etuds_moy_module

    # --- Notes ordinaires:
    note1, note2 = 11.0, 12.0
    sum_copo1 = e1p1 * coef_e1 + e2p1 * coef_e2  # coefs vers UE1
    sum_copo2 = e1p2 * coef_e1 + e2p2 * coef_e2  #
    etuds_moy_module = change_notes(note1, note2)
    moy_ue1 = etuds_moy_module[ue1.id][etudid]
    assert moy_ue1 == ((note1 * e1p1 * coef_e1) + (note2 * e2p1 * coef_e2)) / sum_copo1
    moy_ue2 = etuds_moy_module[ue2.id][etudid]
    assert moy_ue2 == ((note1 * e1p2 * coef_e1) + (note2 * e2p2 * coef_e2)) / sum_copo2
    moy_ue3 = etuds_moy_module[ue3.id][etudid]
    assert np.isnan(moy_ue3)  # car les poids vers UE3 sont nuls

    # --- Une note ABS (comptée comme zéro)
    etuds_moy_module = change_notes(None, note2)
    assert etuds_moy_module[ue1.id][etudid] == (note2 * e2p1 * coef_e2) / sum_copo1
    assert etuds_moy_module[ue2.id][etudid] == (note2 * e2p2 * coef_e2) / sum_copo2
    assert np.isnan(etuds_moy_module[ue3.id][etudid])
    # --- Deux notes ABS
    etuds_moy_module = change_notes(None, None)
    assert etuds_moy_module[ue1.id][etudid] == 0.0
    assert etuds_moy_module[ue2.id][etudid] == 0.0
    assert np.isnan(etuds_moy_module[ue3.id][etudid])
    # --- Note EXC
    etuds_moy_module = change_notes(NOTES_ATTENTE, note2)
    assert np.isnan(
        etuds_moy_module[ue1.id][etudid]
    )  # car l'eval 2 ne touche que l'UE2
    assert etuds_moy_module[ue2.id][etudid] == note2
    assert np.isnan(etuds_moy_module[ue3.id][etudid])
    # --- Toutes notes ATT (ATT se traite comme EXC)
    etuds_moy_module = change_notes(NOTES_NEUTRALISE, NOTES_NEUTRALISE)
    assert np.isnan(etuds_moy_module[ue1.id][etudid])
    assert np.isnan(etuds_moy_module[ue2.id][etudid])
    assert np.isnan(etuds_moy_module[ue3.id][etudid])
    # --- Barème sur 37
    evaluation2.note_max = 37.0
    note1, note2 = 11.0, 12.0
    note_2_37 = note2 / 20 * 37
    etuds_moy_module = change_notes(note1, note_2_37)
    moy_ue1 = etuds_moy_module[ue1.id][etudid]
    assert moy_ue1 == ((note1 * e1p1 * coef_e1) + (note2 * e2p1 * coef_e2)) / sum_copo1
    moy_ue2 = etuds_moy_module[ue2.id][etudid]
    assert moy_ue2 == ((note1 * e1p2 * coef_e1) + (note2 * e2p2 * coef_e2)) / sum_copo2
    moy_ue3 = etuds_moy_module[ue3.id][etudid]
    assert np.isnan(moy_ue3)  # car les poids vers UE3 sont nuls
    # --- Note manquante à l'éval. 1
    note_2_37 = note2 / 20 * 37
    etuds_moy_module = change_notes(NOTES_SUPPRESS, note_2_37)
    assert same_note(etuds_moy_module[ue2.id][etudid], note2)
    # --- Prise en compte immédiate:
    evaluation1.publish_incomplete = True
    etuds_moy_module = change_notes(NOTES_SUPPRESS, note_2_37)
    assert same_note(etuds_moy_module[ue2.id][etudid], note2)