# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2021  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 datetime
import json
from unittest import mock

import pytest
from httmock import HTTMock, remember_called, response, urlmatch
from requests.exceptions import ConnectionError, HTTPError

import tests.utils
from passerelle.apps.photon.models import AddressCacheModel, Photon

CONTENT = {
    "features": [
        {
            "geometry": {"coordinates": [4.8522272, 45.7587414], "type": "Point"},
            "properties": {
                "city": "Lyon 3ème Arrondissement",
                "country": "France",
                "housenumber": "208",
                "osm_id": 154419,
                "osm_key": "place",
                "osm_type": "N",
                "osm_value": "house",
                "postcode": "69003",
                "street": "Rue Garibaldi",
                "type": "house",
            },
            "type": "Feature",
        },
        {
            "geometry": {"coordinates": [4.8522681, 45.7585214], "type": "Point"},
            "properties": {
                "city": "Lyon 3ème Arrondissement",
                "country": "France",
                "housenumber": "208bis",
                "osm_id": 153400,
                "osm_key": "place",
                "osm_type": "N",
                "osm_value": "house",
                "postcode": "69003",
                "street": "Rue Garibaldi",
                "type": "house",
            },
            "type": "Feature",
        },
    ],
    "type": "FeatureCollection",
}

FAKED_CONTENT = json.dumps(CONTENT)


@urlmatch(netloc='^example.net$', path='^/path/api/$')
@remember_called
def photon_search(url, request):
    return response(200, CONTENT, request=request)


@urlmatch(netloc='^example.net$', path='^/path/reverse/$')
@remember_called
def photon_reverse(url, request):
    return response(200, CONTENT, request=request)


@pytest.fixture
def mock_photon_search():
    with HTTMock(photon_search):
        yield photon_search


@pytest.fixture
def mock_photon_reverse():
    with HTTMock(photon_reverse):
        yield photon_reverse


@pytest.fixture
def photon(db):
    return tests.utils.setup_access_rights(
        Photon.objects.create(
            slug='test',
            service_url='http://example.net/path/',
        )
    )


@mock.patch('passerelle.utils.Request.get')
def test_photon_search(mocked_get, app, photon):
    endpoint = tests.utils.generic_endpoint_url('photon', 'search', slug=photon.slug)
    mocked_get.return_value = tests.utils.FakedResponse(content=FAKED_CONTENT, status_code=200)
    resp = app.get(endpoint, params={'q': 'plop'}, status=200)
    assert mocked_get.call_args[0][0].startswith('http://example.net/path/api/?')
    data = resp.json[0]
    assert data['lat'] == '45.7587414'
    assert data['lon'] == '4.8522272'
    assert data['display_name'] == '208, Rue Garibaldi 69003 Lyon 3ème Arrondissement'


def test_photon_search_qs(app, photon, mock_photon_search):
    resp = app.get('/photon/%s/search?q=plop' % photon.slug)
    assert 'display_name' in resp.json[0]


def test_photon_search_qs_zipcode(app, photon, mock_photon_search):
    resp = app.get('/photon/%s/search?q=plop&zipcode=69003' % photon.slug)
    assert 'display_name' in resp.json[0]


@mock.patch('passerelle.utils.Request.get')
def test_photon_search_qs_lat_lon(mocked_get, app, photon):
    app.get('/photon/%s/search?q=plop&lat=0&lon=1' % photon.slug)
    assert 'lat=0' in mocked_get.call_args[0][0]
    assert 'lon=1' in mocked_get.call_args[0][0]


@mock.patch('passerelle.utils.Request.get')
def test_photon_search_qs_viewbox(mocked_get, app, photon):
    app.get('/photon/%s/search?q=plop&viewbox=4.830,45.753,4.831,45.754&bounded=1' % photon.slug)
    assert (
        mocked_get.call_args[0][0]
        == 'http://example.net/path/api/?q=plop&limit=1&lang=fr&bbox=4.830%2C45.753%2C4.831%2C45.754'
    )


def test_photon_search_qs_empty(app, photon, mock_photon_search):
    resp = app.get('/photon/%s/search?q=' % photon.slug)
    assert len(resp.json) == 0


