import datetime
import time
import logging

from typing import Callable, Tuple, Optional

import pytest

from django.contrib.auth import get_user_model
from django.contrib.auth.backends import UserModel
from django.utils.timezone import make_aware
from rest_framework.authtoken.admin import User
from rest_framework.test import APIClient

from greg.models import (
    ConsentType,
    ConsentChoice,
    Identity,
    Invitation,
    InvitationLink,
    OrganizationalUnit,
    Person,
    Role,
    RoleType,
    Sponsor,
)

from gregui.models import EmailTemplate, GregUserProfile

# faker spams the logs with localisation warnings
# see https://github.com/joke2k/faker/issues/753
logging.getLogger("faker").setLevel(logging.ERROR)


# OIDC stuff
@pytest.fixture
def claims():
    return {
        "sub": "subsub",
        "connect-userid_sec": ["feide:frank_foreleser@spusers.feide.no"],
        "dataporten-userid_sec": [
            # "feide:frank_foreleser@spusers.feide.no"
        ],
        "name": "Frank Foreleser Føllesen",
        "email": "noreply@feide.no",
        "email_verified": True,
        "picture": "https://api.dataporten.no/userinfo/v1/user/media/p:2192dff7-6989-4244-83cc-ae5e78875bdd",
    }


@pytest.fixture
def id_token_payload():
    return {
        "iss": "https://auth.dataporten.no",
        "jti": "jtijti",
        "aud": "lalalalala",
        "sub": "subsub",
        "iat": 1605174731,
        "exp": 1605178331,
        "auth_time": 1605174731,
        "nonce": "noncenonce",
    }


@pytest.fixture
def client() -> APIClient:
    client = APIClient()
    return client


@pytest.fixture
def unit_foo() -> OrganizationalUnit:
    ou = OrganizationalUnit.objects.create(name_en="Foo EN", name_nb="Foo NB")
    return OrganizationalUnit.objects.get(id=ou.id)


@pytest.fixture
def role_type_foo() -> RoleType:
    rt = RoleType.objects.create(
        identifier="role_foo", name_en="Role Foo EN", name_nb="Role Foo NB"
    )
    return RoleType.objects.get(id=rt.id)


@pytest.fixture
def create_sponsor() -> Callable[[str, str, str, OrganizationalUnit], Sponsor]:
    def create_sponsor(feide_id, first_name, last_name, unit, work_email=None):
        sponsor = Sponsor(
            feide_id=feide_id,
            first_name=first_name,
            last_name=last_name,
            work_email=work_email,
        )
        sponsor.save()

        sponsor.units.add(unit, through_defaults={"hierarchical_access": False})
        sponsor.save()
        return Sponsor.objects.get(id=sponsor.id)

    return create_sponsor


@pytest.fixture
def sponsor_foo_data() -> dict:
    return dict(
        feide_id="foo@example.org",
        first_name="Sponsor",
        last_name="Bar",
        work_email="foo@example.org",
    )


@pytest.fixture
def sponsor_foo(
    unit_foo: OrganizationalUnit, sponsor_foo_data, create_sponsor
) -> Sponsor:
    return create_sponsor(**sponsor_foo_data, unit=unit_foo)


@pytest.fixture
def sponsor_bar(unit_foo: OrganizationalUnit, create_sponsor) -> Sponsor:
    return create_sponsor(
        feide_id="bar@example.com", first_name="Bar", last_name="Baz", unit=unit_foo
    )


@pytest.fixture
def create_user() -> Callable[[str, str, str, str], UserModel]:
    user_model = get_user_model()

    def create_user(
        username: str, first_name: str = "", last_name: str = "", email: str = ""
    ):
        user = user_model.objects.create(
            username=username,
            email=email,
            first_name=first_name,
            last_name=last_name,
        )
        return user_model.objects.get(id=user.id)

    return create_user


@pytest.fixture
def user_sponsor(sponsor_foo: Sponsor, create_user) -> User:
    user_model = get_user_model()

    # Create a user and link him to a sponsor
    user = create_user(
        username="test_sponsor",
        email="test@example.org",
        first_name="Test",
        last_name="Sponsor",
    )
    GregUserProfile.objects.create(user=user, sponsor=sponsor_foo)

    # This user is a sponsor for unit_foo
    return user_model.objects.get(id=user.id)


@pytest.fixture
def user_person(person_foo: Sponsor, create_user) -> User:
    user_model = get_user_model()

    # Create a user and link him to a sponsor
    user = create_user(
        username="test_person",
        email="person@example.org",
        first_name="Test",
        last_name="Person",
    )
    GregUserProfile.objects.create(user=user, person=person_foo)

    # This user is a sponsor for unit_foo
    return user_model.objects.get(id=user.id)


