# Copyright (C) 2020 Sebastian Pipping <sebastian@pipping.org>
# Licensed under GPL v3 or later

import os
from tempfile import TemporaryDirectory
from textwrap import dedent
from unittest import TestCase
from unittest.mock import Mock

from parameterized import parameterized

from .._engine import DeleteMergedBranches
from .helpers import create_dmb, create_git, run_script


class MergeDetectionTest(TestCase):
    def test_effort_1_truly_merged(self):
        setup_script = dedent("""
            git init

            # Create a commit to base future branches upon
            echo line1 > file.txt
            git add file.txt
            git commit -m 'Add file.txt with one line'

            # Create a merged branch: With HEAD sitting in master's past
            git branch merged1

            # Create a merged branch: Topic branch with original commit
            git checkout -b merged2
            echo line2 >> file.txt
            git commit -a -m 'Add line 2'
            git checkout master
            git merge --no-ff --no-edit merged2

            # Create a not-merged branch
            git checkout -b not-merged1
            echo line3 >> file.txt
            git commit -a -m 'Add line 3'
        """)
        with TemporaryDirectory() as d:
            run_script(setup_script, cwd=d)
            git = create_git(d)
            dmb = create_dmb(git, effort_level=1)
            self.assertEqual(
                git.find_local_branches(), ["master", "merged1", "merged2", "not-merged1"]
            )

            truly_merged, defacto_merged = (
                dmb._find_branches_merged_to_all_targets_for_single_remote(
                    {"master"}, set(), remote_name=None
                )
            )

            self.assertEqual(truly_merged, {"merged1", "merged2"})
            self.assertEqual(defacto_merged, set())

    def test_effort_2_unsquashed_cherries(self):
        setup_script = dedent("""
            git init

            # Create a commit to base future branches upon
            echo line1 > file1.txt
            git add file1.txt
            git commit -m 'Add file1.txt'

            # Create a de-facto merged branch: forward order
            git checkout -b defacto-merged1
            cp file1.txt file2.txt
            git add file2.txt
            git commit -m 'Add file2.txt'
            cp file1.txt file3.txt
            git add file3.txt
            git commit -m 'Add file3.txt'

            # Create a de-facto merged branch: backward order
            git checkout -b defacto-merged2 master
            cp file1.txt file3.txt
            git add file3.txt
            git commit -m 'Add file3.txt'
            cp file1.txt file2.txt
            git add file2.txt
            git commit -m 'Add file2.txt'

            # Add an extra commit on master so that we don't get
            # identical SHA1s when cherry-picking, after
            git checkout master
            cp file1.txt file4.txt
            git add file4.txt
            git commit -m 'Add file4.txt'

            # Get the commits on master that will make
            # branches defacto-merged{1,2} be detected as de-facto merged
            git cherry-pick defacto-merged1{^,}

            # Create a not-defacto-merged branch
            git checkout -b not-defacto-merged1 defacto-merged1
            cp file1.txt file5.txt
            git add file5.txt
            git commit -m 'Add file5.txt'
        """)
        with TemporaryDirectory() as d:
            run_script(setup_script, cwd=d)
            git = create_git(d)
            dmb = create_dmb(git, effort_level=2)
            self.assertEqual(
                git.find_local_branches(),
                ["defacto-merged1", "defacto-merged2", "master", "not-defacto-merged1"],
            )

            truly_merged, defacto_merged = (
                dmb._find_branches_merged_to_all_targets_for_single_remote(
                    {"master"}, set(), remote_name=None
                )
            )

            self.assertEqual(truly_merged, set())
            self.assertEqual(defacto_merged, {"defacto-merged1", "defacto-merged2"})

    def test_effort_3_squashed_cherries(self):
        setup_script = dedent("""
            git init

            # Create a commit to base future branches upon
            echo line1 > file1.txt
            git add file1.txt
            git commit -m 'Add file1.txt'

            # Create a de-facto squash-merged branch: Adding up to full diff
            git checkout -b defacto-squash-merged1
            cp file1.txt file2.txt
            git add file2.txt
            git commit -m 'Add file2.txt'
            cp file1.txt file3.txt
            git add file3.txt
            git commit -m 'Add file3.txt'

            # Create de-facto squash-merged branch: With reverts, adding up
            git checkout -b defacto-squash-merged2
            git revert --no-edit HEAD
            git revert --no-edit HEAD  # i.e. revert the revert

            # Get the a squashed commit on master that will make
            # branches defacto-squash-merged{1,2} be detected as de-facto merged
            git checkout master
            git merge --squash defacto-squash-merged1
            git commit -m "Add squashed copy of 'defacto-squash-merged1'"

            # Create not-defacto-squash-merged branch: Squashed copy commit
            #                                          .. will not have a counterpart
            git checkout -b not-defacto-squash-merged1
            git revert --no-edit HEAD
        """)
        with TemporaryDirectory() as d:
            run_script(setup_script, cwd=d)
            git = create_git(d)
            dmb = create_dmb(git, effort_level=3)
            self.assertEqual(
                git.find_local_branches(),
                [
                    "defacto-squash-merged1",
                    "defacto-squash-merged2",
                    "master",
                    "not-defacto-squash-merged1",
                ],
            )

            truly_merged, defacto_merged = (
                dmb._find_branches_merged_to_all_targets_for_single_remote(
                    {"master"}, set(), remote_name=None
                )
            )

            self.assertEqual(truly_merged, set())
            self.assertEqual(defacto_merged, {"defacto-squash-merged1", "defacto-squash-merged2"})


