# Copyright 2019-2020 Georges Racinet <georges.racinet@octobus.net>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
#
# SPDX-License-Identifier: GPL-2.0-or-later
"""Highest level tests for GitLab hook support.

These tests instantiate `Hook` and thus need a real repository.
Some use the API like in the hg-git wrapper, and thus need to monkey patch
a request recorder in.
"""
import io
import json
import pytest
import requests

from heptapod.testhelpers import (
    LocalRepoWrapper,
    )
from .. import (
    Hook,
    )

ZERO_SHA = "0" * 20


def make_repo(base_dir, name):
    secret = base_dir.join('secret')
    secret.write("TEHTOKEN")
    wrapper = LocalRepoWrapper.init(
        base_dir.join(name),
        config=dict(heptapod={'gitlab-shell': str(base_dir),
                              'gitlab-internal-api-secret-file': str(secret),
                              }),
    )
    # by default, ui.environ IS os.environ (shared) but that isn't true
    # in WSGI contexts. In that case, it is a plain dict, the WSGI environment
    # indeed. For these tests, we need them to be independent.
    wrapper.repo.ui.environ = {}
    return wrapper


def request_recorder(records, responses):
    """Record sent requests and treat each from the given responses.

    :param responses: a list of :class:`request.Response` instances or
                      :class:`Exception` instances. In the latter case,
                      the exception is raised instead of returning the
                      response.
    :param records: list to which the recorder appends the request that is
                    being sent.
    """
    def request(hook, meth, subpath, params, **kwargs):
        records.append((meth, subpath, params, kwargs))
        resp = responses.pop(0)
        if isinstance(resp, Exception):
            raise resp
        return resp

    return request


def make_response(status_code, body):
    """Create a :class:`request.Response` instance.

    :param body: must be `bytes`.
    """
    resp = requests.models.Response()
    resp.status_code = status_code
    resp.raw = io.BytesIO(body)
    return resp


def make_json_response(obj, status_code=200):
    return make_response(status_code, json.dumps(obj).encode())


def test_post_receive_params(tmpdir):
    wrapper = make_repo(tmpdir, 'proj.hg')
    repo = wrapper.repo
    hook = Hook('post-receive', repo)

    repo.ui.environ.update(
        HEPTAPOD_USERINFO_ID='23',
        GL_REPOSITORY='project-1024',
    )
    changes = {
        'branch/newbr': (ZERO_SHA, "beef1234dead" + "a" * 8),
        'topic/default/zz': ("23cafe45" + "0" * 12, "1234" + "b" * 16),
    }

    params = hook.post_receive_params(changes)
    assert params['identifier'] == 'user-23'
    assert params['changes'].splitlines() == [
        '23cafe45000000000000 1234bbbbbbbbbbbbbbbb topic/default/zz',
        '00000000000000000000 beef1234deadaaaaaaaa branch/newbr']
    assert params['gl_repository'] == 'project-1024'


def test_pre_receive_params(tmpdir):
    wrapper = make_repo(tmpdir, 'proj.hg')
    repo = wrapper.repo
    hook = Hook('post-receive', repo)

    repo.ui.environ.update(
        HEPTAPOD_USERINFO_ID='2',
        GL_REPOSITORY='project-3145926',
    )
    changes = {
        'topic/default/with-a-dash': (ZERO_SHA, "beef1234dead" + "a" * 8),
        'branch/default': ("23cafe45" + "0" * 12, "1234" + "b" * 16),
    }
    params = hook.pre_receive_params(changes)

    assert params['user_id'] == '2'
    assert params['changes'].splitlines() == [
        '23cafe45000000000000 1234bbbbbbbbbbbbbbbb branch/default',
        '00000000000000000000 beef1234deadaaaaaaaa topic/default/with-a-dash']
    assert params['gl_repository'] == 'project-3145926'
    assert params['project'] == repo.root


