import json
import os
from multiprocessing.pool import ThreadPool
from unittest import mock
from xml.etree import ElementTree as etree

import pytest
from utils import FakedResponse, get_tests_file_content

from petale.models import CUT, Petal
from petale.utils import etag

pytestmark = pytest.mark.django_db


def test_authentication_failure(app):
    resp = app.get('/api/partner/12345abcde12345abcde12345abcde12/', status=401)
    json.loads(resp.text)


def test_resource_not_found(app, partner_southpark, cut_kevin_uuid, acl):
    app.authorization = ('Basic', ('family', 'family'))
    cut_uuid = 'abcd1234' * 31

    # test with invalid partner
    resp = app.get('/api/partner/%s/whatever/' % cut_uuid, status=403)
    assert resp.json['error'] == 'access-forbidden'

    # test with cut uuid with length over 255
    app.get('/api/southpark/%s11111111111/whatever/' % cut_uuid, status=404)

    # test with invalid id cut
    resp = app.get('/api/southpark/%s/invoices/' % cut_uuid, status=404)
    assert resp.json['error'] == 'cut-not-found'

    # test with invalid key
    url = '/api/southpark/%s/invoices/' % cut_kevin_uuid
    resp = app.get(url, status=404)
    assert resp.json['error'] == 'key-not-found'

    app.head(url, status=404)


def test_access_control_list(app, partner_southpark, cut_kevin_uuid, petal_books, petal_invoice, acl):
    app.authorization = ('Basic', ('arkham', 'arkham'))

    # test permission on requested partner
    resp = app.get('/api/southpark/%s/' % cut_kevin_uuid, status=200)

    # test permission on method
    app.authorization = ('Basic', ('library', 'library'))
    resp = app.put('/api/southpark/%s/books/' % cut_kevin_uuid, status=403)
    assert resp.json['error'] == 'access-forbidden'

    # test permission on key
    resp = app.get('/api/southpark/%s/invoices/' % cut_kevin_uuid, status=403)
    assert resp.json['error'] == 'access-forbidden'

    #
    app.authorization = ('Basic', ('library', 'library'))
    payload = json.loads(get_tests_file_content('books.json'))
    url = '/api/southpark/%s/loans/' % cut_kevin_uuid
    resp = app.put_json(url, params=payload, headers={'If-None-Match': '*'}, status=201)

    app.authorization = ('Basic', ('arkham', 'arkham'))
    resp = app.get(url, status=403)
    assert resp.json['error'] == 'access-forbidden'


