##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2023 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_formsemestres()
        - 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 = Formation.query.get(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,
) -> 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,
    )
    # set responsable (list)
    a_user = User.query.first()
    formsemestre.responsables = [a_user]
    db.session.add(formsemestre)
    # 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,
            jour=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):
    """Saisie les notes des étudiants
    doc : données YAML
    """
    a_user = User.query.first()
    for nom, infos in doc["Etudiants"].items():
        etud: Identite = Identite.query.filter_by(nom=nom).first()
        assert etud is not None
        for titre, sem_infos in infos["formsemestres"].items():
            formsemestre: FormSemestre = FormSemestre.query.filter_by(
                titre=titre
            ).first()
            assert formsemestre is not None
            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_formsemestres(formation: Formation, doc: str):
    """Création des formsemestres
    Le cas échéant associés à leur(s) parcours.
    """
    for titre, infos in doc["FormSemestres"].items():
        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)

        _ = create_formsemestre(
            formation,
            parcours,
            infos["idx"],
            titre,
            infos["date_debut"],
            infos["date_fin"],
        )

    db.session.flush()
    assert FormSemestre.query.count() == len(doc["FormSemestres"])


def inscrit_les_etudiants(formation: Formation, doc: dict):
    """Inscrit les étudiants dans chacun de leurs formsemestres"""
    etudiants = doc.get("Etudiants")
    if not etudiants:
        return
    for nom, infos in etudiants.items():
        etud = Identite.create_etud(
            dept_id=g.scodoc_dept_id,
            nom=nom,
            prenom=infos.get("prenom", "prénom"),
            civilite=infos.get("civilite", "X"),
        )
        db.session.add(etud)
        db.session.commit()
        # L'inscrire à ses formsemestres
        for titre, sem_infos in infos["formsemestres"].items():
            formsemestre: FormSemestre = FormSemestre.query.filter_by(
                titre=titre
            ).first()
            assert formsemestre is not None
            partition_parcours = formsemestre.partitions.filter_by(
                partition_name=scu.PARTITION_PARCOURS
            ).first()
            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:
    for formsemestre in formation.formsemestres:
        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) -> dict:
    """Lit le fichier yaml et construit l'ensemble des objets"""
    with open(filename, encoding="utf-8") as f:
        doc = yaml.safe_load(f.read())

    # 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"])
    setup_formsemestres(formation, doc)
    etudiants = doc.get("Etudiants")
    if etudiants:
        inscrit_les_etudiants(formation, doc)
        note_les_modules(doc)
    return doc