##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2024 Emmanuel Viennet.  All rights reserved.
# See LICENSE
##############################################################################

"""
Met en place une base pour les tests unitaires, à partir d'une description
YAML qui peut donner la formation, son ref. compétences, les formsemestres,
les étudiants et leurs notes et décisions de jury.

Le traitement est effectué dans l'ordre suivant:

setup_from_yaml()

    - setup_formation():
        - import de la formation (le test utilise une seule formation)
    - associe_ues_et_parcours():
        - crée les associations formation <-> référentiel de compétence
    - setup_formsemestre()
        - crée les formsemestres décrits dans le YAML
        avec tous les modules du semestre ou des parcours si indiqués
        et une évaluation dans chaque moduleimpl.
    - inscrit_les_etudiants()
        - inscrit et place dans les groupes de parcours
    - note_les_modules()
        - saisie de toutes les notes indiquées dans le YAML

check_deca_fields()
    - vérifie les champs du deca (nb UE, compétences, ...) mais pas les décisions de jury.

formsemestre_validation_auto_but(only_adm=False)
    - enregistre toutes les décisions "par défaut" proposées (pas seulement les ADM)

test_but_jury()
    - compare décisions attendues indiquées dans le YAML avec celles de ScoDoc
     et enregistre immédiatement APRES la décision manuelle indiquée par `decision_jury`
     dans le YAML.


Les tests unitaires associés sont généralement lents (construction de la base),
et donc marqués par `@pytest.mark.slow`.
"""

import os

import yaml
from flask import g

from app import db

from app.auth.models import User

from app.models import (
    ApcParcours,
    DispenseUE,
    Evaluation,
    Formation,
    FormSemestre,
    Identite,
    Module,
    ModuleImpl,
    UniteEns,
)

from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
from app.scodoc import sco_saisie_notes
from app.scodoc import sco_utils as scu

from tests.conftest import RESOURCES_DIR
from tests.unit import yaml_setup_but


def setup_formation(formation_infos: dict) -> Formation:
    """Importe la formation, qui est lue à partir du fichier XML
    formation_infos["filename"].
    La formation peut être quelconque, on vérifie juste qu'elle est bien créée.
    """
    # Lecture fichier XML local:
    with open(
        os.path.join(RESOURCES_DIR, "formations", formation_infos["filename"]),
        encoding="utf-8",
    ) as f:
        doc = f.read()

    # --- Création de la formation
    formation_id, _, _ = sco_formations.formation_import_xml(doc)
    formation: Formation = db.session.get(Formation, formation_id)
    assert formation
    return formation


def create_formsemestre(
    formation: Formation,
    parcours: list[ApcParcours],
    semestre_id: int,
    titre: str,
    date_debut: str,
    date_fin: str,
    elt_sem_apo: str = None,
    elt_annee_apo: str = None,
    etape_apo: str = None,
) -> FormSemestre:
    "Création d'un formsemestre, avec ses modimpls et évaluations"
    assert formation.is_apc() or not parcours  # parcours seulement si APC
    formsemestre = FormSemestre(
        formation=formation,
        parcours=parcours,
        dept_id=g.scodoc_dept_id,
        titre=titre,
        semestre_id=semestre_id,
        date_debut=date_debut,
        date_fin=date_fin,
        elt_sem_apo=elt_sem_apo,
        elt_annee_apo=elt_annee_apo,
    )
    db.session.add(formsemestre)
    db.session.flush()
    # set responsable (list)
    a_user = User.query.first()
    formsemestre.responsables = [a_user]
    formsemestre.add_etape(etape_apo)
    # Ajoute tous les modules du semestre sans parcours OU avec l'un des parcours indiqués
    sem_parcours_ids = {p.id for p in parcours}
    modules = [
        m
        for m in formsemestre.formation.modules.filter_by(semestre_id=semestre_id)
        if not m.parcours  # module de tronc commun
        or (not sem_parcours_ids)  # semestre sans parcours => tous
        or ({p.id for p in m.parcours} & sem_parcours_ids)
    ]
    for module in modules:
        modimpl = ModuleImpl(module=module, responsable_id=a_user.id)
        db.session.add(modimpl)
        formsemestre.modimpls.append(modimpl)

    # Crée une évaluation dans chaque module
    create_evaluations(formsemestre)

    # Partition par défaut:
    db.session.commit()
    partition_id = sco_groups.partition_create(
        formsemestre.id, default=True, redirect=False
    )
    _ = sco_groups.create_group(partition_id, default=True)

    # Partition de parcours:
    if formsemestre.formation.is_apc():
        formsemestre.setup_parcours_groups()

    return formsemestre


