# This file is part of Buildbot.  Buildbot is free software: you can
# redistribute it and/or modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation, version 2.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright Buildbot Team Members
# Copyright Mamba Team

from future.utils import text_type

from io import BytesIO

from twisted.internet import defer
from twisted.trial import unittest

from buildbot.test.fake.web import FakeRequest
from buildbot.test.fake.web import fakeMasterForHooks
from buildbot.util import unicode2bytes
from buildbot.www import change_hook
from buildbot.www.hooks.bitbucketserver import _HEADER_EVENT

_CT_JSON = b'application/json'

pushJsonPayload = """
{
    "actor": {
        "username": "John",
        "displayName": "John Smith"
    },
    "repository": {
        "scmId": "git",
        "project": {
            "key": "CI",
            "name": "Continuous Integration"
        },
        "slug": "py-repo",
        "links": {
            "self": [
                {
                    "href": "http://localhost:7990/projects/CI/repos/py-repo/browse"
                }
            ]
        },
        "public": false,
        "ownerName": "CI",
        "owner": {
            "username": "CI",
            "displayName": "CI"
        },
        "fullName": "CI/py-repo"
    },
    "push": {
        "changes": [
            {
                "created": false,
                "closed": false,
                "new": {
                    "type": "branch",
                    "name": "branch_1496411680",
                    "target": {
                        "type": "commit",
                        "hash": "793d4754230023d85532f9a38dba3290f959beb4"
                    }
                },
                "old": {
                    "type": "branch",
                    "name": "branch_1496411680",
                    "target": {
                        "type": "commit",
                        "hash": "a87e21f7433d8c16ac7be7413483fbb76c72a8ba"
                    }
                }
            }
        ]
    }
}
"""

pullRequestCreatedJsonPayload = """
{
    "actor": {
        "username": "John",
        "displayName": "John Smith"
    },
    "pullrequest": {
        "id": "21",
        "title": "dot 1496311906",
        "link": "http://localhost:7990/projects/CI/repos/py-repo/pull-requests/21",
        "authorLogin": "John Smith",
        "fromRef": {
            "repository": {
                "scmId": "git",
                "project": {
                    "key": "CI",
                    "name": "Continuous Integration"
                },
                "slug": "py-repo",
                "links": {
                    "self": [
                        {
                            "href": "http://localhost:7990/projects/CI/repos/py-repo/browse"
                        }
                    ]
                },
                "public": false,
                "ownerName": "CI",
                "owner": {
                    "username": "CI",
                    "displayName": "CI"
                },
                "fullName": "CI/py-repo"
            },
            "commit": {
                "message": null,
                "date": null,
                "hash": "a87e21f7433d8c16ac7be7413483fbb76c72a8ba",
                "authorTimestamp": 0
            },
            "branch": {
                "rawNode": "a87e21f7433d8c16ac7be7413483fbb76c72a8ba",
                "name": "branch_1496411680"
            }
        },
        "toRef": {
            "repository": {
                "scmId": "git",
                "project": {
                    "key": "CI",
                    "name": "Continuous Integration"
                },
                "slug": "py-repo",
                "links": {
                    "self": [
                        {
                            "href": "http://localhost:7990/projects/CI/repos/py-repo/browse"
                        }
                    ]
                },
                "public": false,
                "ownerName": "CI",
                "owner": {
                    "username": "CI",
                    "displayName": "CI"
                },
                "fullName": "CI/py-repo"
            },
            "commit": {
                "message": null,
                "date": null,
                "hash": "7aebbb0089c40fce138a6d0b36d2281ea34f37f5",
                "authorTimestamp": 0
            },
            "branch": {
                "rawNode": "7aebbb0089c40fce138a6d0b36d2281ea34f37f5",
                "name": "master"
            }
        }
    },
    "repository": {
        "scmId": "git",
        "project": {
            "key": "CI",
            "name": "Continuous Integration"
        },
        "slug": "py-repo",
        "links": {
            "self": [
                {
                    "href": "http://localhost:7990/projects/CI/repos/py-repo/browse"
                }
            ]
        },
        "public": false,
        "ownerName": "CI",
        "owner": {
            "username": "CI",
            "displayName": "CI"
        },
        "fullName": "CI/py-repo"
    }
}
"""

