# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Tests for gitweb views.
"""

from unittest.mock import Mock, patch

import pytest
from django import urls
from django.contrib.messages.storage.fallback import FallbackStorage
from django.http.response import Http404

from plinth import module_loader
from plinth.modules.gitweb import views

# For all tests, use plinth.urls instead of urls configured for testing
pytestmark = pytest.mark.urls('plinth.urls')

EXISTING_REPOS = [
    {
        'name': 'something',
        'description': '',
        'owner': '',
        'access': 'public',
        'is_private': False,
        'default_branch': 'main',
    },
    {
        'name': 'something2',
        'description': '',
        'owner': '',
        'access': 'private',
        'is_private': True,
        'default_branch': 'main',
    },
]


@pytest.fixture(autouse=True, scope='module')
def fixture_gitweb_urls():
    """Make sure gitweb app's URLs are part of plinth.urls."""
    with patch('plinth.module_loader._modules_to_load', new=[]) as modules, \
            patch('plinth.urls.urlpatterns', new=[]):
        modules.append('plinth.modules.gitweb')
        module_loader.include_urls()
        yield


@pytest.fixture(autouse=True)
def gitweb_patch():
    """Patch gitweb."""
    privileged = 'plinth.modules.gitweb.privileged'
    with patch('plinth.modules.gitweb.get_repo_list') as get_repo_list, \
         patch('plinth.app.App.get') as app_get, \
         patch(f'{privileged}.create_repo'), \
         patch(f'{privileged}.repo_exists') as repo_exists,\
         patch(f'{privileged}.repo_info') as repo_info, \
         patch(f'{privileged}.rename_repo'), \
         patch(f'{privileged}.set_repo_description'), \
         patch(f'{privileged}.set_repo_owner'), \
         patch(f'{privileged}.set_repo_access'), \
         patch(f'{privileged}.set_default_branch'), \
         patch(f'{privileged}.delete_repo'), \
         patch(f'{privileged}.get_branches') as get_branches:
        repo_exists.return_value = True
        repo_info.return_value = dict(EXISTING_REPOS[0])
        get_branches.return_value = {
            'default_branch': 'main',
            'branches': ['main', 'branch1']
        }
        get_repo_list.return_value = [{
            'name': EXISTING_REPOS[0]['name']
        }, {
            'name': EXISTING_REPOS[1]['name']
        }]
        app = Mock()
        app_get.return_value = app
        app.update_service_access.return_value = None

        yield


def make_request(request, view, **kwargs):
    """Make request with a message storage."""
    setattr(request, 'session', 'session')
    messages = FallbackStorage(request)
    setattr(request, '_messages', messages)
    response = view(request, **kwargs)

    return response, messages


def test_repos_view(rf):
    """Test that a repo list has correct view data."""
    with patch('plinth.views.AppView.get_context_data',
               return_value={'is_enabled': True}):
        view = views.GitwebAppView.as_view()
        response, _ = make_request(rf.get(''), view)

        assert response.context_data['repos'] == [{
            'name': EXISTING_REPOS[0]['name']
        }, {
            'name': EXISTING_REPOS[1]['name']
        }]
        assert response.status_code == 200


def test_create_repo_view(rf):
    """Test that repo create view sends correct success message."""
    form_data = {
        'gitweb-name': 'something_other',
        'gitweb-description': 'test description',
        'gitweb-owner': 'test owner',
        'gitweb-is_private': True
    }
    request = rf.post(urls.reverse('gitweb:create'), data=form_data)
    view = views.CreateRepoView.as_view()
    response, messages = make_request(request, view)

    assert list(messages)[0].message == 'Repository created.'
    assert response.status_code == 302


def test_create_repo_duplicate_name_view(rf):
    """Test that repo create view shows correct error message."""
    form_data = {
        'gitweb-name': EXISTING_REPOS[0]['name'],
        'gitweb-description': '',
        'gitweb-owner': ''
    }
    request = rf.post(urls.reverse('gitweb:create'), data=form_data)
    view = views.CreateRepoView.as_view()
    response, _ = make_request(request, view)

    assert response.context_data['form'].errors['name'][
        0] == 'A repository with this name already exists.'
    assert response.status_code == 200