def test_simple_api(app, partner_southpark, cut_kevin_uuid, acl, petal_invoice):
    app.authorization = ('Basic', ('library', 'library'))

    payload = json.loads(get_tests_file_content('books.json'))
    url = '/api/southpark/%s/loans/' % cut_kevin_uuid

    # test create key without content-type
    resp = app.put(
        url,
        params=json.dumps(payload),
        headers={
            'If-None-Match': '*',
            'Content-Type': '',
        },
        status=400,
    )
    assert resp.json['error'] == 'missing-content-type'

    # test create key with cut uuid length over 32
    app.put_json('/api/southaprk/%s12/whatever/' % cut_kevin_uuid, params=json.dumps(payload), status=404)

    # test create key
    resp = app.put_json(url, params=payload, headers={'If-None-Match': '*'}, status=201)
    assert resp.headers['ETag'] == etag(json.dumps(payload))
    cached_etag = resp.headers['Etag']

    # test head
    resp = app.head(url, status=200)
    assert resp.headers['ETag'] == etag(json.dumps(payload))
    assert resp.headers['Content-Type'] == 'application/json'
    assert resp.headers['Content-Length'] == '639'

    # test get key by sending etag
    resp = app.get(url, headers={'If-None-Match': cached_etag}, status=304)

    # test get key
    resp = app.get(url, status=200)
    assert resp.headers['Content-Length'] == '639'
    assert resp.headers['Content-Type'] == 'application/json'
    assert resp.json == payload

    cut_keys_url = '/api/southpark/%s/' % cut_kevin_uuid
    # test get all keys
    resp = app.head(cut_keys_url, status=405)

    resp = app.get(cut_keys_url, status=200)
    assert set(resp.json['keys']) == {'loans'}

    # test get all keys and prefix
    resp = app.get('%s?prefix=invoice' % cut_keys_url, status=200)
    assert resp.json['keys'] == []

    resp = app.get('%s?prefix=whatever' % cut_keys_url, status=200)
    assert resp.json['keys'] == []

    # test key deletion with If-Match
    resp = app.delete(url, status=412, headers={'If-Match': '"xxx"'})
    assert resp.json['error'] == 'concurrent-access'

    # test key deletion with cut uuid length over 32
    app.delete('/api/southpark/%s12/whatever/' % cut_kevin_uuid, status=404)

    # test key deletion
    resp = app.delete(url, status=204)
    assert resp.text == ''
    app.get(url, status=404)

    # test keys purge
    for idx in range(4):
        url = '/api/southpark/%s/loans-%d/' % (cut_kevin_uuid, idx)
        app.put_json(url, params=payload, headers={'If-None-Match': '*'}, status=201)

    resp = app.get('/api/southpark/%s/' % cut_kevin_uuid, status=200)
    assert len(resp.json['keys']) == 4
    app.delete('/api/southpark/%s/' % cut_kevin_uuid, status=204)
    resp = app.get('/api/southpark/%s/' % cut_kevin_uuid, status=200)
    assert len(resp.json['keys']) == 0


def test_binary_data(app, partner_southpark, cut_kevin_uuid, acl):
    app.authorization = ('Basic', ('library', 'library'))

    # test create xml data
    url = '/api/southpark/%s/profile-friends/' % cut_kevin_uuid
    content_type = 'text/xml'
    resp = app.put(
        url,
        params=get_tests_file_content('users.xml'),
        headers={'If-None-Match': '*', 'Content-Type': content_type},
        status=201,
    )
    resp = app.get(url)
    assert resp.headers.get('Content-Type') == content_type
    xml_data = etree.fromstring(resp.content)
    assert xml_data.tag == 'friends'
    assert len(list(xml_data)) == 4

    # test update by changing format
    payload = {'friends': [{'name': 'Token', 'age': 10}, {'name': 'Kenny', 'age': 10}]}
    resp = app.put_json(url, params=payload, status=200)
    resp = app.get(url, status=200)
    data = json.loads(resp.text)
    for datum in data['friends']:
        assert datum['name'] in ('Token', 'Kenny')
        assert datum['age'] == 10

    # test create binary data
    partner_southpark.hard_global_max_size = 1000000000
    partner_southpark.hard_per_key_max_size = 1000000000
    partner_southpark.save()
    url = '/api/southpark/%s/profile-picture/' % cut_kevin_uuid
    content_type = 'application/octet-stream'
    content = get_tests_file_content('fg.jpg') * 100
    resp = app.put(
        url, params=content, headers={'If-None-Match': '*', 'Content-Type': content_type}, status=201
    )
    resp = app.get(url)
    assert resp.headers.get('Content-Type') == content_type
    assert resp.content == content

    # test create binary data
    url = '/api/southpark/%s/profile-invoice/' % cut_kevin_uuid
    content_type = 'application/pdf'
    resp = app.put(
        url,
        params=get_tests_file_content('invoice.pdf'),
        headers={'If-None-Match': '*', 'Content-Type': content_type},
        status=201,
    )
    resp = app.get(url)
    assert resp.headers.get('Content-Type') == content_type