pullRequestUpdatedJsonPayload = """
{
    "actor": {
        "username": "John",
        "displayName": "John Smith"
    },
    "pullrequest": {
        "id": "21",
        "title": "dot 1496311906",
        "link": "http://localhost:7990/projects/CI/repos/py-repo/pull-requests/21",
        "authorLogin": "Buildbot",
        "fromRef": {
            "repository": {
                "scmId": "git",
                "project": {
                    "key": "CI",
                    "name": "Continuous Integration"
                },
                "slug": "py-repo",
                "links": {
                    "self": [
                        {
                            "href": "http://localhost:7990/projects/CI/repos/py-repo/browse"
                        }
                    ]
                },
                "public": false,
                "ownerName": "CI",
                "owner": {
                    "username": "CI",
                    "displayName": "CI"
                },
                "fullName": "CI/py-repo"
            },
            "commit": {
                "message": null,
                "date": null,
                "hash": "a87e21f7433d8c16ac7be7413483fbb76c72a8ba",
                "authorTimestamp": 0
            },
            "branch": {
                "rawNode": "a87e21f7433d8c16ac7be7413483fbb76c72a8ba",
                "name": "branch_1496411680"
            }
        },
        "toRef": {
            "repository": {
                "scmId": "git",
                "project": {
                    "key": "CI",
                    "name": "Continuous Integration"
                },
                "slug": "py-repo",
                "links": {
                    "self": [
                        {
                            "href": "http://localhost:7990/projects/CI/repos/py-repo/browse"
                        }
                    ]
                },
                "public": false,
                "ownerName": "CI",
                "owner": {
                    "username": "CI",
                    "displayName": "CI"
                },
                "fullName": "CI/py-repo"
            },
            "commit": {
                "message": null,
                "date": null,
                "hash": "7aebbb0089c40fce138a6d0b36d2281ea34f37f5",
                "authorTimestamp": 0
            },
            "branch": {
                "rawNode": "7aebbb0089c40fce138a6d0b36d2281ea34f37f5",
                "name": "master"
            }
        }
    },
    "repository": {
        "scmId": "git",
        "project": {
            "key": "CI",
            "name": "Continuous Integration"
        },
        "slug": "py-repo",
        "links": {
            "self": [
                {
                    "href": "http://localhost:7990/projects/CI/repos/py-repo/browse"
                }
            ]
        },
        "public": false,
        "ownerName": "CI",
        "owner": {
            "username": "CI",
            "displayName": "CI"
        },
        "fullName": "CI/py-repo"
    }
}
"""

pullRequestRejectedJsonPayload = """
{
    "actor": {
        "username": "John",
        "displayName": "John Smith"
    },
    "pullrequest": {
        "id": "21",
        "title": "dot 1496311906",
        "link": "http://localhost:7990/projects/CI/repos/py-repo/pull-requests/21",
        "authorLogin": "Buildbot",
        "fromRef": {
            "repository": {
                "scmId": "git",
                "project": {
                    "key": "CI",
                    "name": "Continuous Integration"
                },
                "slug": "py-repo",
                "links": {
                    "self": [
                        {
                            "href": "http://localhost:7990/projects/CI/repos/py-repo/browse"
                        }
                    ]
                },
                "public": false,
                "ownerName": "CI",
                "owner": {
                    "username": "CI",
                    "displayName": "CI"
                },
                "fullName": "CI/py-repo"
            },
            "commit": {
                "message": null,
                "date": null,
                "hash": "a87e21f7433d8c16ac7be7413483fbb76c72a8ba",
                "authorTimestamp": 0
            },
            "branch": {
                "rawNode": "a87e21f7433d8c16ac7be7413483fbb76c72a8ba",
                "name": "branch_1496411680"
            }
        },
        "toRef": {
            "repository": {
                "scmId": "git",
                "project": {
                    "key": "CI",
                    "name": "Continuous Integration"
                },
                "slug": "py-repo",
                "links": {
                    "self": [
                        {
                            "href": "http://localhost:7990/projects/CI/repos/py-repo/browse"
                        }
                    ]
                },
                "public": false,
                "ownerName": "CI",
                "owner": {
                    "username": "CI",
                    "displayName": "CI"
                },
                "fullName": "CI/py-repo"
            },
            "commit": {
                "message": null,
                "date": null,
                "hash": "7aebbb0089c40fce138a6d0b36d2281ea34f37f5",
                "authorTimestamp": 0
            },
            "branch": {
                "rawNode": "7aebbb0089c40fce138a6d0b36d2281ea34f37f5",
                "name": "master"
            }
        }
    },
    "repository": {
        "scmId": "git",
        "project": {
            "key": "CI",
            "name": "Continuous Integration"
        },
        "slug": "py-repo",
        "links": {
            "self": [
                {
                    "href": "http://localhost:7990/projects/CI/repos/py-repo/browse"
                }
            ]
        },
        "public": false,
        "ownerName": "CI",
        "owner": {
            "username": "CI",
            "displayName": "CI"
        },
        "fullName": "CI/py-repo"
    }
}
"""