def test_post_receive_handle_response_gl_12_2(tmpdir):
    wrapper = make_repo(tmpdir, 'proj.hg')
    repo = wrapper.repo
    hook = Hook('post-receive', repo)

    out = hook.post_receive_handle_response(
        # example taken from the functional tests
        {u'broadcast_message': None,
         u'reference_counter_decreased': True,
         u'merge_request_urls': [
             {u'url': u'http://localhost:3000/root/'
              'test_project_1583330760_9216309/merge_requests/1',
              u'branch_name': u'topic/default/antelope',
              u'new_merge_request': False}
         ]})
    out = [l.strip() for l in out.splitlines() if l.strip()]
    assert out == [
        'View merge request for topic/default/antelope:',
        'http://localhost:3000/root/test_project_1583330760_9216309/'
        'merge_requests/1']

    broadcast = "Check out the new tutorial"
    out = hook.post_receive_handle_response(
        {u'broadcast_message': broadcast,
         u'reference_counter_decreased': True,
         })
    out = (l.strip() for l in out.splitlines() if l.strip())
    assert any(broadcast in l for l in out)

    project_created = "Congrats!"
    out = hook.post_receive_handle_response(
        {u'project_created_message': project_created,
         u'reference_counter_decreased': True,
         })
    out = (l.strip() for l in out.splitlines() if l.strip())
    assert any(project_created in l for l in out)

    # GitLab internal API gives warnings as a unique string
    # see lib/api/helpers/internal_helpers.rb
    warnings = "You probably shouldn't have"
    out = hook.post_receive_handle_response(
        {u'warnings': warnings,
         u'reference_counter_decreased': True,
         })
    out = (l.strip() for l in out.splitlines() if l.strip())
    assert any(warnings in l for l in out)


def test_post_receive_handle_response_gl_12_3(tmpdir):
    wrapper = make_repo(tmpdir, 'proj.hg')
    repo = wrapper.repo
    hook = Hook('post-receive', repo)

    out = hook.post_receive_handle_response(
        # example taken from the functional tests
        {u'messages': [
            {u'message': (
                u'To create a merge request '
                u'for topic/default/ant, visit:\n'
                u'http://localhost:3000/root/test_project/merge_requests/new'
                u'?merge_request%5Bsource_branch%5D=topic%2Fdefault%2Fant'
                ),
             u'type': u'basic',
             },
            {u'message': u'This is a TESTING SERVER',
             u'type': u'alert',
             },
            ]})
    out = [l.strip() for l in out.splitlines() if l.strip()]
    assert out == [
        'To create a merge request for topic/default/ant, visit:',
        'http://localhost:3000/root/test_project/merge_requests/new'
        '?merge_request%5Bsource_branch%5D=topic%2Fdefault%2Fant',
        '=' * 72,
        'This is a TESTING SERVER',
        '=' * 72,
    ]


def test_integration_post_receive(tmpdir, monkeypatch):
    records = []
    responses = [make_json_response(
        {u'broadcast_message': None,
         u'reference_counter_decreased': True,
         u'merge_request_urls': [
             {u'url': u'http://localhost:3000/root/'
              'test_project_1583330760_9216309/merge_requests/1',
              u'branch_name': u'topic/default/antelope',
              u'new_merge_request': False}
         ]}),
                 requests.exceptions.Timeout("1 ns is too much ;-)"),
                 ]

    monkeypatch.setattr(Hook, 'gitlab_internal_api_request',
                        request_recorder(records, responses))

    repo_wrapper = make_repo(tmpdir, 'repo.hg')
    repo = repo_wrapper.repo

    repo.ui.environ.update(
        HEPTAPOD_USERINFO_ID='23',
        GL_REPOSITORY='project-1024',
    )

    hook = Hook('post-receive', repo_wrapper.repo)
    changes = {
        'branch/newbr': (ZERO_SHA, "beef1234dead" + "a" * 8),
        'topic/default/zz': ("23cafe45" + "0" * 12, "1234" + "b" * 16),
    }

    return_code, out, err = hook(changes)
    assert return_code == 0
    out = [l.strip() for l in out.splitlines() if l.strip()]
    assert out == [
        'View merge request for topic/default/antelope:',
        'http://localhost:3000/root/test_project_1583330760_9216309/'
        'merge_requests/1']

    return_code, out, err = hook(changes)
    assert return_code == 1
    assert err == 'GitLab is unreachable'


