# coding=utf-8
from datetime import datetime
import json

import botocore.exceptions
import pytest
import sqlalchemy.exc
import sqlalchemy.pool
import utc

from mekansm import adapters


@pytest.fixture()
def db_adapter(request, mekansm_dbconf):
    adapter = adapters.DB(mekansm_dbconf, mekansm_dbconf["engine"])
    request.addfinalizer(lambda: adapter.disconnect())
    return adapter


def sample_deployment_data():
    return {
        "id": "d-Pogchamp",
        "triggered_by": "Kappa",
        "sha": "hah",
        "owner": "BobRoss",
        "repository": "HappyLittleTree",
        "environment": "HappyLittleCloud",
        "account": "aws-account-id",
        "application": "BobRoss-HappyLittleTree",
        "group": "BobRoss-HappyLittleTree-HappyLittleCloud",
        "s3location": "myartifacts/BobRoss/HappyLittleMountain",
        "bundletype": "tgz",
        "config": "CodeDeployDefault.AllAtOnce"
    }


def sample_nodes(number):
    return [
        {"id": "i-{}".format(n), "datacenter": "DC{}".format(n)}
        for n in range(number)
    ]


@pytest.fixture()
def db_adapter_with_data(db_adapter):
    db_adapter.create_deployment(
        {
            "id": "d-1",
            "triggered_by": "Kappa",
            "sha": "hah1",
            "owner": "dta",
            "repository": "mekansm",
            "environment": "production",
            "account": "aws-account-id",
            "application": "dta-mekansm",
            "group": "dta-mekansm-production",
            "s3location": "myartifacts/dta/Mekansm",
            "bundletype": "tgz",
            "config": "CodeDeployDefault.AllAtOnce"
        },
        sample_nodes(3)
    )
    db_adapter.create_deployment(
        {
            "id": "d-2",
            "triggered_by": "Kappa",
            "sha": "hah2",
            "owner": "dta",
            "repository": "cherrypy",
            "environment": "testing",
            "account": "aws-account-id",
            "application": "dta-cherrypy",
            "group": "dta-cherrypy-testing",
            "s3location": "the moon",
            "bundletype": "tgz",
            "config": "CodeDeployDefault.AllAtOnce"
        },
        sample_nodes(2)
    )
    db_adapter.create_deployment(
        {
            "id": "d-3",
            "triggered_by": "Pogchamp",
            "sha": "hah2",
            "owner": "release",
            "repository": "skadi",
            "environment": "testing",
            "account": "aws-account-id",
            "application": "release-skadi",
            "group": "release-skadi-testing",
            "s3location": "myartifacts/dta/Mekansm",
            "bundletype": "tgz",
            "config": "CodeDeployDefault.AllAtOnce"
        },
        sample_nodes(4)
    )
    return db_adapter


def test_db_instantiation_with_a_working_engine(db_adapter):
    assert "Kappa" == db_adapter.engine.execute("SELECT 'Kappa'").fetchone()[0]


def test_db_create_deployment_with_no_data_fields(db_adapter):
    with pytest.raises(sqlalchemy.exc.ProgrammingError):
        db_adapter.create_deployment({}, [])


def test_db_create_deployment_without_id(db_adapter):
    with pytest.raises(sqlalchemy.exc.IntegrityError):
        db_adapter.create_deployment({"triggered_by": "Kappa"}, [])


def test_db_create_deployment_with_incomplete_data_fields(db_adapter):
    with pytest.raises(sqlalchemy.exc.IntegrityError):
        db_adapter.create_deployment(
            {"id": "d-Pogchamp", "triggered_by": "Kappa"}, [])


def test_db_get_non_existant_deployment_returns_none(db_adapter):
    assert db_adapter.get_deployment("Kappa") is None


