# 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
from voluptuous import Invalid

from czc import json
from czc.audit import AUDIT_DATA_KEY, PUBLISH_AUDIT_DATA
from czt.unittest import Context, MockEx

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
        raise lambda_return_value

    test_event = _get_test_iapi_request(external_change_data=external_change_data)

    if lambda_return_value is 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
        raise lambda_return_value

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

    event_body = True
    if lambda_return_value is 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
