# Copyright (C) 2010-2023 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# 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 Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/

import time
import shutil
import datetime

import pytest

from rhodecode.lib.str_utils import safe_bytes
from rhodecode.lib.vcs.backends import get_backend
from rhodecode.lib.vcs.backends.base import Config
from rhodecode.lib.vcs.nodes import FileNode
from rhodecode.tests import get_new_dir
from rhodecode.tests.utils import check_skip_backends, check_xfail_backends


@pytest.fixture()
def vcs_repository_support(
        request, backend_alias, baseapp, _vcs_repo_container):
    """
    Provide a test repository for the test run.

    Depending on the value of `recreate_repo_per_test` a new repo for each
    test will be created.

    The parameter `--backends` can be used to limit this fixture to specific
    backend implementations.
    """
    cls = request.cls

    check_skip_backends(request.node, backend_alias)
    check_xfail_backends(request.node, backend_alias)

    if _should_create_repo_per_test(cls):
        _vcs_repo_container = _create_vcs_repo_container(request)

    repo = _vcs_repo_container.get_repo(cls, backend_alias=backend_alias)

    # TODO: johbo: Supporting old test class api, think about removing this
    cls.repo = repo
    cls.repo_path = repo.path
    cls.default_branch = repo.DEFAULT_BRANCH_NAME
    cls.Backend = cls.backend_class = repo.__class__
    cls.imc = repo.in_memory_commit

    return backend_alias, repo


@pytest.fixture(scope='class')
def _vcs_repo_container(request):
    """
    Internal fixture intended to help support class based scoping on demand.
    """
    return _create_vcs_repo_container(request)


def _create_vcs_repo_container(request):
    repo_container = VcsRepoContainer()
    if not request.config.getoption('--keep-tmp-path'):
        request.addfinalizer(repo_container.cleanup)
    return repo_container


class VcsRepoContainer(object):

    def __init__(self):
        self._cleanup_paths = []
        self._repos = {}

    def get_repo(self, test_class, backend_alias):
        if backend_alias not in self._repos:
            repo = _create_empty_repository(test_class, backend_alias)

            self._cleanup_paths.append(repo.path)
            self._repos[backend_alias] = repo
        return self._repos[backend_alias]

    def cleanup(self):
        for repo_path in reversed(self._cleanup_paths):
            shutil.rmtree(repo_path)


def _should_create_repo_per_test(cls):
    return getattr(cls, 'recreate_repo_per_test', False)


def _create_empty_repository(cls, backend_alias=None):
    Backend = get_backend(backend_alias or cls.backend_alias)
    repo_path = get_new_dir(str(time.time()))
    repo = Backend(repo_path, create=True)
    if hasattr(cls, '_get_commits'):
        commits = cls._get_commits()
        cls.tip = _add_commits_to_repo(repo, commits)

    return repo


@pytest.fixture()
def config():
    """
    Instance of a repository config.

    The instance contains only one value:

    - Section: "section-a"
    - Key:     "a-1"
    - Value:   "value-a-1"

    The intended usage is for cases where a config instance is needed but no
    specific content is required.
    """
    config = Config()
    config.set('section-a', 'a-1', 'value-a-1')
    return config


def _add_commits_to_repo(repo, commits):
    imc = repo.in_memory_commit
    tip = None

    for commit in commits:
        for node in commit.get('added', []):
            if not isinstance(node, FileNode):
                node = FileNode(safe_bytes(node.path), content=node.content)
            imc.add(node)

        for node in commit.get('changed', []):
            if not isinstance(node, FileNode):
                node = FileNode(safe_bytes(node.path), content=node.content)
            imc.change(node)

        for node in commit.get('removed', []):
            imc.remove(FileNode(safe_bytes(node.path)))

        tip = imc.commit(
            message=str(commit['message']),
            author=str(commit['author']),
            date=commit['date'],
            branch=commit.get('branch')
        )

    return tip


@pytest.fixture()
def vcs_repo(request, backend_alias):
    Backend = get_backend(backend_alias)
    repo_path = get_new_dir(str(time.time()))
    repo = Backend(repo_path, create=True)

    @request.addfinalizer
    def cleanup():
        shutil.rmtree(repo_path)

    return repo


@pytest.fixture()
def generate_repo_with_commits(vcs_repo):
    """
    Creates a fabric to generate N commits with some file nodes on a randomly
    generated repository
    """

    def commit_generator(num):
        start_date = datetime.datetime(2010, 1, 1, 20)
        for x in range(num):
            yield {
                'message': 'Commit %d' % x,
                'author': 'Joe Doe <joe.doe@example.com>',
                'date': start_date + datetime.timedelta(hours=12 * x),
                'added': [
                    FileNode(b'file_%d.txt' % x, content=b'Foobar %d' % x),
                ],
                'modified': [
                    FileNode(b'file_%d.txt' % x,
                             content=b'Foobar %d modified' % (x-1)),
                ]
            }

    def commit_maker(num=5):
        _add_commits_to_repo(vcs_repo, commit_generator(num))
        return vcs_repo

    return commit_maker


@pytest.fixture()
def hg_repo(request, vcs_repo):
    repo = vcs_repo

    commits = repo._get_commits()
    _add_commits_to_repo(repo, commits)

    return repo


@pytest.fixture()
def hg_commit(hg_repo):
    return hg_repo.get_commit()


class BackendTestMixin(object):
    """
    This is a backend independent test case class which should be created
    with ``type`` method.

    It is required to set following attributes at subclass:

    - ``backend_alias``: alias of used backend (see ``vcs.BACKENDS``)
    - ``repo_path``: path to the repository which would be created for set of
      tests
    - ``recreate_repo_per_test``: If set to ``False``, repo would NOT be
      created
      before every single test. Defaults to ``True``.
    """
    recreate_repo_per_test = True

    @classmethod
    def _get_commits(cls):
        commits = [
            {
                'message': 'Initial commit',
                'author': 'Joe Doe <joe.doe@example.com>',
                'date': datetime.datetime(2010, 1, 1, 20),
                'added': [
                    FileNode(b'foobar', content=b'Foobar'),
                    FileNode(b'foobar2', content=b'Foobar II'),
                    FileNode(b'foo/bar/baz', content=b'baz here!'),
                ],
            },
            {
                'message': 'Changes...',
                'author': 'Jane Doe <jane.doe@example.com>',
                'date': datetime.datetime(2010, 1, 1, 21),
                'added': [
                    FileNode(b'some/new.txt', content=b'news...'),
                ],
                'changed': [
                    FileNode(b'foobar', b'Foobar I'),
                ],
                'removed': [],
            },
        ]
        return commits