pullRequestFulfilledJsonPayload = """
{
    "actor": {
        "username": "John",
        "displayName": "John Smith"
    },
    "pullrequest": {
        "id": "21",
        "title": "Branch 1496411680",
        "link": "http://localhost:7990/projects/CI/repos/py-repo/pull-requests/21",
        "authorLogin": "Buildbot",
        "fromRef": {
            "repository": {
                "scmId": "git",
                "project": {
                    "key": "CI",
                    "name": "Continuous Integration"
                },
                "slug": "py-repo",
                "links": {
                    "self": [
                        {
                            "href": "http://localhost:7990/projects/CI/repos/py-repo/browse"
                        }
                    ]
                },
                "public": false,
                "ownerName": "CI",
                "owner": {
                    "username": "CI",
                    "displayName": "CI"
                },
                "fullName": "CI/py-repo"
            },
            "commit": {
                "message": null,
                "date": null,
                "hash": "a87e21f7433d8c16ac7be7413483fbb76c72a8ba",
                "authorTimestamp": 0
            },
            "branch": {
                "rawNode": "a87e21f7433d8c16ac7be7413483fbb76c72a8ba",
                "name": "branch_1496411680"
            }
        },
        "toRef": {
            "repository": {
                "scmId": "git",
                "project": {
                    "key": "CI",
                    "name": "Continuous Integration"
                },
                "slug": "py-repo",
                "links": {
                    "self": [
                        {
                            "href": "http://localhost:7990/projects/CI/repos/py-repo/browse"
                        }
                    ]
                },
                "public": false,
                "ownerName": "CI",
                "owner": {
                    "username": "CI",
                    "displayName": "CI"
                },
                "fullName": "CI/py-repo"
            },
            "commit": {
                "message": null,
                "date": null,
                "hash": "7aebbb0089c40fce138a6d0b36d2281ea34f37f5",
                "authorTimestamp": 0
            },
            "branch": {
                "rawNode": "7aebbb0089c40fce138a6d0b36d2281ea34f37f5",
                "name": "master"
            }
        }
    },
    "repository": {
        "scmId": "git",
        "project": {
            "key": "CI",
            "name": "Continuous Integration"
        },
        "slug": "py-repo",
        "links": {
            "self": [
                {
                    "href": "http://localhost:7990/projects/CI/repos/py-repo/browse"
                }
            ]
        },
        "public": false,
        "ownerName": "CI",
        "owner": {
            "username": "CI",
            "displayName": "CI"
        },
        "fullName": "CI/py-repo"
    }
}
"""