def test_integration_pre_receive(tmpdir, monkeypatch):
    records = []
    responses = [make_json_response({u'message': "yes, granted!",
                                     u'status': True,
                                     },
                                    status_code=200),
                 make_json_response({u'status': True},
                                    status_code=200),
                 make_json_response({u'message': "go away!",
                                     u'status': False,
                                     },
                                    status_code=401),
                 make_response(504, b"Bad gateway :-("),
                 requests.exceptions.ConnectionError("Failed"),
                 ]
    monkeypatch.setattr(Hook, 'gitlab_internal_api_request',
                        request_recorder(records, responses))

    repo_wrapper = make_repo(tmpdir, 'repo.hg')
    repo = repo_wrapper.repo

    repo.ui.environ.update(
        HEPTAPOD_USERINFO_ID='23',
        GL_REPOSITORY='project-1024',
    )

    hook = Hook('pre-receive', repo_wrapper.repo)
    changes = {
        'branch/newbr': (ZERO_SHA, "beef1234dead" + "a" * 8),
        'topic/default/zz': ("23cafe45" + "0" * 12, "1234" + "b" * 16),
    }

    return_code, out, err = hook(changes)
    assert return_code == 0
    assert out == 'yes, granted!'

    return_code, out, err = hook(changes)
    assert return_code == 0
    assert out == ''

    return_code, out, err = hook(changes)
    assert return_code == 1
    assert err == 'go away!'

    return_code, out, err = hook(changes)
    assert return_code == 1
    assert err == 'API is not accessible'

    return_code, out, err = hook(changes)
    assert return_code == 1
    assert err == 'GitLab is unreachable'


def test_empty_changes(tmpdir):
    hook = Hook('post-receive', make_repo(tmpdir, 'repo.hg').repo)
    code, out, err = hook(())
    assert code == 0
    assert not out
    assert not err


def test_unknown_hook(tmpdir):
    # that's a typo, the most likely way for it to happen
    name = 'post-recieve'

    repo = make_repo(tmpdir, 'repo.hg').repo
    hook = Hook(name, repo)
    changes = {
        'branch/newbr': (ZERO_SHA, "beef1234dead" + "a" * 8),
        'topic/default/zz': ("23cafe45" + "0" * 12, "1234" + "b" * 16),
    }

    repo.ui.environ.update(
        HEPTAPOD_USERINFO_ID='23',
        GL_REPOSITORY='project-1024',
    )
    with pytest.raises(NotImplementedError) as exc_info:
        hook(changes)
    assert exc_info.value.args[0] == name


def test_missing_secret(tmpdir):
    repo = LocalRepoWrapper.init(tmpdir).repo
    shell = tmpdir.join('heptapod-shell').ensure(dir=True)
    repo.ui.setconfig(b'heptapod', b'gitlab-shell', str(shell).encode())
    with pytest.raises(RuntimeError) as exc_info:
        Hook('post-receive', repo)
    assert "secret-file" in exc_info.value.args[0]

    # defaulting to gitlab-shell secret if available
    shell.join('.gitlab_shell_secret').write('myseekret\n')
    hook = Hook('post-receive', repo)
    assert hook.gitlab_secret == 'myseekret'


def test_hook_missing_user(tmpdir):
    wrapper = make_repo(tmpdir, 'proj2.hg')
    repo = wrapper.repo
    hook = Hook('post-something', repo)

    repo.ui.environ['GL_REPOSITORY'] = 'project-1024'
    with pytest.raises(ValueError) as exc_info:
        hook.environ()
    assert 'User id' in exc_info.value.args[0]

    # and that gets catched, does not break the process
    code, out, err = hook(dict(master=(0, 1)))
    assert code != 0


def test_hook_missing_project(tmpdir):
    wrapper = make_repo(tmpdir, 'proj2.hg')
    repo = wrapper.repo
    hook = Hook('post-something', repo)

    repo.ui.environ['HEPTAPOD_USERINFO_ID'] = '3'
    with pytest.raises(ValueError) as exc_info:
        hook.environ()
    assert 'GL_REPOSITORY' in exc_info.value.args[0]

    # and that gets catched, does not break the process
    code, out, err = hook(dict(master=(0, 1)))
    assert code != 0


def test_hook_skip(tmpdir):
    wrapper = make_repo(tmpdir, 'proj2.hg')
    repo = wrapper.repo
    repo.ui.environ['HEPTAPOD_SKIP_GITLAB_HOOK'] = 'pre-rcv'
    hook = Hook('pre-rcv', repo)
    assert hook(dict(master=(0, 1))) == (0, '', '')


def test_hook_skip_all(tmpdir):
    wrapper = make_repo(tmpdir, 'proj2.hg')
    repo = wrapper.repo
    repo.ui.environ['HEPTAPOD_SKIP_ALL_GITLAB_HOOKS'] = 'no'
    hook = Hook('pre-rcv', repo)
    # it's been called and failed because of lack of User id
    assert hook(dict(master=(0, 1)))[0] == 1

    repo.ui.environ['HEPTAPOD_SKIP_ALL_GITLAB_HOOKS'] = 'yes'
    hook = Hook('pre-rcv', repo)
    assert hook(dict(master=(0, 1))) == (0, '', '')
