import datetime
import json
import os
from unittest import mock
from urllib.parse import parse_qs, urljoin

import pytest
from django.core.management import call_command
from django.core.management.base import CommandError
from requests.exceptions import ConnectionError, HTTPError

import tests.utils
from passerelle.apps.base_adresse.models import (
    AddressCacheModel,
    BaseAdresse,
    CityModel,
    DepartmentModel,
    RegionModel,
    StreetModel,
)

FAKED_CONTENT = json.dumps(
    {
        "limit": 1,
        "attribution": "BAN",
        "version": "draft",
        "licence": "ODbL 1.0",
        "query": "plop",
        "type": "FeatureCollection",
        "features": [
            {
                "geometry": {"type": "Point", "coordinates": [-0.593775, 47.474633]},
                "properties": {
                    "citycode": "49007",
                    "name": "Rue Roger Halope",
                    "id": "49007_6950_be54bd",
                    "city": "Angers",
                    "context": "49, Maine-et-Loire, Pays de la Loire",
                    "score": 0.14097272727272728,
                    "label": "Rue Roger Halope 49000 Angers",
                    "postcode": "49000",
                    "type": "street",
                    "info1": "xxx",
                    "info2": "yyy",
                },
                "type": "Feature",
            }
        ],
    }
)

FAKE_DATA = ''

FAKE_API_GEO_LIST = [
    {
        "code": "75056",
        "codeDepartement": "75",
        "codeRegion": "11",
        "codesPostaux": [
            "75001",
            "75002",
        ],
        "nom": "Paris",
        "population": 2190327,
    },
    {"code": "97501", "codesPostaux": ["97500"], "nom": "Miquelon-Langlade", "population": 596},
]

FAKE_API_GEO = json.dumps(FAKE_API_GEO_LIST)

FAKE_API_GEO_DEPARTMENTS = json.dumps(
    [
        {"code": "75", "codeRegion": "11", "nom": "Paris"},
        {
            "code": "58",
            "codeRegion": "27",
            "nom": "Nièvre",
        },
    ]
)

FAKE_API_GEO_REGIONS = json.dumps(
    [{"code": "11", "nom": "Île-de-France"}, {"code": "27", "nom": "Bourgogne-Franche-Comté"}]
)


@pytest.fixture
def base_adresse(db):
    return tests.utils.setup_access_rights(BaseAdresse.objects.create(slug='base-adresse', zipcode='73'))


@pytest.fixture
def base_adresse_97x(db):
    return tests.utils.setup_access_rights(
        BaseAdresse.objects.create(slug='base-adresse-97x', zipcode='97425')
    )


@pytest.fixture
def base_adresse_corsica(db):
    return tests.utils.setup_access_rights(
        BaseAdresse.objects.create(slug='base-adresse', zipcode='20000, 20100 ')
    )


@pytest.fixture
def base_adresse_multiple(db):
    return tests.utils.setup_access_rights(
        BaseAdresse.objects.create(slug='base-adresse', zipcode='73, 73100, 97425,20000 ')
    )


@pytest.fixture
def base_adresse_coordinates(db):
    return tests.utils.setup_access_rights(
        BaseAdresse.objects.create(slug='base-adresse', latitude=1.2, longitude=2.1)
    )


@pytest.fixture
def street(db):
    return StreetModel.objects.create(
        ban_id='73001_0000',
        city='Chambéry',
        name='Une rüê très äccentuéè',
        zipcode='73000',
        type='street',
        citycode='73001',
        resource=BaseAdresse.objects.first(),
    )


@pytest.fixture
def region(db):
    return RegionModel.objects.create(
        name='Auvergne-Rhône-Alpes', code='84', resource=BaseAdresse.objects.first()
    )


@pytest.fixture
def department(db, region):
    return DepartmentModel.objects.create(
        name='Savoie', code='73', region=region, resource=BaseAdresse.objects.first()
    )


@pytest.fixture
def city(db, region, department):
    return CityModel.objects.create(
        name='Chambéry',
        code='73065',
        zipcode='73000',
        population=42000,
        region=region,
        department=department,
        resource=BaseAdresse.objects.first(),
    )


@pytest.fixture
def miquelon(db):
    return CityModel.objects.create(
        name='Miquelon-Langlade',
        code='97501',
        zipcode='97500',
        population=42,
        resource=BaseAdresse.objects.first(),
    )


@pytest.fixture
def mock_update_api_geo():
    with mock.patch(
        'passerelle.apps.base_adresse.models.BaseAdresse.update_api_geo_data', new=lambda x: None
    ) as _fixture:
        yield _fixture


@pytest.fixture
def mock_update_streets():
    with mock.patch(
        'passerelle.apps.base_adresse.models.BaseAdresse.update_streets_data', new=lambda x: None
    ) as _fixture:
        yield _fixture


@mock.patch('passerelle.utils.Request.get')
def test_base_adresse_search(mocked_get, app, base_adresse):
    endpoint = tests.utils.generic_endpoint_url('base-adresse', 'search', slug=base_adresse.slug)
    assert endpoint == '/base-adresse/base-adresse/search'
    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[0]
    assert data['lat'] == '47.474633'
    assert data['lon'] == '-0.593775'
    assert data['display_name'] == 'Rue Roger Halope 49000 Angers'


