# Copyright 2020 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
#
"""Unit tests for owners_checks.py."""
from pathlib import Path
import tempfile
from typing import Iterable, Sequence
import unittest
from unittest import mock
from pw_presubmit import owners_checks

# ===== Test data =====

bad_duplicate = """\
# Should raise OwnersDuplicateError.
set noparent

file:/foo/OWNERZ
file:../OWNERS
file:../OWNERS
test1@example.com
#Test 2 comment
test2@example.com
"""

bad_duplicate_user = """\
# Should raise OwnersDuplicateError.
set noparent

file:/foo/OWNERZ
file:../OWNERS

*
test1@example.com
#Test 2 comment
test2@example.com
                        test1@example.com
"""

bad_duplicate_wildcard = """\
# Should raise OwnersDuplicateError.
set noparent
*
file:/foo/OWNERZ
file:../OWNERS
test1@example.com
#Test 2 comment
test2@example.com
*
"""

bad_email = """\
# Should raise OwnersInvalidLineError.
set noparent
*
file:/foo/OWNERZ
file:../OWNERS
test1example.com
#Test 2 comment
test2@example.com
*
"""
bad_grant_combo = """\
# Should raise OwnersUserGrantError.

file:/foo/OWNERZ
file:../OWNERS

test1@example.com
#Test noparent comment
set noparent
test2@example.com

*
"""

bad_ordering1 = """\
# Tests formatter reorders groupings of lines into the right order.
file:/foo/OWNERZ
file:bar/OWNERZ

test1@example.com
#Test noparent comment
set noparent
test2@example.com
"""

bad_ordering1_fixed = """\
#Test noparent comment
set noparent

# Tests formatter reorders groupings of lines into the right order.
file:/foo/OWNERZ
file:bar/OWNERZ

test1@example.com
test2@example.com
"""

bad_prohibited1 = """\
# Should raise OwnersProhibitedError.
set noparent

file:/foo/OWNERZ
file:../OWNERS

test1@example.com
#Test 2 comment
test2@example.com

include file1.txt

per-file foo.txt=test3@example.com
"""

bad_moving_comments = """\
# Test comments move with the rule that follows them.
test2@example.com
test1@example.com

# foo comment
file:/foo/OWNERZ
# .. comment
file:../OWNERS

set noparent
"""
bad_moving_comments_fixed = """\
set noparent

# .. comment
file:../OWNERS
# foo comment
file:/foo/OWNERZ

test1@example.com
# Test comments move with the rule that follows them.
test2@example.com
"""

bad_whitespace = """\
 set   noparent



file:/foo/OWNERZ

  file:../OWNERS

test1@example.com
#Test 2    comment
    test2@example.com

"""

bad_whitespace_fixed = """\
set noparent

file:../OWNERS
file:/foo/OWNERZ

test1@example.com
#Test 2    comment
test2@example.com
"""

no_dependencies = """\
# Test no imports are found when there are none.
set noparent

test1@example.com
#Test 2 comment
test2@example.com
"""

has_dependencies_file = """\
# Test if owners checks examine file: imports.
set noparent

file:foo_owners
file:bar_owners

test1@example.com
#Test 2 comment
test2@example.com
"""

has_dependencies_perfile = """\
# Test if owners checks examine per-file imports.
set noparent

test1@example.com
#Test 2 comment
test2@example.com

per-file *.txt=file:foo_owners
per-file *.md=example.google.com
"""

has_dependencies_include = """\
# Test if owners checks examine per-file imports.
set noparent

test1@example.com
#Test 2 comment
test2@example.com

per-file *.txt=file:foo_owners
per-file *.md=example.google.com
"""

dependencies_paths_relative = """\
set noparent

include foo/bar/../include_owners

file:foo/bar/../file_owners

*

per-file *.txt=file:foo/bar/../perfile_owners
"""

dependencies_paths_absolute = """\
set noparent

include /test/include_owners

file:/test/file_owners

*

per-file *.txt=file:/test/perfile_owners
"""

good1 = """\
# Checks should fine this formatted correctly
set noparent

include good1_include

file:good1_file

test1@example.com
#Test 2 comment
test2@example.com #{LAST_RESORT_SUGGESTION}
# LAST LINE
"""

good1_include = """\
test1@example.comom
"""

good1_file = """\
# Checks should fine this formatted correctly.
test1@example.com
"""

foo_owners = """\
test1@example.com
"""

bar_owners = """\
test1@example.com
"""

BAD_TEST_FILES = (
    ("bad_duplicate", owners_checks.OwnersDuplicateError),
    ("bad_duplicate_user", owners_checks.OwnersDuplicateError),
    ("bad_duplicate_wildcard", owners_checks.OwnersDuplicateError),
    ("bad_email", owners_checks.OwnersInvalidLineError),
    ("bad_grant_combo", owners_checks.OwnersUserGrantError),
    ("bad_ordering1", owners_checks.OwnersStyleError),
    ("bad_prohibited1", owners_checks.OwnersProhibitedError),
)

STYLING_CHECKS = (
    ("bad_moving_comments", "bad_moving_comments_fixed"),
    ("bad_ordering1", "bad_ordering1_fixed"),
    ("bad_whitespace", "bad_whitespace_fixed"),
)

DEPENDENCY_TEST_CASES: Iterable[tuple[str, Iterable[str]]] = (
    ("no_dependencies", tuple()),
    ("has_dependencies_file", ("foo_owners", "bar_owners")),
    ("has_dependencies_perfile", ("foo_owners",)),
    ("has_dependencies_include", ("foo_owners",)),
)

