# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2018 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import base64
import datetime
import json
import logging

import httmock
import pytest
import requests
import requests.exceptions

import tests.utils
from passerelle.contrib.mdph13.models import Link, MDPH13Resource
from passerelle.utils.jsonresponse import APIError

NAME_ID = 'xyz'
FILE_NUMBER = '1234'
DOB = datetime.date(1993, 5, 4)
DOB_ISOFORMAT = '1993-05-04'
EMAIL = 'john.doe@example.com'
SECRET = 'secret'
IP = '88.34.56.56'

VALID_RESPONSE = json.dumps(
    {
        'err': 0,
        "data": {
            "numero": FILE_NUMBER,
            "beneficiaire": {
                "nom": "Martini",
                "prenom": "Alfonso",
                "tel_mobile": "06 01 02 03 04",
                "tel_fixe": "04.01.02.03.04",
                "date_de_naissance": "1951-03-23",
                "email": "martini.a@free.fr",
                "entourage": [
                    {
                        "role": "Père",
                        "nom": "DUPONT Henri",
                        "tel_mobile": "0123232323",
                        "tel_fixe": "0202020202",
                        "email": "henri.dupont@xyz.com",
                    },
                    {
                        "role": "Mère",
                        "nom": "DUPONT Marie",
                        "tel_mobile": "0123232323",
                        "tel_fixe": "0202020202",
                        "email": "marie.dupont@xyz.com",
                    },
                    {
                        "role": "Aidant",
                        "nom": "ROBERT Fanny",
                        "tel_mobile": "0123232323",
                        "tel_fixe": "0202020202",
                        "email": "frobert@xyz.com",
                    },
                ],
                "adresse": {
                    "adresse_2": "Bliblibli",
                    "adresse_3": "Bliblibli",
                    "adresse_4": "CHEMIN DE LA CARRAIRE",
                    "adresse_5": "Bliblibli",
                    "code_postal": "13500",
                    "ville": "MARTIGUES",
                },
                "incapacite": {"taux": "Taux >=80%", "date_fin_effet": "2019-06-30"},
            },
            "demandes": [
                {
                    "numero": "1544740",
                    "date_demande": "2015-11-26",
                    "type_demande": "Renouvellement",
                    "prestation": "Carte d'invalidité (de priorité) pour personne handicapée",
                    "statut": "Instruction administrative terminée en attente de passage en évaluation",
                    "typologie": "Demande En Cours",
                    "date_decision": None,
                },
                {
                    "numero": "1210524",
                    "date_demande": "2014-06-13",
                    "type_demande": "Renouvellement",
                    "prestation": "Carte d'invalidité (de priorité) pour personne handicapée",
                    "statut": "Décision prononcée et expédition réalisée (traitement terminé)",
                    "typologie": "Traitée non expédiée",
                    "date_decision": "2014-07-10",
                    "date_debut_effet": "2014-08-01",
                    "date_fin_effet": "2016-05-01",
                },
                {
                    "numero": "1231345",
                    "date_demande": "2014-07-22",
                    "type_demande": "Recours Gracieux",
                    "prestation": "Carte d'invalidité (de priorité) pour personne handicapée",
                    "statut": "Décision prononcée et expédition réalisée (traitement terminé)",
                    "typologie": "Traitée et expédiée",
                    "date_decision": "2014-09-17",
                    "date_debut_effet": "2014-08-01",
                    "date_fin_effet": "2016-05-01",
                },
                {
                    "numero": "666660",
                    "date_demande": "2012-08-13",
                    "type_demande": "Recours Gracieux",
                    "prestation": "Carte d'invalidité (de priorité) pour personne handicapée",
                    "statut": "Décision prononcée et expédition réalisée (traitement terminé)",
                    "typologie": "Traitée et expédiée",
                    "date_decision": "2012-09-26",
                    "date_debut_effet": "2012-07-19",
                    "date_fin_effet": "2014-08-01",
                },
                {
                    "numero": "605280",
                    "date_demande": "2012-04-05",
                    "type_demande": "1ère demande",
                    "prestation": "Carte d'invalidité (de priorité) pour personne handicapée",
                    "statut": "Décision prononcée et expédition réalisée (traitement terminé)",
                    "typologie": "Traitée et expédiée",
                    "date_decision": "2012-07-19",
                    "date_debut_effet": "2012-07-19",
                    "date_fin_effet": "2014-05-01",
                },
                {
                    "numero": "1544741",
                    "date_demande": "2015-11-26",
                    "type_demande": "Renouvellement",
                    "prestation": "Carte d'invalidité (de priorité) pour personne handicapée",
                    "statut": "Décision prononcée et expédition réalisée (traitement terminé)",
                    "typologie": "Traitée et expédiée",
                    "date_decision": "2015-12-22",
                    "date_debut_effet": "2016-05-01",
                    "date_fin_effet": "2026-05-01",
                },
                {
                    "numero": "1210526",
                    "date_demande": "2014-06-13",
                    "type_demande": "Renouvellement",
                    "prestation": "Carte européenne de Stationnement",
                    "statut": "Décision prononcée et expédition réalisée (traitement terminé)",
                    "typologie": "Traitée et expédiée",
                    "date_decision": "2014-07-04",
                    "date_debut_effet": "2014-05-01",
                    "date_fin_effet": "2015-05-01",
                },
                {
                    "numero": "605281",
                    "date_demande": "2012-04-05",
                    "type_demande": "1ère demande",
                    "prestation": "Carte européenne de Stationnement",
                    "statut": "Décision prononcée et expédition réalisée (traitement terminé)",
                    "typologie": "Traitée et expédiée",
                    "date_decision": "2012-07-04",
                    "date_debut_effet": "2012-05-01",
                    "date_fin_effet": "2014-05-01",
                },
            ],
        },
    }
)