def test_caching(app, partner_southpark, cut_kevin_uuid, acl):
    payload = {
        'favourites': [
            {
                'name': 'bus stations',
                'url': 'https://southpark.com/bus/station',
                'items': ['33', '42'],
            },
            {
                'name': 'weather',
                'url': 'https://southpark.com/weather/',
            },
        ]
    }

    url = '/api/southpark/%s/favourites/' % cut_kevin_uuid
    app.authorization = ('Basic', ('cityhall', 'cityhall'))
    resp = app.put_json(url, params=payload, headers={'If-None-Match': '*'}, status=201)
    cache = resp.headers['ETag']

    # try to create the same data
    app.authorization = ('Basic', ('cityhall', 'cityhall'))
    resp = app.put_json(url, params=payload, headers={'If-None-Match': '*'}, status=412)
    data = json.loads(resp.text)
    assert resp.json['error'] == 'concurrent-access'

    # try to create by sending list of eatgs
    app.authorization = ('Basic', ('cityhall', 'cityhall'))
    etags = [
        '"sha1:5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9"',
        '"sha1:c75ac194bf3ed4ef3f3e14343585c2fd7ed9b06bbdfbf0eb9f817b7337b966ea"  ',
        '  "sha1:6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b"',
    ]
    etags.append(cache)
    resp = app.put_json(url, params=payload, headers={'If-None-Match': ','.join(etags)}, status=412)
    data = json.loads(resp.text)
    assert resp.json['error'] == 'concurrent-access'

    # update from the library service
    app.authorization = ('Basic', ('cityhall', 'cityhall'))
    resp = app.get(url, status=200)
    data = json.loads(resp.text)
    data['favourites'].append(
        {
            'name': 'best books',
            'url': 'https://southpark.com/library',
            'items': ['Guide To Life by E. Cartman', 'Gingers Have Sools by Kyle'],
        }
    )

    etag = resp.headers['Etag']
    resp = app.put_json(url, params=data, headers={'If-Match': etag}, status=200)

    # update attempts from the cityhall
    app.authorization = ('Basic', ('cityhall', 'cityhall'))
    new_item = {
        'name': 'Green zones',
        'url': 'https://southpark.com/parks',
        'items': ['Main Street', 'Emo Kids Street'],
    }
    payload['favourites'].append(new_item)
    resp = app.put_json(url, params=payload, headers={'If-Match': cache}, status=412)
    assert resp.json['error'] == 'concurrent-access'

    resp = app.get(url, status=200)
    data = json.loads(resp.text)
    data['favourites'].append(new_item)
    etag = resp.headers['Etag']
    resp = app.put_json(url, params=payload, headers={'If-Match': etag}, status=200)


