import json
import time
from http import HTTPStatus
from threading import Thread
from typing import Union, List, Optional
from unittest import TestCase
from uuid import uuid4

import requests
from requests.auth import HTTPBasicAuth

from openbrokerapi import api, errors
from openbrokerapi.catalog import ServicePlan
from openbrokerapi.service_broker import (
    ServiceBroker,
    Service,
    ProvisionDetails,
    ProvisionedServiceSpec,
    ProvisionState,
    LastOperation,
    OperationState,
    DeprovisionDetails,
    DeprovisionServiceSpec,
    BindDetails,
    Binding,
    BindState,
    UnbindDetails,
    UnbindSpec,
    GetInstanceDetailsSpec,
    GetBindingSpec)


class FullBrokerTestCase(TestCase):

    def setUp(self) -> None:
        broker_username = str(uuid4())
        broker_passsword = str(uuid4())
        self.request_ads = {
            'auth': HTTPBasicAuth(broker_username, broker_passsword),
            'headers': {'X-Broker-Api-Version': '2.15', 'Content-Type': 'application/json'}
        }

        self.service_guid = str(uuid4())
        self.plan_guid = str(uuid4())
        self.broker = InMemoryBroker(self.service_guid, self.plan_guid)

        def run_server():
            api.serve(self.broker, api.BrokerCredentials(broker_username, broker_passsword), port=5001)

        # self.server = Process(target=run_server)
        self.server = Thread(target=run_server)
        self.server.setDaemon(True)
        self.server.start()
        time.sleep(2)

    def test_lifecycle(self):
        # GIVEN
        org_guid = str(uuid4())
        space_guid = str(uuid4())
        instace_guid = str(uuid4())
        binding_guid = str(uuid4())

        # CATALOG
        self.check_catalog(self.service_guid, self.plan_guid)

        # ASYNC PROVISION
        operation = self.check_provision(instace_guid, org_guid, space_guid, self.service_guid, self.plan_guid)
        self.check_last_operation_after_provision(instace_guid, operation)

        # GET INSTANCE
        self.check_instance_retrievable(instace_guid)

        # ASYNC BIND
        operation = self.check_bind(binding_guid, instace_guid)
        self.check_last_operation_after_bind(binding_guid, instace_guid, operation)

        # GET BINDING
        response = requests.get(
            "http://localhost:5001/v2/service_instances/{}/service_bindings/{}".format(instace_guid, binding_guid),
            **self.request_ads)
        self.assertEqual(HTTPStatus.OK, response.status_code)
        self.assertDictEqual({}, response.json())

        # ASYNC UNBIND
        operation = self.check_unbind(binding_guid, instace_guid)
        self.check_last_operation_after_unbind(binding_guid, instace_guid, operation)

        # ASYNC DEPROVISION
        operation = self.check_deprovision(instace_guid, operation)
        self.check_last_operation_after_deprovision(instace_guid, operation)

        # DEPROVISION TWICE
        self.check_deprovision_after_deprovision_done(instace_guid)

    def check_instance_retrievable(self, instace_guid):
        response = requests.get(
            "http://localhost:5001/v2/service_instances/{}".format(instace_guid), **self.request_ads)
        self.assertEqual(HTTPStatus.OK, response.status_code)
        self.assertEqual(self.service_guid, response.json()['service_id'])
        self.assertEqual(self.plan_guid, response.json()['plan_id'])

    def check_unbind(self, binding_guid, instace_guid):
        response = requests.delete(
            "http://localhost:5001/v2/service_instances/{}/service_bindings/{}".format(instace_guid, binding_guid),
            params={
                "service_id": self.service_guid,
                "plan_id": self.plan_guid,
                'accepts_incomplete': 'true'
            },
            **self.request_ads
        )
        self.assertEqual(HTTPStatus.ACCEPTED, response.status_code)
        operation = response.json().get('operation')
        self.assertEqual('unbind', operation)
        return operation

    def check_last_operation_after_bind(self, binding_guid, instace_guid, operation):
        response = requests.get(
            'http://localhost:5001/v2/service_instances/{}/service_bindings/{}/last_operation'.format(instace_guid,
                                                                                                      binding_guid),
            params={
                'service_id': self.service_guid,
                'plan_id': self.plan_guid,
                'operation': operation,
            },
            **self.request_ads)
        self.assertEqual(HTTPStatus.OK, response.status_code)
        self.assertEqual('succeeded', response.json()['state'])

    def check_last_operation_after_unbind(self, binding_guid, instace_guid, operation):
        response = requests.get(
            'http://localhost:5001/v2/service_instances/{}/service_bindings/{}/last_operation'.format(instace_guid,
                                                                                                      binding_guid),
            params={
                'service_id': self.service_guid,
                'plan_id': self.plan_guid,
                'operation': operation,
            },
            **self.request_ads)
        self.assertEqual(HTTPStatus.OK, response.status_code)
        self.assertEqual('succeeded', response.json()['state'])

    def check_bind(self, binding_guid, instace_guid):
        response = requests.put(
            "http://localhost:5001/v2/service_instances/{}/service_bindings/{}?accepts_incomplete=true".format(
                instace_guid, binding_guid),
            data=json.dumps({
                "service_id": self.service_guid,
                "plan_id": self.plan_guid
            }),
            **self.request_ads
        )
        self.assertEqual(HTTPStatus.ACCEPTED, response.status_code)
        operation = response.json().get('operation')
        self.assertEqual('bind', operation)
        return operation

    def check_deprovision_after_deprovision_done(self, instace_guid):
        response = requests.delete(
            "http://localhost:5001/v2/service_instances/{}".format(instace_guid),
            params={
                'service_id': self.service_guid,
                'plan_id': self.plan_guid,
                'accepts_incomplete': 'true'
            },
            **self.request_ads)
        self.assertEqual(HTTPStatus.GONE, response.status_code)

    def check_deprovision(self, instace_guid, operation):
        response = requests.delete(
            "http://localhost:5001/v2/service_instances/{}".format(instace_guid),
            params={
                'service_id': self.service_guid,
                'plan_id': self.plan_guid,
                'accepts_incomplete': 'true'
            },
            **self.request_ads)
        self.assertEqual(HTTPStatus.ACCEPTED, response.status_code)
        operation = response.json()['operation']
        self.assertEqual('deprovision', operation)
        return operation

    def check_last_operation_after_deprovision(self, instace_guid, operation):
        response = requests.get(
            "http://localhost:5001/v2/service_instances/{}/last_operation".format(instace_guid),
            params={
                'service_id': self.service_guid,
                'plan_id': self.plan_guid,
                'operation': operation
            },
            **self.request_ads)
        self.assertEqual(HTTPStatus.GONE, response.status_code)
        self.assertEqual('succeeded', response.json()['state'])

    def check_last_operation_after_provision(self, instace_guid, operation):
        response = requests.get(
            "http://localhost:5001/v2/service_instances/{}/last_operation".format(instace_guid),
            params={
                'service_id': self.service_guid,
                'plan_id': self.plan_guid,
                'operation': operation
            },
            **self.request_ads)
        self.assertEqual(HTTPStatus.OK, response.status_code)
        self.assertEqual('succeeded', response.json()['state'])

    def check_provision(self, instace_guid, org_guid, space_guid, service_guid, plan_guid):
        response = requests.put(
            "http://localhost:5001/v2/service_instances/{}?accepts_incomplete=true".format(instace_guid),
            data=json.dumps({
                "organization_guid": org_guid,
                "space_guid": space_guid,
                "service_id": service_guid,
                "plan_id": plan_guid,
                # "context": {
                #     "organization_guid": "org-guid-here",
                #     "space_guid": "space-guid-here",
                # }
            }),
            **self.request_ads)
        self.assertEqual(HTTPStatus.ACCEPTED, response.status_code)

        operation = response.json().get('operation')
        self.assertEqual('provision', operation)

        return operation

    def check_catalog(self, service_guid, plan_guid):
        response = requests.get('http://localhost:5001/v2/catalog', **self.request_ads)
        catalog = response.json()
        self.assertEqual(HTTPStatus.OK, response.status_code)
        # find service
        for service in catalog['services']:
            if service['name'] == 'InMemService':
                break
        else:
            service = None
        self.assertIsNotNone(service)
        self.assertEqual(service_guid, service.get('id'))
        self.assertTrue(service.get('instances_retrievable'))
        self.assertTrue(service.get('bindings_retrievable'))

        # find plan
        for plan in service['plans']:
            if plan['name'] == 'standard':
                break
        else:
            plan = None
        self.assertIsNotNone(plan)
        self.assertEqual(plan_guid, plan.get('id'))