deleteTagJsonPayload = """
{
    "actor": {
        "username": "John",
        "displayName": "John Smith"
    },
    "repository": {
        "scmId": "git",
        "project": {
            "key": "CI",
            "name": "Continuous Integration"
        },
        "slug": "py-repo",
        "links": {
            "self": [
                {
                    "href": "http://localhost:7990/projects/CI/repos/py-repo/browse"
                }
            ]
        },
        "ownerName": "BUIL",
        "public": false,
        "owner": {
            "username": "CI",
            "displayName": "CI"
        },
        "fullName": "CI/py-repo"
    },
    "push": {
        "changes": [
            {
                "created": false,
                "closed": true,
                "old": {
                    "type": "tag",
                    "name": "1.0.0",
                    "target": {
                        "type": "commit",
                        "hash": "793d4754230023d85532f9a38dba3290f959beb4"
                    }
                },
                "new": null
            }
        ]
    }
}
"""

deleteBranchJsonPayload = """
{
    "actor": {
        "username": "John",
        "displayName": "John Smith"
    },
    "repository": {
        "scmId": "git",
        "project": {
            "key": "CI",
            "name": "Continuous Integration"
        },
        "slug": "py-repo",
        "links": {
            "self": [
                {
                    "href": "http://localhost:7990/projects/CI/repos/py-repo/browse"
                }
            ]
        },
        "ownerName": "CI",
        "public": false,
        "owner": {
            "username": "CI",
            "displayName": "CI"
        },
        "fullName": "CI/py-repo"
    },
    "push": {
        "changes": [
            {
                "created": false,
                "closed": true,
                "old": {
                    "type": "branch",
                    "name": "branch_1496758965",
                    "target": {
                        "type": "commit",
                        "hash": "793d4754230023d85532f9a38dba3290f959beb4"
                    }
                },
                "new": null
            }
        ]
    }
}
"""

newTagJsonPayload = """
{
    "actor": {
        "username": "John",
        "displayName": "John Smith"
    },
    "repository": {
        "scmId": "git",
        "project": {
            "key": "CI",
            "name": "Continuous Integration"
        },
        "slug": "py-repo",
        "links": {
            "self": [
                {
                    "href": "http://localhost:7990/projects/CI/repos/py-repo/browse"
                }
            ]
        },
        "public": false,
        "ownerName": "CI",
        "owner": {
            "username": "CI",
            "displayName": "CI"
        },
        "fullName": "CI/py-repo"
    },
    "push": {
        "changes": [
            {
                "created": true,
                "closed": false,
                "old": null,
                "new": {
                    "type": "tag",
                    "name": "1.0.0",
                    "target": {
                        "type": "commit",
                        "hash": "793d4754230023d85532f9a38dba3290f959beb4"
                    }
                }
            }
        ]
    }
}
"""


def _prepare_request(payload, headers=None, change_dict=None):
    headers = headers or {}
    request = FakeRequest(change_dict)
    request.uri = b"/change_hook/bitbucketserver"
    request.method = b"POST"
    if isinstance(payload, text_type):
        payload = unicode2bytes(payload)
    request.content = BytesIO(payload)
    request.received_headers[b'Content-Type'] = _CT_JSON
    request.received_headers.update(headers)
    return request


