import functools
import tempfile
import unittest
from unittest.mock import sentinel

import pygit2
import pytest

import gitubuntu.git_repository
from gitubuntu.repo_builder import (
    Blob,
    Commit,
    ExecutableBlob,
    Placeholder,
    Repo,
    Source,
    SourceTree,
    Symlink,
    Tree,
    find_node,
    replace_placeholders,
)
from gitubuntu.test_fixtures import (
    repo,
    pygit2_repo,
)


class TestObjectCreation(unittest.TestCase):
    def setUp(self):
        self.git_dir = tempfile.TemporaryDirectory()
        self.repo = pygit2.init_repository(self.git_dir.name)

    def tearDown(self):
        self.repo = None
        self.git_dir.cleanup()

    def testBlobCreation(self):
        ref = Blob(b'foo').write(self.repo)
        assert self.repo.get(ref).data == b'foo'

    def testExecutableBlobTreeCreation(self):
        tree_ref = Tree({'foo': ExecutableBlob(b'bar')}).write(self.repo)
        tree_entry = self.repo.get(tree_ref)['foo']
        assert self.repo.get(tree_entry.id).data == b'bar'
        assert tree_entry.filemode == pygit2.GIT_FILEMODE_BLOB_EXECUTABLE

    def testSymlinkTreeCreation(self):
        tree_ref = Tree({'foo': Symlink(b'bar')}).write(self.repo)
        tree_entry = self.repo.get(tree_ref)['foo']
        assert tree_entry.filemode == pygit2.GIT_FILEMODE_LINK

    def testCommitCreation(self):
        tree = Tree({'foo': Blob(b'bar')})
        tree_ref = tree.write(self.repo)
        commit_ref = Commit(tree).write(self.repo)
        commit = self.repo.get(commit_ref)
        assert commit.tree_id == tree_ref

    def testCommitMessage(self):
        ref = Commit(Tree({}), message='foo').write(self.repo)
        commit = self.repo.get(ref)
        assert commit.message == 'foo'

    def testCommitParents(self):
        tree = Tree({})
        top = Commit(tree, message='top')
        top_ref = top.write(self.repo)
        child_a = Commit(tree, parents=[top], message='child_a')
        child_a_ref = child_a.write(self.repo)
        child_b = Commit(tree, parents=[top], message='child_b')
        child_b_ref = child_b.write(self.repo)
        merge = Commit(tree, parents=[child_a, child_b], message='merge')
        merge_ref = merge.write(self.repo)

        merge_commit = self.repo.get(merge_ref)
        assert merge_commit.parent_ids == [child_a_ref, child_b_ref]

        child_a_commit = self.repo.get(child_a_ref)
        assert child_a_commit.parent_ids == [top_ref]

        top_commit = self.repo.get(top_ref)
        assert top_commit.parent_ids == []

    def testCommitAuthor(self):
        """The author parameter should make it through to the commit"""
        input_commit = Commit(author=pygit2.Signature(
            'Test Author',
            'test@example.com',
            1,
            2,
        ))
        output_commit_ref = input_commit.write(self.repo)
        output_commit = self.repo.get(output_commit_ref)
        assert output_commit.author.name == 'Test Author'
        assert output_commit.author.email == 'test@example.com'
        assert output_commit.author.time == 1
        assert output_commit.author.offset == 2

    def testCommitCommitter(self):
        """The committer parameter should make it through to the commit"""
        input_commit = Commit(committer=pygit2.Signature(
            'Test Committer',
            'test@example.com',
            1,
            2,
        ))
        output_commit_ref = input_commit.write(self.repo)
        output_commit = self.repo.get(output_commit_ref)
        assert output_commit.committer.name == 'Test Committer'
        assert output_commit.committer.email == 'test@example.com'
        assert output_commit.committer.time == 1
        assert output_commit.committer.offset == 2

    def testNameSearch(self):
        inner_tree = Tree({'foo': Blob(b'bar', name='blob')}, name='inner')
        outer_tree = Tree({'baz': inner_tree}, name='outer')
        commit = Commit(outer_tree, name='commit')
        assert find_node(commit, 'commit') is commit
        assert find_node(commit, 'inner') is inner_tree
        assert find_node(commit, 'outer') is outer_tree
        assert isinstance(find_node(commit, 'blob'), Blob)
        with pytest.raises(KeyError):
            find_node(commit, 'absent')

    def testRepo(self):
        graph = Repo([
            Commit(Tree({}), parents=[Placeholder('parent')]),
            Commit(Tree({}), name='parent'),
        ])
        child_ref = graph.write(self.repo)
        child = self.repo.get(child_ref)
        assert child.parent_ids == [graph.commit_list[1].write(self.repo)]

    def testRepoBranchesTags(self):
        graph = Repo(
            commits=[
                Commit(
                    Tree({}),
                    parents=[Placeholder('parent')],
                    name='child',
                ),
                Commit(Tree({}), name='parent'),
            ],
            branches={
                'branch1': Placeholder('parent'),
                'branch2': Commit(Tree({'foo': Blob(b'qux')})),
            },
            tags={
                'tag1': Placeholder('child'),
                'tag2': Commit(Tree({'foo': Blob(b'quz')})),
            },
        )
        child_ref = graph.write(self.repo)
        child = self.repo.get(child_ref)
        assert self.repo.lookup_reference(
            'refs/heads/branch1'
        ).peel(pygit2.Commit).id == graph.commit_list[1].write(self.repo)
        assert self.repo.lookup_reference('refs/heads/branch2')
        assert self.repo.lookup_reference(
            'refs/tags/tag1'
        ).peel(pygit2.Commit).id == graph.commit_list[0].write(self.repo)
        assert self.repo.lookup_reference('refs/tags/tag2')
        assert child.parent_ids == [graph.commit_list[1].write(self.repo)]

    def testRepoTagger(self):
        """The tagger parameter should make it through to the tag"""
        input_repo = Repo(
            commits=[Commit(name='root')],
            tags={'root': Placeholder('root')},
            tagger=pygit2.Signature(
                'Test Tagger',
                'test@example.com',
                1,
                2,
            ),
        )
        input_repo.write(self.repo)
        root_ref = self.repo.lookup_reference('refs/tags/root')
        root_tag = root_ref.peel(pygit2.Tag)
        assert root_tag.tagger.name == 'Test Tagger'
        assert root_tag.tagger.email == 'test@example.com'
        assert root_tag.tagger.time == 1
        assert root_tag.tagger.offset == 2


