from typing import Callable, List, Optional, Tuple, Union
from unittest import mock

from django.test import TestCase

from ...models import Administre
from ...models import AvisPosteChoices as AvisPoste
from ...models import CustomUser, Decision, DecisionChoices, Poste, SousVivier
from ...models import StatutPamChoices as StatutPam
from ...utils.decisions import (DECISIONS, KEY_CHOICES, KEY_CREATE,
                                KEY_PROFILES, KEY_UPDATE, ExtraDecisions,
                                get_all_decisions, get_available_decisions)
from ...utils.permissions import (KEY_READ, KEY_WRITE, Profiles,
                                  get_profiles_by_adm)


class DecisionsTest(TestCase):

    def setUp(self):
        """ vérifie qu'il n'existe aucun administré ou poste avant le test """

        self.assertEqual(Administre.objects.exists(), False, "pas d'administré avant un test")
        self.assertEqual(Poste.objects.exists(), False, "pas de poste avant un test")

    def tearDown(self):
        """ supprime tous les administrés, les postes, les décisions en fin de test """

        Decision.objects.all().delete()
        Poste.objects.all().delete()
        Administre.objects.all().delete()

    def __load_administres(self) -> List[Administre]:
        """ charge les administrés de la base à partir des IDs """

        self.assertTrue(self.adm_ids, "il est nécessaire de renseigner des IDs d'administrés pour le chargement")
        return list(Administre.objects.filter(pk__in=self.adm_ids or ()))

    def __setup_decisions(self, decision: Optional[Union[DecisionChoices, ExtraDecisions]]) -> None:
        """ supprime et recrée (si nécessaire) les postes et les décisions des administrés """

        Decision.objects.all().delete()
        Poste.objects.all().delete()
        if decision and decision != ExtraDecisions.EMPTY:
            for adm_id in self.adm_ids:
                poste = Poste.objects.create(pk=str(adm_id), p_avis=AvisPoste.P1)
                Decision.objects.create(administre_id=adm_id, poste=poste, de_decision=decision)

    def __do_for_profiles(self, action: Callable[[Profiles], None], profiles: Tuple[Profiles], decisions: Tuple[Union[DecisionChoices, ExtraDecisions]] = ()) -> None:
        """
        exécute une action (un test) avec un sous-test :
        - soit pour chaque profil (utilise un mock)
        - soit pour chaque profil et chaque statut de décision (utilise un mock)
        implicite : self.adm_ids
        """

        def run_subTest(profile: Profiles, decision: Optional[Union[DecisionChoices, ExtraDecisions]] = None) -> None:
            with self.subTest(profile=profile.name, **({'decision': decision.name} if decision else {})):
                @mock.patch(
                    f'{get_available_decisions.__module__}.{get_profiles_by_adm.__name__}',
                    return_value={adm_id: {KEY_READ: (), KEY_WRITE: (profile,)} for adm_id in self.adm_ids}
                )
                def do_with_profile(mock):
                    action(profile, decision)
                do_with_profile()

        for p in profiles:
            if decisions:
                for decision in decisions:
                    self.__setup_decisions(decision)
                    run_subTest(p, decision)
            else:
                run_subTest(p)

    def __assert_common(self, decisions_by_adm):
        """
        assertions communes à propos des décisions renvoyées pour les administrés
        implicite : self.adm_ids
        """

        self.assertIsInstance(decisions_by_adm, dict, "le résultat n'est jamais None")
        for adm_id in self.adm_ids:
            self.assertTrue(adm_id in decisions_by_adm, "tous les administrés doivent être présents dans le résultat")

            decisions = decisions_by_adm.get(adm_id)
            self.assertIsInstance(decisions, dict, "le format des décisions est un dictionnaire")
            for key in (KEY_CREATE, KEY_UPDATE):
                self.assertTrue(key in decisions, f"les décisions doivent contenir la clé {key}")
                self.assertIsInstance(decisions.get(key), tuple, f"la valeur de {key} des décisions n'a pas le type attendu")

    def test_tree_attribute(self):
        """ vérifie que le comportement reste correct avec l'ajout de nouveaux attributs """

        attr_trees = 'trees'
        for choice in DecisionChoices:
            self.assertIsInstance(choice.label, str, f"{choice} : le libellé n'a pas le bon type")
            self.assertIsInstance(choice.value, str, f"{choice} : la valeur n'a pas le bon type")
            self.assertEqual(choice.value, choice, f"{choice} : n'est pas égal à sa valeur")
            self.assertIsInstance(getattr(choice, attr_trees), tuple, f"{choice} : l'attribut {attr_trees} n'a pas le bon type")

    def test_champ_decisions(self):
        """ vérifie la structure de DECISIONS """

        to_test = DECISIONS

        self.assertIsInstance(to_test, dict, "la structure n'a pas le type attendu")
        self.assertTrue(to_test, "la structure ne doit pas être vide")

        for key, value in to_test.items():
            self.assertTrue(key is None or isinstance(key, DecisionChoices), f"la décision {key} n'a pas le type attendu")
            self.assertIsInstance(value, dict, f"la valeur pour la décision {key} n'a pas le type attendu")

            profiles = value.get(KEY_PROFILES)
            self.assertTrue(profiles is None or isinstance(profiles, tuple), f"la valeur de {KEY_PROFILES} de la valeur pour la décision {key} n'a pas le type attendu")
            for p in profiles:
                self.assertIsInstance(p, Profiles, f"un élément de la clé {KEY_PROFILES} de la valeur pour la décision {key} n'a pas le type attendu")

            choices = value.get(KEY_CHOICES)
            self.assertTrue(choices is None or isinstance(choices, tuple), f"la valeur de {KEY_CHOICES} de la valeur pour la décision {key} n'a pas le type attendu")
            for c in choices:
                self.assertIsInstance(c, (DecisionChoices, ExtraDecisions), f"un élément de la clé {KEY_CHOICES} de la valeur pour la décision {key} n'a pas le type attendu")

    def test_get_all_decisions(self):
        """ vérifie les valeurs de get_all_decisions """

        to_test = get_all_decisions()

        self.assertIsInstance(to_test, tuple, "le résultat n'a pas le type attendu")
        self.assertTrue(to_test, "le résultat ne doit pas être vide")

        for value in to_test:
            self.assertIsInstance(value, DecisionChoices, f"la valeur {value} n'a pas le type attendu")

    def test_pam_wihout_choice(self):
        """ pour ces statuts PAM il n'y a aucun choix """

        user = CustomUser()
        self.adm_ids = tuple(a.a_id_sap for a in (
            Administre.objects.create(pk=i + 1, a_statut_pam=status) for i, status in enumerate(StatutPam) if not status.dec_enabled
        ))

        def action(_p, _d):
            decisions_by_adm = get_available_decisions(self.__load_administres(), user=user)

            self.__assert_common(decisions_by_adm)
            for adm_id in self.adm_ids:
                decisions = decisions_by_adm.get(adm_id)
                self.assertFalse(decisions.get(KEY_CREATE))
                self.assertFalse(decisions.get(KEY_UPDATE))

        self.__do_for_profiles(action, profiles=tuple(Profiles))

    def test_pam_with_choices(self):
        """ pour ces statuts PAM c'est l'arbre de décision qui est utilisé """

        user = CustomUser()
        self.adm_ids = tuple(a.a_id_sap for a in (
            Administre.objects.create(pk=i + 1, a_statut_pam=status) for i, status in enumerate(StatutPam) if status.dec_enabled
        ))
        D = DecisionChoices
        EX = ExtraDecisions

        def action(p, d):
            decisions_by_adm = get_available_decisions(self.__load_administres(), user=user)

            self.__assert_common(decisions_by_adm)
            for adm_id in self.adm_ids:
                decisions = decisions_by_adm.get(adm_id)

                # create
                decisions_create = decisions.get(KEY_CREATE)
                if p == Profiles.FILIERE:
                    self.assertCountEqual(decisions_create, (D.PROPOSITION_FE, D.HME_PROPOSITION_VIVIER))
                else:
                    self.assertFalse(decisions_create)

                # update
                decisions_update = decisions.get(KEY_UPDATE)
                if not d and p == Profiles.FILIERE:
                    self.assertCountEqual(decisions_update, (D.PROPOSITION_FE, D.HME_PROPOSITION_VIVIER))

                elif d == D.PROPOSITION_FE and p in (Profiles.PCP, Profiles.PCP_ACTUEL):
                    self.assertCountEqual(decisions_update, (D.DIALOGUE_EN_COURS, D.FOREMP_EN_COURS))

                elif d == D.DIALOGUE_EN_COURS and p in (Profiles.PCP, Profiles.PCP_ACTUEL):
                    self.assertCountEqual(decisions_update, (D.DIALOGUE_TERMINE, D.DIALOGUE_INFRUCTUEUX))

                elif d == D.DIALOGUE_TERMINE and p in (Profiles.PCP, Profiles.PCP_ACTUEL):
                    self.assertCountEqual(decisions_update, (D.FOREMP_EN_COURS,))

                elif d == D.DIALOGUE_INFRUCTUEUX and p in (Profiles.PCP, Profiles.PCP_ACTUEL):
                    self.assertCountEqual(decisions_update, (D.FOREMP_EN_COURS, D.REMIS_A_DISPOSITION))

                elif d == D.FOREMP_EN_COURS and p in (Profiles.PCP, Profiles.PCP_ACTUEL):
                    self.assertCountEqual(decisions_update, (D.FOREMP_TERMINE,))

                elif d == D.FOREMP_TERMINE and p in (Profiles.PCP, Profiles.PCP_ACTUEL):
                    self.assertCountEqual(decisions_update, (D.PREPOSITIONNE, D.REMIS_A_DISPOSITION))

                elif d == D.PREPOSITIONNE and p in (Profiles.PCP, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.POSITIONNE, D.REMIS_A_DISPOSITION))

                elif d == D.POSITIONNE and p in (Profiles.PCP, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.OMIP_EN_COURS, D.OMI_EN_COURS, D.REMIS_A_DISPOSITION))

                elif d == D.OMIP_EN_COURS and p in (Profiles.PCP, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.OMIP_TERMINE, D.REMIS_A_DISPOSITION))

                elif d == D.OMIP_TERMINE and p in (Profiles.PCP, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.ATTENTE_AVIONAGE, D.OMI_EN_COURS, D.REMIS_A_DISPOSITION))

                elif d == D.ATTENTE_AVIONAGE and p in (Profiles.PCP, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.OMI_EN_COURS,))

                elif d == D.OMI_EN_COURS and p in (Profiles.PCP, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.OMI_ACTIVE, D.REMIS_A_DISPOSITION))

                elif d == D.OMI_ACTIVE and p in (Profiles.PCP, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.OMI_ANNULE,))

                elif d == D.OMI_ANNULE and p in (Profiles.PCP, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.REMIS_A_DISPOSITION,))

                elif d == D.REMIS_A_DISPOSITION and p == Profiles.FILIERE:
                    self.assertCountEqual(decisions_update, (EX.EMPTY,))

                elif d == D.HME_PROPOSITION_VIVIER and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.HME_DIALOGUE_INITIE,))

                elif d == D.HME_ETUDE_DESISTEMENT and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.HME_DESISTEMENT,))

                elif d == D.HME_DESISTEMENT and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.REMIS_A_DISPOSITION,))

                elif d == D.HME_DIALOGUE_INITIE and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.HME_DIALOGUE_EN_COURS,))

                elif d == D.HME_DIALOGUE_EN_COURS and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.HME_DIALOGUE_TERMINE, D.HME_DIALOGUE_INFRUCTUEUX))

                elif d == D.HME_DIALOGUE_TERMINE and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.HME_PREPOSITIONNE,))

                elif d == D.HME_DIALOGUE_INFRUCTUEUX and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.HME_ETUDE_DESISTEMENT, D.HME_PREPOSITIONNE, D.REMIS_A_DISPOSITION))

                elif d == D.HME_FOREMP_EN_COURS and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.HME_FOREMP_TERMINE, D.HME_OMI_EN_COURS))

                elif d == D.HME_FOREMP_TERMINE and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.HME_OMIP_EN_COURS, D.HME_OMI_EN_COURS))

                elif d == D.HME_PREPOSITIONNE and p == Profiles.HME:
                    self.assertCountEqual(decisions_update, (D.HME_VALIDATION_EXPERT, D.HME_REFUS_EXPERT))

                elif d == D.HME_POSITIONNE and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.HME_OMIP_EN_COURS, D.HME_OMI_EN_COURS, D.HME_FOREMP_EN_COURS))

                elif d == D.HME_VALIDATION_EXPERT and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.HME_POSITIONNE,))

                elif d == D.HME_REFUS_EXPERT and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.HME_PREPOSITIONNE, D.REMIS_A_DISPOSITION))

                elif d == D.HME_OMIP_EN_COURS and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.HME_OMIP_TERMINE,))

                elif d == D.HME_OMIP_TERMINE and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.HME_ATTENTE_AVIONAGE, D.HME_OMI_EN_COURS))

                elif d == D.HME_ATTENTE_AVIONAGE and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.HME_OMI_EN_COURS,))

                elif d == D.HME_OMI_EN_COURS and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, (D.HME_OMI_ACTIVE,))

                elif d == D.HME_OMI_ACTIVE and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
                    self.assertCountEqual(decisions_update, ())

                else:
                    self.assertFalse(decisions_update)

        self.__do_for_profiles(action, profiles=tuple(Profiles), decisions=(EX.EMPTY, *get_all_decisions()))