DEPENDENCY_PATH_TEST_CASES: Iterable[str] = (
    "dependencies_paths_relative",
    "dependencies_paths_absolute",
)

GOOD_TEST_CASES = (("good1", "good1_include", "good1_file"),)


# ===== Unit Tests =====
class TestOwnersChecks(unittest.TestCase):
    """Unittest class for owners_checks.py."""

    maxDiff = 2000

    @staticmethod
    def _create_temp_files(
        temp_dir: str, file_list: Sequence[tuple[str, str]]
    ) -> Sequence[Path]:
        real_files = []
        temp_dir_path = Path(temp_dir)
        for name, contents in file_list:
            file_path = temp_dir_path / name
            file_path.write_text(contents)
            real_files.append(file_path)
        return real_files

    def test_bad_files(self):
        # First test_file is the "primary" owners file followed by any needed
        # "secondary" owners.
        for test_file, expected_exception in BAD_TEST_FILES:
            with self.subTest(
                i=test_file
            ), tempfile.TemporaryDirectory() as temp_dir, self.assertRaises(
                expected_exception
            ):
                file_contents = globals()[test_file]
                primary_file = self._create_temp_files(
                    temp_dir=temp_dir, file_list=((test_file, file_contents),)
                )[0]
                owners_file = owners_checks.OwnersFile(primary_file)
                owners_file.look_for_owners_errors()
                owners_file.check_style()

    def test_good(self):
        # First test_file is the "primary" owners file followed by any needed
        # "secondary" owners.
        for test_files in GOOD_TEST_CASES:
            with self.subTest(
                i=test_files[0]
            ), tempfile.TemporaryDirectory() as temp_dir:
                files = [
                    (file_name, globals()[file_name])
                    for file_name in test_files
                ]
                primary_file = self._create_temp_files(
                    temp_dir=temp_dir, file_list=files
                )[0]
                self.assertDictEqual(
                    {}, owners_checks.run_owners_checks(primary_file)
                )

    def test_style_proposals(self):
        for unstyled_file, styled_file in STYLING_CHECKS:
            with self.subTest(
                i=unstyled_file
            ), tempfile.TemporaryDirectory() as temp_dir:
                unstyled_contents = globals()[unstyled_file]
                styled_contents = globals()[styled_file]
                unstyled_real_file = self._create_temp_files(
                    temp_dir=temp_dir,
                    file_list=((unstyled_file, unstyled_contents),),
                )[0]
                owners_file = owners_checks.OwnersFile(unstyled_real_file)
                formatted_content = "\n".join(owners_file.formatted_lines)
                self.assertEqual(styled_contents, formatted_content)

    def test_dependency_discovery(self):
        for file_under_test, expected_deps in DEPENDENCY_TEST_CASES:
            # During test make the test file directory the "git root"
            with tempfile.TemporaryDirectory() as temp_dir:
                temp_dir_path = Path(temp_dir).resolve()
                with self.subTest(i=file_under_test), mock.patch(
                    "pw_presubmit.owners_checks.git_repo.root",
                    return_value=temp_dir_path,
                ):
                    primary_file = (file_under_test, globals()[file_under_test])
                    deps_files = tuple(
                        (dep, (globals()[dep])) for dep in expected_deps
                    )

                    primary_file = self._create_temp_files(
                        temp_dir=temp_dir, file_list=(primary_file,)
                    )[0]
                    dep_files = self._create_temp_files(
                        temp_dir=temp_dir, file_list=deps_files
                    )

                    owners_file = owners_checks.OwnersFile(primary_file)

                    # get_dependencies is expected to resolve() files
                    found_deps = owners_file.get_dependencies()
                    expected_deps_path = [path.resolve() for path in dep_files]
                    expected_deps_path.sort()
                    found_deps.sort()
                    self.assertListEqual(expected_deps_path, found_deps)

    def test_dependency_path_creation(self):
        """Confirm paths care constructed for absolute and relative paths."""
        for file_under_test in DEPENDENCY_PATH_TEST_CASES:
            # During test make the test file directory the "git root"
            with tempfile.TemporaryDirectory() as temp_dir:
                temp_dir_path = Path(temp_dir).resolve()
                with self.subTest(i=file_under_test), mock.patch(
                    "pw_presubmit.owners_checks.git_repo.root",
                    return_value=temp_dir_path,
                ):
                    owners_file_path = (
                        temp_dir_path / "owners" / file_under_test
                    )
                    owners_file_path.parent.mkdir(parents=True)
                    owners_file_path.write_text(globals()[file_under_test])
                    owners_file = owners_checks.OwnersFile(owners_file_path)

                    # get_dependencies is expected to resolve() files
                    found_deps = owners_file.get_dependencies()

                    if "absolute" in file_under_test:
                        # Absolute paths start with the git/project root
                        expected_prefix = temp_dir_path / "test"
                    else:
                        # Relative paths start with owners file dir
                        expected_prefix = owners_file_path.parent / "foo"

                    expected_deps_path = [
                        (expected_prefix / filename).resolve()
                        for filename in (
                            "include_owners",
                            "file_owners",
                            "perfile_owners",
                        )
                    ]
                    expected_deps_path.sort()
                    found_deps.sort()
                    self.assertListEqual(expected_deps_path, found_deps)


if __name__ == "__main__":
    unittest.main()
