# Copyright (C) 2008, 2009, 2010, 2012, 2016 Canonical Ltd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# 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 General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

"""Tests for content filtering conformance."""

import os

from breezy.controldir import ControlDir
from breezy.filters import ContentFilter
from breezy.switch import switch
from breezy.tests.per_workingtree import TestCaseWithWorkingTree
from breezy.workingtree import WorkingTree


def _converter_helper(chunks, fn):
    result = []
    for chunk in chunks:
        result.append(getattr(chunk, fn)())
    return iter(result)


def _swapcase(chunks, context=None):
    """A converter that swaps the case of text."""
    return _converter_helper(chunks, "swapcase")


def _uppercase(chunks, context=None):
    """A converter that converts text to uppercase."""
    return _converter_helper(chunks, "upper")


def _lowercase(chunks, context=None):
    """A converter that converts text to lowercase."""
    return _converter_helper(chunks, "lower")


_trailer_string = b"\nend string\n"


def _append_text(chunks, context=None):
    """A content filter that appends a string to the end of the file.

    This tests filters that change the length.
    """
    return chunks + [_trailer_string]


def _remove_appended_text(chunks, context=None):
    """Remove the appended text."""
    text = b"".join(chunks)
    if text.endswith(_trailer_string):
        text = text[: -len(_trailer_string)]
    return [text]


