# This file contains tests additional to importer_test.py grouped here because
# they are considerable on their own.

from unittest.mock import (
    patch,
)

import pygit2
import pytest

import gitubuntu.repo_builder as repo_builder
from gitubuntu.repo_builder import Commit, Placeholder
import gitubuntu.repo_comparator as repo_comparator
import gitubuntu.source_builder as source_builder

import gitubuntu.importer as target

from gitubuntu.test_fixtures import repo
from gitubuntu.importer_test import MockSPI, patch_get_commit_environment


# When importing a publishing record, there are two potential logical
# consequences:
#
#    1) Some new commit is created, with an associated tag; and/or
#    2) Some branch is adjusted to point at some commit
#
# If an existing tag with the same Git tree is found as a new publish
# event, we should reuse the tag, and adjust the corresponding branch to
# the same tagged commit.
#
# If no existing tag with the same Git tree is found, a new tree and a new
# import/applied tag that points to it should be created, as appropriate, and
# the corresponding branch should be adjusted to the the new tagged commit.
#
# If a new commit is created, appropriate commit parents are found in the
# existing Git commit graph. A changelog parent is the nearest (reverse
# chronologically) tagged version from the debian/changelog of the publishing
# record.

@pytest.mark.parametrize(
    [
        'input_repo',
        'expected_output_refs',
        'validation_repo_delta',
        'validation_repo_expected_identical_refs',
        'reuse',
    ],
    [
        # 1) An existing import tag (or reimport tag) with the same Git tree
        #   - Reuse import tag
        pytest.param(
            # input_repo:
            repo_builder.Repo(
                commits=[Commit.from_spec(name='import')],
                branches={
                    'importer/ubuntu/trusty-proposed': Placeholder('import'),
                },
                tags={'importer/import/1-1': Placeholder('import')},
            ),
            # expected_output_refs:
            [
                'refs/heads/do-not-push',
                'refs/tags/importer/upstream/ubuntu/1.gz',
                'refs/heads/importer/importer/ubuntu/dsc',
                'refs/heads/importer/importer/ubuntu/pristine-tar',
            ],
            # validation_repo_delta:
            {
                'update_branches': {
                     'importer/ubuntu/trusty': Placeholder('import'),
                },
            },
            # validation_repo_expected_identical_refs:
            [
                'refs/heads/importer/ubuntu/trusty-proposed',
                'refs/heads/importer/ubuntu/trusty',
                'refs/tags/importer/import/1-1',
            ],
            # reuse:
            True,
        ),
        pytest.param(
            # input_repo:
            repo_builder.Repo(
                commits=[
                    Commit.from_spec(
                        name='import',
                        mutate='import tag contents',
                    ),
                    Commit.from_spec(name='reimport'),
                ],
                branches={
                    'importer/ubuntu/trusty-proposed': Placeholder('import'),
                },
                tags={
                    'importer/import/1-1': Placeholder('import'),
                    'importer/reimport/import/1-1/0': Placeholder('import'),
                    'importer/reimport/import/1-1/1': Placeholder('reimport'),
                },
            ),
            # expected_output_refs:
            [
                'refs/heads/do-not-push',
                'refs/tags/importer/upstream/ubuntu/1.gz',
                'refs/heads/importer/importer/ubuntu/dsc',
                'refs/heads/importer/importer/ubuntu/pristine-tar',
            ],
            # validation_repo_delta:
            {
                'update_branches': {
                     'importer/ubuntu/trusty': Placeholder('reimport'),
                },
            },
            # validation_repo_expected_identical_refs:
            [
                'refs/heads/importer/ubuntu/trusty-proposed',
                'refs/heads/importer/ubuntu/trusty',
                'refs/tags/importer/import/1-1',
                'refs/tags/importer/reimport/import/1-1/0',
                'refs/tags/importer/reimport/import/1-1/1',
            ],
            # reuse:
            True,
        ),

        # 2) An existing import tag with a different Git tree and an existing
        #    upload tag with the same Git tree
        #    - Reuse upload tag, create reimport tag
        pytest.param(
            # input_repo:
            repo_builder.Repo(
                commits=[
                    Commit.from_spec(
                        name='import',
                        mutate='The import tag contents',
                    ),
                    Commit.from_spec(name='upload'),
                ],
                branches={
                    'importer/ubuntu/trusty-proposed': Placeholder('import'),
                },
                tags={
                    'importer/import/1-1': Placeholder('import'),
                    'importer/upload/1-1': Placeholder('upload'),
                },
            ),
            # expected_output_refs:
            [
                'refs/heads/do-not-push',
                'refs/tags/importer/upstream/ubuntu/1.gz',
                'refs/heads/importer/importer/ubuntu/dsc',
                'refs/heads/importer/importer/ubuntu/pristine-tar',
                'refs/notes/importer/importer',
            ],
            # validation_repo_delta:
            {
                'update_tags': {
                    'importer/reimport/import/1-1/0': Placeholder('import'),
                    'importer/reimport/import/1-1/1': Placeholder('upload'),
                },
                'update_branches': {
                     'importer/ubuntu/trusty': Placeholder('upload'),
                },
            },
            # validation_repo_expected_identical_refs:
            [
                'refs/heads/importer/ubuntu/trusty-proposed',
                'refs/heads/importer/ubuntu/trusty',
                'refs/tags/importer/import/1-1',
                'refs/tags/importer/upload/1-1',
                'refs/tags/importer/reimport/import/1-1/0',
                'refs/tags/importer/reimport/import/1-1/1',
            ],
            # reuse:
            True,
        ),

        # 3) An existing import tag with a different Git tree and an existing
        #    upload tag with a different Git tree
        #    - Create reimport tag
        pytest.param(
            # input_repo:
            repo_builder.Repo(
                commits=[
                    Commit.from_spec(
                        name='import',
                        mutate='The import tag contents',
                    ),
                    Commit.from_spec(
                        name='upload',
                        mutate='The upload tag contents',
                    ),
                ],
                branches={
                    'importer/ubuntu/trusty-proposed': Placeholder('import'),
                },
                tags={
                    'importer/import/1-1': Placeholder('import'),
                    'importer/upload/1-1': Placeholder('upload'),
                },
            ),
            # expected_output_refs:
            [
                'refs/heads/do-not-push',
                'refs/tags/importer/upstream/ubuntu/1.gz',
                'refs/heads/importer/importer/ubuntu/dsc',
                'refs/heads/importer/importer/ubuntu/pristine-tar',
                'refs/notes/importer/changelog',
                'refs/notes/importer/importer',
            ],
            # validation_repo_delta:
            {
                'add_commits': [Commit.from_spec(
                    name='reimport',
                    message='Test commit (new)',
                )],
                'update_tags': {
                    'importer/reimport/import/1-1/0': Placeholder('import'),
                    'importer/reimport/import/1-1/1': Placeholder('reimport'),
                },
                'update_branches': {
                     'importer/ubuntu/trusty': Placeholder('reimport'),
                },
            },
            # validation_repo_expected_identical_refs:
            [
                'refs/heads/importer/ubuntu/trusty-proposed',
                'refs/heads/importer/ubuntu/trusty',
                'refs/tags/importer/import/1-1',
                'refs/tags/importer/upload/1-1',
                'refs/tags/importer/reimport/import/1-1/0',
                'refs/tags/importer/reimport/import/1-1/1',
            ],
            # reuse:
            False,
        ),

        # 4) An existing import tag with a different Git tree and no upload tag
        #    - Create reimport tag
        pytest.param(
            # input_repo:
            repo_builder.Repo(
                commits=[
                    Commit.from_spec(
                        name='import',
                        mutate='The import tag contents',
                    ),
                ],
                branches={
                    'importer/ubuntu/trusty-proposed': Placeholder('import'),
                },
                tags={'importer/import/1-1': Placeholder('import')},
            ),
            # expected_output_refs:
            [
                'refs/heads/do-not-push',
                'refs/tags/importer/upstream/ubuntu/1.gz',
                'refs/heads/importer/importer/ubuntu/dsc',
                'refs/heads/importer/importer/ubuntu/pristine-tar',
                'refs/notes/importer/changelog',
                'refs/notes/importer/importer',
            ],
            # validation_repo_delta:
            {
                'add_commits': [Commit.from_spec(
                    name='reimport',
                    message='Test commit (new)',
                )],
                'update_tags': {
                    'importer/reimport/import/1-1/0': Placeholder('import'),
                    'importer/reimport/import/1-1/1': Placeholder('reimport'),
                },
                'update_branches': {
                     'importer/ubuntu/trusty': Placeholder('reimport'),
                },
            },
            # validation_repo_expected_identical_refs:
            [
                'refs/heads/importer/ubuntu/trusty-proposed',
                'refs/heads/importer/ubuntu/trusty',
                'refs/tags/importer/import/1-1',
                'refs/tags/importer/reimport/import/1-1/0',
                'refs/tags/importer/reimport/import/1-1/1',
            ],
            # reuse:
            False,
        ),

        # 5) No import tag and an existing upload tag with the same Git tree
        #    - Reuse upload tag, create import tag
        pytest.param(
            # input_repo:
            repo_builder.Repo(
                commits=[Commit.from_spec(name='upload')],
                tags={'importer/upload/1-1': Placeholder('upload')},
            ),
            # expected_output_refs:
            [
                'refs/heads/do-not-push',
                'refs/tags/importer/upstream/ubuntu/1.gz',
                'refs/heads/importer/importer/ubuntu/dsc',
                'refs/heads/importer/importer/ubuntu/pristine-tar',
                'refs/notes/importer/importer',
            ],
            # validation_repo_delta:
            {
                'update_tags': {
                    'importer/import/1-1': Placeholder('upload'),
                },
                'update_branches': {
                     'importer/ubuntu/trusty': Placeholder('upload'),
                },
            },
            # validation_repo_expected_identical_refs:
            [
                'refs/heads/importer/ubuntu/trusty',
                'refs/tags/importer/import/1-1',
                'refs/tags/importer/upload/1-1',
            ],
            # reuse:
            True,
        ),

        # 6) No import tag and an existing upload tag with a different Git tree
        #    - Create import tag
        pytest.param(
            # input_repo:
            repo_builder.Repo(
                commits=[
                    Commit.from_spec(
                        name='upload',
                        mutate='The upload tag contents',
                    ),
                ],
                tags={'importer/upload/1-1': Placeholder('upload')},
            ),
            # expected_output_refs:
            [
                'refs/heads/do-not-push',
                'refs/tags/importer/upstream/ubuntu/1.gz',
                'refs/heads/importer/importer/ubuntu/dsc',
                'refs/heads/importer/importer/ubuntu/pristine-tar',
                'refs/notes/importer/changelog',
                'refs/notes/importer/importer',
            ],
            # validation_repo_delta:
            {
                'add_commits': [
                    Commit.from_spec(
                        name='publish',
                        message='Test commit (new)',
                    ),
                ],
                'update_tags': {
                    'importer/import/1-1': Placeholder('publish'),
                },
                'update_branches': {
                    'importer/ubuntu/trusty': Placeholder('publish'),
                },
            },
            # validation_repo_expected_identical_refs:
            [
                'refs/heads/importer/ubuntu/trusty',
                'refs/tags/importer/import/1-1',
                'refs/tags/importer/upload/1-1',
            ],
            # reuse:
            False,
        ),

        # 7) No import tags or upload tags
        #    - Create import tag
        pytest.param(
            # input_repo:
            repo_builder.Repo(),
            # expected_output_refs:
            [
                'refs/heads/do-not-push',
                'refs/tags/importer/upstream/ubuntu/1.gz',
                'refs/heads/importer/importer/ubuntu/dsc',
                'refs/heads/importer/importer/ubuntu/pristine-tar',
                'refs/notes/importer/changelog',
                'refs/notes/importer/importer',
            ],
            # validation_repo_delta:
            {
                'add_commits': [
                    Commit.from_spec(
                        name='publish',
                        message='Test commit (new)',
                    ),
                ],
                'update_tags': {
                    'importer/import/1-1': Placeholder('publish'),
                },
                'update_branches': {
                     'importer/ubuntu/trusty': Placeholder('publish'),
                },
            },
            # validation_repo_expected_identical_refs:
            [
                'refs/heads/importer/ubuntu/trusty',
                'refs/tags/importer/import/1-1'
            ],
            # reuse:
            False,
        ),
    ]
)
@patch('gitubuntu.importer.get_import_tag_msg')
@patch('gitubuntu.importer.get_import_commit_msg')
def test_import_unapplied_spi_tags(
    get_import_commit_msg_mock,
    get_import_tag_msg_mock,
    repo,
    input_repo,
    expected_output_refs,
    validation_repo_delta,
    validation_repo_expected_identical_refs,
    reuse,
):
    """Test that unapplied tags are correctly created, adjusted and/or reused

    :param unittest.mock.Mock get_import_commit_msg_mock: mock of the function
        that determines the commit message to use for a given import
    :param unittest.mock.Mock get_import_tag_msg_mock: mock of the function
        that determines the tag message to use for a given import
    :param repo gitubuntu.git_repository.GitUbuntuRepository: fixture to hold a
        temporary output repository
    :param repo_builder.Repo input_repo: input repository data
    :param list(str) expected_output_refs: refs that must exist in the output
        repository
    :param dict validation_repo_delta: how to transform the input
        repository into a "validation repository", expressed as a dict to
        provide as **kwargs to gitubuntu.repo_builder.Repo.copy() against the
        input repository. The validation repository is then used for the
        purposes of comparison against the output repository.
    :param list(str) validation_repo_expected_identical_refs: refs that must be
        identical between the validation repository and the output repository
    :param bool reuse: if set, the output ref importer/ubuntu/trusty must be
        one supplied by the input repository (assumed to have a commit message
        of "Test commit") and not one created by the importer in this run
        (arranged by this test to have a different commit message)

    The input repository data is written into the output repository and then a
    fake non-native source package publication of version 1-1 in the Trusty
    release pocket is imported into it by calling import_unapplied_spi()
    directly. expected_output_refs, validation_repo_expected_identical_refs and
    reuse are then asserted. It is further asserted that no other refs exist in
    the output repository except for those listed in expected_output_refs and
    validation_repo_expected_identical_refs.
    """
    # Match the repo_builder objects
    get_import_tag_msg_mock.return_value = 'Test tag'
    # Importantly, the following commit message must not be the same as the
    # commit messages used by the test input repository commits, so that we can
    # later detect the difference between commits that were already there and
    # new commits created by the importer for the purposes of asserting the
    # reuse parameter correctly.
    get_import_commit_msg_mock.return_value = b'Test commit (new)'

    input_repo.write(repo.raw_repo)

    publish_spec = source_builder.SourceSpec(
        version='1-1',
        native=False,
    )

    with source_builder.Source(publish_spec) as dsc_path:
        target.import_unapplied_spi(
            repo=repo,
            spi=MockSPI(dsc_path, publish_spec.version),
            namespace='importer',
            skip_orig=False,
            parent_overrides={},
        )

    if reuse:
        # Test that the previous commit was reused. In this case, the
        # commit message should be one created by the repo_builder.Commit()
        # default, and not one created by the importer.
        tip_ref = repo.raw_repo.lookup_reference(
            'refs/heads/importer/ubuntu/trusty'
        )
        message = tip_ref.peel(pygit2.Commit).message
        assert message == 'Test commit'

    validation_repo = input_repo.copy(**validation_repo_delta)

    # Verify validation_repo_expected_identical_refs
    assert repo_comparator.equals(
        repoA=repo.raw_repo,
        repoB=validation_repo,
        test_refs=validation_repo_expected_identical_refs,
    )

    # Verify expected_output_refs
    for ref in expected_output_refs:
        assert repo.raw_repo.lookup_reference(ref)

    # Verify that no other refs exist
    all_expected_output_refs = (
        frozenset(validation_repo_expected_identical_refs) |
        frozenset(expected_output_refs)
    )
    all_actual_output_refs = frozenset(repo.raw_repo.listall_references())
    assert all_expected_output_refs == all_actual_output_refs