@mock.patch('passerelle.utils.Request.get')
def test_base_adresse_search_limit_to_200(mocked_get, app, base_adresse):
    endpoint = tests.utils.generic_endpoint_url('base-adresse', 'search', slug=base_adresse.slug)
    assert endpoint == '/base-adresse/base-adresse/search'
    mocked_get.return_value = tests.utils.FakedResponse(content=FAKED_CONTENT, status_code=200)
    app.get(endpoint, params={'q': 'plop' * 200}, status=200)
    assert len(parse_qs(mocked_get.call_args[0][0].split('?')[1])['q'][0]) == 200


@mock.patch('passerelle.utils.Request.get')
def test_base_adresse_search_path(mocked_get, app, base_adresse):
    base_adresse.service_url = 'http://example.net/path/'
    base_adresse.save()
    endpoint = tests.utils.generic_endpoint_url('base-adresse', 'search', slug=base_adresse.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/search/?')
    data = resp.json[0]
    assert data['lat'] == '47.474633'
    assert data['lon'] == '-0.593775'
    assert data['display_name'] == 'Rue Roger Halope 49000 Angers'


def test_base_adresse_search_qs(app, base_adresse, mock_api_adresse_data_gouv_fr_search):
    resp = app.get('/base-adresse/%s/search?q=plop' % base_adresse.slug)
    assert 'display_name' in resp.json[0]


def test_base_adresse_search_qs_zipcode(app, base_adresse, mock_api_adresse_data_gouv_fr_search):
    resp = app.get('/base-adresse/%s/search?q=plop&zipcode=49000' % base_adresse.slug)
    assert 'display_name' in resp.json[0]


@mock.patch('passerelle.utils.Request.get')
def test_base_adresse_search_qs_citycode(mocked_get, app, base_adresse):
    app.get('/base-adresse/%s/search?q=plop&citycode=31555' % base_adresse.slug)
    assert 'citycode=31555' in mocked_get.call_args[0][0]


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


def test_base_adresse_search_qs_empty(app, base_adresse, mock_api_adresse_data_gouv_fr_search):
    resp = app.get('/base-adresse/%s/search?q=' % base_adresse.slug)
    assert len(resp.json) == 0
    resp = app.get('/base-adresse/%s/search?q=   ' % base_adresse.slug)
    assert len(resp.json) == 0


def test_base_adresse_search_qs_not_alphanumeric(app, base_adresse, mock_api_adresse_data_gouv_fr_search):
    resp = app.get('/base-adresse/%s/search?q=**notalphanumeric' % base_adresse.slug)
    assert len(resp.json) == 0
    resp = app.get('/base-adresse/%s/search?q=  **notalpha  ' % base_adresse.slug)
    assert len(resp.json) == 0


def test_base_adresse_search_qs_too_short(app, base_adresse, mock_api_adresse_data_gouv_fr_search):
    resp = app.get('/base-adresse/%s/search?q=12' % base_adresse.slug)
    assert len(resp.json) == 0
    resp = app.get('/base-adresse/%s/search?q=  ab  ' % base_adresse.slug)
    assert len(resp.json) == 0


def test_base_adresse_search_qs_parameters_error(app, base_adresse, mock_api_adresse_data_gouv_fr_search):
    resp = app.get('/base-adresse/%s/search' % base_adresse.slug, status=400)
    assert resp.json['err'] == 1
    assert resp.json['err_class'] == 'passerelle.views.WrongParameter'
    assert resp.json['err_desc'] == "missing parameters: 'q'."
    # json-api serializer
    resp = app.get('/base-adresse/%s/streets?zipcode=13400&coin=zz' % base_adresse.slug, status=400)
    assert resp.json['err'] == 1
    assert 'coin' in resp.json['err_desc']
    # signature and format are ignored
    app.get(
        '/base-adresse/%s/streets?zipcode=13400&signature=zz&format=jsonp'
        '&raise=1&jsonpCallback=f' % base_adresse.slug
    )


@mock.patch('passerelle.utils.Request.get')
def test_base_adresse_search_api_error(mocked_get, app, base_adresse):
    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('/base-adresse/%s/search' % base_adresse.slug, params={'q': 'plop'}, status=200)

    assert resp.json['err'] == 1


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

    assert resp.status_code == 200
    assert resp.json['err'] == 1
    assert (
        resp.json['err_desc']
        == 'failed to get https://api-adresse.data.gouv.fr/search/?q=plop&limit=1: Remote end closed connection without response'
    )


def test_base_adresse_reverse(app, base_adresse, mock_api_adresse_data_gouv_fr_reverse):
    resp = app.get('/base-adresse/%s/reverse?lon=-0.593775&lat=47.474633' % base_adresse.slug)
    data = resp.json
    assert 'display_name' in data
    assert 'address' in data
    assert data['address']['city'] == 'Angers'
    assert data['address']['postcode'] == '49000'
    assert data['address']['citycode'] == '49007'


@mock.patch('passerelle.utils.Request.get')
def test_base_adresse_reverse_having_several(mocked_get, app, base_adresse):
    content = json.loads(FAKED_CONTENT)
    content['features'].append(
        {
            "geometry": {"type": "Point", "coordinates": [-0.593775, 47.474633]},
            "properties": {
                "citycode": "49007",
                "name": "Rue Eugène Bardon",
                "id": "49007_6950_aaaaa",
                "city": "Angers",
                "context": "49, Maine-et-Loire, Pays de la Loire",
                "score": 0.2,
                "label": "Rue Eugène Bardon 49000 Angers",
                "postcode": "49000",
                "type": "street",
            },
            "type": "Feature",
        }
    )
    faked_content = json.dumps(content)
    mocked_get.return_value = tests.utils.FakedResponse(content=faked_content, status_code=200)
    resp = app.get('/base-adresse/%s/reverse?lon=-0.593775&lat=47.474633' % base_adresse.slug)
    data = resp.json
    assert data['address']['road'] == 'Rue Roger Halope'


@mock.patch('passerelle.utils.Request.get')
def test_base_adresse_reverse_path(mocked_get, app, base_adresse):
    mocked_get.return_value = tests.utils.FakedResponse(content=json.dumps({'features': []}), status_code=200)
    base_adresse.service_url = 'http://example.net/path/'
    base_adresse.save()
    app.get('/base-adresse/%s/reverse?lon=-0.593775&lat=47.474633' % base_adresse.slug)
    assert mocked_get.call_args[0][0].startswith('http://example.net/path/reverse/?')


@mock.patch('passerelle.utils.Request.get')
def test_base_adresse_reverse_api_timeout(mocked_get, app, base_adresse):
    mocked_get.side_effect = ConnectionError('Remote end closed connection without response')
    resp = app.get('/base-adresse/%s/reverse?lon=-0.593775&lat=47.474633' % base_adresse.slug)

    assert resp.status_code == 200
    assert resp.json['err'] == 1
    assert (
        resp.json['err_desc']
        == 'failed to get https://api-adresse.data.gouv.fr/reverse/?lat=47.474633&lon=-0.593775: Remote end closed connection without response'
    )


def test_base_adresse_streets_unaccent(app, base_adresse, street):
    resp = app.get('/base-adresse/%s/streets?q=une rue tres acc' % base_adresse.slug)
    data = json.loads(resp.text)
    assert 'data' in data
    result = data['data'][0]
    assert result['city'] == street.city
    assert result['text'] == street.name
    assert result['citycode'] == street.citycode
    assert result['zipcode'] == street.zipcode
    assert result['id'] == str(street.ban_id)


def test_base_adresse_streets_get_by_id(app, base_adresse, street):
    for i in range(10):
        # create additional streets
        other_street = StreetModel.objects.create(
            ban_id='%d_000T' % (73001 + i),
            city='Chambéry',
            name='Une rue différente',
            zipcode=str(73001 + i),
            type='street',
            citycode=str(73001 + i),
            resource=base_adresse,
        )

    resp = app.get('/base-adresse/%s/streets?q=une rue tres acc' % base_adresse.slug)
    assert 'data' in resp.json
    result = resp.json['data'][0]
    assert result['id'] == '73001_0000'  # it's the "street" fixture

    resp = app.get('/base-adresse/%s/streets?id=73001_0000' % base_adresse.slug)
    assert len(resp.json['data']) == 1
    result2 = resp.json['data'][0]
    assert result2['text'] == result['text']

    # get by legacy id
    resp = app.get('/base-adresse/%s/streets?id=%d' % (base_adresse.slug, other_street.id))
    assert len(resp.json['data']) == 1
    result3 = resp.json['data'][0]
    assert result3['text'] == other_street.name

    # non existing and non integer id
    resp = app.get('/base-adresse/%s/streets?id=%s' % (base_adresse.slug, 'XXX'))
    assert len(resp.json['data']) == 0

    # integer but without match.
    resp = app.get('/base-adresse/%s/streets?id=%s' % (base_adresse.slug, '-20'))
    assert len(resp.json['data']) == 0


def test_base_adresse_streets_get_by_codes(app, base_adresse, street):
    for i in range(20):
        StreetModel.objects.create(
            city='Paris %d' % i,
            name='La rue %d' % i,
            zipcode=str(75000 + i * 10),
            type='street',
            citycode=str(75000 + i * 11),
            resource=base_adresse,
        )

    resp = app.get('/base-adresse/%s/streets?zipcode=75' % base_adresse.slug)
    assert 'data' in resp.json
    assert resp.json['err'] == 0
    assert len(resp.json['data']) == 20
    assert {street['zipcode'][:2] for street in resp.json['data']} == {'75'}
    resp = app.get('/base-adresse/%s/streets?zipcode=75010' % base_adresse.slug)
    assert 'data' in resp.json
    assert resp.json['err'] == 0
    assert len(resp.json['data']) == 1
    assert resp.json['data'][0]['zipcode'] == '75010'
    resp = app.get('/base-adresse/%s/streets?zipcode=12345' % base_adresse.slug)
    assert 'data' in resp.json
    assert resp.json['err'] == 0
    assert len(resp.json['data']) == 0

    resp = app.get('/base-adresse/%s/streets?citycode=75' % base_adresse.slug)
    assert 'data' in resp.json
    assert resp.json['err'] == 0
    assert len(resp.json['data']) == 20
    assert {street['citycode'][:2] for street in resp.json['data']} == {'75'}
    resp = app.get('/base-adresse/%s/streets?citycode=75044' % base_adresse.slug)
    assert 'data' in resp.json
    assert resp.json['err'] == 0
    assert len(resp.json['data']) == 1
    assert resp.json['data'][0]['citycode'] == '75044'
    resp = app.get('/base-adresse/%s/streets?citycode=12345' % base_adresse.slug)
    assert 'data' in resp.json
    assert resp.json['err'] == 0
    assert len(resp.json['data']) == 0


@pytest.mark.usefixtures('mock_update_api_geo')
@mock.patch('passerelle.utils.Request.get')
def test_base_adresse_command_update(mocked_get, db, base_adresse):
    filepath = os.path.join(os.path.dirname(__file__), 'data', 'update_streets_test.gz')
    with open(filepath, 'rb') as ban_file:
        mocked_get.return_value = tests.utils.FakedResponse(content=ban_file.read(), status_code=200)
    call_command('cron', 'daily')
    mocked_get.assert_called_once_with(
        'https://adresse.data.gouv.fr/data/ban/adresses/latest/addok/adresses-addok-73.ndjson.gz'
    )
    streets = StreetModel.objects.all()
    assert len(streets) == 3
    streets = streets.filter(ban_id='73001_0004')
    assert streets.count() == 1
    street = streets.first()
    assert street.name == 'Chemin de la Vie, LA GRANGE DU TRIEU'
    assert street.zipcode == '73610'
    assert street.type == 'street'
    assert street.city == 'Aiguebelette-le-Lac'
    assert street.citycode == '73001'
    assert street.ban_id == '73001_0004'

    # check a new call downloads again
    call_command('cron', 'daily')
    assert mocked_get.call_count == 2


@pytest.mark.usefixtures('mock_update_api_geo')
@mock.patch('passerelle.utils.Request.get')
def test_base_adresse_command_job_update(mocked_get, db, base_adresse):
    base_adresse.update_api_geo_data = lambda: None
    filepath = os.path.join(os.path.dirname(__file__), 'data', 'update_streets_test.gz')
    with open(filepath, 'rb') as ban_file:
        mocked_get.return_value = tests.utils.FakedResponse(content=ban_file.read(), status_code=200)
    # check the job added at save() downloads streets
    base_adresse.jobs()
    mocked_get.assert_called_once_with(
        'https://adresse.data.gouv.fr/data/ban/adresses/latest/addok/adresses-addok-73.ndjson.gz'
    )
    assert StreetModel.objects.all().count() == 3

    # second save doesn't download anything
    base_adresse.save()
    base_adresse.jobs()
    assert mocked_get.call_count == 1

    # but changing zipcode does
    base_adresse.zipcode = '74'
    base_adresse.save()
    base_adresse.jobs()
    assert mocked_get.call_count == 2


@pytest.mark.usefixtures('mock_update_api_geo')
@mock.patch('passerelle.utils.Request.get')
def test_base_adresse_command_update_97x(mocked_get, db, base_adresse_97x):
    base_adresse_97x.update_api_geo_data = lambda: None
    filepath = os.path.join(os.path.dirname(__file__), 'data', 'update_streets_test.gz')
    with open(filepath, 'rb') as ban_file:
        mocked_get.return_value = tests.utils.FakedResponse(content=ban_file.read(), status_code=200)
    call_command('cron', 'daily')
    mocked_get.assert_called_once_with(
        'https://adresse.data.gouv.fr/data/ban/adresses/latest/addok/adresses-addok-974.ndjson.gz'
    )
    assert StreetModel.objects.count() == 2


@pytest.mark.usefixtures('mock_update_api_geo')
@mock.patch('passerelle.utils.Request.get')
def test_base_adresse_command_update_corsica(mocked_get, db, base_adresse_corsica):
    base_adresse_corsica.update_api_geo_data = lambda: None
    filepath = os.path.join(os.path.dirname(__file__), 'data', 'update_streets_test.gz')
    with open(filepath, 'rb') as ban_file:
        mocked_get.return_value = tests.utils.FakedResponse(content=ban_file.read(), status_code=200)
    call_command('cron', 'daily')
    assert mocked_get.call_count == 2
    mocked_get.assert_any_call(
        'https://adresse.data.gouv.fr/data/ban/adresses/latest/addok/adresses-addok-2A.ndjson.gz'
    )
    mocked_get.assert_any_call(
        'https://adresse.data.gouv.fr/data/ban/adresses/latest/addok/adresses-addok-2B.ndjson.gz'
    )
    assert StreetModel.objects.count() == 0


@pytest.mark.usefixtures('mock_update_api_geo')
@mock.patch('passerelle.utils.Request.get')
def test_base_adresse_command_update_multiple(mocked_get, db, base_adresse_multiple):
    base_adresse_multiple.update_api_geo_data = lambda: None
    filepath = os.path.join(os.path.dirname(__file__), 'data', 'update_streets_test.gz')
    with open(filepath, 'rb') as ban_file:
        mocked_get.return_value = tests.utils.FakedResponse(content=ban_file.read(), status_code=200)
    call_command('cron', 'daily')
    assert mocked_get.call_count == 4
    mocked_get.assert_any_call(
        'https://adresse.data.gouv.fr/data/ban/adresses/latest/addok/adresses-addok-73.ndjson.gz'
    )
    mocked_get.assert_any_call(
        'https://adresse.data.gouv.fr/data/ban/adresses/latest/addok/adresses-addok-974.ndjson.gz'
    )
    mocked_get.assert_any_call(
        'https://adresse.data.gouv.fr/data/ban/adresses/latest/addok/adresses-addok-2A.ndjson.gz'
    )
    mocked_get.assert_any_call(
        'https://adresse.data.gouv.fr/data/ban/adresses/latest/addok/adresses-addok-2B.ndjson.gz'
    )
    assert StreetModel.objects.count() == 5


def test_base_adresse_cities(app, base_adresse, city, miquelon, department, region):
    resp = app.get('/base-adresse/%s/cities?q=chambe' % base_adresse.slug)
    assert len(resp.json['data']) == 1
    result = resp.json['data'][0]
    assert result['name'] == city.name
    assert result['text'] == '%s %s' % (city.zipcode, city.name)
    assert result['code'] == city.code
    assert result['zipcode'] == city.zipcode
    assert result['id'] == '%s.%s' % (city.code, city.zipcode)
    assert result['population'] == city.population
    assert result['region_code'] == city.region.code
    assert result['region_name'] == city.region.name
    assert result['department_code'] == city.department.code
    assert result['department_name'] == city.department.name

    resp = app.get('/base-adresse/%s/cities?q=73' % base_adresse.slug)
    assert len(resp.json['data']) == 1
    assert resp.json['data'][0] == result

    resp = app.get('/base-adresse/%s/cities?code=73065' % base_adresse.slug)
    assert len(resp.json['data']) == 1
    assert resp.json['data'][0] == result

    resp = app.get('/base-adresse/%s/cities?code=73065,97501,75056' % base_adresse.slug)
    assert len(resp.json['data']) == 2
    assert resp.json['data'][0] == result
    assert resp.json['data'][1]['name'] == 'Miquelon-Langlade'


def test_base_adresse_cities_missing_region_and_department(app, base_adresse, miquelon):
    resp = app.get('/base-adresse/%s/cities?q=miqu' % base_adresse.slug)
    result = resp.json['data'][0]
    assert result['name'] == miquelon.name
    assert not result['department_code']
    assert not result['region_code']
    assert not result['department_name']
    assert not result['region_name']


def test_base_adresse_cities_dash_in_q(app, base_adresse, miquelon):
    resp = app.get('/base-adresse/%s/cities?q=miquelon-langlad' % base_adresse.slug)
    result = resp.json['data'][0]
    assert result['name'] == miquelon.name


def test_base_adresse_cities_region_department(app, base_adresse, miquelon, city):
    reg = RegionModel.objects.create(name='IdF', code='11', resource=base_adresse)
    dep = DepartmentModel.objects.create(name='Paris', code='75', region=reg, resource=base_adresse)
    CityModel.objects.create(
        name='Paris',
        code='75056',
        zipcode='75014',
        population=2000000,
        region=reg,
        department=dep,
        resource=base_adresse,
    )

    resp = app.get('/base-adresse/%s/cities?department_code=73' % base_adresse.slug)
    result = resp.json['data']
    assert len(result) == 1
    assert result[0]['name'] == city.name

    resp = app.get('/base-adresse/%s/cities?region_code=84' % base_adresse.slug)
    result = resp.json['data']
    assert len(result) == 1
    assert result[0]['name'] == city.name

    resp = app.get('/base-adresse/%s/cities?region_code=84&department_code=75' % base_adresse.slug)
    result = resp.json['data']
    assert not result


def test_base_adresse_cities_sort_order(app, base_adresse, miquelon, city):
    assert miquelon.population < city.population
    resp = app.get('/base-adresse/%s/cities' % base_adresse.slug)
    result = resp.json['data']
    assert result[0]['name'] == city.name
    assert result[1]['name'] == miquelon.name


def test_base_adresse_cities_get_by_id(app, base_adresse, city):
    for i in range(1, 10):
        # create additional cities
        city.pk = None
        city.zipcode = int(city.zipcode) + i
        city.save()

    resp = app.get('/base-adresse/%s/cities?q=cham' % base_adresse.slug)
    result = resp.json['data'][0]
    assert len(resp.json['data']) == 10
    city_id = result['id']

    resp = app.get('/base-adresse/%s/cities?id=%s' % (base_adresse.slug, city_id))
    assert len(resp.json['data']) == 1
    result2 = resp.json['data'][0]
    assert result2['text'] == result['text']

    # non integer id.
    resp = app.get('/base-adresse/%s/cities?id=%s' % (base_adresse.slug, 'XXX'))
    assert resp.json['err'] == 1

    # integer but without match.
    resp = app.get('/base-adresse/%s/cities?id=%s' % (base_adresse.slug, '1.1'))
    assert len(resp.json['data']) == 0


def test_base_adresse_departments(app, base_adresse, department, region):
    resp = app.get('/base-adresse/%s/departments?q=sav' % base_adresse.slug)
    result = resp.json['data'][0]
    assert result['name'] == department.name
    assert result['code'] == department.code
    assert result['id'] == department.code
    assert result['text'] == '%s %s' % (department.code, department.name)
    assert result['region_code'] == region.code
    assert result['region_name'] == region.name

    resp = app.get('/base-adresse/%s/departments?q=73' % base_adresse.slug)
    result = resp.json['data'][0]
    assert result['name'] == department.name

    resp = app.get('/base-adresse/%s/departments?id=%s' % (base_adresse.slug, department.code))
    result = resp.json['data'][0]
    assert result['name'] == department.name


def test_base_adresse_departments_region(app, base_adresse, department):
    reg = RegionModel.objects.create(name='IdF', code='11', resource=base_adresse)
    DepartmentModel.objects.create(name='Paris', code='75', region=reg, resource=base_adresse)

    resp = app.get('/base-adresse/%s/departments?region_code=84' % base_adresse.slug)
    result = resp.json['data']
    assert len(result) == 1
    assert result[0]['name'] == department.name


def test_base_adresse_regions(app, base_adresse, region):
    resp = app.get('/base-adresse/%s/regions?q=au' % base_adresse.slug)
    result = resp.json['data'][0]
    assert result['name'] == region.name
    assert result['code'] == region.code
    assert result['id'] == region.code
    assert result['text'] == '%s %s' % (region.code, region.name)

    resp = app.get('/base-adresse/%s/regions?id=%s' % (base_adresse.slug, region.code))
    result = resp.json['data'][0]
    assert result['name'] == region.name


@pytest.mark.usefixtures('mock_update_streets')
@mock.patch('passerelle.utils.Request.get')
def test_base_adresse_command_update_geo(mocked_get, db, base_adresse, base_adresse_97x):
    return_values = [
        tests.utils.FakedResponse(content=content, status_code=200)
        for content in (FAKE_API_GEO_REGIONS, FAKE_API_GEO_DEPARTMENTS, FAKE_API_GEO) * 2
    ]
    mocked_get.side_effect = return_values
    call_command('cron', 'daily')
    assert mocked_get.call_count == 6  # 3 downloads * 2 BaseAdresse instances
    mocked_get.assert_any_call(urljoin(base_adresse.api_geo_url, 'communes'))
    mocked_get.assert_any_call(urljoin(base_adresse.api_geo_url, 'regions'))
    mocked_get.assert_any_call(urljoin(base_adresse.api_geo_url, 'departements'))

    for resource in (base_adresse, base_adresse_97x):
        regions = resource.regionmodel_set
        assert regions.count() == 2
        idf = regions.get(name='Île-de-France')
        assert idf.code == '11'
        centre = regions.get(name='Bourgogne-Franche-Comté')
        assert centre.code == '27'

        departments = resource.departmentmodel_set
        assert departments.count() == 2
        paris_dep = departments.get(name='Paris')
        assert paris_dep.code == '75'
        assert paris_dep.region == idf
        nievre = departments.get(name='Nièvre')
        assert nievre.code == '58'
        assert nievre.region == centre

        cities = resource.citymodel_set
        assert cities.count() == 3
        paris = cities.get(zipcode='75001')
        assert paris.name == 'Paris'
        assert paris.code == '75056'
        assert paris.population == 2190327
        assert paris.department.code == '75'
        assert paris.region.code == '11'

        paris2 = cities.get(zipcode='75002')
        paris_json = paris.to_json()
        for key, value in paris2.to_json().items():
            if key not in ['id', 'text', 'zipcode']:
                assert paris_json[key] == value

        miquelon = cities.get(zipcode='97500')
        assert miquelon.name == 'Miquelon-Langlade'
        assert miquelon.code == '97501'
        assert miquelon.population == 596
        assert not miquelon.department
        assert not miquelon.region

    assert CityModel.objects.count() == 6
    assert DepartmentModel.objects.count() == 4
    assert RegionModel.objects.count() == 4

    # check a new call downloads again
    mocked_get.side_effect = return_values
    call_command('cron', 'daily')
    assert mocked_get.call_count == 12
    # and doesn't delete anything
    assert CityModel.objects.count() == 6
    assert DepartmentModel.objects.count() == 4
    assert RegionModel.objects.count() == 4


@pytest.mark.usefixtures('mock_update_streets')
@mock.patch('passerelle.utils.Request.get')
def test_base_adresse_command_update_geo_delete(mocked_get, db, base_adresse):
    return_values = [
        tests.utils.FakedResponse(content=content, status_code=200)
        for content in (FAKE_API_GEO_REGIONS, FAKE_API_GEO_DEPARTMENTS, FAKE_API_GEO)
    ]
    mocked_get.side_effect = return_values
    call_command('cron', 'daily')
    assert CityModel.objects.count() == 3

    new_fake_api_geo = json.dumps([FAKE_API_GEO_LIST[1]])
    return_values = [
        tests.utils.FakedResponse(content=content, status_code=200)
        for content in (FAKE_API_GEO_REGIONS, FAKE_API_GEO_DEPARTMENTS, new_fake_api_geo)
    ]
    mocked_get.side_effect = return_values
    call_command('cron', 'daily')
    assert CityModel.objects.count() == 1


@pytest.mark.usefixtures('mock_update_streets')
@mock.patch('passerelle.utils.Request.get')
def test_base_adresse_command_job_update_geo(mocked_get, db, base_adresse):
    return_values = [
        tests.utils.FakedResponse(content=content, status_code=200)
        for content in (FAKE_API_GEO_REGIONS, FAKE_API_GEO_DEPARTMENTS, FAKE_API_GEO)
    ]
    mocked_get.side_effect = return_values
    # check the job added at save() downloads data
    base_adresse.jobs()
    assert mocked_get.call_count == 3
    assert CityModel.objects.count() == 3
    # second save doesn't download anything
    base_adresse.save()
    base_adresse.jobs()
    assert mocked_get.call_count == 3


@pytest.mark.usefixtures('mock_update_streets')
@mock.patch('passerelle.utils.Request.get')
def test_base_adresse_command_update_geo_invalid(mocked_get, db, base_adresse):
    mocked_get.return_value = tests.utils.FakedResponse(content='{}', status_code=200)
    with pytest.raises(CommandError):
        call_command('cron', 'daily')
    assert mocked_get.call_count == 1
    assert not RegionModel.objects.exists()

    mocked_get.return_value = tests.utils.FakedResponse(content=FAKE_API_GEO, status_code=500)
    call_command('cron', 'daily')
    assert mocked_get.call_count == 4
    assert not RegionModel.objects.exists()

    mocked_get.return_value = tests.utils.FakedResponse(content='not-json', status_code=200)
    call_command('cron', 'daily')
    assert mocked_get.call_count == 7
    assert not RegionModel.objects.exists()


@pytest.mark.usefixtures('mock_update_streets')
@mock.patch('passerelle.utils.Request.get')
def test_base_adresse_command_update_geo_region_not_found(mocked_get, db, base_adresse):
    new_fake_api_geo_regions = json.dumps([json.loads(FAKE_API_GEO_REGIONS)[1]])
    return_values = [
        tests.utils.FakedResponse(content=content, status_code=200)
        for content in (
            new_fake_api_geo_regions,  # first call, get regions
            FAKE_API_GEO_DEPARTMENTS,  # then, get departments
            FAKE_API_GEO,  # and get communes
        )
    ] + [
        # region code 11 not found, try to get it again
        tests.utils.FakedResponse(content='not-json', status_code=200),
        tests.utils.FakedResponse(content='not-json', status_code=200),
        tests.utils.FakedResponse(content='not-json', status_code=200),
    ]
    mocked_get.side_effect = return_values
    call_command('cron', 'daily')
    assert mocked_get.call_args_list == [
        mock.call('https://geo.api.gouv.fr/regions'),
        mock.call('https://geo.api.gouv.fr/departements'),
        mock.call('https://geo.api.gouv.fr/communes'),
        mock.call('https://geo.api.gouv.fr/regions/11'),
        mock.call('https://geo.api.gouv.fr/regions/11'),
        mock.call('https://geo.api.gouv.fr/regions/11'),
    ]
    assert RegionModel.objects.count() == 1
    assert RegionModel.objects.get().code == '27'
    assert DepartmentModel.objects.count() == 1
    assert DepartmentModel.objects.get().code == '58'


@pytest.mark.usefixtures('mock_update_api_geo')
@mock.patch('passerelle.utils.Request.get', side_effect=ConnectionError)
def test_base_adresse_command_update_street_timeout(mocked_get, db, base_adresse):
    call_command('cron', 'daily')
    assert mocked_get.call_count == 1
    assert not RegionModel.objects.exists()


@pytest.mark.usefixtures('mock_update_streets')
@mock.patch('passerelle.utils.Request.get', side_effect=ConnectionError)
def test_base_adresse_command_update_geo_no_connection(mocked_get, db, base_adresse):
    call_command('cron', 'daily')
    assert mocked_get.call_count == 3
    assert not RegionModel.objects.exists()


@mock.patch('passerelle.utils.Request.get')
def test_base_adresse_addresses(mocked_get, app, base_adresse):
    endpoint = tests.utils.generic_endpoint_url('base-adresse', 'addresses', slug=base_adresse.slug)
    assert endpoint == '/base-adresse/base-adresse/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'] == '47.474633'
    assert data['lon'] == '-0.593775'
    assert data['display_name'] == 'Rue Roger Halope 49000 Angers'
    assert data['text'] == 'Rue Roger Halope 49000 Angers'
    assert data['id'] == '49007_6950_be54bd~47.474633~-0.593775~Rue Roger Halope 49000 Angers'
    assert data['address']['city'] == 'Angers'
    assert data['address']['postcode'] == '49000'
    assert data['address']['citycode'] == '49007'
    assert data['address']['road'] == 'Rue Roger Halope'
    assert data['ban_id'] == '49007_6950_be54bd'
    assert data['extra']['info1'] == 'xxx'
    assert data['extra']['info2'] == 'yyy'


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

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

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