def create_evaluations(formsemestre: FormSemestre, publish_incomplete=True):
    """Crée une évaluation dans chaque module du semestre"""
    for modimpl in formsemestre.modimpls:
        evaluation = Evaluation(
            moduleimpl=modimpl,
            date_debut=formsemestre.date_debut,
            description=f"Exam {modimpl.module.titre}",
            coefficient=1.0,
            note_max=20.0,
            numero=1,
            publish_incomplete=publish_incomplete,
        )
        db.session.add(evaluation)

        if formsemestre.formation.is_apc():
            # Affecte les mêmes poids que les coefs APC:
            ue_coef_dict = modimpl.module.get_ue_coef_dict()  # { ue_id : coef }
            evaluation.set_ue_poids_dict(ue_coef_dict)


def note_les_modules(doc: dict, formsemestre_titre: str = ""):
    """Saisie les notes des étudiants pour ce formsemestre
    doc : données YAML
    """
    a_user = User.query.first()
    formsemestre: FormSemestre = FormSemestre.query.filter_by(
        titre=formsemestre_titre
    ).first()
    assert formsemestre is not None
    for nom, infos in doc["Etudiants"].items():
        etud: Identite = Identite.query.filter_by(nom=nom).first()
        assert etud is not None
        sem_infos = infos["formsemestres"].get(formsemestre_titre)
        if sem_infos:
            for code_module, note in sem_infos.get("notes_modules", {}).items():
                modimpl = (
                    formsemestre.modimpls.join(Module)
                    .filter_by(code=code_module)
                    .first()
                )
                # le sem. doit avoir un module du code indiqué:
                if modimpl is None:
                    raise ValueError(f"module de code '{code_module}' introuvable")
                for evaluation in modimpl.evaluations:
                    # s'il y a plusieurs evals, affecte la même note à chacune
                    note_value, invalid = sco_saisie_notes.convert_note_from_string(
                        str(note),
                        20.0,
                        note_min=scu.NOTES_MIN,
                        etudid=etud.id,
                        absents=[],
                        tosuppress=[],
                        invalids=[],
                    )
                    assert not invalid  # valeur note invalide
                    assert isinstance(note_value, float)
                    sco_saisie_notes.notes_add(
                        a_user,
                        evaluation.id,
                        [(etud.id, note_value)],
                        comment="note_les_modules",
                    )


def setup_formsemestre(
    formation: Formation, doc: str, formsemestre_titre: str = ""
) -> FormSemestre:
    """Création du formsemestre de titre indiqué.
    Le cas échéant associé à son parcours.
    """
    infos = doc["FormSemestres"][formsemestre_titre]

    codes_parcours = infos.get("codes_parcours", [])
    assert formation.is_apc() or not codes_parcours  # parcours seulement en APC
    parcours = []
    for code_parcour in codes_parcours:
        parcour = formation.referentiel_competence.parcours.filter_by(
            code=code_parcour
        ).first()
        assert parcour is not None
        parcours.append(parcour)

    elt_sem_apo = infos.get("elt_sem_apo")
    elt_annee_apo = infos.get("elt_annee_apo")
    etape_apo = infos.get("etape_apo")

    formsemestre = create_formsemestre(
        formation,
        parcours,
        infos["idx"],
        formsemestre_titre,
        infos["date_debut"],
        infos["date_fin"],
        elt_sem_apo=elt_sem_apo,
        elt_annee_apo=elt_annee_apo,
        etape_apo=etape_apo,
    )

    db.session.flush()
    return formsemestre