def test_create_repo_invalid_name_view(rf):
    """Test that repo create view shows correct error message."""
    form_data = {
        'gitweb-name': '.something_other',
        'gitweb-description': '',
        'gitweb-owner': ''
    }
    request = rf.post(urls.reverse('gitweb:create'), data=form_data)
    view = views.CreateRepoView.as_view()
    response, _ = make_request(request, view)

    assert response.context_data['form'].errors['name'][
        0] == 'Invalid repository name.'
    assert response.status_code == 200


def test_create_repo_failed_view(rf):
    """Test that repo creation failure sends correct error message."""
    general_error_message = "An error occurred while creating the repository."
    error_description = 'some error'
    with patch('plinth.modules.gitweb.create_repo',
               side_effect=PermissionError(error_description)):
        form_data = {
            'gitweb-name': 'something_other',
            'gitweb-description': '',
            'gitweb-owner': ''
        }
        request = rf.post(urls.reverse('gitweb:create'), data=form_data)
        view = views.CreateRepoView.as_view()
        response, messages = make_request(request, view)

        assert list(messages)[0].message == '{0} {1}'.format(
            general_error_message, error_description)
        assert response.status_code == 302


def test_clone_repo_view(rf):
    """Test that cloning repo sends correct succcess message."""
    form_data = {
        'gitweb-name': 'https://example.com/test.git',
        'gitweb-description': '',
        'gitweb-owner': ''
    }
    request = rf.post(urls.reverse('gitweb:create'), data=form_data)
    view = views.CreateRepoView.as_view()
    response, messages = make_request(request, view)

    with pytest.raises(AttributeError):
        getattr(response.context_data['form'], 'errors')

    assert list(messages)[0].message == 'Repository created.'
    assert response.status_code == 302


def test_clone_repo_missing_remote_view(rf):
    """Test that cloning non-existing repo shows correct error message."""
    with patch('plinth.modules.gitweb.privileged.repo_exists',
               return_value=False):
        form_data = {
            'gitweb-name': 'https://example.com/test.git',
            'gitweb-description': '',
            'gitweb-owner': ''
        }
        request = rf.post(urls.reverse('gitweb:create'), data=form_data)
        view = views.CreateRepoView.as_view()
        response, _ = make_request(request, view)

        assert response.context_data['form'].errors['name'][
            0] == 'Remote repository is not available.'
        assert response.status_code == 200


def test_edit_repository_view(rf):
    """Test that editing repo sends correct success message."""
    form_data = {
        'gitweb-name': 'something_other.git',
        'gitweb-description': 'test-description',
        'gitweb-owner': 'test-owner',
        'gitweb-is_private': True,
        'gitweb-default_branch': 'branch1',
    }
    url = urls.reverse('gitweb:edit',
                       kwargs={'name': EXISTING_REPOS[0]['name']})
    request = rf.post(url, data=form_data)
    view = views.EditRepoView.as_view()
    response, messages = make_request(request, view,
                                      name=EXISTING_REPOS[0]['name'])

    assert list(messages)[0].message == 'Repository edited.'
    assert response.status_code == 302


def test_edit_nonexisting_repository_view(rf):
    """Test that trying to edit non-existing repository raises 404."""
    non_existing_repo = {'name': 'something_other'}
    request = rf.post(urls.reverse('gitweb:edit', kwargs=non_existing_repo),
                      data={})
    view = views.EditRepoView.as_view()

    with pytest.raises(Http404):
        make_request(request, view, **non_existing_repo)