@mock.patch('passerelle.utils.Request.get')
def test_base_adresse_addresses_qs_citycode(mocked_get, app, base_adresse):
    app.get('/base-adresse/%s/addresses?q=plop&citycode=31555' % base_adresse.slug)
    assert 'citycode=31555' in mocked_get.call_args[0][0]


@mock.patch('passerelle.utils.Request.get')
def test_base_adresse_addresses_qs_coordinates(mocked_get, app, base_adresse_coordinates):
    app.get('/base-adresse/%s/addresses?q=plop' % base_adresse_coordinates.slug)
    assert 'lat=%s' % base_adresse_coordinates.latitude in mocked_get.call_args[0][0]
    assert 'lon=%s' % base_adresse_coordinates.longitude in mocked_get.call_args[0][0]

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


def test_base_adresse_addresses_cache(app, base_adresse, mock_api_adresse_data_gouv_fr_search, caplog):
    resp = app.get('/base-adresse/%s/addresses?q=plop' % base_adresse.slug)
    assert mock_api_adresse_data_gouv_fr_search.call['count'] == 1

    data = resp.json['data'][0]
    assert data['text'] == 'Rue Roger Halope 49000 Angers'

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

    resp = app.get('/base-adresse/%s/addresses?id=%s' % (base_adresse.slug, api_id))
    assert mock_api_adresse_data_gouv_fr_search.call['count'] == 1  # no new call
    assert data['text'] == 'Rue Roger Halope 49000 Angers'
    assert 'address' in data

    resp = app.get('/base-adresse/%s/addresses?q=plop' % base_adresse.slug)
    assert AddressCacheModel.objects.count() == 1  # no new object has been created
    assert mock_api_adresse_data_gouv_fr_search.call['count'] == 2

    # no cache
    AddressCacheModel.objects.all().delete()
    resp = app.get('/base-adresse/%s/addresses?id=%s' % (base_adresse.slug, api_id))
    assert AddressCacheModel.objects.count() == 1
    assert mock_api_adresse_data_gouv_fr_search.call['count'] == 3
    assert data['text'] == 'Rue Roger Halope 49000 Angers'
    assert 'address' in data

    # no cache and id has changed
    AddressCacheModel.objects.all().delete()
    api_id = '49007_XXXX_be54bd~47.474633~-0.593775~Rue%20Roger%20Halope%2049000%20Angers'
    resp = app.get('/base-adresse/%s/addresses?id=%s' % (base_adresse.slug, api_id))
    assert resp.json['err'] == 'Address ID not found'