def test_source_tree(pygit2_repo):
    commit_str = Commit(SourceTree(Source())).write(pygit2_repo)
    commit = pygit2_repo.get(commit_str)
    assert gitubuntu.git_repository.follow_symlinks_to_blob(
        repo=pygit2_repo,
        treeish_object=commit,
        path='debian/changelog',
    )


@pytest.mark.parametrize('cls', [
    functools.partial(Blob, b'qux'),
    functools.partial(ExecutableBlob, b'qux'),
    functools.partial(Symlink, 'target'),
    functools.partial(Tree, {}),
    functools.partial(SourceTree, {}),
])
def test_replace_placeholders(cls):
    common_blob = cls(name='name')
    top = Tree({'foo': common_blob, 'bar': Placeholder('name')})
    assert top.entries['foo'] is not top.entries['bar']
    replace_placeholders(top)
    assert top.entries['foo'] is top.entries['bar']
    assert isinstance(top.entries['bar'], type(common_blob))


# The following test uses replace directly, because
# replace_placeholders' use of find_node will raise its own KeyError.
@pytest.mark.parametrize('testobj', [
    Tree({}),
    Commit(Tree({})),
    Repo(),
])
def test_replace_missing_placeholder(testobj):
    commit = Commit(Tree({}))
    with pytest.raises(KeyError):
        testobj.replace(Placeholder('name'), commit)


def test_repo_placeholder():
    empty_tree = Tree({})
    graph = Repo(
        commits=[
            Commit(empty_tree, message='top', name='top'),
            Commit(
                empty_tree,
                message='child',
                parents=[Placeholder('top')],
                name='child',
            ),
        ],
        branches={
            'branch1': Placeholder('top'),
            'branch2': Placeholder('child'),
        },
        tags={
            'tag1': Placeholder('child'),
            'tag2': Placeholder('top'),
        },
    )
    replace_placeholders(graph)
    assert graph.commit_list[1].parents[0] is graph.commit_list[0]
    assert graph.branches['branch1'] is graph.commit_list[0]
    assert graph.branches['branch2'] is graph.commit_list[1]
    assert graph.tags['tag2'] is graph.commit_list[0]
    assert graph.tags['tag1'] is graph.commit_list[1]


def test_copy():
    graph = Repo(
        commits=[
            Commit(Tree({}), message='top', name='top'),
            Commit(
                Tree({}),
                message='child',
                parents=[Placeholder('top')],
                name='child',
            ),
        ],
        branches={
            'branch1': Placeholder('top'),
            'branch2': Placeholder('child'),
        },
        tags={
            'tag1': Placeholder('child'),
        },
    )
    copy_graph = graph.copy(
        add_commits=[
             Commit(Tree({}), message='new', name='new'),
        ],
        update_branches={
            'branch2': Placeholder('new'),
            'branch3': Placeholder('new'),
        },
        update_tags={
            'tag2': Placeholder('new'),
            'tag3': Placeholder('top'),
        }
    )
    replace_placeholders(graph)
    replace_placeholders(copy_graph)

    assert graph.commit_list[1].parents[0] is graph.commit_list[0]
    assert graph.branches['branch1'] is graph.commit_list[0]
    assert graph.tags['tag1'] is graph.commit_list[1]

    assert copy_graph.commit_list[2].message == 'new'
    assert copy_graph.branches['branch1'] is copy_graph.commit_list[0]
    assert copy_graph.branches['branch2'] is copy_graph.commit_list[2]
    assert copy_graph.branches['branch3'] is copy_graph.commit_list[2]
    assert copy_graph.tags['tag1'] is copy_graph.commit_list[1]
    assert copy_graph.tags['tag2'] is copy_graph.commit_list[2]
    assert copy_graph.tags['tag3'] is copy_graph.commit_list[0]


def test_commit_from_spec_parents():
    """Argument parents should end up in Commit object parents attribute"""
    commit = Commit.from_spec(parents=sentinel.parents).parents
    assert commit == sentinel.parents


def test_commit_from_spec_message():
    """Argument message should end up in Commit object message attribute"""
    commit =  Commit.from_spec(message=sentinel.message).message
    assert commit == sentinel.message


def test_commit_from_spec_name():
    """Argument name should end up in Commit object name attribute"""
    assert Commit.from_spec(name=sentinel.name).name == sentinel.name


@pytest.mark.parametrize('patches_applied', [True, False])
def test_commit_from_spec_patches_applied(patches_applied):
    """Check behaviour of patches_applied argument"""
    commit = Commit.from_spec(patches_applied=patches_applied)
    # Underlying SourceTree object should now have a matching patches_applied
    # attribute
    assert commit.tree.patches_applied == patches_applied
    # Underlying SourceSpec object should now have a matching has_patches
    # attribute
    assert commit.tree.source.spec.has_patches == patches_applied


def test_commit_from_spec_kwargs():
    """Arbitrary arguments should result in adjusted behaviour in SourceSpec"""
    assert Commit.from_spec(native=True).tree.source.spec.native
