# 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

import freezegun
import pytest
import simplejson as json
from czt.unittest import Context, MockEx
from voluptuous import Invalid

from czc.audit import AUDIT_DATA_KEY, PUBLISH_AUDIT_DATA
from tests.sample_data import (ORGANIZATION_ID, USER_ID,
                               get_programmatic_api_test_event, get_test_event)

GIT_SHA = 'g368g91b1011g86473dd4751d859f39ad4f754cf'
TEST_EXTERNAL_CHANGE_DATA = {
    'url': 'http://www.github.com',
    'id': GIT_SHA
}


def _get_test_response_data(publish_audit_data=True):
    return {
        'data': 'some data',
        AUDIT_DATA_KEY: {
            PUBLISH_AUDIT_DATA: publish_audit_data,
            'version': '1'
        }
    }


def _get_test_iapi_request(
        external_change_data=TEST_EXTERNAL_CHANGE_DATA):
    return {
        AUDIT_DATA_KEY: {
            'cz_organization_id': ORGANIZATION_ID,
            'user_id': 'dev@cloudzero.com',
            'external_change_data': external_change_data
        }
    }


@pytest.fixture(scope='function')
def context(mocker, environ, lambda_context):
    import czc.audit as audit
    with Context(mocker, audit, modules_to_mock=[
        'event_bridge.events_client.put_events', 'sentry'
    ], extra={
        'audit': audit,
        'lambda_context': lambda_context
    }) as context:
        yield context


def _assert_sentry(test_context):
    exception = "required key not provided @ data['audit_data']['publish_audit_data']"
    decorated_lambda = test_context.lambda_context.invoked_function_arn
    tags = {'alarm_name': 'invalid-audited-lambda-response'}

    assert MockEx(test_context.mock_sentry.send).was_called_once_with({'exception': exception,
                                                                       'decorated_lambda': decorated_lambda},
                                                                      tags=tags
                                                                      )


def _assert_entries(context, external_change_data,
                    internal_change_data,
                    user_id=USER_ID,
                    cz_organization_id=ORGANIZATION_ID,
                    event_body=False, called=True):

    if called:
        def get_internal_change_data():
            if event_body:
                return {'event_body': json.loads(internal_change_data)}
            return internal_change_data

        expected_detail = {
            'timestamp': '1987-12-24T00:00:00+00:00',
            'feature_name': 'czc',
            'namespace': 'test-ns',
            'event_name': 'audit-event-configuration-czc-some-config',
            'user_id': user_id,
            'cz_organization_id': cz_organization_id,
            'publisher': context.lambda_context.invoked_function_arn,
            'target': 'some-config',
            'external_change_data': external_change_data,
            'internal_change_data': get_internal_change_data()
        }

        expected_entries = [{
            'Source': 'cz.audit.configuration',
            'DetailType': 'Czc Some Config',
            'EventBusName': 'cz-test-ns-events-primary',
            'Detail': json.dumps(expected_detail)
        }]

        assert MockEx(context.mock_event_bridge_events_client_put_events).was_called_once_with(Entries=expected_entries)
    else:
        assert context.mock_event_bridge_events_client_put_events.call_args_list == []