def test_base_adresse_addresses_cache_err(app, base_adresse, mock_api_adresse_data_gouv_fr_search):
    resp = app.get('/base-adresse/%s/addresses?id=%s' % (base_adresse.slug, 'wrong_id'))
    assert mock_api_adresse_data_gouv_fr_search.call['count'] == 0
    assert 'err' in resp.json


@pytest.mark.usefixtures('mock_update_api_geo', 'mock_update_streets')
def test_base_adresse_addresses_clean_cache(app, base_adresse, freezer, mock_api_adresse_data_gouv_fr_search):
    app.get('/base-adresse/%s/addresses?q=plop' % base_adresse.slug)
    assert AddressCacheModel.objects.count() == 1

    freezer.move_to(datetime.timedelta(minutes=30))
    call_command('cron', 'hourly')
    assert AddressCacheModel.objects.count() == 1

    freezer.move_to(datetime.timedelta(minutes=30, seconds=1))
    call_command('cron', 'hourly')
    assert AddressCacheModel.objects.count() == 0

    app.get('/base-adresse/%s/addresses?q=plop' % base_adresse.slug)
    assert AddressCacheModel.objects.count() == 1

    # asking for the address again resets the timestamp
    freezer.move_to(datetime.timedelta(hours=1, seconds=1))
    app.get('/base-adresse/%s/addresses?q=plop' % base_adresse.slug)
    call_command('cron', 'hourly')
    assert AddressCacheModel.objects.count() == 1

    freezer.move_to(datetime.timedelta(hours=1, seconds=1))
    app.get(
        '/base-adresse/%s/addresses?id=%s'
        % (base_adresse.slug, '49007_6950_be54bd~47.474633~-0.593775~Rue%20Roger%20Halope%2049000%20Angers')
    )
    call_command('cron', 'hourly')
    assert AddressCacheModel.objects.count() == 1


