# 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
from toolz.dicttoolz import get_in, keyfilter, valmap
from voluptuous import ALLOW_EXTRA, Schema

import czc.smoke.token as token
from czc import json
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