class RefreshTargetBranchesTest(TestCase):
    def test_refresh_gets_branches_back_in_sync(self):
        setup_script = dedent("""
            mkdir upstream
            cd upstream
                git init
                git commit --allow-empty -m 'Dummy commit #1'
                git branch pull-works
                git branch pull-trouble
                git checkout -b checkout-trouble
                    echo line1 > collision.txt
                    git add collision.txt
                    git commit -m 'Add collision.txt'
                git checkout master
            cd ..
            git clone -o upstream upstream downstream
            cd downstream
                git branch --track checkout-trouble upstream/checkout-trouble
                git branch --track pull-trouble upstream/pull-trouble
                git branch --track pull-works upstream/pull-works
            cd ..
            cd upstream
                git checkout pull-trouble
                    git merge --ff checkout-trouble
                git checkout pull-works
                    git commit --allow-empty -m 'Dummy commit #2'
            cd ..
            cd downstream
                git checkout -b topic1
                echo line1 > collision.txt  # uncommitted, just present
        """)

        with TemporaryDirectory() as d:
            run_script(setup_script, cwd=d)

            downstream_git = create_git(os.path.join(d, "downstream"))
            downstream_dmb = create_dmb(downstream_git, effort_level=3)
            self.assertEqual(downstream_git.find_current_branch(), "topic1")
            self.assertEqual(
                downstream_git.find_local_branches(),
                ["checkout-trouble", "master", "pull-trouble", "pull-works", "topic1"],
            )
            downstream_dmb.refresh_remotes(["upstream"])
            self.assertEqual(len(downstream_git.cherry("pull-works", "upstream/pull-works")), 1)

            downstream_dmb.refresh_target_branches(
                ["checkout-trouble", "pull-trouble", "pull-works"]
            )

            self.assertEqual(len(downstream_git.cherry("pull-works", "upstream/pull-works")), 0)
            self.assertEqual(downstream_git.find_current_branch(), "topic1")


class GitConfigKeysContainDotsTest(TestCase):
    @parameterized.expand(
        [
            (
                DeleteMergedBranches.find_required_branches,
                "branch.release-1.0.x.dmb-required",
                "release-1.0.x",
            ),
            (
                DeleteMergedBranches.find_excluded_branches,
                "branch.release-1.0.x.dmb-excluded",
                "release-1.0.x",
            ),
            (
                DeleteMergedBranches.find_enabled_remotes,
                "remote.linux-6.x.dmb-enabled",
                "linux-6.x",
            ),
        ]
    )
    def test_supports_branch_names_containing_dots(
        self, extractor_function, git_config_dict_key, expected_value
    ):
        assert "." in expected_value
        git_config_dict = {
            git_config_dict_key: "true",
        }
        self.assertEqual(extractor_function(git_config_dict), [expected_value])


class DetermineExcludedBranchesTest(TestCase):
    @parameterized.expand(
        [
            ("git config exclude", ["b1", "b2", "b3"], ["b1", "b3"], [], [], {"b1", "b3"}),
            ("--exclude", ["b1", "b2", "b3"], [], ["b1", "b3"], [], {"b1", "b3"}),
            ("--exclude + config exclude", ["b1", "b2", "b3"], ["b1"], ["b3"], [], {"b1", "b3"}),
            (
                "--include-regex match full",
                ["b1", "b2", "b3"],
                [],
                [],
                ["^..$", "^b2$"],
                {"b1", "b3"},
            ),
            ("--include-regex match partial", ["b1", "b2", "b3"], [], [], [r"\d", "b"], set()),
            (
                "--include-regex mismatch",
                ["b1", "b2", "b3"],
                [],
                [],
                ["^b1$", "^b2$"],
                {"b1", "b2", "b3"},
            ),
            (
                "--include-regex + --exclude + config exclude",
                ["b1", "b2", "b3"],
                ["b1"],
                ["b3"],
                [r"^b\d$"],
                {"b1", "b3"},
            ),
        ]
    )
    def test(
        self,
        _label,
        existing_branches,
        excluded_branches_from_config,
        excluded_branches_extra,
        included_branches_patterns,
        expected_exclusion_set,
    ):
        git_mock = Mock(find_all_branch_names=Mock(return_value=existing_branches))
        dmb = DeleteMergedBranches(
            git=git_mock, messenger=Mock(), confirmation=Mock(), selector=Mock(), effort_level=999
        )
        git_config_key_format = DeleteMergedBranches._FORMAT_BRANCH_EXCLUDED
        git_config = {
            git_config_key_format.format(name=branch_name): DeleteMergedBranches._CONFIG_VALUE_TRUE
            for branch_name in excluded_branches_from_config
        }

        actual_exclusion_set = dmb.determine_excluded_branches(
            git_config, excluded_branches_extra, included_branches_patterns
        )

        self.assertEqual(actual_exclusion_set, expected_exclusion_set)