class InMemoryBroker(ServiceBroker):
    CREATING = 'CREATING'
    CREATED = 'CREATED'
    BINDING = 'BINDING'
    BOUND = 'BOUND'
    UNBINDING = 'UNBINDING'
    DELETING = 'DELETING'

    def __init__(self, service_guid, plan_guid):
        self.service_guid = service_guid
        self.plan_guid = plan_guid

        self.service_instances = dict()

    def catalog(self) -> Union[Service, List[Service]]:
        return Service(
            id=self.service_guid,
            name='InMemService',
            description='InMemService',
            bindable=True,
            plans=[
                ServicePlan(
                    id=self.plan_guid,
                    name='standard',
                    description='standard plan',
                    free=False,
                )
            ],
            instances_retrievable=True,
            bindings_retrievable=True
        )

    def provision(self,
                  instance_id: str,
                  details: ProvisionDetails,
                  async_allowed: bool,
                  **kwargs) -> ProvisionedServiceSpec:
        if not async_allowed:
            raise errors.ErrAsyncRequired()

        self.service_instances[instance_id] = {
            'provision_details': details,
            'state': self.CREATING
        }

        return ProvisionedServiceSpec(
            state=ProvisionState.IS_ASYNC,
            operation='provision'
        )

    def bind(self, instance_id: str, binding_id: str, details: BindDetails, async_allowed: bool, **kwargs) -> Binding:
        if not async_allowed:
            raise errors.ErrAsyncRequired()

        instance = self.service_instances.get(instance_id, {})
        if instance and instance.get('state') == self.CREATED:
            instance['state'] = self.BINDING
            return Binding(BindState.IS_ASYNC, operation='bind')

    def unbind(self, instance_id: str, binding_id: str, details: UnbindDetails, async_allowed: bool,
               **kwargs) -> UnbindSpec:
        if not async_allowed:
            raise errors.ErrAsyncRequired()

        instance = self.service_instances.get(instance_id, {})
        if instance and instance.get('state') == self.BOUND:
            instance['state'] = self.UNBINDING
            return UnbindSpec(True, 'unbind')

    def deprovision(self, instance_id: str, details: DeprovisionDetails, async_allowed: bool,
                    **kwargs) -> DeprovisionServiceSpec:
        if not async_allowed:
            raise errors.ErrAsyncRequired()

        instance = self.service_instances.get(instance_id)
        if instance is None:
            raise errors.ErrInstanceDoesNotExist()

        if instance.get('state') == self.CREATED:
            instance['state'] = self.DELETING
            return DeprovisionServiceSpec(True, 'deprovision')

    def last_operation(self, instance_id: str, operation_data: Optional[str], **kwargs) -> LastOperation:
        instance = self.service_instances.get(instance_id)
        if instance is None:
            raise errors.ErrInstanceDoesNotExist()

        if instance.get('state') == self.CREATING:
            instance['state'] = self.CREATED
            return LastOperation(OperationState.SUCCEEDED)
        elif instance.get('state') == self.DELETING:
            del self.service_instances[instance_id]
            raise errors.ErrInstanceDoesNotExist()

    def last_binding_operation(self,
                               instance_id: str,
                               binding_id: str,
                               operation_data: Optional[str],
                               **kwargs
                               ) -> LastOperation:
        instance = self.service_instances.get(instance_id, {})
        if instance.get('state') == self.BINDING:
            instance['state'] = self.BOUND
            return LastOperation(OperationState.SUCCEEDED)
        elif instance.get('state') == self.UNBINDING:
            instance['state'] = self.CREATED
            return LastOperation(OperationState.SUCCEEDED)

    def get_instance(self, instance_id: str, **kwargs) -> GetInstanceDetailsSpec:
        instance = self.service_instances.get(instance_id)
        if instance is None:
            raise errors.ErrInstanceDoesNotExist()

        return GetInstanceDetailsSpec(
            self.service_guid,
            self.plan_guid
        )

    def get_binding(self, instance_id: str, binding_id: str, **kwargs) -> GetBindingSpec:
        instance = self.service_instances.get(instance_id)
        if instance is None:
            raise errors.ErrInstanceDoesNotExist()

        if instance.get('state') == self.BOUND:
            return GetBindingSpec()