def test_photon_search_qs_parameters_error(app, photon, mock_photon_search):
    resp = app.get('/photon/%s/search' % photon.slug, status=400)
    assert resp.json['err'] == 1
    assert resp.json['err_class'] == 'passerelle.views.WrongParameter'
    assert resp.json['err_desc'] == "missing parameters: 'q'."


@mock.patch('passerelle.utils.Request.get')
def test_photon_api_timeout(mocked_get, app, photon):
    mocked_get.side_effect = ConnectionError('Remote end closed connection without response')
    resp = app.get('/photon/%s/search' % photon.slug, params={'q': 'plop'})

    assert resp.status_code == 200
    assert resp.json['err'] == 1
    assert (
        resp.json['err_desc']
        == 'failed to get http://example.net/path/api/?q=plop&limit=1&lang=fr: Remote end closed connection without response'
    )


@mock.patch('passerelle.utils.Request.get')
def test_photon_api_error(mocked_get, app, photon):
    def raise_for_status():
        raise HTTPError("400 Client Error: Bad Request for url: xxx")

    response = tests.utils.FakedResponse(content=json.dumps({'title': 'error'}), status_code=400)
    response.raise_for_status = raise_for_status
    mocked_get.return_value = response
    resp = app.get('/photon/%s/search' % photon.slug, params={'q': 'plop'}, status=200)

    assert resp.json['err'] == 1
    assert (
        resp.json['err_desc']
        == 'failed to get http://example.net/path/api/?q=plop&limit=1&lang=fr: 400 Client Error: Bad Request for url: xxx'
    )


@mock.patch('passerelle.utils.Request.get')
def test_photon_reverse_path(mocked_get, app, photon):
    mocked_get.return_value = tests.utils.FakedResponse(content=json.dumps({'features': []}), status_code=200)
    app.get('/photon/%s/reverse?lon=4.8522272&lat=45.7587414' % photon.slug)
    assert mocked_get.call_args[0][0].startswith('http://example.net/path/reverse/?')


def test_photon_reverse(app, photon, mock_photon_reverse):
    resp = app.get('/photon/%s/reverse?lon=4.8522272&lat=45.7587414' % photon.slug)
    data = resp.json
    assert 'display_name' in data
    assert data['address']['city'] == 'Lyon 3ème Arrondissement'
    assert data['address']['postcode'] == '69003'
    assert data['address']['house_number'] == '208'


@mock.patch('passerelle.utils.Request.get')
def test_photon_addresses(mocked_get, app, photon):
    endpoint = tests.utils.generic_endpoint_url('photon', 'addresses', slug=photon.slug)
    assert endpoint == '/photon/test/addresses'
    mocked_get.return_value = tests.utils.FakedResponse(content=FAKED_CONTENT, status_code=200)
    resp = app.get(endpoint, params={'q': 'plop'}, status=200)
    data = resp.json['data'][0]
    assert data['lat'] == '45.7587414'
    assert data['lon'] == '4.8522272'
    assert data['display_name'] == '208, Rue Garibaldi 69003 Lyon 3ème Arrondissement'
    assert data['text'] == '208, Rue Garibaldi 69003 Lyon 3ème Arrondissement'
    assert data['id'] == '2644ca352fdfef476b3f83ee99f0e8e5'
    assert data['address']['city'] == 'Lyon 3ème Arrondissement'
    assert data['address']['postcode'] == '69003'
    assert data['address']['road'] == 'Rue Garibaldi'


@mock.patch('passerelle.utils.Request.get')
def test_photon_addresses_qs_page_limit(mocked_get, app, photon):
    resp = app.get('/photon/%s/addresses?q=plop&page_limit=1' % photon.slug)
    assert 'limit=1' in mocked_get.call_args[0][0]

    resp = app.get('/photon/%s/addresses?q=plop&page_limit=100' % photon.slug)
    assert 'limit=20' in mocked_get.call_args[0][0]

    resp = app.get('/photon/%s/addresses?q=plop&page_limit=blabla' % photon.slug, status=400)
    assert 'invalid value' in resp.json['err_desc']


def test_photon_addresses_qs_zipcode(app, photon, mock_photon_search):
    resp = app.get('/photon/%s/addresses?q=plop&zipcode=69003' % photon.slug)
    assert 'display_name' in resp.json['data'][0]