DOSSIER_INCONNU = {
    'status_code': 404,
    'content': json.dumps(
        {
            'err': 1,
            'err_code': 'dossier-inconnu',
        }
    ),
}

SECRET_INVALIDE = {
    'status_code': 404,
    'content': json.dumps(
        {
            'err': 1,
            'err_code': 'secret-invalide',
        }
    ),
}


@pytest.fixture
def mdph13(db):
    return tests.utils.make_resource(
        MDPH13Resource,
        title='Test 1',
        slug='test1',
        description='Connecteur de test',
        webservice_base_url='http://cd13.fr/',
    )


@pytest.fixture
def mock_http():
    class MockHttp:
        def __init__(self):
            self.requests = []
            self.responses = []

        def add_response(self, response):
            self.responses.append(response)

        @property
        def last_request(self):
            return self.requests[-1]

        def request_handler(self, url, request):
            idx = len(self.requests)
            self.requests.append(request)
            response = self.responses[idx]
            if isinstance(response, Exception):
                raise response
            if hasattr(response, '__call__'):
                response = response(url, request)
            return response

    mock_http = MockHttp()
    with httmock.HTTMock(httmock.urlmatch()(mock_http.request_handler)):
        yield mock_http


def test_situation_dossier_url(mdph13):
    assert mdph13.situation_dossier_url(1234) == 'http://cd13.fr/situation/dossier/1234'


def test_call_situation_dossier(mdph13, mock_http):
    mock_http.add_response(VALID_RESPONSE)
    mdph13.call_situation_dossier(1234, SECRET, DOB)
    request = mock_http.last_request
    headers = request.headers
    url = request.url
    assert url == 'http://cd13.fr/situation/dossier/1234'
    assert headers['X-CD13-Secret'] == base64.b64encode(SECRET.encode('utf-8')).decode('ascii')
    assert headers['X-CD13-DateNaissBenef'] == '1993-05-04'
    assert headers['X-CD13-Email'] == 'appel-sans-utilisateur@cd13.fr'
    assert 'X-CD13-IP' not in headers


def test_call_situation_dossier_with_email_and_ip(mdph13, mock_http):
    mock_http.add_response(VALID_RESPONSE)
    mdph13.call_situation_dossier(1234, SECRET, DOB, email=EMAIL, ip=IP)
    request = mock_http.last_request
    headers = request.headers
    url = request.url
    assert url == 'http://cd13.fr/situation/dossier/1234'
    assert headers['X-CD13-Secret'] == base64.b64encode(SECRET.encode('utf-8')).decode('ascii')
    assert headers['X-CD13-DateNaissBenef'] == DOB_ISOFORMAT
    assert headers['X-CD13-Email'] == EMAIL
    assert headers['X-CD13-IP'] == IP