def test_partner_size_limit(app, cut_kevin_uuid, acl, petal_invoice, petal_books, mailoutbox, partner_gotham):
    # gotham sizes:
    # Global hard max: 20Ko
    # Global soft max: 10Ko
    # Per key hard max: 2Ko
    # Per key soft max: 1Ko
    app.authorization = ('Basic', ('arkham', 'arkham'))

    # test sending data sized above per key hard max size
    url = '/api/gotham/%s/taxes-fail/' % cut_kevin_uuid
    resp = app.put(
        url,
        params='a' * 3 * 1024,  # 3ko
        headers={'If-None-Match': '*'},
        content_type='text/plain',
        status=500,
    )
    assert resp.json['error'] == 'key-space-exhausted'

    # test sending data sized within soft and hard key limit
    resp = app.put(
        url,
        params='a' * (1024 + 1),  # 1ko + 1
        headers={'If-None-Match': '*'},
        content_type='text/plain',
        status=201,
    )
    assert len(mailoutbox) == 2
    sent_mail = mailoutbox[0]
    assert sent_mail.to[0] == 'b.wayne@gotham.gov'
    assert 'taxes-fail' in sent_mail.subject
    assert 'gotham' in sent_mail.subject
    assert 'est épuis' in sent_mail.subject
    assert '3072' in sent_mail.message().as_string()
    assert '2048' in sent_mail.message().as_string()
    sent_mail = mailoutbox[1]
    assert sent_mail.to[0] == 'b.wayne@gotham.gov'
    assert 'taxes-fail' in sent_mail.subject
    assert 'gotham' in sent_mail.subject
    assert 'est presque épuis' in sent_mail.subject
    assert '1025' in sent_mail.message().as_string()
    assert '2048' in sent_mail.message().as_string()

    for i in range(18):
        url = '/api/gotham/%s/taxes-%d/' % (cut_kevin_uuid, i)
        app.put(url, params='a' * 1024, headers={'If-None-Match': '*'}, content_type='text/plain', status=201)

    assert len(mailoutbox) == 3
    sent_mail = mailoutbox[2]
    assert sent_mail.to[0] == 'b.wayne@gotham.gov'
    assert 'gotham' in sent_mail.subject
    assert '10241' in sent_mail.message().as_string()
    assert str(1024 * 20) in sent_mail.message().as_string()

    partner_gotham.refresh_from_db()
    assert partner_gotham.size == (1025 + 18 * 1024)

    url = '/api/gotham/%s/taxes-100/' % cut_kevin_uuid
    resp = app.put(
        url,
        params='a' * (1024 * 2 - 1),
        headers={'If-None-Match': '*'},
        content_type='text/plain',
        status=500,
    )
    assert resp.json['error'] == 'global-space-exhausted'

    partner_gotham.refresh_from_db()
    assert partner_gotham.size == (1025 + 18 * 1024)

    for petal in partner_gotham.petal_set.all():
        petal.delete()
    partner_gotham.refresh_from_db()
    assert partner_gotham.size == 0


def test_api_logging(caplog, app, cut_kevin_uuid, acl):
    app.authorization = ('Basic', ('library', 'library'))
    payload = {'friends': [{'name': 'Token', 'age': 10}, {'name': 'Kenny', 'age': 10}]}
    url = '/api/southpark/%s/profile-friends/' % cut_kevin_uuid
    resp = app.put_json(url, params=payload, status=201, headers={'If-None-Match': '*'})

    records = [record for record in caplog.records if record.name == 'petale']
    assert len(records) == 5
    for record in records:
        assert record.name == 'petale'
        if getattr(record, 'request_url', None):
            assert url in record.request_url
        if getattr(record, 'request_headers', None):
            assert 'http_if_none_match' in record.request_headers.lower()
        if getattr(record, 'request_body', None):
            assert 'Token' in record.request_body
        if getattr(record, 'response_headers', None):
            assert resp.headers['Etag'] in record.response_headers
        if getattr(record, 'response_body', None):
            assert '{}' in record.response_body
        if getattr(record, 'response_status_code', None):
            record.response_status_code == 201

    app.get('/api/southpark/%s/' % cut_kevin_uuid)


@mock.patch('petale.authentication.requests.post')
def test_idp_based_partner_authentication(mocked_post, app, cut_kevin_uuid, acl):
    app.authorization = ('Basic', ('library', 'whatever'))
    payload = {'friends': [{'name': 'Token', 'age': 10}, {'name': 'Kenny', 'age': 10}]}
    url = '/api/southpark/%s/profile-whatever/' % cut_kevin_uuid
    # failure
    response = {'result': 0, 'errors': ['Invalid username/password.']}
    mocked_post.return_value = FakedResponse(content=json.dumps(response))
    app.put_json(url, params=payload, status=401, headers={'If-None-Match': '*'})
    response = {'result': 1, 'errors': []}
    # test with RP
    client_id = client_secret = 'a1b2' * 8
    app.authorization = ('Basic', (client_id, client_secret))
    mocked_post.return_value = FakedResponse(content=json.dumps(response))
    app.put_json(url, params=payload, status=201, headers={'If-None-Match': '*'})
    # success
    app.authorization = ('Basic', ('library', 'whatever'))
    mocked_post.return_value = FakedResponse(content=json.dumps(response))
    url = '/api/southpark/%s/profile-whatever2/' % cut_kevin_uuid
    app.put_json(url, params=payload, status=201, headers={'If-None-Match': '*'})