@mock.patch('passerelle.utils.Request.get')
def test_photon_addresses_qs_coordinates(mocked_get, app, photon):
    photon.latitude = 1.2
    photon.longitude = 2.1
    photon.save()
    app.get('/photon/%s/addresses?q=plop' % photon.slug)
    assert 'lat=%s' % photon.latitude in mocked_get.call_args[0][0]
    assert 'lon=%s' % photon.longitude in mocked_get.call_args[0][0]

    app.get('/photon/%s/addresses?q=plop&lat=42&lon=43' % photon.slug)
    assert 'lat=42' in mocked_get.call_args[0][0]
    assert 'lon=43' in mocked_get.call_args[0][0]


def test_photon_addresses_cache(app, photon, mock_photon_search):
    resp = app.get('/photon/%s/addresses?q=plop' % photon.slug)
    assert mock_photon_search.call['count'] == 1

    data = resp.json['data'][0]
    assert data['text'] == '208, Rue Garibaldi 69003 Lyon 3ème Arrondissement'

    api_id = data['id']
    assert AddressCacheModel.objects.filter(api_id=api_id).exists()
    assert AddressCacheModel.objects.count() == 2

    resp = app.get('/photon/%s/addresses?id=%s' % (photon.slug, api_id))
    assert mock_photon_search.call['count'] == 1  # no new call
    assert data['text'] == '208, Rue Garibaldi 69003 Lyon 3ème Arrondissement'
    assert 'address' in data

    resp = app.get('/photon/%s/addresses?q=plop' % photon.slug)
    assert AddressCacheModel.objects.count() == 2  # no new object has been created


def test_photon_addresses_cache_err(app, photon, mock_photon_search):
    resp = app.get('/photon/%s/addresses?id=%s' % (photon.slug, 'wrong_id'))
    assert mock_photon_search.call['count'] == 0
    assert 'err' in resp.json


@mock.patch('passerelle.utils.Request.get')
def test_photon_addresses_data_change(mocked_get, app, photon):
    endpoint = tests.utils.generic_endpoint_url('photon', 'addresses', slug=photon.slug)
    mocked_get.return_value = tests.utils.FakedResponse(content=FAKED_CONTENT, status_code=200)

    # one user selects an address
    resp = app.get(endpoint, params={'q': 'plop'}, status=200)
    data = resp.json['data'][0]
    address_id, address_text = data['id'], data['text']

    # another requests the same while upstream data has been updated
    new_content = json.loads(FAKED_CONTENT)
    new_content['features'][0]['properties']['housenumber'] = 'changed'
    mocked_get.return_value = tests.utils.FakedResponse(content=json.dumps(new_content), status_code=200)
    resp = app.get(endpoint, params={'q': 'plop'}, status=200)

    # first user saves the form, data should not have changed
    resp = app.get(endpoint, params={'id': address_id}, status=200)
    assert resp.json['data'][0]['text'] == address_text

    # when cache is cleared, we get the updated data
    AddressCacheModel.objects.all().delete()
    resp = app.get(endpoint, params={'q': 'plop'}, status=200)
    assert 'changed' in resp.json['data'][0]['text']


def test_photon_reverse_cache(app, photon, freezer, mock_photon_reverse, mock_photon_search):
    assert AddressCacheModel.objects.count() == 0
    resp = app.get('/photon/%s/reverse?lon=4.8522272&lat=45.7587414' % photon.slug)
    assert mock_photon_reverse.call['count'] == 1

    data = resp.json
    assert data['text'] == '208, Rue Garibaldi 69003 Lyon 3ème Arrondissement'

    api_id = data['id']
    assert AddressCacheModel.objects.filter(api_id=api_id).exists()
    assert AddressCacheModel.objects.count() == 1
    first_timestamp = AddressCacheModel.objects.get().timestamp

    resp = app.get('/photon/%s/addresses?id=%s' % (photon.slug, api_id))
    assert mock_photon_search.call['count'] == 0
    assert data['text'] == '208, Rue Garibaldi 69003 Lyon 3ème Arrondissement'
    assert 'address' in data

    # check caching timestamp update
    freezer.move_to(datetime.timedelta(hours=1, seconds=1))
    resp = app.get('/photon/%s/reverse?lon=4.8522272&lat=45.7587414' % photon.slug)
    assert mock_photon_reverse.call['count'] == 2
    assert AddressCacheModel.objects.get().timestamp > first_timestamp
