# Copyright 2023 The Bazel Authors. All rights reserved.
#
# 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
#
#     http://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.

import os
import pathlib
import shutil
import tempfile
import unittest
from typing import Optional, Set

from python.pip_install.tools.wheel_installer import namespace_pkgs


class TempDir:
    def __init__(self) -> None:
        self.dir = tempfile.mkdtemp()

    def root(self) -> str:
        return self.dir

    def add_dir(self, rel_path: str) -> None:
        d = pathlib.Path(self.dir, rel_path)
        d.mkdir(parents=True)

    def add_file(self, rel_path: str, contents: Optional[str] = None) -> None:
        f = pathlib.Path(self.dir, rel_path)
        f.parent.mkdir(parents=True, exist_ok=True)
        if contents:
            with open(str(f), "w") as writeable_f:
                writeable_f.write(contents)
        else:
            f.touch()

    def remove(self) -> None:
        shutil.rmtree(self.dir)


class TestImplicitNamespacePackages(unittest.TestCase):
    def assertPathsEqual(self, actual: Set[pathlib.Path], expected: Set[str]) -> None:
        self.assertEqual(actual, {pathlib.Path(p) for p in expected})

    def test_in_current_directory(self) -> None:
        directory = TempDir()
        directory.add_file("foo/bar/biz.py")
        directory.add_file("foo/bee/boo.py")
        directory.add_file("foo/buu/__init__.py")
        directory.add_file("foo/buu/bii.py")
        cwd = os.getcwd()
        os.chdir(directory.root())
        expected = {
            "foo",
            "foo/bar",
            "foo/bee",
        }
        try:
            actual = namespace_pkgs.implicit_namespace_packages(".")
            self.assertPathsEqual(actual, expected)
        finally:
            os.chdir(cwd)
            directory.remove()

    def test_finds_correct_namespace_packages(self) -> None:
        directory = TempDir()
        directory.add_file("foo/bar/biz.py")
        directory.add_file("foo/bee/boo.py")
        directory.add_file("foo/buu/__init__.py")
        directory.add_file("foo/buu/bii.py")

        expected = {
            directory.root() + "/foo",
            directory.root() + "/foo/bar",
            directory.root() + "/foo/bee",
        }
        actual = namespace_pkgs.implicit_namespace_packages(directory.root())
        self.assertPathsEqual(actual, expected)

    def test_ignores_empty_directories(self) -> None:
        directory = TempDir()
        directory.add_file("foo/bar/biz.py")
        directory.add_dir("foo/cat")

        expected = {
            directory.root() + "/foo",
            directory.root() + "/foo/bar",
        }
        actual = namespace_pkgs.implicit_namespace_packages(directory.root())
        self.assertPathsEqual(actual, expected)

    def test_empty_case(self) -> None:
        directory = TempDir()
        directory.add_file("foo/__init__.py")
        directory.add_file("foo/bar/__init__.py")
        directory.add_file("foo/bar/biz.py")

        actual = namespace_pkgs.implicit_namespace_packages(directory.root())
        self.assertEqual(actual, set())

    def test_ignores_non_module_files_in_directories(self) -> None:
        directory = TempDir()
        directory.add_file("foo/__init__.pyi")
        directory.add_file("foo/py.typed")

        actual = namespace_pkgs.implicit_namespace_packages(directory.root())
        self.assertEqual(actual, set())

    def test_parent_child_relationship_of_namespace_pkgs(self):
        directory = TempDir()
        directory.add_file("foo/bar/biff/my_module.py")
        directory.add_file("foo/bar/biff/another_module.py")

        expected = {
            directory.root() + "/foo",
            directory.root() + "/foo/bar",
            directory.root() + "/foo/bar/biff",
        }
        actual = namespace_pkgs.implicit_namespace_packages(directory.root())
        self.assertPathsEqual(actual, expected)

    def test_parent_child_relationship_of_namespace_and_standard_pkgs(self):
        directory = TempDir()
        directory.add_file("foo/bar/biff/__init__.py")
        directory.add_file("foo/bar/biff/another_module.py")

        expected = {
            directory.root() + "/foo",
            directory.root() + "/foo/bar",
        }
        actual = namespace_pkgs.implicit_namespace_packages(directory.root())
        self.assertPathsEqual(actual, expected)

    def test_parent_child_relationship_of_namespace_and_nested_standard_pkgs(self):
        directory = TempDir()
        directory.add_file("foo/bar/__init__.py")
        directory.add_file("foo/bar/biff/another_module.py")
        directory.add_file("foo/bar/biff/__init__.py")
        directory.add_file("foo/bar/boof/big_module.py")
        directory.add_file("foo/bar/boof/__init__.py")
        directory.add_file("fim/in_a_ns_pkg.py")

        expected = {
            directory.root() + "/foo",
            directory.root() + "/fim",
        }
        actual = namespace_pkgs.implicit_namespace_packages(directory.root())
        self.assertPathsEqual(actual, expected)

    def test_recognized_all_nonstandard_module_types(self):
        directory = TempDir()
        directory.add_file("ayy/my_module.pyc")
        directory.add_file("bee/ccc/dee/eee.so")
        directory.add_file("eff/jee/aych.pyd")

        expected = {
            directory.root() + "/ayy",
            directory.root() + "/bee",
            directory.root() + "/bee/ccc",
            directory.root() + "/bee/ccc/dee",
            directory.root() + "/eff",
            directory.root() + "/eff/jee",
        }
        actual = namespace_pkgs.implicit_namespace_packages(directory.root())
        self.assertPathsEqual(actual, expected)

    def test_skips_ignored_directories(self):
        directory = TempDir()
        directory.add_file("foo/boo/my_module.py")
        directory.add_file("foo/bar/another_module.py")

        expected = {
            directory.root() + "/foo",
            directory.root() + "/foo/bar",
        }
        actual = namespace_pkgs.implicit_namespace_packages(
            directory.root(),
            ignored_dirnames=[directory.root() + "/foo/boo"],
        )
        self.assertPathsEqual(actual, expected)


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