def test_db_create_deployment_with_incomplete_optional_fields(db_adapter):
    dep = sample_deployment_data()
    id_ = dep["id"]
    assert db_adapter.get_deployment(id_) is None
    got = db_adapter.create_deployment(dep, [])
    deployment_data = db_adapter.get_deployment(id_)
    assert deployment_data is not None
    assert got == deployment_data
    assert isinstance(deployment_data["created_at"], datetime)
    assert deployment_data["status"] == "InProgress"


def test_db_create_duplicate_deployment_fails(db_adapter):
    dep = sample_deployment_data()
    nodes = sample_nodes(3)
    db_adapter.create_deployment(dep, nodes)
    with pytest.raises(sqlalchemy.exc.IntegrityError):
        db_adapter.create_deployment(dep, nodes)


def test_db_create_deployment_respects_optional_fields(db_adapter):
    dep = sample_deployment_data()
    dt = utc.datetime(2010, 3, 3, 16, 20, 10)
    new_status = "Failed"
    dep.update({
        "created_at": dt,
        "status": new_status
    })
    assert db_adapter.get_deployment(dep["id"]) is None
    got = db_adapter.create_deployment(dep, [])
    deployment_data = db_adapter.get_deployment(dep["id"])
    assert deployment_data is not None
    assert got == deployment_data
    assert deployment_data["created_at"] == dt
    assert deployment_data["status"] == new_status


def test_db_create_deployment_with_one_node(db_adapter):
    dep = sample_deployment_data()
    nodes = sample_nodes(1)
    assert db_adapter.get_deployment(dep["id"]) is None
    got = db_adapter.create_deployment(dep, nodes)
    deployment_data = db_adapter.get_deployment(dep["id"])
    assert deployment_data is not None
    assert got == deployment_data
    got_nodes = db_adapter.get_nodes({"deployment": (dep["id"])})
    assert got_nodes == nodes
    for node in nodes:
        assert db_adapter.get_node(node["id"]) == node


def test_db_create_deployment_with_multiple_nodes(db_adapter):
    dep = sample_deployment_data()
    nodes = sample_nodes(10)
    assert db_adapter.get_deployment(dep["id"]) is None
    got = db_adapter.create_deployment(dep, nodes)
    deployment_data = db_adapter.get_deployment(dep["id"])
    assert deployment_data is not None
    assert got == deployment_data
    got_nodes = db_adapter.get_nodes({"deployment": (dep["id"])})
    assert got_nodes == nodes
    for node in nodes:
        assert db_adapter.get_node(node["id"]) == node


def test_db_change_status_of_deployment_updates_nodes(db_adapter):
    dep = sample_deployment_data()
    nodes = sample_nodes(10)
    new_status = "Failed"
    db_adapter.create_deployment(dep, nodes)
    db_adapter.set_deployment_status(dep["id"], new_status)
    assert db_adapter.get_deployment(dep["id"])["status"] == new_status
    for node in nodes:
        s = db_adapter.get_deployment_instance_status(dep["id"], node["id"])
        assert s["status"] == new_status


def test_db_change_status_of_deployment_updates_time(db_adapter):
    dep = sample_deployment_data()
    nodes = sample_nodes(10)
    new_status = "Failed"
    db_adapter.create_deployment(dep, nodes)
    old_time = {}
    for node in nodes:
        s = db_adapter.get_deployment_instance_status(dep["id"], node["id"])
        old_time[node["id"]] = s["update_time"]
    db_adapter.set_deployment_status(dep["id"], new_status)
    for node in nodes:
        s = db_adapter.get_deployment_instance_status(dep["id"], node["id"])
        assert s["update_time"] > old_time[node["id"]]


def idfn(val):
    return json.dumps(val)