@pytest.mark.unit
@pytest.mark.parametrize('lambda_return_value,internal_change_data,external_change_data,expected_response', [
    pytest.param(_get_test_response_data(),
                 {'version': '1'}, TEST_EXTERNAL_CHANGE_DATA,
                 {'data': 'some data'},
                 id='happy_path'),
    pytest.param(_get_test_response_data(publish_audit_data=False),
                 {'version': '1'}, TEST_EXTERNAL_CHANGE_DATA,
                 {'data': 'some data'},
                 id='do_not_publish_event'),
    pytest.param(Exception, None,
                 TEST_EXTERNAL_CHANGE_DATA,
                 None,
                 id='raises_general_exception')
])
@freezegun.freeze_time('1987-12-24')
def test_iapi_audit_event_publisher(context, lambda_return_value,
                                    internal_change_data, external_change_data,
                                    expected_response):

    @context.audit.iapi_audit_event_publisher(target='some_config')
    def iapi_handler(event, context, **kwargs):
        if isinstance(lambda_return_value, dict):
            return lambda_return_value
        else:
            raise lambda_return_value

    test_event = _get_test_iapi_request(
        external_change_data=external_change_data)

    if lambda_return_value == Exception:
        with pytest.raises(Exception):
            iapi_handler(test_event, context.lambda_context)
    else:
        response = iapi_handler(test_event, context.lambda_context)
        assert response == expected_response

        _assert_entries(context, external_change_data,
                        internal_change_data, user_id='dev@cloudzero.com',
                        called=lambda_return_value['audit_data']['publish_audit_data'])


@pytest.mark.unit
@pytest.mark.parametrize('lambda_return_value,internal_change_data,external_change_data,expected_response', [
    pytest.param(_get_test_response_data(),
                 {'version': '1'}, None,
                 {'data': 'some data'},
                 id='happy_path'),
    pytest.param(_get_test_response_data(publish_audit_data=False),
                 {'version': '1'}, TEST_EXTERNAL_CHANGE_DATA,
                 {'data': 'some data'},
                 id='do_not_publish_event'),
    pytest.param(Exception,
                 json.dumps({'config': 'new-config'}), None,
                 None,
                 id='handler_raises_general_exception'),
])
@freezegun.freeze_time('1987-12-24')
def test_public_api_audit_event_publisher(context, lambda_return_value,
                                          internal_change_data, external_change_data,
                                          expected_response):

    @context.audit.public_api_audit_event_publisher(target='some-config')
    def public_api_handler(event, context, **kwargs):
        if isinstance(lambda_return_value, dict):
            return lambda_return_value
        else:
            raise lambda_return_value

    test_event = get_test_event()
    test_event['body'] = internal_change_data

    event_body = True
    if lambda_return_value == Exception:
        with pytest.raises(Exception):
            public_api_handler(test_event, context.lambda_context)
    else:
        response = public_api_handler(test_event, context.lambda_context)
        event_body = False
        assert response == expected_response

        _assert_entries(context, external_change_data,
                        internal_change_data,
                        event_body=event_body,
                        called=lambda_return_value['audit_data']['publish_audit_data'])


@pytest.mark.unit
@pytest.mark.parametrize('test_event,lambda_return_value,sentry_published,raised_exception', [
    pytest.param({'audit_data': {}},
                 _get_test_response_data(),
                 False,
                 Invalid,
                 id='invalid_request'),
    pytest.param(_get_test_iapi_request(),
                 {'audit_data': {'some_key': '3'}},
                 True,
                 None,
                 id='invalid_response'),
])
@freezegun.freeze_time('1987-12-24')
def test_iapi_audit_event_publisher_invalid_data(context, test_event,
                                                 lambda_return_value, sentry_published,
                                                 raised_exception):

    @context.audit.iapi_audit_event_publisher(target='some-config')
    def iapi_handler(event, context, **kwargs):
        return lambda_return_value

    if raised_exception:
        with pytest.raises(raised_exception):
            iapi_handler(test_event, context.lambda_context)
    else:
        iapi_handler(test_event, context.lambda_context)
        _assert_sentry(context)

    _assert_entries(context, None,
                    None, called=False)


@pytest.mark.unit
@pytest.mark.parametrize('lambda_return_value,sentry_published,raised_exception', [
    pytest.param({'audit_data': {'some_key': '3'}},
                 True,
                 None,
                 id='invalid_response'),
])
@freezegun.freeze_time('1987-12-24')
def test_public_api_audit_event_publisher_invalid_data(context,
                                                       lambda_return_value, sentry_published,
                                                       raised_exception):

    @context.audit.public_api_audit_event_publisher(target='some-config')
    def public_api_handler(event, context, **kwargs):
        return lambda_return_value

    test_event = get_test_event()
    if raised_exception:
        with pytest.raises(raised_exception):
            public_api_handler(test_event, context.lambda_context)
    else:
        public_api_handler(test_event, context.lambda_context)
        _assert_sentry(context)

    _assert_entries(context, None,
                    None, called=False)