@pytest.fixture
def create_greg_user_profile() -> Callable[
    [UserModel, Optional[Person], Optional[Sponsor]], GregUserProfile
]:
    user_model = get_user_model()

    def create_greg_user_profile(
        user: user_model,
        person: Optional[Person] = None,
        sponsor: Optional[Sponsor] = None,
    ):
        user_profile = GregUserProfile(
            user=user,
            person=person,
            sponsor=sponsor,
        )
        return GregUserProfile.objects.get(id=user_profile.id)

    return create_greg_user_profile


@pytest.fixture
def create_role() -> Callable[[Person, Sponsor, OrganizationalUnit, RoleType], Role]:
    """Returns a function for creating roles."""

    def create_role(
        person: Person, sponsor: Sponsor, unit: OrganizationalUnit, role_type: RoleType
    ) -> Role:
        role = Role(
            person=person,
            sponsor=sponsor,
            orgunit=unit,
            end_date="2050-10-15",
            type=role_type,
        )
        role.save()
        return Role.objects.get(id=role.id)

    return create_role


@pytest.fixture
def role(person_invited, sponsor_foo, unit_foo, role_type_foo, create_role) -> Role:
    return create_role(person_invited, sponsor_foo, unit_foo, role_type_foo)


@pytest.fixture
def create_invitation() -> Callable[[Role], Invitation]:
    """Returns a function for creating invitations."""

    def create_invitation(role: Role) -> Invitation:
        invitation = Invitation(role=role)
        invitation.save()
        return Invitation.objects.get(id=invitation.id)

    return create_invitation


@pytest.fixture
def invitation(role, create_invitation) -> Invitation:
    return create_invitation(role)


@pytest.fixture
def invitation_valid_date() -> datetime.datetime:
    return make_aware(datetime.datetime(2060, 10, 15))


@pytest.fixture
def invitation_expired_date() -> datetime.datetime:
    return make_aware(datetime.datetime(1970, 1, 1))


@pytest.fixture
def create_invitation_link(
    invitation_valid_date,
) -> Callable[[Invitation, datetime.datetime], InvitationLink]:
    """Returns a function for creating invitation links."""

    def create_invitation_link(
        invitation: Invitation, expire_date: datetime.datetime = invitation_valid_date
    ) -> InvitationLink:

        invitation_link = InvitationLink(
            invitation=invitation,
            expire=expire_date,
        )
        invitation_link.save()
        return InvitationLink.objects.get(id=invitation_link.id)

    return create_invitation_link


@pytest.fixture
def invitation_link(
    invitation, invitation_valid_date, create_invitation_link
) -> InvitationLink:
    return create_invitation_link(
        invitation=invitation,
        expire_date=invitation_valid_date,
    )


@pytest.fixture
def invitation_link_expired(
    invitation, invitation_expired_date, create_invitation_link
) -> InvitationLink:
    return create_invitation_link(
        invitation=invitation,
        expire_date=invitation_expired_date,
    )


@pytest.fixture
def create_person() -> Callable[[str, str, str, Optional[str], Optional[str]], Person]:
    # TODO fix the typing...
    def create_person(
        first_name: str,
        last_name: str,
        email: str = None,
        nin: str = None,
        feide_id: str = None,
    ) -> Person:
        person = Person.objects.create(
            first_name=first_name,
            last_name=last_name,
        )

        if nin:
            Identity.objects.create(
                type=Identity.IdentityType.NORWEGIAN_NATIONAL_ID_NUMBER,
                value=nin,
                person=person,
            )
        if feide_id:
            Identity.objects.create(
                type=Identity.IdentityType.FEIDE_ID,
                value=feide_id,
                person=person,
            )
        if email:
            Identity.objects.create(
                type=Identity.IdentityType.PRIVATE_EMAIL,
                value=email,
                person=person,
            )
        return Person.objects.get(id=person.id)

    return create_person


@pytest.fixture
def person_foo_data() -> dict:
    return dict(
        first_name="Foo",
        last_name="Bar",
        email="foo@bar.com",
        feide_id="bar@baz.org",
        nin="12345612345",
    )


@pytest.fixture
def person_foo(create_person) -> Person:
    person = create_person(
        first_name="Foo",
        last_name="Bar",
        email="foo@bar.com",
        feide_id="bar@baz.org",
        nin="12345612345",
    )
    return Person.objects.get(id=person.id)


@pytest.fixture
def person_invited(create_person) -> Person:
    """Invited person before registration."""
    person = create_person(first_name="Foo", last_name="Bar", email="foo@example.org")
    return Person.objects.get(id=person.id)


@pytest.fixture
def invited_person(
    create_role,
    create_invitation,
    create_invitation_link,
    person_invited,
    sponsor_foo,
    unit_foo,
    role_type_foo,
) -> Tuple[Person, InvitationLink]:
    """
    Invited person
    """

    role = create_role(
        person=person_invited,
        sponsor=sponsor_foo,
        unit=unit_foo,
        role_type=role_type_foo,
    )

    invitation = create_invitation(role=role)
    invitation_link = create_invitation_link(invitation=invitation)

    return Person.objects.get(id=person_invited.id), InvitationLink.objects.get(
        id=invitation_link.id
    )