@pytest.mark.parametrize(
    "search, expected",
    [
        ({"owner": "dta"}, ["d-1", "d-2"]),
        ({"owner": "dta", "repo": "mekansm"}, ["d-1"]),
        ({"environment": "testing"}, ["d-2", "d-3"]),
        ({"node": "i-0"}, ["d-1", "d-2", "d-3"]),
        ({"status": "InProgress"}, ["d-1", "d-2", "d-3"]),
        (
            {
                "owner": "dta",
                "repo": "mekansm",
                "environment": "production",
                "node": "i-0",
                "status": "InProgress",
                "datacenter": "DC0"
            },
            ["d-1"]
        ),
        (
            {
                "owner": "ヽ༼ຈل͜ຈ༽ﾉ",
                "repo": "༼ つ ◕_◕ ༽つ",
                "environment": "(☞ﾟ∀ﾟ)☞",
                "node": "☚(ﾟヮﾟ☚)",
                "status": "InProgress",
                "datacenter": "୧༼ಠ益ಠ༽୨"
            },
            []
        )
    ],
    ids=idfn
)
def test_db_get_deployments(db_adapter_with_data, search, expected):
    got = {d["id"] for d in db_adapter_with_data.get_deployments(search)}
    assert got == set(expected)


@pytest.mark.parametrize(
    "search, expected",
    [
        ({"owner": "dta"}, ["i-0", "i-1", "i-2"]),
        ({"owner": "dta", "repo": "mekansm"}, ["i-0", "i-1", "i-2"]),
        ({"environment": "testing"}, ["i-0", "i-1", "i-2", "i-3"]),
        ({"node": "i-0"}, ["i-0", "i-1", "i-2", "i-3"]),
        ({"status": "InProgress"}, ["i-0", "i-1", "i-2", "i-3"]),
        (
            {
                "owner": "dta",
                "repo": "mekansm",
                "environment": "production",
                "node": "i-0",
                "status": "InProgress",
                "datacenter": "DC0",
                "deployment_status": "InProgress"
            },
            ["i-0", "i-1", "i-2"]
        ),
        (
            {
                "owner": "ヽ༼ຈل͜ຈ༽ﾉ",
                "repo": "༼ つ ◕_◕ ༽つ",
                "environment": "(☞ﾟ∀ﾟ)☞",
                "node": "☚(ﾟヮﾟ☚)",
                "status": "InProgress",
                "datacenter": "୧༼ಠ益ಠ༽୨"
            },
            []
        )
    ],
    ids=idfn
)
def test_db_get_nodes(db_adapter_with_data, search, expected):
    got = {d["id"] for d in db_adapter_with_data.get_nodes(search)}
    assert got == set(expected)


def test_aws_get_deployment_instance_status():
    deployment_id = "d-Kappa"
    node_id = "i-pogchamp"
    update_time = utc.now()
    status = "Failed"
    cd_response = {
        "instanceSummary": {
            "deploymentId": deployment_id,
            "instanceId": node_id,
            "status": status,
            "lastUpdatedAt": update_time,
            "lifecycleEvents": []
        }
    }

    class FakeBotoSession:
        def client(self, service_name):
            return {
                "codedeploy": FakeCodeDeploy(),
                "ec2": FakeEc2()
            }[service_name]

    class FakeCodeDeploy:

        def get_deployment_instance(self, **kwargs):
            assert kwargs == {
                "deploymentId": deployment_id,
                "instanceId": node_id
            }
            return cd_response

    class FakeEc2:
        def describe_tags(self, **kwargs):
            return {"Tags": [{"ResourceId": "i-pogchamp"}]}

    api = adapters.AwsClient(FakeBotoSession(), "fakeprofile")
    got = api.get_deployment_instance_status(deployment_id, node_id)
    expected = {"deployment": deployment_id, "node": node_id,
                "update_time": update_time, "status": status, "events": []}
    assert got == expected