def test_link_bad_file_number(mdph13):
    with pytest.raises(APIError) as e:
        mdph13.link(
            request=None, NameID=NAME_ID, numero_dossier='x', secret=None, date_de_naissance=None, email=None
        )
    assert str(e.value) == 'numero_dossier must be a number'


def test_link_bad_date_de_naissance(app, mdph13):
    url = '/mdph13/test1/link/?NameID=%s&numero_dossier=%s&date_de_naissance=%s&secret=%s&email=%s'
    url = url % (NAME_ID, FILE_NUMBER, '34-45-6', None, None)
    response = app.post(url, status=400)
    assert response.json['err_class'] == 'passerelle.views.InvalidParameterValue'
    assert (
        response.json['err_desc'] == 'invalid value for parameter "date_de_naissance (YYYY-MM-DD expected)"'
    )


def test_link_bad_email(mdph13):
    with pytest.raises(APIError) as e:
        mdph13.link(
            request=None,
            NameID=NAME_ID,
            numero_dossier=FILE_NUMBER,
            secret=None,
            date_de_naissance=DOB_ISOFORMAT,
            email='xxx@@vvv',
        )
    assert str(e.value) == 'email is not valid'


def test_link_nok_dossier_inconnu(app, mdph13, mock_http):
    mock_http.add_response(DOSSIER_INCONNU)
    url = '/mdph13/test1/link/?NameID=%s&numero_dossier=%s&date_de_naissance=%s&secret=%s&email=%s'
    url = url % (NAME_ID, FILE_NUMBER, DOB_ISOFORMAT, SECRET, EMAIL)
    response = app.post(url)
    assert response.json['err_class'] == 'passerelle.utils.jsonresponse.APIError'
    assert response.json['err_desc'] == 'dossier-inconnu'


def test_link_nok_secret_invalide(app, mdph13, mock_http):
    mock_http.add_response(SECRET_INVALIDE)
    url = '/mdph13/test1/link/?NameID=%s&numero_dossier=%s&date_de_naissance=%s&secret=%s&email=%s'
    url = url % (NAME_ID, FILE_NUMBER, DOB_ISOFORMAT, SECRET, EMAIL)
    response = app.post(url)
    assert response.json['err_class'] == 'passerelle.utils.jsonresponse.APIError'
    assert response.json['err_desc'] == 'secret-invalide'


def test_link_numero_dont_match(app, mdph13, mock_http):
    mock_http.add_response(json.dumps({'err': 0, 'data': {'numero': '456'}}))
    url = '/mdph13/test1/link/?NameID=%s&numero_dossier=%s&date_de_naissance=%s&secret=%s&email=%s'
    url = url % (NAME_ID, FILE_NUMBER, DOB_ISOFORMAT, SECRET, EMAIL)
    response = app.post(url)
    assert response.json['err_class'] == 'passerelle.utils.jsonresponse.APIError'
    assert response.json['err_desc'] == 'numero-must-match-numero-dossier'


def test_link_ok(app, mdph13, mock_http):
    # check first time link
    mock_http.add_response(VALID_RESPONSE)
    assert not Link.objects.count()
    url = '/mdph13/test1/link/?NameID=%s&numero_dossier=%s&date_de_naissance=%s&secret=%s&email=%s&ip=%s'
    url = url % (NAME_ID, FILE_NUMBER, DOB_ISOFORMAT, SECRET, EMAIL, IP)
    response = app.post(url)
    link = Link.objects.get()
    assert response.json == {'err': 0, 'link_id': link.pk, 'created': True, 'updated': False}
    # check relinking with update
    mock_http.add_response(VALID_RESPONSE)
    url = '/mdph13/test1/link/?NameID=%s&numero_dossier=%s&date_de_naissance=%s&secret=%s&email=%s&ip=%s'
    url = url % (NAME_ID, FILE_NUMBER, DOB_ISOFORMAT, SECRET + 'a', EMAIL, IP)
    response = app.post(url)
    assert response.json == {
        'err': 0,
        'link_id': link.pk,
        'created': False,
        'updated': True,
    }