class TestWorkingTreeWithContentFilters(TestCaseWithWorkingTree):
    def create_cf_tree(self, txt_reader, txt_writer, dir="."):
        tree = self.make_branch_and_tree(dir)

        def _content_filter_stack(path=None, file_id=None):
            if path.endswith(".txt"):
                return [ContentFilter(txt_reader, txt_writer)]
            else:
                return []

        tree._content_filter_stack = _content_filter_stack
        self.build_tree_contents(
            [(dir + "/file1.txt", b"Foo Txt"), (dir + "/file2.bin", b"Foo Bin")]
        )
        tree.add(["file1.txt", "file2.bin"])
        tree.commit("commit raw content")
        return tree, "file1.txt", "file2.bin"

    def create_cf_tree_with_two_revisions(self, txt_reader, txt_writer, dir="."):
        tree = self.make_branch_and_tree(dir)

        def _content_filter_stack(path=None, file_id=None):
            if path.endswith(".txt"):
                return [ContentFilter(txt_reader, txt_writer)]
            else:
                return []

        tree._content_filter_stack = _content_filter_stack
        self.build_tree_contents(
            [
                (dir + "/file1.txt", b"Foo Txt"),
                (dir + "/file2.bin", b"Foo Bin"),
                (dir + "/file3.txt", b"Bar Txt"),
            ]
        )
        tree.add(["file1.txt", "file2.bin", "file3.txt"])
        tree.commit("commit raw content")
        # Commit another revision with various changes. We make sure
        # the change includes a modification, an addition and a deletion.
        # Renames are more complex and need a separate set of tests later.
        self.build_tree_contents(
            [
                (dir + "/file1.txt", b"Foo ROCKS!"),
                (dir + "/file4.txt", b"Hello World"),
            ]
        )
        tree.add(["file4.txt"])
        tree.remove(["file3.txt"], keep_files=False)
        tree.commit("change, add and rename stuff")
        return tree, "file1.txt", "file2.bin", "file3.txt", "file4.txt"

    def patch_in_content_filter(self):
        # Patch in a custom, symmetric content filter stack. It's pretty gross
        # that we need to monkey-patch a class method to do this, bit it's
        # the easiest way currently given we don't have easy access to the
        # WorkingTree after it is created but before the filter stack is used
        # to populate content.
        def new_stack(tree, path=None, file_id=None):
            if path.endswith(".txt"):
                return [ContentFilter(_swapcase, _swapcase)]
            else:
                return []

        self.overrideAttr(WorkingTree, "_content_filter_stack", new_stack)

    def assert_basis_content(self, expected_content, branch, path):
        # Note: We need to use try/finally here instead of addCleanup()
        # as the latter leaves the read lock in place too long
        basis = branch.basis_tree()
        with basis.lock_read():
            self.assertEqual(expected_content, basis.get_file_text(path))

    def test_symmetric_content_filtering(self):
        # test handling when read then write gives back the initial content
        tree, txt_path, bin_path = self.create_cf_tree(
            txt_reader=_swapcase, txt_writer=_swapcase
        )
        # Check that the basis tree has the expected content
        basis = tree.basis_tree()
        basis.lock_read()
        self.addCleanup(basis.unlock)
        if tree.supports_content_filtering():
            expected = b"fOO tXT"
        else:
            expected = b"Foo Txt"
        self.assertEqual(expected, basis.get_file_text(txt_path))
        self.assertEqual(b"Foo Bin", basis.get_file_text(bin_path))
        # Check that the working tree has the original content
        tree.lock_read()
        self.addCleanup(tree.unlock)
        with tree.get_file(txt_path, filtered=False) as f:
            self.assertEqual(b"Foo Txt", f.read())
        with tree.get_file(bin_path, filtered=False) as f:
            self.assertEqual(b"Foo Bin", f.read())

    def test_readonly_content_filtering(self):
        # test handling with a read filter but no write filter
        tree, txt_path, bin_path = self.create_cf_tree(
            txt_reader=_uppercase, txt_writer=None
        )
        # Check that the basis tree has the expected content
        basis = tree.basis_tree()
        basis.lock_read()
        self.addCleanup(basis.unlock)
        if tree.supports_content_filtering():
            expected = b"FOO TXT"
        else:
            expected = b"Foo Txt"
        self.assertEqual(expected, basis.get_file_text(txt_path))
        self.assertEqual(b"Foo Bin", basis.get_file_text(bin_path))
        # We expect the workingtree content to be unchanged (for now at least)
        tree.lock_read()
        self.addCleanup(tree.unlock)
        with tree.get_file(txt_path, filtered=False) as f:
            self.assertEqual(b"Foo Txt", f.read())
        with tree.get_file(bin_path, filtered=False) as f:
            self.assertEqual(b"Foo Bin", f.read())

    def test_branch_source_filtered_target_not(self):
        # Create a source branch with content filtering
        source, txt_path, bin_path = self.create_cf_tree(
            txt_reader=_uppercase, txt_writer=_lowercase, dir="source"
        )
        if not source.supports_content_filtering():
            return
        self.assertFileEqual(b"Foo Txt", "source/file1.txt")
        self.assert_basis_content(b"FOO TXT", source, txt_path)

        # Now branch it
        self.run_bzr("branch source target")
        target = WorkingTree.open("target")
        # Even though the content in source and target are different
        # due to different filters, iter_changes should be clean
        self.assertFileEqual(b"FOO TXT", "target/file1.txt")
        changes = target.changes_from(source.basis_tree())
        self.assertFalse(changes.has_changed())

    def test_branch_source_not_filtered_target_is(self):
        # Create a source branch with content filtering
        source, txt_path, bin_path = self.create_cf_tree(
            txt_reader=None, txt_writer=None, dir="source"
        )
        if not source.supports_content_filtering():
            return
        self.assertFileEqual(b"Foo Txt", "source/file1.txt")
        self.assert_basis_content(b"Foo Txt", source, txt_path)

        # Now patch in content filtering and branch the source
        self.patch_in_content_filter()
        self.run_bzr("branch source target")
        target = WorkingTree.open("target")
        # Even though the content in source and target are different
        # due to different filters, iter_changes should be clean
        self.assertFileEqual(b"fOO tXT", "target/file1.txt")
        changes = target.changes_from(source.basis_tree())
        self.assertFalse(changes.has_changed())

    def test_path_content_summary(self):
        """path_content_summary should always talk about the canonical form."""
        # see https://bugs.launchpad.net/bzr/+bug/415508
        #
        # set up a tree where the canonical form has a string added to the
        # end
        source, txt_path, bin_path = self.create_cf_tree(
            txt_reader=_append_text, txt_writer=_remove_appended_text, dir="source"
        )
        if not source.supports_content_filtering():
            return
        source.lock_read()
        self.addCleanup(source.unlock)

        expected_canonical_form = b"Foo Txt\nend string\n"
        with source.get_file(txt_path, filtered=True) as f:
            self.assertEqual(f.read(), expected_canonical_form)
        with source.get_file(txt_path, filtered=False) as f:
            self.assertEqual(f.read(), b"Foo Txt")

        # results are: kind, size, executable, sha1_or_link_target
        result = source.path_content_summary("file1.txt")

        self.assertEqual(result, ("file", None, False, None))

        # we could give back the length of the canonical form, but in general
        # that will be expensive to compute, so it's acceptable to just return
        # None.

    def test_content_filtering_applied_on_pull(self):
        # Create a source branch with two revisions
        source, path1, path2, path3, path4 = self.create_cf_tree_with_two_revisions(
            txt_reader=None, txt_writer=None, dir="source"
        )
        if not source.supports_content_filtering():
            return
        self.assertFileEqual(b"Foo ROCKS!", "source/file1.txt")
        self.assert_basis_content(b"Foo ROCKS!", source, path1)

        # Now patch in content filtering and branch from revision 1
        self.patch_in_content_filter()
        self.run_bzr("branch -r1 source target")
        target = WorkingTree.open("target")
        self.assert_basis_content(b"Foo Txt", target, path1)
        self.assertFileEqual(b"fOO tXT", "target/file1.txt")
        self.assert_basis_content(b"Foo Bin", target, path2)
        self.assertFileEqual(b"Foo Bin", "target/file2.bin")
        self.assert_basis_content(b"Bar Txt", target, path3)
        self.assertFileEqual(b"bAR tXT", "target/file3.txt")

        # Pull the latter change and check the target tree is updated
        self.run_bzr("pull -d target")
        self.assert_basis_content(b"Foo ROCKS!", target, path1)
        self.assertFileEqual(b"fOO rocks!", "target/file1.txt")
        self.assert_basis_content(b"Foo Bin", target, path2)
        self.assert_basis_content(b"Hello World", target, path4)
        self.assertFileEqual(b"hELLO wORLD", "target/file4.txt")

    def test_content_filtering_applied_on_merge(self):
        # Create a source branch with two revisions
        source, path1, path2, path3, path4 = self.create_cf_tree_with_two_revisions(
            txt_reader=None, txt_writer=None, dir="source"
        )
        if not source.supports_content_filtering():
            return
        self.assert_basis_content(b"Foo ROCKS!", source, path1)
        self.assertFileEqual(b"Foo ROCKS!", "source/file1.txt")
        self.assert_basis_content(b"Foo Bin", source, path2)
        self.assert_basis_content(b"Hello World", source, path4)
        self.assertFileEqual(b"Hello World", "source/file4.txt")

        # Now patch in content filtering and branch from revision 1
        self.patch_in_content_filter()
        self.run_bzr("branch -r1 source target")
        target = WorkingTree.open("target")
        self.assert_basis_content(b"Foo Txt", target, path1)
        self.assertFileEqual(b"fOO tXT", "target/file1.txt")
        self.assertFileEqual(b"Foo Bin", "target/file2.bin")
        self.assertFileEqual(b"bAR tXT", "target/file3.txt")

        # Merge the latter change and check the target tree is updated
        self.run_bzr("merge -d target source")
        self.assertFileEqual(b"fOO rocks!", "target/file1.txt")
        self.assertFileEqual(b"hELLO wORLD", "target/file4.txt")

        # Commit the merge and check the right content is stored
        target.commit("merge file1.txt changes from source")
        self.assert_basis_content(b"Foo ROCKS!", target, path1)
        self.assert_basis_content(b"Hello World", target, path4)

    def test_content_filtering_applied_on_switch(self):
        # Create a source branch with two revisions
        source, path1, path2, path3, path4 = self.create_cf_tree_with_two_revisions(
            txt_reader=None, txt_writer=None, dir="branch-a"
        )
        if not source.supports_content_filtering():
            return

        # Now patch in content filtering and branch from revision 1
        self.patch_in_content_filter()
        self.run_bzr("branch -r1 branch-a branch-b")

        # Now create a lightweight checkout referring to branch-b
        self.run_bzr("checkout --lightweight branch-b checkout")
        self.assertFileEqual(b"fOO tXT", "checkout/file1.txt")

        # Switch it to branch-b and check the tree is updated
        checkout_control_dir = ControlDir.open_containing("checkout")[0]
        switch(checkout_control_dir, source.branch)
        self.assertFileEqual(b"fOO rocks!", "checkout/file1.txt")
        self.assertFileEqual(b"hELLO wORLD", "checkout/file4.txt")

    def test_content_filtering_applied_on_revert_delete(self):
        # Create a source branch with content filtering
        source, txt_path, bin_path = self.create_cf_tree(
            txt_reader=_uppercase, txt_writer=_lowercase, dir="source"
        )
        if not source.supports_content_filtering():
            return
        self.assertFileEqual(b"Foo Txt", "source/file1.txt")
        self.assert_basis_content(b"FOO TXT", source, txt_path)

        # Now delete the file, revert it and check the content
        os.unlink("source/file1.txt")
        self.assertFalse(os.path.exists("source/file1.txt"))
        source.revert(["file1.txt"])
        self.assertTrue(os.path.exists("source/file1.txt"))
        # Note: we don't get back exactly what was in the tree
        # previously because lower(upper(text)) is a lossy transformation
        self.assertFileEqual(b"foo txt", "source/file1.txt")

    def test_content_filtering_applied_on_revert_rename(self):
        # Create a source branch with content filtering
        source, txt_path, bin_path = self.create_cf_tree(
            txt_reader=_uppercase, txt_writer=_lowercase, dir="source"
        )
        if not source.supports_content_filtering():
            return
        self.assertFileEqual(b"Foo Txt", "source/file1.txt")
        self.assert_basis_content(b"FOO TXT", source, txt_path)

        # Now modify & rename a file, revert it and check the content
        self.build_tree_contents([("source/file1.txt", b"Foo Txt with new content")])
        source.rename_one("file1.txt", "file1.bin")
        self.assertTrue(os.path.exists("source/file1.bin"))
        self.assertFalse(os.path.exists("source/file1.txt"))
        self.assertFileEqual(b"Foo Txt with new content", "source/file1.bin")
        source.revert(["file1.bin"])
        self.assertFalse(os.path.exists("source/file1.bin"))
        self.assertTrue(os.path.exists("source/file1.txt"))
        # Note: we don't get back exactly what was in the tree
        # previously because lower(upper(text)) is a lossy transformation
        self.assertFileEqual(b"foo txt", "source/file1.txt")