def test_aws_get_deployment_instance_status_missing():

    class FakeBotoSession:
        def client(self, service_name):
            return {
                "codedeploy": FakeCodeDeploy(),
                "ec2": FakeEc2()
            }[service_name]

    class FakeCodeDeploy:
        def get_deployment_instance(self, **kwargs):
            assert kwargs == {
                "deploymentId": "d-Kappa", "instanceId": "i-pogchamp"}
            raise botocore.exceptions.ClientError(
                error_response={
                    "Error": {
                        "Message": "The requested Kappa does not exist.",
                        "Code": "InstanceDoesNotExistException"
                    },
                    "ResponseMetadata": {
                        "HTTPStatusCode": 400,
                        "RequestId": "98b57a46-98b9-11e5-a263-6f4a4be2294e"
                    }},
                operation_name="GetDeploymentInstance"
            )

    class FakeEc2:
        def describe_tags(self, **kwargs):
            return {"Tags": [{"ResourceId": "i-pogchamp"}]}

    api = adapters.AwsClient(FakeBotoSession(), "fakeprofile")
    got = api.get_deployment_instance_status("d-Kappa", "i-pogchamp")
    assert got is None


def test_aws_create_deployment():
    data = sample_deployment_data()
    expected = dict(
        applicationName="BobRoss-HappyLittleTree",
        deploymentGroupName="BobRoss-HappyLittleTree-HappyLittleCloud",
        revision={
            "revisionType": "S3",
            "s3Location": {
                "bucket": "myartifacts",
                "key": "BobRoss/HappyLittleMountain",
                "bundleType": "tgz"
            }
        },
        deploymentConfigName="CodeDeployDefault.AllAtOnce",
        description={
            "sha": "hah"
        }
    )

    class FakeBotoSession:
        def client(self, service_name):
            if service_name == "codedeploy":
                return FakeCodeDeploy()

    class FakeCodeDeploy:

        def create_deployment(self, **kwargs):
            # The description field is jsoninified, so deserialize it for
            # comparison with the expected value
            kwargs["description"] = json.loads(kwargs.pop("description"))
            assert kwargs == expected
            return {"deploymentId": "Kappa"}

    api = adapters.AwsClient(FakeBotoSession(), "fakeprofile")
    assert api.create_deployment(data) == "Kappa"


@pytest.mark.parametrize("instance_id,expected", [
    ("abcd567890", False),
    ("i-abcd5678", True),
    ("i-abcd-678", False),
    ("i-1234567", False),
    ("i-abcd56789012-a567", False),
    ("i-abcd5678901234567", True),
    ("abcd567890123456789", False),
    ("i-abcd56789012345678", False)
])
def test_is_instance_id(instance_id, expected):
    assert adapters.AwsClient.is_instance_id(instance_id) is expected


def test_context_get_deployment_instance_status():
    deployment = "d-1"
    node = "i-1"

    class FakeAws:
        def get_deployment_instance_status(self, deployment_id, node_id):
            return {
                "node": node,
                "deployment": deployment,
                "update_time": utc.now(),
                "status": "Failed"
            }

    class FakeDb:
        def set_deployment_instance_status(self, deployment_id, node_id, status):
            assert deployment_id == deployment
            assert node_id == node
            assert status == "Failed"
            return 1

    class FakeContext(adapters.Context):
        def __init__(self):
            self.db = FakeDb()
            self.aws = FakeAws()

    ctx = FakeContext()
    ctx.get_deployment_instance_status(deployment, node) == 1


def test_aws_get_hostnames_from_instance_ids():
    node_id = "i-pogchamp"

    class FakeBotoSession:
        def client(self, service_name):
            return {
                "codedeploy": FakeCodeDeploy(),
                "ec2": FakeEc2()
            }[service_name]

    class FakeCodeDeploy:
        pass

    class FakeEc2:
        def describe_tags(self, **kwargs):
            return {
                "Tags": [
                    {
                        "ResourceId": node_id,
                        "Value": "something-$ID.test",
                        "Key": "Name"
                    }
                ]
            }

    api = adapters.AwsClient(FakeBotoSession(), "fakeprofile")
    got = api.get_hostnames_from_instance_ids([node_id])
    expected = {node_id: "something-pogchamp.test"}
    assert got == expected
