import pytest
from math import ceil
from aiohttp import request
from unittest.mock import Mock, patch
from urllib.parse import parse_qs, urlparse

from fhirpy import AsyncFHIRClient
from fhirpy.base.utils import AttrDict
from fhirpy.lib import AsyncFHIRResource, AsyncFHIRReference
from fhirpy.base.exceptions import (
    ResourceNotFound, OperationOutcome, MultipleResourcesFound
)
from .config import FHIR_SERVER_URL, FHIR_SERVER_AUTHORIZATION


class TestLibAsyncCase:
    URL = FHIR_SERVER_URL
    client = None
    identifier = [{'system': 'http://example.com/env', 'value': 'fhirpy'}]

    @classmethod
    def get_search_set(cls, resource_type):
        return cls.client.resources(resource_type).search(
            **{'identifier': 'fhirpy'}
        )

    @pytest.fixture(autouse=True)
    @pytest.mark.asyncio
    async def clearDb(self):
        for resource_type in ['Patient', 'Practitioner']:
            search_set = self.get_search_set(resource_type)
            async for item in search_set:
                await item.delete()

    @classmethod
    def setup_class(cls):
        cls.client = AsyncFHIRClient(
            cls.URL, authorization=FHIR_SERVER_AUTHORIZATION
        )

    async def create_resource(self, resource_type, **kwargs):
        p = self.client.resource(
            resource_type, identifier=self.identifier, **kwargs
        )
        await p.save()

        return p

    @pytest.mark.asyncio
    async def test_create_patient(self):
        await self.create_resource(
            'Patient', id='patient', name=[{
                'text': 'My patient'
            }]
        )

        patient = await self.client.resources('Patient') \
            .search(_id='patient').get()
        assert patient['name'] == [{'text': 'My patient'}]

    @pytest.mark.asyncio
    async def test_update_patient(self):
        patient = await self.create_resource(
            'Patient', id='patient', name=[{
                'text': 'My patient'
            }]
        )
        patient['active'] = True
        patient.birthDate = '1945-01-12'
        patient.name[0].text = 'SomeName'
        await patient.save()

        check_patient = await self.client.resources('Patient') \
            .search(_id='patient').get()
        assert check_patient.active is True
        assert check_patient['birthDate'] == '1945-01-12'
        assert check_patient.get_by_path(['name', 0, 'text']) == 'SomeName'

    @pytest.mark.asyncio
    async def test_count(self):
        search_set = self.get_search_set('Patient')

        assert await search_set.count() == 0

        await self.create_resource(
            'Patient', id='patient1', name=[{
                'text': 'John Smith FHIRPy'
            }]
        )

        assert await search_set.count() == 1

    @pytest.mark.asyncio
    async def test_create_without_id(self):
        patient = await self.create_resource('Patient')

        assert patient.id is not None

    @pytest.mark.asyncio
    async def test_delete(self):
        patient = await self.create_resource('Patient', id='patient')
        await patient.delete()

        with pytest.raises(ResourceNotFound):
            await self.get_search_set('Patient').search(_id='patient').get()

    @pytest.mark.asyncio
    async def test_get_not_existing_id(self):
        with pytest.raises(ResourceNotFound):
            await self.client.resources('Patient') \
                .search(_id='FHIRPypy_not_existing_id').get()

    @pytest.mark.asyncio
    async def test_get_more_than_one_resources(self):
        await self.create_resource('Patient', birthDate='1901-05-25')
        await self.create_resource('Patient', birthDate='1905-05-25')
        with pytest.raises(MultipleResourcesFound):
            await self.client.resources('Patient').get()
        with pytest.raises(MultipleResourcesFound):
            await self.client.resources('Patient') \
                .search(birthdate__gt='1900').get()

    @pytest.mark.asyncio
    async def test_get_resource_by_id_is_deprecated(self):
        await self.create_resource('Patient', id='patient', gender='male')
        with pytest.warns(DeprecationWarning):
            patient = await self.client.resources('Patient') \
                .search(gender='male').get(id='patient')
        assert patient.id == 'patient'

    @pytest.mark.asyncio
    async def test_get_resource_by_search_with_id(self):
        await self.create_resource('Patient', id='patient', gender='male')
        patient = await self.client.resources('Patient') \
            .search(gender='male', _id='patient').get()
        assert patient.id == 'patient'
        with pytest.raises(ResourceNotFound):
            await self.client.resources('Patient') \
                .search(gender='female', _id='patient').get()

    @pytest.mark.asyncio
    async def test_get_resource_by_search(self):
        await self.create_resource(
            'Patient', id='patient1', gender='male', birthDate='1901-05-25'
        )
        await self.create_resource(
            'Patient', id='patient2', gender='female', birthDate='1905-05-25'
        )
        patient_1 = await self.client.resources('Patient') \
            .search(gender='male', birthdate='1901-05-25').get()
        assert patient_1.id == 'patient1'
        patient_2 = await self.client.resources('Patient') \
            .search(gender='female', birthdate='1905-05-25').get()
        assert patient_2.id == 'patient2'

    @pytest.mark.asyncio
    async def test_not_found_error(self):
        with pytest.raises(ResourceNotFound):
            await self.client.resources('FHIRPyNotExistingResource').fetch()

    @pytest.mark.asyncio
    async def test_operation_outcome_error(self):
        with pytest.raises(OperationOutcome):
            await self.create_resource('Patient', name='invalid')

    @pytest.mark.asyncio
    async def test_to_resource_for_local_reference(self):
        await self.create_resource('Patient', id='p1', name=[{'text': 'Name'}])

        patient_ref = self.client.reference('Patient', 'p1')
        result = (await patient_ref.to_resource()).serialize()
        result.pop('meta')
        result.pop('identifier')

        assert result == {
            'resourceType': 'Patient',
            'id': 'p1',
            'name': [{
                'text': 'Name'
            }]
        }

    @pytest.mark.asyncio
    async def test_to_resource_for_external_reference(self):
        reference = self.client.reference(
            reference='http://external.com/Patient/p1'
        )

        with pytest.raises(ResourceNotFound):
            await reference.to_resource()

    @pytest.mark.asyncio
    async def test_to_resource_for_resource(self):
        resource = self.client.resource(
            'Patient', id='p1', name=[{
                'text': 'Name'
            }]
        )
        resource_copy = await resource.to_resource()
        assert isinstance(resource_copy, AsyncFHIRResource)
        assert resource_copy.serialize() == {
            'resourceType': 'Patient',
            'id': 'p1',
            'name': [{
                'text': 'Name'
            }]
        }

    def test_to_reference_for_resource_without_id(self):
        resource = self.client.resource('Patient')
        with pytest.raises(ResourceNotFound):
            resource.to_reference()

    @pytest.mark.asyncio
    async def test_to_reference_for_resource(self):
        patient = await self.create_resource('Patient', id='p1')

        assert patient.to_reference().serialize() == \
            {'reference': 'Patient/p1'}

        assert patient.to_reference(display='patient').serialize() == {
            'reference': 'Patient/p1',
            'display': 'patient',
        }

    @pytest.mark.asyncio
    async def test_create_bundle(self):
        bundle = {
            'resourceType':
                'bundle',
            'type':
                'transaction',
            'entry':
                [
                    {
                        'request': {
                            'method': 'POST',
                            'url': '/Patient'
                        },
                        'resource':
                            {
                                'id': 'bundle_patient_1',
                                'identifier': self.identifier,
                            }
                    },
                    {
                        'request': {
                            'method': 'POST',
                            'url': '/Patient'
                        },
                        'resource':
                            {
                                'id': 'bundle_patient_2',
                                'identifier': self.identifier,
                            }
                    },
                ],
        }
        await self.create_resource('Bundle', **bundle)
        await self.client.resources('Patient').search(
            _id='bundle_patient_1'
        ).get()
        await self.client.resources('Patient').search(
            _id='bundle_patient_2'
        ).get()

    @pytest.mark.asyncio
    async def test_is_valid(self):
        resource = self.client.resource
        assert await resource('Patient', id='id123').is_valid() is True
        assert await resource('Patient', gender='female') \
            .is_valid(raise_exception=True) is True

        assert await resource('Patient', gender=True).is_valid() is False
        with pytest.raises(OperationOutcome):
            await resource('Patient', gender=True) \
                .is_valid(raise_exception=True)

        assert await resource('Patient', gender='female', custom_prop='123') \
            .is_valid() is False
        with pytest.raises(OperationOutcome):
            await resource('Patient', gender='female', custom_prop='123') \
                .is_valid(raise_exception=True)

        assert await resource('Patient', gender='female', custom_prop='123') \
            .is_valid() is False
        with pytest.raises(OperationOutcome):
            await resource('Patient', birthDate='date', custom_prop='123', telecom=True) \
                .is_valid(raise_exception=True)

    @pytest.mark.asyncio
    async def test_get_first(self):
        await self.create_resource(
            'Patient', id='patient_first', name=[{
                'text': 'Abc'
            }]
        )
        await self.create_resource(
            'Patient', id='patient_second', name=[{
                'text': 'Bbc'
            }]
        )
        patient = await self.client.resources('Patient').sort('name').first()
        assert isinstance(patient, AsyncFHIRResource)
        assert patient.id == 'patient_first'

    @pytest.mark.asyncio
    async def test_fetch_raw(self):
        await self.create_resource('Patient', name=[{'text': 'RareName'}])
        await self.create_resource('Patient', name=[{'text': 'RareName'}])
        bundle = await self.client.resources('Patient').search(
            name='RareName').fetch_raw()
        assert bundle.resourceType == 'Bundle'
        for entry in bundle.entry:
            assert isinstance(entry.resource, AsyncFHIRResource)
        assert len(bundle.entry) == 2

    async def create_test_patients(self, count=10, name='Not Rare Name'):
        bundle = {
            'type': 'transaction',
            'entry': [],
        }
        patient_ids = set()
        for i in range(count):
            p_id = f'patient-{i}'
            patient_ids.add(p_id)
            bundle['entry'].append(
                {
                    'request': {
                        'method': 'POST',
                        'url': '/Patient'
                    },
                    'resource':
                        {
                            'id': p_id,
                            'name': [{
                                'text': f'{name}{i}'
                            }],
                            'identifier': self.identifier
                        }
                }
            )
        await self.create_resource('Bundle', **bundle)
        return patient_ids

    @pytest.mark.asyncio
    async def test_fetch_all(self):
        patients_count = 18
        name = 'Jack Johnson J'
        patient_ids = await self.create_test_patients(patients_count, name)
        patient_set = self.client.resources('Patient') \
            .search(name=name) \
            .limit(5)

        mocked_request = Mock(wraps=request)
        with patch('aiohttp.request', mocked_request):
            patients = await patient_set.fetch_all()

        received_ids = set(p.id for p in patients)

        assert len(received_ids) == patients_count
        assert patient_ids == received_ids

        assert mocked_request.call_count == ceil(patients_count / 5)

        first_call_args = mocked_request.call_args_list[0][0]
        first_call_url = list(first_call_args)[1]
        parsed = urlparse(first_call_url)
        params = parse_qs(parsed.query)
        path = parsed.path
        assert '/Patient' in path
        assert params == {
            'name': [name],
            '_format': ['json'],
            '_count': ['5']
        }

    @pytest.mark.asyncio
    async def test_async_for_iterator(self):
        patients_count = 22
        name = 'Rob Robinson R'
        patient_ids = await self.create_test_patients(patients_count, name)
        patient_set = self.client.resources('Patient') \
            .search(name=name) \
            .limit(3)

        received_ids = set()
        mocked_request = Mock(wraps=request)
        with patch('aiohttp.request', mocked_request):
            async for patient in patient_set:
                received_ids.add(patient.id)

        assert mocked_request.call_count == ceil(patients_count / 3)

        assert len(received_ids) == patients_count
        assert patient_ids == received_ids

    def test_build_request_url(self):
        url = f'{FHIR_SERVER_URL}/Patient?_count=100&name=ivan&name=petrov'
        request_url = self.client._build_request_url(url, None)
        assert request_url == url

    def test_build_request_url_wrong_path(self):
        url = f'https://example.com/Patient?_count=100&name=ivan&name=petrov'
        with pytest.raises(ValueError):
            self.client._build_request_url(url, None)

    @pytest.mark.asyncio
    async def test_save_fields(self):
        patient = await self.create_resource(
            'Patient', id='patient_to_update',
            gender='female',
            active=False,
            birthDate='1998-01-01',
            name=[{'text': 'Abc'}]
        )
        patient['gender'] = 'male'
        patient['birthDate'] = '1998-02-02'
        patient['active'] = True
        patient['name'] = [{'text': 'Bcd'}]
        await patient.save(fields=['gender', 'birthDate'])

        patient_refreshed = await patient.to_reference().to_resource()
        assert patient_refreshed['gender'] == patient['gender']
        assert patient_refreshed['birthDate'] == patient['birthDate']
        assert patient_refreshed['active'] is False
        assert patient_refreshed['name'] == [{'text': 'Abc'}]

    @pytest.mark.asyncio
    async def test_update(self):
        patient = await self.create_resource(
            'Patient', id='patient_to_update',
            name=[{'text': 'J London'}],
            active=False
        )
        new_name = [{
            'text': 'Jack London',
            'family': 'London',
            'given': ['Jack'],
        }]
        await patient.update(active=True, name=new_name)
        patient_refreshed = await patient.to_reference().to_resource()
        assert patient_refreshed.serialize() == patient.serialize()
        assert patient['name'] == new_name
        assert patient['active'] is True

    @pytest.mark.asyncio
    async def test_update_without_id(self):
        patient = self.client.resource(
            'Patient',
            identifier=self.identifier,
            name=[{'text': 'J London'}])
        new_name = [{
            'text': 'Jack London',
            'family': 'London',
            'given': ['Jack'],
        }]
        with pytest.raises(TypeError):
            await patient.update(active=True, name=new_name)
        with pytest.raises(TypeError):
            patient['name'] = new_name
            await patient.save(fields=['name'])
        await patient.save()

    @pytest.mark.asyncio
    async def test_refresh(self):
        patient_id = 'refresh-patient-id'
        patient = await self.create_resource('Patient', id=patient_id, active=True)

        test_patient = await self.client.reference('Patient', patient_id).to_resource()
        await test_patient.update(gender='male', name=[{'text': 'Jack London'}])
        assert patient.serialize() != test_patient.serialize()

        await patient.refresh()
        assert patient.serialize() == test_patient.serialize()

    @pytest.mark.asyncio
    async def test_client_execute_lastn(self):
        patient = await self.create_resource(
            'Patient', name=[{
                'text': 'John First'
            }]
        )
        observation = await self.create_resource(
            'Observation',
            status='registered',
            subject=patient,
            category=[
                {
                    'coding':
                        [
                            {
                                'code':
                                    'vital-signs',
                                'system':
                                    'http://terminology.hl7.org/CodeSystem/observation-category',
                                'display':
                                    'Vital Signs',
                            }
                        ]
                }
            ],
            code={
                'coding': [{
                    'code': '10000-8',
                    'system': 'http://loinc.org'
                }]
            }
        )
        response = await self.client.execute(
            'Observation/$lastn',
            method='get',
            params={
                'patient': f'Patient/{patient.id}',
                'category': 'vital-signs'
            }
        )
        assert response['resourceType'] == 'Bundle'
        assert response['total'] == 1
        assert response['entry'][0]['resource']['id'] == observation['id']

    @pytest.mark.asyncio
    async def test_resource_execute_lastn(self):
        patient = await self.create_resource(
            'Patient', name=[{
                'text': 'John First'
            }]
        )
        observation = await self.create_resource(
            'Observation',
            status='registered',
            subject=patient,
            category=[
                {
                    'coding':
                        [
                            {
                                'code':
                                    'vital-signs',
                                'system':
                                    'http://terminology.hl7.org/CodeSystem/observation-category',
                                'display':
                                    'Vital Signs',
                            }
                        ]
                }
            ],
            code={
                'coding': [{
                    'code': '10000-8',
                    'system': 'http://loinc.org'
                }]
            }
        )
        response = await patient.execute(
            'Observation/$lastn',
            method='get',
            params={'category': 'vital-signs'}
        )
        assert response['resourceType'] == 'Bundle'
        assert response['total'] == 1
        assert response['entry'][0]['resource']['id'] == observation['id']

    @pytest.mark.asyncio
    async def test_client_execute_history(self):
        patient = await self.create_resource(
            'Patient', name=[{
                'text': 'John First'
            }]
        )
        response = await self.client.execute(
            f'Patient/{patient.id}/_history', 'get'
        )
        assert response['resourceType'] == 'Bundle'
        assert response['type'] == 'history'
        assert 'entry' in response

    @pytest.mark.asyncio
    async def test_resource_execute_history(self):
        patient = await self.create_resource(
            'Patient', name=[{
                'text': 'John First'
            }]
        )
        response = await patient.execute('_history', 'get')
        assert response['resourceType'] == 'Bundle'
        assert response['type'] == 'history'
        assert response['total'] == 1
        assert 'entry' in response

    @pytest.mark.asyncio
    async def test_reference_execute_history(self):
        patient = await self.create_resource(
            'Patient', name=[{
                'text': 'John First'
            }]
        )
        patient_ref = patient.to_reference()
        response = await patient_ref.execute('_history', 'get')
        assert response['resourceType'] == 'Bundle'
        assert response['type'] == 'history'
        assert response['total'] == 1
        assert 'entry' in response

    @pytest.mark.asyncio
    async def test_reference_execute_history_not_local(self):
        patient_ref = self.client.reference(reference='http://external.com/Patient/p1')
        with pytest.raises(ResourceNotFound):
            await patient_ref.execute('_history', 'get')

    @pytest.mark.asyncio
    async def test_references_after_save(self):
        patient = await self.create_resource(
            'Patient', name=[{
                'text': 'John First'
            }]
        )
        practitioner = await self.create_resource(
            'Practitioner', name=[{
                'text': 'Jack'
            }]
        )
        appointment = self.client.resource(
            "Appointment",
            **{
                "status": "booked",
                "participant": [
                    {"actor": patient, "status": "accepted"},
                    {"actor": practitioner, "status": "accepted"},
                ],
            },
        )
        await appointment.save()
        assert isinstance(appointment.participant[0].actor, AsyncFHIRReference)
        assert isinstance(appointment.participant[0], AttrDict)
        test_patient = await appointment.participant[0].actor.to_resource()
        assert test_patient

        assert isinstance(appointment.participant[1].actor, AsyncFHIRReference)
        assert isinstance(appointment.participant[1], AttrDict)
        test_practitioner = await appointment.participant[1].actor.to_resource()
        assert test_practitioner

    @pytest.mark.asyncio
    async def test_references_in_resource(self):
        patient = await self.create_resource(
            'Patient', name=[{
                'text': 'John First'
            }]
        )
        practitioner = await self.create_resource(
            'Practitioner', name=[{
                'text': 'Jack'
            }]
        )
        appointment = self.client.resource(
            "Appointment",
            **{
                "status": "booked",
                "participant": [
                    {"actor": patient, "status": "accepted"},
                    {"actor": practitioner, "status": "accepted"},
                ],
            },
        )
        await appointment.save()
        test_appointment = await self.client.resources('Appointment') \
            .search(_id=appointment.id).get()

        assert isinstance(test_appointment.participant[0].actor, AsyncFHIRReference)
        assert isinstance(test_appointment.participant[0], AttrDict)
        test_patient = await test_appointment.participant[0].actor.to_resource()
        assert test_patient

        assert isinstance(test_appointment.participant[1].actor, AsyncFHIRReference)
        assert isinstance(test_appointment.participant[1], AttrDict)
        test_practitioner = await test_appointment.participant[1].actor.to_resource()
        assert test_practitioner