import json
from unittest.mock import patch

from asana.error import ForbiddenError
import django
from django.http import Http404
from django.test import override_settings, TestCase, RequestFactory

from djasana import models, views
from djasana.tests.fixtures import attachment, project, story, task, user
from djasana.utils import sign_sha256_hmac

if django.VERSION >= (2, 0, 0):
    from django.urls import reverse
else:
    from django.core.urlresolvers import reverse


@override_settings(
    ASANA_ACCESS_TOKEN="foo",
    ASANA_WORKSPACE=None,
    DJASANA_WEBHOOK_URL="https://example.com/hooks/",
    ROOT_URLCONF="djasana.urls",
)
class WebhookViewTestCase(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.factory = RequestFactory()
        cls.workspace = models.Workspace.objects.create(
            remote_id=1, name="New Workspace"
        )
        cls.team = models.Team.objects.create(remote_id=2, name="New Team")
        cls.project = models.Project.objects.create(
            remote_id=3,
            name="New Project",
            public=True,
            team=cls.team,
            workspace=cls.workspace,
        )
        cls.user = models.User.objects.create(remote_id=4, name="New User")
        cls.url = reverse("djasana_webhook", kwargs={"remote_id": 3})
        cls.secret = "1d6207f8818f063890758a32d3833914754ba788cb8993e644701bac7257f59e"
        cls.data = {
            "events": [
                {
                    "action": "changed",
                    "created_at": "2017-08-21T18:20:37.972Z",
                    "parent": None,
                    "resource": {
                        "gid": "1337",
                        "resource_type": "task",
                    },
                    "user": {
                        "gid": "1123",
                        "resource_type": "user",
                    },
                },
            ]
        }

    def _get_mock_response(self, mock_client, data):
        message = json.dumps(data)
        signature = sign_sha256_hmac(self.secret, message)
        mock_client.access_token().projects.find_by_id.return_value = project(gid="3")
        mock_client.access_token().tasks.find_by_id.return_value = task()
        request = self.factory.post(
            "",
            content_type="application/json",
            data=message,
            **{"X-Hook-Signature": signature}
        )
        return views.WebhookView.as_view()(request, remote_id=3)

    def test_webhook_created(self):
        """Asserts a webhook is established"""
        request = self.factory.post(
            "", content_type="application/json", **{"X-Hook-Secret": self.secret}
        )
        response = views.WebhookView.as_view()(
            request, remote_id=self.project.remote_id
        )
        self.assertEqual(200, response.status_code)
        self.assertTrue("x-hook-secret" in response)
        self.assertEqual(self.secret, response["x-hook-secret"])
        try:
            webhook = models.Webhook.objects.get(project=self.project)
        except models.Webhook.DoesNotExist:
            self.fail("Webhook not created")
        self.assertEqual(self.secret, webhook.secret)

    def test_alternative_webhook_meta_key(self):
        """Asserts a webhook is established with an alternative META tag"""
        request = self.factory.post(
            "", content_type="application/json", **{"HTTP_X_HOOK_SECRET": self.secret}
        )
        response = views.WebhookView.as_view()(
            request, remote_id=self.project.remote_id
        )
        self.assertEqual(200, response.status_code)
        self.assertTrue("x-hook-secret" in response)
        self.assertEqual(self.secret, response["x-hook-secret"])

    def test_short_secret(self):
        """Asserts a secret that is 32 chars is accepted"""
        request = self.factory.post(
            "",
            content_type="application/json",
            data=json.dumps(self.data),
            **{"X-Hook-Secret": self.secret[:32]}
        )
        response = views.WebhookView.as_view()(request, remote_id=3)
        self.assertEqual(200, response.status_code)
        self.assertEqual(self.secret[:32], response["x-hook-secret"])

    def test_bad_short_secret(self):
        """Asserts a malicious endpoint posts a wrong secret that is not 64 chars"""
        request = self.factory.post(
            "",
            content_type="application/json",
            data=json.dumps(self.data),
            **{"X-Hook-Secret": "foo"}
        )
        response = views.WebhookView.as_view()(request, remote_id=3)
        self.assertEqual(403, response.status_code)

    def test_bad_short_signature(self):
        """Asserts a malicious endpoint posts a wrong signature that is not 64 chars"""
        request = self.factory.post(
            "",
            content_type="application/json",
            data=json.dumps(self.data),
            **{"X-Hook-Signature": "foo"}
        )
        response = views.WebhookView.as_view()(request, remote_id=3)
        self.assertEqual(403, response.status_code)

    def test_bad_signature(self):
        """Asserts a malicious endpoint posts a wrong signature"""
        models.Webhook.objects.create(project=self.project, secret=self.secret)
        request = self.factory.post(
            "",
            content_type="application/json",
            data=json.dumps(self.data),
            **{"X-Hook-Signature": "x" * 64}
        )
        response = views.WebhookView.as_view()(request, remote_id=3)
        self.assertEqual(403, response.status_code)

    def test_bad_project_id(self):
        """Asserts a malicious endpoint posts a wrong project id"""
        request = self.factory.post(
            "",
            content_type="application/json",
            data=json.dumps(self.data),
            **{"X-Hook-Secret": self.secret}
        )
        with self.assertRaises(Http404):
            views.WebhookView.as_view()(request, remote_id=99)

    @patch("djasana.connect.Client")
    def test_valid_request(self, mock_client):
        models.Webhook.objects.create(project=self.project, secret=self.secret)
        task_ = models.Task.objects.create(remote_id=99, name="Old task Name")
        mock_client.access_token().tasks.find_by_id.return_value = task(gid="99")
        mock_client.access_token().attachments.find_by_task.return_value = [
            attachment()
        ]
        mock_client.access_token().attachments.find_by_id.return_value = attachment()
        data = {
            "events": [
                {
                    "action": "changed",
                    "created_at": "2017-08-21T18:20:37.972Z",
                    "parent": None,
                    "resource": {
                        "gid": "99",
                        "resource_type": "task",
                    },
                    "user": {
                        "gid": "1123",
                        "resource_type": "user",
                    },
                },
            ]
        }
        response = self._get_mock_response(mock_client, data)
        self.assertEqual(200, response.status_code)
        self.assertFalse("x-hook-secret" in response)
        task_.refresh_from_db()
        self.assertEqual("Test Task", task_.name)
        try:
            models.Attachment.objects.get(remote_id=1)
        except models.Attachment.DoesNotExist:
            self.fail("Attachment not created")

    @patch("djasana.connect.Client")
    def test_bad_task_id(self, mock_client):
        """Asserts an event is received for a task that is now deleted in Asana"""
        mock_client.access_token().tasks.find_by_id.side_effect = ForbiddenError
        request = self.factory.post(
            "",
            content_type="application/json",
            data=json.dumps(self.data),
            **{"X-Hook-Secret": self.secret}
        )
        response = views.WebhookView.as_view()(request, remote_id=3)
        self.assertEqual(200, response.status_code)

    @patch("djasana.connect.Client")
    def test_task_with_parent(self, mock_client):
        models.Webhook.objects.create(project=self.project, secret=self.secret)
        parent_task = task(id=10, assignee=user())
        child_task = task(id=11, assignee=user(), parent=parent_task.copy())
        mock_client.access_token().tasks.find_all.return_value = [child_task]
        mock_client.access_token().tasks.find_by_id.side_effect = [
            child_task,
            parent_task,
        ]
        data = {
            "events": [
                {
                    "action": "added",
                    "created_at": "2017-08-21T18:20:37.972Z",
                    "parent": {"gid": "10"},
                    "resource": {
                        "gid": "1337",
                        "resource_type": "task",
                    },
                    "user": {
                        "gid": "1123",
                        "resource_type": "user",
                    },
                },
            ]
        }
        message = json.dumps(data)
        signature = sign_sha256_hmac(self.secret, message)
        request = self.factory.post(
            "",
            content_type="application/json",
            data=message,
            **{"X-Hook-Signature": signature}
        )
        views.WebhookView.as_view()(request, remote_id=3)
        self.assertEqual(2, models.Task.objects.count())
        parent, child = tuple(models.Task.objects.order_by("remote_id"))
        self.assertEqual(parent, child.parent)

    @staticmethod
    def setup_task_for_delete(mock_client):
        mock_client.access_token().tasks.find_by_id.return_value = task()
        mock_client.access_token().attachments.find_by_task.return_value = [
            attachment()
        ]
        mock_client.access_token().attachments.find_by_id.return_value = attachment()
        mock_client.access_token().stories.find_by_task.return_value = [story()]
        mock_client.access_token().stories.find_by_id.return_value = story()

    @patch("djasana.connect.Client")
    def test_task_removed(self, mock_client):
        models.Webhook.objects.create(project=self.project, secret=self.secret)
        task_ = models.Task.objects.create(remote_id=1337, name="Name")
        self.setup_task_for_delete(mock_client)
        data = self.data.copy()
        data["events"][0]["action"] = "removed"
        response = self._get_mock_response(mock_client, data)
        self.assertEqual(200, response.status_code)
        self.assertFalse("x-hook-secret" in response)
        with self.assertRaises(models.Task.DoesNotExist):
            models.Task.objects.get(pk=task_.pk)

    @patch("djasana.connect.Client")
    def test_task_deleted(self, mock_client):
        models.Webhook.objects.create(project=self.project, secret=self.secret)
        task_ = models.Task.objects.create(remote_id=1337, name="Name")
        self.setup_task_for_delete(mock_client)
        data = self.data.copy()
        data["events"][0]["action"] = "deleted"
        response = self._get_mock_response(mock_client, data)
        self.assertEqual(200, response.status_code)
        self.assertFalse("x-hook-secret" in response)
        with self.assertRaises(models.Task.DoesNotExist):
            models.Task.objects.get(pk=task_.pk)

    @patch("djasana.connect.Client")
    def test_project_updated(self, mock_client):
        models.Webhook.objects.create(project=self.project, secret=self.secret)
        data = {
            "events": [
                {
                    "action": "changed",
                    "created_at": "2017-08-21T18:20:37.972Z",
                    "parent": None,
                    "resource": {
                        "gid": "3",
                        "resource_type": "project",
                    },
                    "user": {
                        "gid": "1123",
                        "resource_type": "user",
                    },
                },
            ]
        }
        response = self._get_mock_response(mock_client, data)
        self.assertEqual(200, response.status_code)
        self.assertFalse("x-hook-secret" in response)
        self.project.refresh_from_db()
        self.assertEqual("Test Project", self.project.name)

    @patch("djasana.connect.Client")
    def test_project_deleted(self, mock_client):
        models.Webhook.objects.create(project=self.project, secret=self.secret)
        data = {
            "events": [
                {
                    "action": "removed",
                    "created_at": "2017-08-21T18:20:37.972Z",
                    "parent": None,
                    "resource": {
                        "gid": "3",
                        "resource_type": "project",
                    },
                    "user": {
                        "gid": "1123",
                        "resource_type": "user",
                    },
                },
            ]
        }
        mock_client.access_token().projects.find_by_id.return_value = project()
        mock_client.access_token().tasks.find_by_id.return_value = task(parent=task())
        response = self._get_mock_response(mock_client, data)
        self.assertEqual(200, response.status_code)
        self.assertFalse("x-hook-secret" in response)
        with self.assertRaises(models.Project.DoesNotExist):
            models.Project.objects.get(pk=self.project.pk)

    @patch("djasana.connect.Client")
    def test_new_story(self, mock_client):
        models.Webhook.objects.create(project=self.project, secret=self.secret)
        data = {
            "events": [
                {
                    "action": "added",
                    "created_at": "2017-08-21T18:20:37.972Z",
                    "parent": {
                        "gid": "1337",
                        "resource_type": "task",
                    },
                    "resource": {
                        "gid": "12",
                        "resource_type": "story",
                        "resource_subtype": "comment_added",
                    },
                    "user": {
                        "gid": "1123",
                        "resource_type": "user",
                    },
                },
            ]
        }
        mock_client.access_token().projects.find_by_id.return_value = project()
        mock_client.access_token().stories.find_by_id.return_value = story()
        response = self._get_mock_response(mock_client, data)
        self.assertEqual(200, response.status_code)
        try:
            models.Story.objects.get(remote_id=12)
        except models.Story.DoesNotExist:
            self.fail("Story not created")