@pytest.mark.parametrize(
    [
        'input_repo',
        'validation_repo_delta',
        'validation_repo_expected_treewise_refs',
        'reuse',
    ],
    [
        # 1) An existing applied tag (or reimport tag) with the same Git tree
        #    - Reuse applied tag
        pytest.param(
            # input_repo:
            repo_builder.Repo(
                commits=[
                    Commit.from_spec(name='unapplied', has_patches=True),
                    Commit.from_spec(name='applied', patches_applied=True),
                ],
                branches={
                    'importer/ubuntu/trusty': Placeholder('unapplied'),
                    'importer/applied/ubuntu/trusty': Placeholder('applied'),
                },
                tags={
                    'importer/import/1-1': Placeholder('unapplied'),
                    'importer/applied/1-1': Placeholder('applied'),
                },
            ),
            # validation_repo_delta:
            {
                # no output repository delta
            },
            # validation_repo_expected_treewise_refs:
            [
                'refs/heads/importer/ubuntu/trusty',
                'refs/heads/importer/applied/ubuntu/trusty',
                'refs/tags/importer/import/1-1',
                'refs/tags/importer/applied/1-1',
            ],
            # reuse:
            True,
        ),

        # 2) An existing applied tag with a different Git tree
        #    - Create reimport tag
        pytest.param(
            # input_repo:
            repo_builder.Repo(
                commits=[
                    Commit.from_spec(
                        name='unapplied',
                        has_patches=True,
                        mutate='import tag contents',
                    ),
                    Commit.from_spec(
                        name='unapplied_reimport',
                        has_patches=True,
                    ),
                    Commit.from_spec(
                        name='applied',
                        patches_applied=True,
                        mutate='import tag contents',
                    )
                ],
                branches={'importer/ubuntu/trusty': Placeholder('unapplied')},
                tags={
                    'importer/import/1-1':
                        Placeholder('unapplied'),
                    'importer/reimport/import/1-1/0':
                        Placeholder('unapplied'),
                    'importer/reimport/import/1-1/1':
                        Placeholder('unapplied_reimport'),
                    'importer/applied/1-1':
                        Placeholder('applied'),
                },
            ),
            # validation_repo_delta:
            {
                'add_commits': [
                    Commit.from_spec(
                        name='applied_reimport',
                        patches_applied=True,
                        parents=[Placeholder('unapplied_reimport')],
                    ),
                ],
                'update_tags': {
                    'importer/reimport/applied/1-1/0':
                        Placeholder('applied'),
                    'importer/reimport/applied/1-1/1':
                        Placeholder('applied_reimport'),
                },
                'update_branches': {
                     'importer/applied/ubuntu/trusty':
                        Placeholder('applied_reimport'),
                },
            },
            # validation_repo_expected_treewise_refs:
            [
                'refs/heads/importer/ubuntu/trusty',
                'refs/heads/importer/applied/ubuntu/trusty',
                'refs/tags/importer/import/1-1',
                'refs/tags/importer/reimport/import/1-1/0',
                'refs/tags/importer/reimport/import/1-1/1',
                'refs/tags/importer/applied/1-1',
                'refs/tags/importer/reimport/applied/1-1/0',
                'refs/tags/importer/reimport/applied/1-1/1',
            ],
            # reuse:
            False,

            marks=pytest.mark.xfail(reason='LP: #1755247'),
        ),

        # 3) No applied tags
        #    - Create applied tag
        pytest.param(
            # input_repo:
            repo_builder.Repo(
                commits=[Commit.from_spec(name='unapplied', has_patches=True)],
                branches={'importer/ubuntu/trusty': Placeholder('unapplied')},
                tags={'importer/import/1-1': Placeholder('unapplied')},
            ),
            # validation_repo_delta:
            {
                'add_commits': [
                    Commit.from_spec(
                        name='applied',
                        patches_applied=True,
                        parents=[Placeholder('unapplied')],
                    ),
                ],
                'update_tags': {
                    'importer/applied/1-1': Placeholder('applied')
                },
                'update_branches': {
                     'importer/applied/ubuntu/trusty': Placeholder('applied'),
                },
            },
            # validation_repo_expected_treewise_refs:
            [
                'refs/heads/importer/ubuntu/trusty',
                'refs/heads/importer/applied/ubuntu/trusty',
                'refs/tags/importer/import/1-1',
                'refs/tags/importer/applied/1-1'
            ],
            # reuse:
            False,
        ),
    ]
)
@patch('gitubuntu.importer.get_import_tag_msg')
@patch('gitubuntu.importer.get_import_commit_msg')
def test_import_applied_spi_tags(
    get_import_commit_msg_mock,
    get_import_tag_msg_mock,
    repo,
    input_repo,
    validation_repo_delta,
    validation_repo_expected_treewise_refs,
    reuse,
):
    """Test that applied tags are correctly created, adjusted and/or reused

    :param unittest.mock.Mock get_import_commit_msg_mock: mock of the function
        that determines the commit message to use for a given import
    :param unittest.mock.Mock get_import_tag_msg_mock: mock of the function
        that determines the tag message to use for a given import
    :param repo gitubuntu.git_repository.GitUbuntuRepository: fixture to hold a
        temporary output repository
    :param repo_builder.Repo input_repo: input repository data
    :param dict validation_repo_delta: how to transform the input repository
        into a "validation repository", expressed as a dict to
        provide as **kwargs to gitubuntu.repo_builder.Repo.copy() against the
        input repository. The validation repository is then used for the
        purposes of comparison against the output repository.
    :param list(str) validation_repo_expected_treewise_refs: refs whose trees
        must be identical between the validation repository and the output
        repository
    :param bool reuse: if set, the output ref importer/ubuntu/trusty must be
        one supplied by the input repository (assumed to have a commit message
        of "Test commit") and not one created by the importer in this run
        (arranged by this test to have a different commit message)

    The input repository data is written into the output repository and then a
    fake non-native source package publication of version 1-1 in the Trusty
    release pocket is imported into it by calling import_applied_spi()
    directly. reuse and validation_repo_expected_treewise_refs are then
    asserted.

    This is similar to test_unapplied_spi_tags except that it calls
    import_applied_spi() instead of import_unapplied_spi() and only treewise
    ref comparisons are made.
    """
    # Match the repo_builder objects
    get_import_tag_msg_mock.return_value = 'Test tag'
    # Importantly, the following commit message must not be the same as the
    # commit messages used by the test input repository commits, so that we can
    # later detect the difference between commits that were already there and
    # new commits created by the importer for the purposes of asserting the
    # reuse parameter correctly.
    get_import_commit_msg_mock.return_value = b'Test commit (new)'

    input_repo.write(repo.raw_repo)

    publish_spec = source_builder.SourceSpec(
        version='1-1',
        native=False,
        has_patches=True,
    )

    with source_builder.Source(publish_spec) as dsc_path:
        target.import_applied_spi(
            repo=repo,
            spi=MockSPI(dsc_path, publish_spec.version),
            namespace='importer',
            allow_applied_failures=False,
            parent_overrides={},
        )

    if reuse:
        # Test that the previous commit was reused. In this case,
        # the commit message should be one created by the
        # repo_builder.Commit() default, and not one created by the
        # importer.
        tip_ref = repo.raw_repo.lookup_reference(
            'refs/heads/importer/ubuntu/trusty'
        )
        message = tip_ref.peel(pygit2.Commit).message
        assert message == 'Test commit'

    validation_repo = input_repo.copy(**validation_repo_delta)
    assert repo_comparator.equals(
        repoA=repo.raw_repo,
        repoB=validation_repo,
        test_refs=validation_repo_expected_treewise_refs,
        # this should be Commit, but we do not have a method yet
        # to obtain the stage-wise `quilt push` commits for a
        # patches-applied import
        comparison_type=repo_comparator.RepoComparisonType.Tree,
    )