def test_unlink_nok_bad_link_id(mdph13):
    with pytest.raises(APIError) as e:
        mdph13.unlink(None, None, 'e')
    assert str(e.value) == 'link_id-must-be-a-number'


def test_unlink_ok(mdph13):
    link = Link.objects.create(
        resource=mdph13, name_id=NAME_ID, file_number=FILE_NUMBER, secret=SECRET, dob=DOB
    )
    result = mdph13.unlink(None, NAME_ID, str(link.pk))
    assert result['deleted'] == 1
    result = mdph13.unlink(None, NAME_ID, str(link.pk))
    assert result['deleted'] == 0


def test_unlink_with_delete(mdph13, app):
    link = Link.objects.create(
        resource=mdph13, name_id=NAME_ID, file_number=FILE_NUMBER, secret=SECRET, dob=DOB
    )
    response = app.delete('/mdph13/%s/unlink/?NameID=%s&link_id=%s' % (mdph13.slug, NAME_ID, link.pk))
    assert response.json['deleted'] == 1


def test_unlink_all_ok(mdph13):
    Link.objects.create(resource=mdph13, name_id=NAME_ID, file_number=FILE_NUMBER, secret=SECRET, dob=DOB)
    Link.objects.create(resource=mdph13, name_id=NAME_ID, file_number='12345', secret=SECRET, dob=DOB)
    result = mdph13.unlink(None, NAME_ID, 'all')
    assert result['deleted'] == 2
    result = mdph13.unlink(None, NAME_ID, 'all')
    assert result['deleted'] == 0


def test_dossier_ok(mdph13, mock_http):
    link = Link.objects.create(
        resource=mdph13, name_id=NAME_ID, file_number=FILE_NUMBER, secret=SECRET, dob=DOB
    )
    mock_http.add_response(VALID_RESPONSE)
    response = mdph13.dossiers(None, NAME_ID, EMAIL, ip=IP)
    assert response['data']
    assert response['data'][0]['id'] == str(link.pk)
    assert response['data'][0]['numero_dossier'] == FILE_NUMBER
    assert response['data'][0]['date_de_naissance'] == DOB.isoformat()
    assert response['data'][0]['dossier']['numero'] == FILE_NUMBER
    assert response['data'][0]['text'] == 'Alfonso Martini #%s' % FILE_NUMBER
    assert len(response['data'][0]['dossier']['beneficiaire']['entourage']) == 2
    assert len(response['data'][0]['dossier']['beneficiaire']['entourage']['parents']) == 2
    assert len(response['data'][0]['dossier']['beneficiaire']['entourage']['aidants']) == 1
    assert len(response['data'][0]['dossier']['demandes']) == 2
    assert len(response['data'][0]['dossier']['demandes']['en_cours']) == 1
    assert len(response['data'][0]['dossier']['demandes']['historique']) == 7
    # check demands are ordered by date
    assert response['data'][0]['dossier']['demandes']['historique'][0]['date_demande'] == '2015-11-26'
    dates = [demande['date_demande'] for demande in response['data'][0]['dossier']['demandes']['historique']]
    assert sorted(dates, reverse=True) == dates

    # check no demandes
    valid_response = json.loads(VALID_RESPONSE)
    del valid_response['data']['demandes']
    mock_http.add_response(json.dumps(valid_response))
    response = mdph13.dossiers(None, NAME_ID, EMAIL, ip=IP)
    assert response['data']
    assert response['data'][0]['err'] == 0
    assert 'demandes' not in response['data'][0]['dossier']


def test_dossier_with_link_id_ok(mdph13, mock_http):
    link = Link.objects.create(
        resource=mdph13, name_id=NAME_ID, file_number=FILE_NUMBER, secret=SECRET, dob=DOB
    )
    mock_http.add_response(VALID_RESPONSE)
    response = mdph13.dossiers(None, NAME_ID, EMAIL, link_id=str(link.pk), ip=IP)
    assert response['data']
    assert response['data']['id'] == str(link.pk)
    assert response['data']['numero_dossier'] == FILE_NUMBER
    assert response['data']['date_de_naissance'] == DOB.isoformat()
    assert response['data']['dossier']['numero'] == FILE_NUMBER
    assert response['data']['text'] == 'Alfonso Martini #%s' % FILE_NUMBER
    assert len(response['data']['dossier']['beneficiaire']['entourage']) == 2
    assert len(response['data']['dossier']['beneficiaire']['entourage']['parents']) == 2
    assert len(response['data']['dossier']['beneficiaire']['entourage']['aidants']) == 1
    assert len(response['data']['dossier']['demandes']) == 2
    assert len(response['data']['dossier']['demandes']['en_cours']) == 1
    assert len(response['data']['dossier']['demandes']['historique']) == 7


