"""Unit tests."""

import inspect
import json
from mock import Mock
import os
import sys
import uuid

import pytest

# Add the lambda directory to the python library search path
lambda_dir = os.path.join(
    os.path.dirname(inspect.getfile(inspect.currentframe())), '..')
sys.path.append(lambda_dir)

import lambdautils.utils


@pytest.mark.parametrize(
    "key,environment,stage,namespace,table,nkey", [
        ("k", "e", "s", None, "e-s-secrets", "k"),
        ("k", "e", None, None, "e-dummystage-secrets", "k"),
        ("k", "e", None, "n", "e-dummystage-secrets", "n:k"),
        ("k", "e", "s", "n", "e-s-secrets", "n:k")])
def test_get_secret_from_vault(key, environment, stage, namespace, table, nkey,
                               boto3_resource, boto3_client, monkeypatch):
    """Gets a secret from the DynamoDB secrets vault."""
    # Call to the DynamoDB client to retrieve the encrypted secret
    monkeypatch.setattr("boto3.resource", boto3_resource)
    monkeypatch.setattr("boto3.client", boto3_client)
    secret = lambdautils.utils.get_secret(key,
                                          namespace=namespace,
                                          environment=environment,
                                          stage=stage)
    assert secret == "dummy"
    boto3_client("dynamodb").get_item.assert_called_with(
        TableName=table,
        Key={"id": {"S": nkey}})

    # Call to the KMS client to decrypt the secret
    boto3_client('kms').decrypt.assert_called_with(CiphertextBlob="encrypted")


def test_get_secret_from_env(monkeypatch):
    """Get a secret from an (encrypted) environment variable."""
    key = str(uuid.uuid4()).replace('-', '.')
    value = str(uuid.uuid4())
    monkeypatch.setenv(key.replace('.', '_').upper(), value)
    secret = lambdautils.utils.get_secret(key)
    assert secret == value


def test_get_setting(monkeypatch):
    """Should be an alias for get_secret."""
    resp = str(uuid.uuid4())
    arg = str(uuid.uuid4())
    kwarg = str(uuid.uuid4())
    get_secret = Mock(return_value=resp)
    monkeypatch.setattr("lambdautils.state.get_secret", get_secret)
    resp2 = lambdautils.state.get_setting(arg, kwarg=kwarg)
    assert resp2 == resp
    get_secret.assert_called_with(arg, kwarg=kwarg)


@pytest.mark.parametrize(
    "key,environment,layer,stage,shard_id,namespace,table,consistent,nkey", [
        ("k", "e", "l", "s", None, None, "e-l-s-state", False, "k"),
        ("k", "e", "l", "s", None, "n", "e-l-s-state", False, "n:k"),
        ("k", "e", "l", "s", "s-012", "n", "e-l-s-state", True, "s-012:n:k"),
        ("k", "e", "l", "s", "s-0001", None, "e-l-s-state", True, "s-0001:k")])
def test_get_state(boto3_resource, monkeypatch, key, environment, layer,
                   stage, shard_id, namespace, table, consistent, nkey):
    """Get a state value from DynamoDB."""
    monkeypatch.setattr("boto3.resource", boto3_resource)
    lambdautils.utils.get_state(key, environment=environment, layer=layer,
                                stage=stage, shard_id=shard_id,
                                namespace=namespace,
                                consistent=consistent)
    boto3_resource("dynamodb").Table.assert_called_with(table)
    if consistent is None:
        # The default setting: use consistent reads
        consistent = True
    boto3_resource("dynamodb").Table().get_item.assert_called_with(
        Key={"id": nkey}, ConsistentRead=consistent)


def test_no_state_table(boto3_resource, monkeypatch):
    """Test accessing state variable without having a state table."""
    monkeypatch.setattr("boto3.resource", boto3_resource)
    monkeypatch.delenv("HUMILIS_ENVIRONMENT")
    with pytest.raises(lambdautils.state.StateTableError):
        lambdautils.utils.set_state("sample_state_key", "sample_state_value")

    with pytest.raises(lambdautils.state.StateTableError):
        lambdautils.utils.delete_state("sample_state_key")

    with pytest.raises(lambdautils.state.StateTableError):
        lambdautils.utils.get_state("sample_state_key")


@pytest.mark.parametrize(
    "key,value,environment,layer,stage,shard_id,namespace,table,nkey", [
        ("k", "v", "e", "l", "s", None, None, "e-l-s-state", "k"),
        ("k", "v", "e", "l", "s", None, "n", "e-l-s-state", "n:k"),
        ("k", "v", "e", "l", "s", "s1", "n", "e-l-s-state", "s1:n:k"),
        ("k", "v", "e", "l", "s", "s2", None, "e-l-s-state", "s2:k")])