@pytest.mark.unit
@freezegun.freeze_time('1987-12-24')
def test_public_api_audit_event_publisher_fails(context, caplog):
    @context.audit.public_api_audit_event_publisher(target='some-config')
    def public_api_handler(event, context, **kwargs):
        return _get_test_response_data()

    test_event = get_test_event()
    context.mock_event_bridge_events_client_put_events.side_effect = Exception

    with caplog.at_level('INFO'):
        response = public_api_handler(test_event, context.lambda_context)

    assert response == {'data': 'some data'}
    assert "Failed to publish audit event" in caplog.text


@pytest.mark.unit
@freezegun.freeze_time('1987-12-24')
@pytest.mark.parametrize('changes,valid', [
    pytest.param({
        'external_change_data': TEST_EXTERNAL_CHANGE_DATA,
        'internal_change_data': {'version': '1'}
    }, True, id='both_change_data'),
    pytest.param({
        'external_change_data': None,
        'internal_change_data': {'version': '1'}
    }, True, id='internal_change_data'),
    pytest.param({
        'external_change_data': TEST_EXTERNAL_CHANGE_DATA,
        'internal_change_data': None
    }, True, id='external_change_data'),
    pytest.param({
        'external_change_data': None,
        'internal_change_data': None
    }, False, id='neither_change_data'),
])
def test_publish_audit_event(context, changes, valid):
    detail = {
        'user_id': USER_ID,
        'cz_organization_id': ORGANIZATION_ID,
        'publisher': context.lambda_context.invoked_function_arn,
        'target': 'some-config',
        **changes
    }
    if not valid:
        with pytest.raises(Invalid):
            context.audit.publish_audit_event('configuration', 'some-config', detail)
    else:
        context.audit.publish_audit_event('configuration', 'some-config', detail)
        _assert_entries(context,
                        changes['external_change_data'],
                        changes['internal_change_data'])


@pytest.mark.unit
@pytest.mark.parametrize('expected_organization_id', [ORGANIZATION_ID, '99844796-cc53-444e-b997-400adf1e474e'])
@pytest.mark.parametrize('context_api_key_name,expected_api_key_name', [
    pytest.param('', 'organization-api-key', id='empty-api-key-name-reverts-to-default'),
    pytest.param('valid-api-key-name', 'valid-api-key-name', id='valid-api-key-name')
])
@freezegun.freeze_time('1987-12-24')
def test_programmatic_api_audit_event_publisher_sends_expected_ids_in_audit_event_detail(context, context_api_key_name, expected_api_key_name, expected_organization_id):
    lambda_return_value = _get_test_response_data()

    @context.audit.programmatic_api_audit_event_publisher(target='some-config')
    def handler(event, context):
        return lambda_return_value

    event = get_programmatic_api_test_event(cz_organization_id=expected_organization_id, authorization='some-api-key-value', api_key_name=context_api_key_name)
    handler(event, context.lambda_context)
    event_body = False

    _assert_entries(context, None, {'version': '1'},
                    event_body=event_body,
                    user_id=expected_api_key_name,
                    cz_organization_id=expected_organization_id,
                    called=True)


@pytest.mark.unit
def test_programmatic_api_audit_event_publisher_raises_key_error_if_cz_organization_id_not_present(context):
    lambda_return_value = _get_test_response_data()

    @context.audit.programmatic_api_audit_event_publisher(target='some-config')
    def handler(event, context):
        return lambda_return_value

    event = {'requestContext': {'authorizer': {}}}
    handler(event, context.lambda_context)
    assert context.mock_event_bridge_events_client_put_events.call_count == 0