def test_dossier_partial_failure(mdph13, mock_http):
    link1 = Link.objects.create(
        resource=mdph13, name_id=NAME_ID, file_number=FILE_NUMBER, secret=SECRET, dob=DOB
    )
    link2 = Link.objects.create(
        resource=mdph13, name_id=NAME_ID, file_number=FILE_NUMBER + '2', secret=SECRET, dob=DOB
    )
    mock_http.add_response(VALID_RESPONSE)
    VALID_RESPONSE2 = json.loads(VALID_RESPONSE).copy()
    VALID_RESPONSE2['data']['numero'] = FILE_NUMBER + '2'
    mock_http.add_response(json.dumps(VALID_RESPONSE2))
    # test that display_name is updated if changed after any access
    link2.refresh_from_db()
    assert not link2.display_name
    response = mdph13.dossiers(None, NAME_ID, EMAIL)
    link2.refresh_from_db()
    assert link2.display_name
    mock_http.add_response(VALID_RESPONSE)
    mock_http.add_response({'status_code': 500, 'content': ''})
    response = mdph13.dossiers(None, NAME_ID, EMAIL, ip=IP)
    assert response['data']
    assert response['data'][0]['id'] == str(link1.pk)
    assert response['data'][0]['err'] == 0
    assert response['data'][0]['text'] == 'Alfonso Martini #' + FILE_NUMBER
    assert response['data'][1]['id'] == str(link2.pk)
    assert response['data'][1]['err'] == 1
    # verify display_name is returned even in case of failure
    assert response['data'][1]['text'] == 'Alfonso Martini #' + FILE_NUMBER + '2'


def test_dossier_bad_date(mdph13, mock_http):
    link = Link.objects.create(
        resource=mdph13, name_id=NAME_ID, file_number=FILE_NUMBER, secret=SECRET, dob=DOB
    )
    INVALID_RESPONSE = json.loads(VALID_RESPONSE)
    INVALID_RESPONSE['data']['demandes'][0]['date_demande'] = 'xxx'
    mock_http.add_response(json.dumps(INVALID_RESPONSE))
    with pytest.raises(APIError) as exc_info:
        mdph13.dossiers(None, NAME_ID, EMAIL, link_id=str(link.pk))
    assert str(exc_info.value) == 'invalid-response-format'


def test_dossier_http_error(app, mdph13, mock_http, caplog):
    url = '/mdph13/test1/link/?NameID=%s&numero_dossier=%s&date_de_naissance=%s&secret=%s&email=%s&ip=%s'
    url = url % (NAME_ID, FILE_NUMBER, DOB, SECRET, EMAIL, IP)
    mock_http.add_response({'status_code': 401, 'content': 'wtf', 'reason': 'Authentication required'})
    response = app.post(url, status=500)
    assert response.json['err_class'] == 'requests.exceptions.HTTPError'
    for record in caplog.records:
        if (
            record.getMessage() == 'GET http://cd13.fr/situation/dossier/1234 (=> 401)'
            and record.levelno == logging.ERROR
        ):
            break
    else:
        assert False, '401 caplog error message expected'
    assert hasattr(record.request, 'META')

    def raise_ssl_error(url, request):
        raise requests.exceptions.SSLError(request=request)

    caplog.clear()
    mock_http.add_response(raise_ssl_error)
    response = app.post(url, status=500)
    assert response.json['err_class'] == 'requests.exceptions.SSLError'
    for record in caplog.records:
        if (
            record.getMessage() == 'GET http://cd13.fr/situation/dossier/1234 (=> SSLError())'
            and record.levelno == logging.ERROR
        ):
            break
    else:
        assert False, 'SSLError caplog error message expected'
    assert hasattr(record.request, 'META')