def test_set_state(boto3_resource, monkeypatch, key, value, environment, layer,
                   stage, shard_id, namespace, table, nkey):
    """Tests setting a state variable."""
    monkeypatch.setattr("boto3.resource", boto3_resource)
    lambdautils.utils.set_state(key, value, environment=environment,
                                layer=layer, stage=stage, shard_id=shard_id,
                                namespace=namespace)
    boto3_resource("dynamodb").Table.assert_called_with(table)
    boto3_resource("dynamodb").Table().put_item.assert_called_with(
        Item={"id": nkey, "value": json.dumps(value)})


@pytest.mark.parametrize(
    "key,environment,layer,stage,shard_id,namespace,table,nkey", [
        ("k", "e", "l", "s", None, None, "e-l-s-state", "k"),
        ("k", "e", "l", "s", None, "n", "e-l-s-state", "n:k"),
        ("k", "e", "l", "s", "s1", "n", "e-l-s-state", "s1:n:k"),
        ("k", "e", "l", "s", "s2", None, "e-l-s-state", "s2:k")])
def test_delete_state(boto3_resource, monkeypatch, key, environment,
                      layer, stage, shard_id, namespace, table, nkey):
    """Tests setting a state variable."""
    monkeypatch.setattr("boto3.resource", boto3_resource)
    lambdautils.utils.delete_state(key, environment=environment,
                                   layer=layer, stage=stage, shard_id=shard_id,
                                   namespace=namespace)
    boto3_resource("dynamodb").Table.assert_called_with(table)
    boto3_resource("dynamodb").Table().delete_item.assert_called_with(
        Key={"id": nkey})


def test_sentry_monitor_bad_client(boto3_client, raven_client, context,
                                   monkeypatch):
    """Test that sentry_monitor handles raven client errors gracefully."""

    class ClientError(Exception):
        pass

    def raise_error(dsn):
        raise ClientError

    monkeypatch.setattr("raven.Client", Mock(side_effect=raise_error))
    monkeypatch.setattr("boto3.client", boto3_client)

    @lambdautils.utils.sentry_monitor(environment="dummyenv",
                                      stage="dummystage")
    def lambda_handler(event, context):
        pass

    lambda_handler(None, context)
    raven_client.captureException.assert_not_called()


@pytest.mark.parametrize(
    "kstream, fstream, rcalls, kcalls, fcalls, ev", [
        ("a", "b", 1, 0, 0, {"Records": [{}]}),
        (None, "b", 1, 0, 0, {"Records": [{}]}),
        (None, None, 1, 0, 0, None),
        (None, None, 1, 0, 0, None),
        ("a", "b", 1, 0, 0, None),
        ("a", None, 1, 0, 0, None)])
def test_sentry_monitor_exception(
        kstream, fstream, rcalls, kcalls, fcalls, ev,
        boto3_client, raven_client, context, kinesis_event, monkeypatch):
    """Tests the sentry_monitor decorator when throwing an exception and
    lacking an error stream where to dump the errors."""

    if ev is None:
        # Default to a Kinesis event
        ev = kinesis_event

    monkeypatch.setattr("boto3.client", boto3_client)
    monkeypatch.setattr("raven.Client", Mock(return_value=raven_client))
    monkeypatch.setattr("lambdautils.monitor.SentryHandler", Mock())
    monkeypatch.setattr("lambdautils.utils.get_secret",
                        Mock(return_value="dummydsn"))

    error_stream = {
        "kinesis_stream": kstream,
        "firehose_delivery_stream": fstream}

    @lambdautils.utils.sentry_monitor(error_stream=error_stream)
    def lambda_handler(event, context):
        """Raise an error."""
        raise KeyError

    with pytest.raises(KeyError):
        lambda_handler(ev, context)

    # Should have captured only 1 error:
    # * The original KeyError
    assert raven_client.captureException.call_count == rcalls

    # And should have send the events to the Kinesis and FH error streams
    assert boto3_client("kinesis").put_records.call_count == kcalls
    assert boto3_client("firehose").put_record_batch.call_count == fcalls


def test_send_to_kinesis_stream(search_events, boto3_client, monkeypatch):
    """Tests sending events to a Kinesis stream."""
    monkeypatch.setattr("boto3.client", boto3_client)
    lambdautils.utils.send_to_kinesis_stream(search_events, "dummy_stream")
    boto3_client("kinesis").put_records.call_count == 1


def test_send_to_delivery_stream(search_events, boto3_client, monkeypatch):
    """Tests sending events to a Firehose delivery stream."""
    monkeypatch.setattr("boto3.client", boto3_client)
    lambdautils.utils.send_to_delivery_stream(search_events, "dummy_stream")
    boto3_client("firehose").put_record_batch.call_count == 1


@pytest.mark.parametrize("deserializer, embed_ts", [
    [json.loads, False],
    [json.loads, "kinesis_timestamp"],
    [None, False]])
