"""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)

[((273, 300), 'sys.path.append', 'sys.path.append', (['lambda_dir'], {}), '(lambda_dir)\n', (288, 300), False, 'import sys\n'), ((330, 612), 'pytest.mark.parametrize', 'pytest.mark.parametrize', (['"""key,environment,stage,namespace,table,nkey"""', "[('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    )\n", (353, 612), False, 'import pytest\n'), ((2272, 2654), 'pytest.mark.parametrize', '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',\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')])\n", (2295, 2654), False, 'import pytest\n'), ((4060, 4418), 'pytest.mark.parametrize', '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',\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')])\n", (4083, 4418), False, 'import pytest\n'), ((5064, 5395), 'pytest.mark.parametrize', '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',\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')])\n", (5087, 5395), False, 'import pytest\n'), ((6705, 6985), 'pytest.mark.parametrize', 'pytest.mark.parametrize', (['"""kstream, fstream, rcalls, kcalls, fcalls, ev"""', "[('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)])\n", (6728, 6985), False, 'import pytest\n'), ((9046, 9173), 'pytest.mark.parametrize', 'pytest.mark.parametrize', (['"""deserializer, embed_ts"""', "[[json.loads, False], [json.loads, 'kinesis_timestamp'], [None, False]]"], {}), "('deserializer, embed_ts', [[json.loads, False], [\n    json.loads, 'kinesis_timestamp'], [None, False]])\n", (9069, 9173), False, 'import pytest\n'), ((2040, 2063), 'mock.Mock', 'Mock', ([], {'return_value': 'resp'}), '(return_value=resp)\n', (2044, 2063), False, 'from mock import Mock\n'), ((1705, 1717), 'uuid.uuid4', 'uuid.uuid4', ([], {}), '()\n', (1715, 1717), False, 'import uuid\n'), ((1951, 1963), 'uuid.uuid4', 'uuid.uuid4', ([], {}), '()\n', (1961, 1963), False, 'import uuid\n'), ((1979, 1991), 'uuid.uuid4', 'uuid.uuid4', ([], {}), '()\n', (1989, 1991), False, 'import uuid\n'), ((2009, 2021), 'uuid.uuid4', 'uuid.uuid4', ([], {}), '()\n', (2019, 2021), False, 'import uuid\n'), ((3694, 3742), 'pytest.raises', 'pytest.raises', (['lambdautils.state.StateTableError'], {}), '(lambdautils.state.StateTableError)\n', (3707, 3742), False, 'import pytest\n'), ((3832, 3880), 'pytest.raises', 'pytest.raises', (['lambdautils.state.StateTableError'], {}), '(lambdautils.state.StateTableError)\n', (3845, 3880), False, 'import pytest\n'), ((3951, 3999), 'pytest.raises', 'pytest.raises', (['lambdautils.state.StateTableError'], {}), '(lambdautils.state.StateTableError)\n', (3964, 3999), False, 'import pytest\n'), ((6354, 6383), 'mock.Mock', 'Mock', ([], {'side_effect': 'raise_error'}), '(side_effect=raise_error)\n', (6358, 6383), False, 'from mock import Mock\n'), ((7499, 7530), 'mock.Mock', 'Mock', ([], {'return_value': 'raven_client'}), '(return_value=raven_client)\n', (7503, 7530), False, 'from mock import Mock\n'), ((7593, 7599), 'mock.Mock', 'Mock', ([], {}), '()\n', (7597, 7599), False, 'from mock import Mock\n'), ((7681, 7710), 'mock.Mock', 'Mock', ([], {'return_value': '"""dummydsn"""'}), "(return_value='dummydsn')\n", (7685, 7710), False, 'from mock import Mock\n'), ((7983, 8006), 'pytest.raises', 'pytest.raises', (['KeyError'], {}), '(KeyError)\n', (7996, 8006), False, 'import pytest\n'), ((241, 263), 'inspect.currentframe', 'inspect.currentframe', ([], {}), '()\n', (261, 263), False, 'import inspect\n'), ((1657, 1669), 'uuid.uuid4', 'uuid.uuid4', ([], {}), '()\n', (1667, 1669), False, 'import uuid\n'), ((5041, 5058), 'json.dumps', 'json.dumps', (['value'], {}), '(value)\n', (5051, 5058), False, 'import json\n')]