@pytest.fixture
def invited_person_no_ids(
    create_person,
    create_role,
    create_invitation,
    create_invitation_link,
    sponsor_foo,
    unit_foo,
    role_type_foo,
) -> Tuple[Person, InvitationLink]:
    """
    Invited person, with no ids.

    """
    person = create_person(
        first_name="foo",
        last_name="bar",
        email="foo@bar.com",
    )

    role = create_role(
        person=person, sponsor=sponsor_foo, unit=unit_foo, role_type=role_type_foo
    )

    invitation = create_invitation(role=role)
    invitation_link = create_invitation_link(invitation=invitation)
    return Person.objects.get(id=person.id), InvitationLink.objects.get(
        id=invitation_link.id
    )


@pytest.fixture
def invited_person_verified_nin(
    create_person,
    create_role,
    create_invitation,
    create_invitation_link,
    sponsor_foo,
    unit_foo,
    role_type_foo,
) -> Tuple[Person, InvitationLink]:
    """
    Invited person, with a verified NIN.
    """
    person = create_person(
        first_name="Victor",
        last_name="Verified",
        email="foo@bar2.com",
        nin="12345678912",
    )
    fnr = person.identities.get(type=Identity.IdentityType.NORWEGIAN_NATIONAL_ID_NUMBER)
    fnr.verified = Identity.Verified.AUTOMATIC
    fnr.save()

    role = create_role(
        person=person, sponsor=sponsor_foo, unit=unit_foo, role_type=role_type_foo
    )

    invitation = create_invitation(role=role)
    invitation_link = create_invitation_link(invitation=invitation)
    return Person.objects.get(id=person.id), InvitationLink.objects.get(
        id=invitation_link.id
    )


@pytest.fixture
def log_in(client) -> Callable[[UserModel], APIClient]:
    def _log_in(user):
        client.force_login(user=user)
        # It seems like the session was not updated automatically this way
        session = client.session
        session["oidc_id_token_payload"] = {"iat": time.time()}
        session.save()
        return client

    return _log_in


@pytest.fixture
def confirmation_template():
    et = EmailTemplate.objects.create(
        template_key=EmailTemplate.EmailType.SPONSOR_CONFIRMATION,
        subject="confirmation subject",
        body="""Dette er en automatisk generert melding fra gjesteregistreringstjenesten.
Din gjest, {{ guest }}, har fullført registrering, bekreft gjesten her: {{ confirmation_link }}

This message has been automatically generated by the guest registration system.
Your guest, {{ guest }}, has completed their registration, please confirm the guest here: {{ confirmation_link }}""",
    )
    return EmailTemplate.objects.get(id=et.id)


@pytest.fixture
def registration_template():
    et = EmailTemplate.objects.create(
        template_key=EmailTemplate.EmailType.GUEST_REGISTRATION,
        subject="registration subject",
        body="""Dette er en automatisk generert melding fra gjesteregistreringstjenesten.
Du har blitt registrert som gjest på {{ institution }} av {{ sponsor }}.
For å fullføre registreringen av gjestekontoen følg denne lenken: {{ registration_link }}

This message has been automatically generated by the guest registration system.
You have been registered as a guest at {{ institution }} by {{ sponsor }}.
To complete the registration of your guest account, please follow this link: {{ registration_link }}""",
    )
    return EmailTemplate.objects.get(id=et.id)


@pytest.fixture
def consent_type_foo() -> ConsentType:
    type_foo = ConsentType.objects.create(
        identifier="foo",
        name_en="Foo",
        name_nb="Fu",
        name_nn="F",
        description_en="Description",
        description_nb="Beskrivelse",
        description_nn="Beskriving",
        valid_from="2018-01-20",
        user_allowed_to_change=True,
        mandatory=False,
    )
    ConsentChoice.objects.create(
        consent_type=type_foo,
        value="yes",
        text_en="Yes",
        text_nb="Ja",
        text_nn="Ja",
    )
    ConsentChoice.objects.create(
        consent_type=type_foo,
        value="no",
        text_en="No",
        text_nb="Nei",
        text_nn="Nei",
    )
    return ConsentType.objects.get(id=type_foo.id)


@pytest.fixture
def consent_type_bar() -> ConsentType:
    type_bar = ConsentType.objects.create(
        identifier="bar",
        name_en="Bar",
        name_nb="Ba",
        name_nn="B",
        description_en="Description",
        description_nb="Beskrivelse",
        description_nn="Beskriving",
        valid_from="2018-01-20",
        user_allowed_to_change=True,
        mandatory=True,
    )
    ConsentChoice.objects.create(
        consent_type=type_bar,
        value="yes",
        text_en="Yes",
        text_nb="Ja",
        text_nn="Ja",
    )
    return ConsentType.objects.get(id=type_bar.id)