def test_unpack_kinesis_event(kinesis_event, deserializer, embed_ts):
    """Extracts json-serialized events from a Kinesis events."""
    events, shard_id = lambdautils.utils.unpack_kinesis_event(
        kinesis_event, deserializer=deserializer, embed_timestamp=embed_ts)
    # There should be one event per kinesis record
    assert len(events) == len(kinesis_event["Records"])
    assert shard_id == kinesis_event["Records"][0]["eventID"].split(":")[0]
    if embed_ts:
        assert all(embed_ts in ev for ev in events)

[((15, 0, 15, 27), 'sys.path.append', 'sys.path.append', ({(15, 16, 15, 26): 'lambda_dir'}, {}), '(lambda_dir)', False, 'import sys\n'), ((20, 1, 25, 52), 'pytest.mark.parametrize', 'pytest.mark.parametrize', ({(21, 4, 21, 48): '"""key,environment,stage,namespace,table,nkey"""', (21, 50, 25, 51): "[('k', 'e', 's', None, 'e-s-secrets', 'k'), ('k', 'e', None, None,\n    'e-dummystage-secrets', 'k'), ('k', 'e', None, 'n',\n    'e-dummystage-secrets', 'n:k'), ('k', 'e', 's', 'n', 'e-s-secrets', 'n:k')]"}, {}), "('key,environment,stage,namespace,table,nkey', [('k',\n    'e', 's', None, 'e-s-secrets', 'k'), ('k', 'e', None, None,\n    'e-dummystage-secrets', 'k'), ('k', 'e', None, 'n',\n    'e-dummystage-secrets', 'n:k'), ('k', 'e', 's', 'n', 'e-s-secrets', 'n:k')]\n    )", False, 'import pytest\n'), ((66, 1, 71, 79), 'pytest.mark.parametrize', 'pytest.mark.parametrize', ({(67, 4, 67, 74): '"""key,environment,layer,stage,shard_id,namespace,table,consistent,nkey"""', (67, 76, 71, 78): "[('k', 'e', 'l', 's', None, None, 'e-l-s-state', False, 'k'), ('k', 'e',\n    'l', 's', None, 'n', 'e-l-s-state', False, 'n:k'), ('k', 'e', 'l', 's',\n    's-012', 'n', 'e-l-s-state', True, 's-012:n:k'), ('k', 'e', 'l', 's',\n    's-0001', None, 'e-l-s-state', True, 's-0001:k')]"}, {}), "(\n    'key,environment,layer,stage,shard_id,namespace,table,consistent,nkey',\n    [('k', 'e', 'l', 's', None, None, 'e-l-s-state', False, 'k'), ('k', 'e',\n    'l', 's', None, 'n', 'e-l-s-state', False, 'n:k'), ('k', 'e', 'l', 's',\n    's-012', 'n', 'e-l-s-state', True, 's-012:n:k'), ('k', 'e', 'l', 's',\n    's-0001', None, 'e-l-s-state', True, 's-0001:k')])", False, 'import pytest\n'), ((102, 1, 107, 70), 'pytest.mark.parametrize', 'pytest.mark.parametrize', ({(103, 4, 103, 69): '"""key,value,environment,layer,stage,shard_id,namespace,table,nkey"""', (103, 71, 107, 69): "[('k', 'v', 'e', 'l', 's', None, None, 'e-l-s-state', 'k'), ('k', 'v', 'e',\n    'l', 's', None, 'n', 'e-l-s-state', 'n:k'), ('k', 'v', 'e', 'l', 's',\n    's1', 'n', 'e-l-s-state', 's1:n:k'), ('k', 'v', 'e', 'l', 's', 's2',\n    None, 'e-l-s-state', 's2:k')]"}, {}), "(\n    'key,value,environment,layer,stage,shard_id,namespace,table,nkey', [(\n    'k', 'v', 'e', 'l', 's', None, None, 'e-l-s-state', 'k'), ('k', 'v',\n    'e', 'l', 's', None, 'n', 'e-l-s-state', 'n:k'), ('k', 'v', 'e', 'l',\n    's', 's1', 'n', 'e-l-s-state', 's1:n:k'), ('k', 'v', 'e', 'l', 's',\n    's2', None, 'e-l-s-state', 's2:k')])", False, 'import pytest\n'), ((120, 1, 125, 65), 'pytest.mark.parametrize', 'pytest.mark.parametrize', ({(121, 4, 121, 63): '"""key,environment,layer,stage,shard_id,namespace,table,nkey"""', (121, 65, 125, 64): "[('k', 'e', 'l', 's', None, None, 'e-l-s-state', 'k'), ('k', 'e', 'l', 's',\n    None, 'n', 'e-l-s-state', 'n:k'), ('k', 'e', 'l', 's', 's1', 'n',\n    'e-l-s-state', 's1:n:k'), ('k', 'e', 'l', 's', 's2', None,\n    'e-l-s-state', 's2:k')]"}, {}), "(\n    'key,environment,layer,stage,shard_id,namespace,table,nkey', [('k', 'e',\n    'l', 's', None, None, 'e-l-s-state', 'k'), ('k', 'e', 'l', 's', None,\n    'n', 'e-l-s-state', 'n:k'), ('k', 'e', 'l', 's', 's1', 'n',\n    'e-l-s-state', 's1:n:k'), ('k', 'e', 'l', 's', 's2', None,\n    'e-l-s-state', 's2:k')])", False, 'import pytest\n'), ((160, 1, 167, 36), 'pytest.mark.parametrize', 'pytest.mark.parametrize', ({(161, 4, 161, 50): '"""kstream, fstream, rcalls, kcalls, fcalls, ev"""', (161, 52, 167, 35): "[('a', 'b', 1, 0, 0, {'Records': [{}]}), (None, 'b', 1, 0, 0, {'Records': [\n    {}]}), (None, None, 1, 0, 0, None), (None, None, 1, 0, 0, None), ('a',\n    'b', 1, 0, 0, None), ('a', None, 1, 0, 0, None)]"}, {}), "('kstream, fstream, rcalls, kcalls, fcalls, ev', [(\n    'a', 'b', 1, 0, 0, {'Records': [{}]}), (None, 'b', 1, 0, 0, {'Records':\n    [{}]}), (None, None, 1, 0, 0, None), (None, None, 1, 0, 0, None), ('a',\n    'b', 1, 0, 0, None), ('a', None, 1, 0, 0, None)])", False, 'import pytest\n'), ((219, 1, 222, 19), 'pytest.mark.parametrize', 'pytest.mark.parametrize', ({(219, 25, 219, 49): '"""deserializer, embed_ts"""', (219, 51, 222, 18): "[[json.loads, False], [json.loads, 'kinesis_timestamp'], [None, False]]"}, {}), "('deserializer, embed_ts', [[json.loads, False], [\n    json.loads, 'kinesis_timestamp'], [None, False]])", False, 'import pytest\n'), ((59, 17, 59, 40), 'mock.Mock', 'Mock', (), '', False, 'from mock import Mock\n'), ((48, 16, 48, 28), 'uuid.uuid4', 'uuid.uuid4', ({}, {}), '()', False, 'import uuid\n'), ((56, 15, 56, 27), 'uuid.uuid4', 'uuid.uuid4', ({}, {}), '()', False, 'import uuid\n'), ((57, 14, 57, 26), 'uuid.uuid4', 'uuid.uuid4', ({}, {}), '()', False, 'import uuid\n'), ((58, 16, 58, 28), 'uuid.uuid4', 'uuid.uuid4', ({}, {}), '()', False, 'import uuid\n'), ((92, 9, 92, 57), 'pytest.raises', 'pytest.raises', ({(92, 23, 92, 56): 'lambdautils.state.StateTableError'}, {}), '(lambdautils.state.StateTableError)', False, 'import pytest\n'), ((95, 9, 95, 57), 'pytest.raises', 'pytest.raises', ({(95, 23, 95, 56): 'lambdautils.state.StateTableError'}, {}), '(lambdautils.state.StateTableError)', False, 'import pytest\n'), ((98, 9, 98, 57), 'pytest.raises', 'pytest.raises', ({(98, 23, 98, 56): 'lambdautils.state.StateTableError'}, {}), '(lambdautils.state.StateTableError)', False, 'import pytest\n'), ((148, 40, 148, 69), 'mock.Mock', 'Mock', (), '', False, 'from mock import Mock\n'), ((179, 40, 179, 71), 'mock.Mock', 'Mock', (), '', False, 'from mock import Mock\n'), ((180, 61, 180, 67), 'mock.Mock', 'Mock', ({}, {}), '()', False, 'from mock import Mock\n'), ((182, 24, 182, 53), 'mock.Mock', 'Mock', (), '', False, 'from mock import Mock\n'), ((193, 9, 193, 32), 'pytest.raises', 'pytest.raises', ({(193, 23, 193, 31): 'KeyError'}, {}), '(KeyError)', False, 'import pytest\n'), ((14, 36, 14, 58), 'inspect.currentframe', 'inspect.currentframe', ({}, {}), '()', False, 'import inspect\n'), ((47, 14, 47, 26), 'uuid.uuid4', 'uuid.uuid4', ({}, {}), '()', False, 'import uuid\n'), ((117, 35, 117, 52), 'json.dumps', 'json.dumps', ({(117, 46, 117, 51): 'value'}, {}), '(value)', False, 'import json\n')]