def test_edit_repository_duplicate_name_view(rf):
    """Test that renaming to already existing repo name shows correct error
    message.

    """
    form_data = {
        'gitweb-name': EXISTING_REPOS[1]['name'],
        'gitweb-description': 'test-description',
        'gitweb-owner': 'test-owner'
    }
    url = urls.reverse('gitweb:edit',
                       kwargs={'name': EXISTING_REPOS[0]['name']})
    request = rf.post(url, data=form_data)
    view = views.EditRepoView.as_view()
    response, _ = make_request(request, view, **EXISTING_REPOS[0])

    assert response.context_data['form'].errors['name'][
        0] == 'A repository with this name already exists.'
    assert response.status_code == 200


def test_edit_repository_invalid_name_view(rf):
    """Test that renaming repo to invalid name shows correct error message."""
    form_data = {
        'gitweb-name': '.something',
        'gitweb-description': 'test-description',
        'gitweb-owner': 'test-owner'
    }
    url = urls.reverse('gitweb:edit',
                       kwargs={'name': EXISTING_REPOS[0]['name']})
    request = rf.post(url, data=form_data)
    view = views.EditRepoView.as_view()
    response, _ = make_request(request, view, **EXISTING_REPOS[0])

    assert response.context_data['form'].errors['name'][
        0] == 'Invalid repository name.'
    assert response.status_code == 200


def test_edit_repository_no_change_view(rf):
    """Test that not changing any values don't edit the repo."""
    with patch('plinth.modules.gitweb.edit_repo') as edit_repo:
        form_data = {
            'gitweb-name': EXISTING_REPOS[0]['name'],
            'gitweb-description': EXISTING_REPOS[0]['description'],
            'gitweb-owner': EXISTING_REPOS[0]['owner'],
            'gitweb-default_branch': EXISTING_REPOS[0]['default_branch'],
        }
        request = rf.post(
            urls.reverse('gitweb:edit',
                         kwargs={'name': EXISTING_REPOS[0]['name']}),
            data=form_data)
        view = views.EditRepoView.as_view()
        response, _ = make_request(request, view, **EXISTING_REPOS[0])

        assert not edit_repo.called, 'method should not have been called'
        assert response.status_code == 302


def test_edit_repository_failed_view(rf):
    """Test that failed repo editing sends correct error message."""
    with patch('plinth.modules.gitweb.edit_repo',
               side_effect=PermissionError('Error')):
        form_data = {
            'gitweb-name': 'something_other',
            'gitweb-description': 'test-description',
            'gitweb-owner': 'test-owner',
            'gitweb-default_branch': 'main',
        }
        request = rf.post(
            urls.reverse('gitweb:edit',
                         kwargs={'name': EXISTING_REPOS[0]['name']}),
            data=form_data)
        view = views.EditRepoView.as_view()
        response, messages = make_request(request, view, **EXISTING_REPOS[0])

        assert list(
            messages)[0].message == 'An error occurred during configuration.'
        assert response.status_code == 302


def test_delete_repository_confirmation_view(rf):
    """Test that repo deletion confirmation shows correct repo name."""
    response, _ = make_request(rf.get(''), views.delete,
                               name=EXISTING_REPOS[0]['name'])

    assert response.context_data['name'] == EXISTING_REPOS[0]['name']
    assert response.status_code == 200


def test_delete_repository_view(rf):
    """Test that repo deletion sends correct success message."""
    response, messages = make_request(rf.post(''), views.delete,
                                      name=EXISTING_REPOS[0]['name'])

    assert list(messages)[0].message == '{} deleted.'.format(
        EXISTING_REPOS[0]['name'])
    assert response.status_code == 302


def test_delete_repository_fail_view(rf):
    """Test that failed repository deletion sends correct error message."""

    with patch('plinth.modules.gitweb.privileged.delete_repo',
               side_effect=FileNotFoundError('Error')):
        response, messages = make_request(rf.post(''), views.delete,
                                          name=EXISTING_REPOS[0]['name'])

        assert list(
            messages)[0].message == 'Could not delete {}: Error'.format(
                EXISTING_REPOS[0]['name'])
        assert response.status_code == 302


def test_delete_non_existing_repository_view(rf):
    """Test that deleting non-existing repository raises 404."""
    with pytest.raises(Http404):
        make_request(rf.post(''), views.delete, name='unknown_repo')