@mock.patch('petale.api_views.requests.post')
def test_cut_uuid_idp_checking(mocked_post, settings, app, acl):
    cut_uuid = 'a1b2' * 8
    app.authorization = ('Basic', ('library', 'library'))
    payload = {'username': 't0k3n', 'email': 't0k3n@deadsec.org'}
    headers = {'Content-Type': 'application/json'}

    # failure with AUTHENTIC_PETALE improperly defined
    settings.PETALE_AUTHENTIC_URL = ''
    resp = app.put_json('/api/southpark/%s/profile/' % cut_uuid, params=payload, headers=headers, status=404)
    assert resp.json['error'] == 'cut-not-found'
    # failure when cut uuid doesn't exist on idp
    response = {'unknown_uuids': [cut_uuid], 'result': 1}
    mocked_post.return_value = FakedResponse(content=json.dumps(response), status_code=200)
    resp = app.put_json('/api/southpark/%s/profile/' % cut_uuid, params=payload, headers=headers, status=404)
    assert resp.json['error'] == 'cut-not-found'

    resp = app.get('/api/southpark/%s/' % cut_uuid, headers=headers, status=404)
    assert resp.json['error'] == 'cut-not-found'

    resp = app.get('/api/southpark/%s/profile/' % cut_uuid, headers=headers, status=404)
    assert resp.json['error'] == 'cut-not-found'

    # sucess
    settings.PETALE_AUTHENTIC_URL = 'http://example.net/idp/'
    response = {'unknown_uuids': [], 'result': 1}
    mocked_post.return_value = FakedResponse(content=json.dumps(response), status_code=200)

    resp = app.get('/api/southpark/%s/profile/' % cut_uuid, headers=headers, status=404)
    assert resp.json['error'] == 'key-not-found'

    resp = app.get('/api/southpark/%s/' % cut_uuid, headers=headers)
    assert resp.json['keys'] == []

    resp = app.put_json('/api/southpark/%s/profile/' % cut_uuid, params=payload, headers=headers, status=201)
    assert CUT.objects.get(uuid=cut_uuid)
    assert Petal.objects.get(name='profile', cut__uuid=cut_uuid, partner__name='southpark')


def test_storage_error(app, partner_southpark, cut_kevin_uuid, acl, caplog):
    app.authorization = ('Basic', ('library', 'library'))

    payload = json.loads(get_tests_file_content('books.json'))
    url = '/api/southpark/%s/loans/' % cut_kevin_uuid

    app.put_json(url, params=payload, headers={'If-None-Match': '*'}, status=201)

    os.unlink(Petal.objects.get().data.path)

    app.get(url, status=500)


def test_concurrent_put(app, transactional_db, settings):
    '''Test concurrent PUT to the same key'''
    from utils import create_acl_record, create_cut, create_partner, create_service

    uuid = create_cut('a' * 255).uuid
    southpark = create_partner('southpark', hg=20240, hk=19728)
    library = create_service('library')
    create_acl_record(1, southpark, library, 'loans', methods='GET,HEAD,PUT,DELETE')

    app.authorization = ('Basic', ('library', 'library'))

    payload = json.loads(get_tests_file_content('books.json'))
    url = '/api/%s/%s/loans/' % (southpark.name, uuid)

    pool_count = 5
    put_count = 100

    response = app.put_json(url, params=payload)
    assert response.status_code == 201

    def f(i):
        from django.db import connection

        if i % 2 == 0:
            response = app.get(url, status=200)
        else:
            response = app.put_json(url, params=payload, status=200)
        connection.close()
        return i

    with ThreadPool(pool_count) as pool:
        results = pool.map(f, range(put_count))

    assert len(results) == put_count
    assert sum(len(files) for _, _, files in os.walk(settings.MEDIA_ROOT)) == 1