def inscrit_les_etudiants(doc: dict, formsemestre_titre: str = ""):
    """Inscrit les étudiants dans chacun de leurs formsemestres"""
    etudiants = doc.get("Etudiants")
    if not etudiants:
        return
    formsemestre: FormSemestre = FormSemestre.query.filter_by(
        titre=formsemestre_titre
    ).first()
    assert formsemestre is not None
    partition_parcours = formsemestre.partitions.filter_by(
        partition_name=scu.PARTITION_PARCOURS
    ).first()
    for nom, infos in etudiants.items():
        # Création des étudiants (sauf si déjà existants)
        prenom = infos.get("prenom", "prénom")
        civilite = infos.get("civilite", "X")
        code_nip = infos.get("code_nip", None)
        etud = Identite.query.filter_by(
            nom=nom, prenom=prenom, civilite=civilite
        ).first()
        if etud is None:
            etud = Identite.create_etud(
                dept_id=g.scodoc_dept_id,
                nom=nom,
                prenom=prenom,
                civilite=civilite,
                code_nip=code_nip,
            )
            db.session.add(etud)
            db.session.commit()

        # L'inscrire au formsemestre
        sem_infos = infos["formsemestres"].get(formsemestre_titre)
        if sem_infos:
            if partition_parcours is not None and "parcours" in sem_infos:
                group = partition_parcours.groups.filter_by(
                    group_name=sem_infos["parcours"]
                ).first()
                assert group is not None  # le groupe de parcours doit exister
                group_ids = [group.id]
            else:
                group_ids = []
            # Génère des dispenses d'UEs
            if "dispense_ues" in sem_infos:
                etud_dispense_ues(formsemestre, etud, sem_infos["dispense_ues"])
            sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
                formsemestre.id,
                etud.id,
                group_ids=group_ids,
                etat=scu.INSCRIT,
                method="tests/unit/inscrit_les_etudiants",
            )
            # Met à jour les inscriptions:
            formsemestre.update_inscriptions_parcours_from_groups()


def etud_dispense_ues(
    formsemestre: FormSemestre, etud: Identite, ue_acronymes: list[str]
):
    """Génère des dispenses d'UE"""
    for ue_acronyme in set(ue_acronymes):
        ue: UniteEns = formsemestre.formation.ues.filter_by(
            acronyme=ue_acronyme
        ).first()
        assert ue
        disp = DispenseUE(formsemestre_id=formsemestre.id, ue_id=ue.id, etudid=etud.id)
        db.session.add(disp)


def setup_from_yaml(filename: str) -> tuple[dict, Formation, list[str]]:
    """Lit le fichier yaml et construit l'ensemble des objets"""
    with open(filename, encoding="utf-8") as f:
        doc = yaml.safe_load(f.read())

    # Passe tous les noms en majuscules (car stockés ainsi en base)
    if "Etudiants" in doc:
        doc["Etudiants"] = {k.upper(): v for (k, v) in doc["Etudiants"].items()}
        for e in doc["Etudiants"].values():
            if "prenom" in e:
                e["prenom"] = e["prenom"].upper()

    # Charge de ref. comp. avant la formation, de façon à pouvoir
    # re-créer les associations UE/Niveaux
    yaml_setup_but.setup_formation_referentiel(doc.get("ReferentielCompetences", {}))
    formation = setup_formation(doc["Formation"])

    yaml_setup_but.associe_ues_et_parcours(formation, doc["Formation"])

    formsemestre_titres = list(doc["FormSemestres"].keys())
    return doc, formation, formsemestre_titres


def create_formsemestre_with_etuds(
    doc: dict, formation: Formation, formsemestre_titre: str
) -> FormSemestre:
    """Création formsemestre de titre indiqué, puis inscrit ses étudianst et les note"""
    formsemestre = setup_formsemestre(
        formation, doc, formsemestre_titre=formsemestre_titre
    )
    etudiants = doc.get("Etudiants")
    if etudiants:
        inscrit_les_etudiants(doc, formsemestre_titre=formsemestre_titre)
        note_les_modules(doc, formsemestre_titre=formsemestre_titre)
    return formsemestre