# Copyright (c) 2020 CloudZero - ALL RIGHTS RESERVED - PROPRIETARY AND CONFIDENTIAL
# Unauthorized copying of this file and/or project, via any medium is strictly prohibited.
# Direct all questions to legal@cloudzero.com

from collections import abc
from collections.abc import Mapping
from pprint import pformat
from typing import Any

import boto3
import pytest
import requests
import simplejson as json
from toolz.dicttoolz import get_in, keyfilter, valmap
from voluptuous import ALLOW_EXTRA, Schema

import czc.smoke.token as token
from czc.constants import FEATURE_NAME
from czc.internal_api.client import RemoteApiError
from czc.logging import get_logger
from czc.smoke import api_key

logger = get_logger()

lba = boto3.client('lambda')


class ApiTestRunner:
    _namespace: str
    _resolved_test: Mapping
    _org_id: str
    _user_id: str
    _path: str
    _result: Any
    _token: Any
    _state: Any

    @property
    def namespace(self):
        return self._namespace

    @property
    def org_id(self):
        return self._org_id

    @property
    def user_id(self):
        return self._user_id

    @property
    def token(self):
        return self._token

    @property
    def state(self):
        return self._state

    def __init__(self, namespace, credentials, configuration, test, state=None) -> None:
        if namespace in test.get('exclude_namespaces', []):
            pytest.skip(f'Not running test for namespace {namespace}')
        super().__init__()
        self._state = state
        self._namespace = namespace
        username = test['user']
        user = (username, credentials['password'])
        self._token = token.fetch(namespace, credentials['tenant'], user)
        self._org_id, self._user_id = token.token_to_ids(credentials['tenant'][0], self._token)
        self._resolved_test = self._resolve(test)
        logger.debug(pformat(configuration))
        self._path = (
            get_in(['endpoints', self._resolved_test['endpoint'], 'path'], configuration)
            if 'endpoint' in self._resolved_test
            else ''
        )
        self._result = self._resolved_test['result']

    def _resolve(self, test):
        def f(x):
            if isinstance(x, abc.Mapping):
                return self._resolve(x)
            if callable(x) and ('voluptuous' not in str(type(x))):
                return x(self)
            return x

        return valmap(f, test)

    def _validate_response(self, response):
        logger.debug(f'response_dict {pformat(response)}')
        logger.debug(f'result {pformat(self._result)}')
        s = Schema(self._result, extra=ALLOW_EXTRA)
        s(response)


class PublicApiTestRunner(ApiTestRunner):
    _url: str
    _path_parameters: Mapping
    _method: str
    _request_kwargs: Mapping
    _use_api_key: bool = False
    _api_key: Any

    def __init__(self, namespace, credentials, configuration, test, public_url, state=None, use_api_key=False) -> None:
        super().__init__(namespace, credentials, configuration, test, state)
        path_parameters = {
            'organizationId': self._org_id,
            'userId': self._user_id,
            **self._resolved_test.get('pathParameters', {}),
        }
        self._url = f'{public_url.rstrip("/")}/{self._path.format(**path_parameters).lstrip("/")}'
        self._method = get_in(['endpoints', self._resolved_test['endpoint'], 'method'], configuration)
        self._request_kwargs = keyfilter(lambda x: x in {'json', 'body', 'params'}, self._resolved_test)
        self._api_key = api_key.fetch(namespace, self._org_id) if use_api_key else None

    def run(self):
        headers = {
            'Authorization': self._api_key or f'Bearer {self._token}',
            'Origin': 'https://smoke-test.cloudzero.com',
        }
        logger.debug(f'Sending {self._method} request {self._url} with {pformat(self._request_kwargs)}')
        response_body = {}
        response = requests.request(self._method, self._url, headers=headers, **self._request_kwargs, timeout=20)
        try:
            response_body = response.json()
        except Exception:
            response_body = response.text
        response_dict = {
            'statusCode': response.status_code,
            'body': response_body,
        }

        self._validate_response(response_dict)
        return response_dict


class PrivateApiTestRunner(ApiTestRunner):
    _request_kwargs: Mapping

    def __init__(self, namespace, credentials, configuration, test, state=None) -> None:
        super().__init__(namespace, credentials, configuration, test, state)
        self._request_kwargs = self._resolved_test['params']

    def run(self):
        if 'cz_organization_id' in self._request_kwargs:
            self._request_kwargs['cz_organization_id'] = self._request_kwargs['cz_organization_id'].format(
                cz_organization_id=self._org_id
            )

        logger.debug(f'Calling lambda {self._path} with {pformat(self._request_kwargs)}')

        invoke_response = lba.invoke(
            FunctionName=self._path.format(Namespace=self._namespace, FeatureName=FEATURE_NAME),
            InvocationType='RequestResponse',
            Payload=json.dumps(self._request_kwargs),
        )

        response = {
            'statusCode': invoke_response['StatusCode'],
            'body': json.loads(invoke_response['Payload'].read().decode('utf-8')),
        }

        self._validate_response(response)
        return response


class AppSyncApiTestRunner(ApiTestRunner):
    _url: str
    _path_parameters: Mapping
    _method: str
    _request_kwargs: Mapping

    def get_org_id(self):
        # Currently the base ApiTestRunner resolves the tests BEFORE getting the login token
        # but some tests need the org Id when resolving. So the work around for now is to
        # catch the exception, return a pointer to the function and then re-run resolve on
        # the "_request_kwargs" before executing the tests. When this gets moved to czc,
        # we can address the base class issue to avoid this problem.
        #
        # This fix is very specific to appsync tests that need the org ID in the body of the
        # request.
        try:
            return self.org_id
        except AttributeError:
            return AppSyncApiTestRunner.get_org_id

    def __init__(self, namespace, credentials, configuration, test, appsync_api_url, state=None) -> None:
        super().__init__(namespace, credentials, configuration, test, state)

        self._url = f'{appsync_api_url.rstrip("/")}'
        self._path = ''
        self._method = get_in(['endpoints', self._resolved_test['endpoint'], 'method'], configuration)
        self._request_kwargs = keyfilter(lambda x: x in {'json', 'body', 'params'}, self._resolved_test)

    def run(self):
        headers = {
            'Authorization': self._token,
            'Origin': 'https://smoke-test.cloudzero.com',
        }
        logger.debug(f'Sending {self._method} request {self._url} with {pformat(self._request_kwargs)}')
        response_body = {}
        # As noted above, here is where we re-resolve the kwargs.
        resolved_kwargs = self._resolve(self._request_kwargs)
        response = requests.request(self._method, self._url, headers=headers, **resolved_kwargs, timeout=20)
        try:
            response_body = response.json()
        except Exception:
            response_body = response.text
        response_dict = {
            'statusCode': response.status_code,
            'body': response_body,
        }

        self._validate_response(response_dict)
        return response_dict


class IapiClientTestRunner(ApiTestRunner):
    _request_kwargs: Mapping

    def __init__(self, namespace, credentials, configuration, test, state=None) -> None:
        super().__init__(namespace, credentials, configuration, test, state)
        self._request_kwargs = self._resolved_test['params']

    def run(self, test_call):
        if 'cz_organization_id' in self._request_kwargs:
            self._request_kwargs['cz_organization_id'] = self._request_kwargs['cz_organization_id'].format(
                cz_organization_id=self._org_id
            )

        logger.debug(f'Calling lambda {self._path} with {pformat(self._request_kwargs)}')
        try:
            invoke_response = test_call(self.org_id, self._request_kwargs)
        except RemoteApiError as api_error:
            invoke_response = {'error': api_error.error}
        self._validate_response(invoke_response)
        return invoke_response