@mock.patch('passerelle.utils.Request.get')
def test_base_adresse_addresses_data_change(mocked_get, app, base_adresse):
    endpoint = tests.utils.generic_endpoint_url('base-adresse', 'addresses', slug=base_adresse.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']['label'] = '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 resp.json['data'][0]['text'] == 'changed'


def test_base_adresse_reverse_cache(
    app, base_adresse, freezer, mock_api_adresse_data_gouv_fr_reverse, mock_api_adresse_data_gouv_fr_search
):
    assert AddressCacheModel.objects.count() == 0
    resp = app.get('/base-adresse/%s/reverse?lon=-0.593775&lat=47.474633' % base_adresse.slug)
    assert mock_api_adresse_data_gouv_fr_reverse.call['count'] == 1

    data = resp.json
    assert data['text'] == 'Rue Roger Halope 49000 Angers'

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

    resp = app.get('/base-adresse/%s/addresses?id=%s' % (base_adresse.slug, api_id))
    data = resp.json['data'][0]
    assert mock_api_adresse_data_gouv_fr_search.call['count'] == 0
    assert data['text'] == 'Rue Roger Halope 49000 Angers'
    assert 'address' in data

    # check caching timestamp update
    freezer.move_to(datetime.timedelta(hours=1, seconds=1))
    resp = app.get('/base-adresse/%s/reverse?lon=-0.593775&lat=47.474633' % base_adresse.slug)
    assert mock_api_adresse_data_gouv_fr_reverse.call['count'] == 2
    assert AddressCacheModel.objects.get().timestamp > first_timestamp

    # check lookup id is kept
    resp = app.get('/base-adresse/%s/addresses?id=%s' % (base_adresse.slug, api_id.lower()))
    data = resp.json['data'][0]
    assert mock_api_adresse_data_gouv_fr_search.call['count'] == 0
    assert data['id'] != api_id
    assert data['id'] == api_id.lower()

    # without cache
    assert AddressCacheModel.objects.all().delete()
    resp = app.get('/base-adresse/%s/addresses?id=%s' % (base_adresse.slug, api_id.lower()))
    data = resp.json['data'][0]
    assert mock_api_adresse_data_gouv_fr_search.call['count'] == 1
    assert data['id'] != api_id
    assert data['id'] == api_id.lower()