class TestChangeHookConfiguredWithGitChange(unittest.TestCase):

    def setUp(self):
        self.change_hook = change_hook.ChangeHookResource(
            dialects={'bitbucketserver': {}}, master=fakeMasterForHooks(self))

    def _checkPush(self, change):
        self.assertEqual(
            change['repository'],
            'http://localhost:7990/projects/CI/repos/py-repo/')
        self.assertEqual(change['author'], 'John Smith <John>')
        self.assertEqual(change['project'], 'Continuous Integration')
        self.assertEqual(change['revision'],
                         '793d4754230023d85532f9a38dba3290f959beb4')
        self.assertEqual(
            change['comments'], 'Bitbucket Server commit '
                                '793d4754230023d85532f9a38dba3290f959beb4')
        self.assertEqual(
            change['revlink'],
            'http://localhost:7990/projects/CI/repos/py-repo/commits/'
            '793d4754230023d85532f9a38dba3290f959beb4')

    @defer.inlineCallbacks
    def testHookWithChangeOnPushEvent(self):

        request = _prepare_request(
            pushJsonPayload, headers={_HEADER_EVENT: 'repo:push'})

        yield request.test_render(self.change_hook)

        self.assertEqual(len(self.change_hook.master.data.updates.changesAdded), 1)
        change = self.change_hook.master.data.updates.changesAdded[0]
        self._checkPush(change)
        self.assertEqual(change['branch'], 'refs/heads/branch_1496411680')
        self.assertEqual(change['category'], 'push')

    @defer.inlineCallbacks
    def testHookWithNonDictOption(self):
        self.change_hook.dialects = {'bitbucketserver': True}
        yield self.testHookWithChangeOnPushEvent()

    def _checkPullRequest(self, change):
        self.assertEqual(
            change['repository'],
            'http://localhost:7990/projects/CI/repos/py-repo/')
        self.assertEqual(change['author'], 'John Smith <John>')
        self.assertEqual(change['project'], 'Continuous Integration')
        self.assertEqual(change['comments'],
                         'Bitbucket Server Pull Request #21')
        self.assertEqual(change['revlink'],
                         'http://localhost:7990/projects/'
                         'CI/repos/py-repo/pull-requests/21')
        self.assertEqual(change['revision'],
                         'a87e21f7433d8c16ac7be7413483fbb76c72a8ba')
        pr_url = change['properties'].get('pullrequesturl')
        self.assertNotEqual(pr_url, None)
        self.assertEqual(
            pr_url,
            "http://localhost:7990/projects/CI/repos/py-repo/pull-requests/21")

    @defer.inlineCallbacks
    def testHookWithChangeOnPullRequestCreated(self):
        request = _prepare_request(
            pullRequestCreatedJsonPayload,
            headers={_HEADER_EVENT: 'pullrequest:created'})

        yield request.test_render(self.change_hook)

        self.assertEqual(len(self.change_hook.master.data.updates.changesAdded), 1)
        change = self.change_hook.master.data.updates.changesAdded[0]
        self._checkPullRequest(change)
        self.assertEqual(change['branch'], 'refs/pull-requests/21/merge')
        self.assertEqual(change['category'], 'pull-created')

    @defer.inlineCallbacks
    def testHookWithChangeOnPullRequestUpdated(self):
        request = _prepare_request(
            pullRequestUpdatedJsonPayload,
            headers={_HEADER_EVENT: 'pullrequest:updated'})

        yield request.test_render(self.change_hook)

        self.assertEqual(len(self.change_hook.master.data.updates.changesAdded), 1)
        change = self.change_hook.master.data.updates.changesAdded[0]
        self._checkPullRequest(change)
        self.assertEqual(change['branch'], 'refs/pull-requests/21/merge')
        self.assertEqual(change['category'], 'pull-updated')

    @defer.inlineCallbacks
    def testHookWithChangeOnPullRequestRejected(self):
        request = _prepare_request(
            pullRequestRejectedJsonPayload,
            headers={_HEADER_EVENT: 'pullrequest:rejected'})

        yield request.test_render(self.change_hook)

        self.assertEqual(len(self.change_hook.master.data.updates.changesAdded), 1)
        change = self.change_hook.master.data.updates.changesAdded[0]
        self._checkPullRequest(change)
        self.assertEqual(change['branch'], 'refs/heads/branch_1496411680')
        self.assertEqual(change['category'], 'pull-rejected')

    @defer.inlineCallbacks
    def testHookWithChangeOnPullRequestFulfilled(self):
        request = _prepare_request(
            pullRequestFulfilledJsonPayload,
            headers={_HEADER_EVENT: 'pullrequest:fulfilled'})

        yield request.test_render(self.change_hook)

        self.assertEqual(len(self.change_hook.master.data.updates.changesAdded), 1)
        change = self.change_hook.master.data.updates.changesAdded[0]
        self._checkPullRequest(change)
        self.assertEqual(change['branch'], 'refs/heads/master')
        self.assertEqual(change['category'], 'pull-fulfilled')

    @defer.inlineCallbacks
    def _checkCodebase(self, event_type, expected_codebase):
        payloads = {
            'repo:push': pushJsonPayload,
            'pullrequest:updated': pullRequestUpdatedJsonPayload}
        request = _prepare_request(
            payloads[event_type], headers={_HEADER_EVENT: event_type})
        yield request.test_render(self.change_hook)
        self.assertEqual(len(self.change_hook.master.data.updates.changesAdded), 1)
        change = self.change_hook.master.data.updates.changesAdded[0]
        self.assertEqual(change['codebase'], expected_codebase)

    @defer.inlineCallbacks
    def testHookWithCodebaseValueOnPushEvent(self):
        self.change_hook.dialects = {
            'bitbucketserver': {'codebase': 'super-codebase'}}
        yield self._checkCodebase('repo:push', 'super-codebase')

    @defer.inlineCallbacks
    def testHookWithCodebaseFunctionOnPushEvent(self):
        self.change_hook.dialects = {
            'bitbucketserver': {
                'codebase':
                    lambda payload: payload['repository']['project']['key']}}
        yield self._checkCodebase('repo:push', 'CI')

    @defer.inlineCallbacks
    def testHookWithCodebaseValueOnPullEvent(self):
        self.change_hook.dialects = {
            'bitbucketserver': {'codebase': 'super-codebase'}}
        yield self._checkCodebase('pullrequest:updated', 'super-codebase')

    @defer.inlineCallbacks
    def testHookWithCodebaseFunctionOnPullEvent(self):
        self.change_hook.dialects = {
            'bitbucketserver': {
                'codebase':
                    lambda payload: payload['repository']['project']['key']}}
        yield self._checkCodebase('pullrequest:updated', 'CI')

    @defer.inlineCallbacks
    def testHookWithUnhandledEvent(self):
        request = _prepare_request(
            pushJsonPayload, headers={_HEADER_EVENT: 'invented:event'})
        yield request.test_render(self.change_hook)
        self.assertEqual(len(self.change_hook.master.data.updates.changesAdded), 0)
        self.assertEqual(request.written, b"Unknown event: invented_event")

    @defer.inlineCallbacks
    def testHookWithChangeOnCreateTag(self):
        request = _prepare_request(
            newTagJsonPayload, headers={_HEADER_EVENT: 'repo:push'})
        yield request.test_render(self.change_hook)
        self.assertEqual(len(self.change_hook.master.data.updates.changesAdded), 1)
        change = self.change_hook.master.data.updates.changesAdded[0]
        self._checkPush(change)
        self.assertEqual(change['branch'], 'refs/tags/1.0.0')
        self.assertEqual(change['category'], 'push')

    @defer.inlineCallbacks
    def testHookWithChangeOnDeleteTag(self):
        request = _prepare_request(
            deleteTagJsonPayload, headers={_HEADER_EVENT: 'repo:push'})
        yield request.test_render(self.change_hook)
        self.assertEqual(len(self.change_hook.master.data.updates.changesAdded), 1)
        change = self.change_hook.master.data.updates.changesAdded[0]
        self._checkPush(change)
        self.assertEqual(change['branch'], 'refs/tags/1.0.0')
        self.assertEqual(change['category'], 'ref-deleted')

    @defer.inlineCallbacks
    def testHookWithChangeOnDeleteBranch(self):
        request = _prepare_request(
            deleteBranchJsonPayload, headers={_HEADER_EVENT: 'repo:push'})
        yield request.test_render(self.change_hook)
        self.assertEqual(len(self.change_hook.master.data.updates.changesAdded), 1)
        change = self.change_hook.master.data.updates.changesAdded[0]
        self._checkPush(change)
        self.assertEqual(change['branch'], 'refs/heads/branch_1496758965')
        self.assertEqual(change['category'], 'ref-deleted')

    @defer.inlineCallbacks
    def testHookWithInvalidContentType(self):
        request = _prepare_request(
            pushJsonPayload, headers={_HEADER_EVENT: b'repo:push'})
        request.received_headers[b'Content-Type'] = b'invalid/content'
        yield request.test_render(self.change_hook)
        self.assertEqual(len(self.change_hook.master.data.updates.changesAdded), 0)
        self.assertEqual(request.written,
                         b"Unknown content type: invalid/content")
