import os
import pathlib
from os.path import dirname
import platform
import shutil
import sys
import tempfile
from textwrap import dedent
import unittest
from unittest.mock import MagicMock, patch

from green import loader
from green.loader import GreenTestLoader, flattenTestSuite


class TestToProtoTestList(unittest.TestCase):
    def test_moduleImportFailure(self):
        """
        toProtoTestList() raises import errors normally
        """
        suite = MagicMock()
        suite.__class__.__name__ = "ModuleImportFailure"
        suite.__str__.return_value = "exception_method (other_stuff)"
        suite.exception_method.side_effect = AttributeError
        self.assertRaises(AttributeError, loader.toProtoTestList, (suite,))

    def test_moduleImportFailureIgnored(self):
        """
        toProtoTestList() does not raise errors when doing completions
        """
        suite = MagicMock()
        suite.__class__.__name__ = "ModuleImportFailure"
        suite.__str__.return_value = "exception_method other_stuff"
        suite.exception_method.side_effect = AttributeError
        self.assertEqual(loader.toProtoTestList(suite, doing_completions=True), [])


class TestToParallelTargets(unittest.TestCase):
    def setUp(self):
        super().setUp()

        class FakeModule:
            pass

        class FakeModule2:
            pass

        self._fake_module_name = "my_test_module"
        self._fake_module_name2 = "my_test_module2"
        sys.modules[self._fake_module_name] = FakeModule
        sys.modules[self._fake_module_name2] = FakeModule2

    def tearDown(self):
        del sys.modules[self._fake_module_name]
        del sys.modules[self._fake_module_name2]
        super().tearDown()

    def test_methods_with_no_constraints(self):
        """
        toParallelTargets() returns only module names.
        """

        class NormalTestCase(unittest.TestCase):
            def runTest(self):
                pass

        NormalTestCase.__module__ = self._fake_module_name

        targets = loader.toParallelTargets(NormalTestCase(), [])
        self.assertEqual(targets, [self._fake_module_name])

    def test_methods_with_constraints(self):
        """
        toParallelTargets() returns test names when constrained.
        """

        class NormalTestCase(unittest.TestCase):
            def runTest(self):
                pass

        NormalTestCase.__module__ = self._fake_module_name
        full_name = "my_test_module.NormalTestCase.runTest"

        # Pass full_name as a single string to validate that it is handled correctly.
        targets = loader.toParallelTargets(NormalTestCase(), full_name)
        self.assertEqual(targets, [full_name])

    def test_filter_out_dot(self):
        """
        toParallelTargets() correctly returns modules when '.' is in target list
        """

        class NormalTestCase(unittest.TestCase):
            def runTest(self):
                pass

        class NormalTestCase2(unittest.TestCase):
            def runTest(self):
                pass

        NormalTestCase.__module__ = self._fake_module_name
        NormalTestCase2.__module__ = self._fake_module_name2

        targets = loader.toParallelTargets([NormalTestCase(), NormalTestCase2()], ["."])
        self.assertEqual(targets, ["my_test_module", "my_test_module2"])

    def test_ignore_doctest(self):
        """
        toParallelTargets() ignores"""


class TestCompletions(unittest.TestCase):
    def test_completionBad(self):
        """
        Bad match generates no completions
        """
        self.assertEqual("", loader.getCompletions("garbage.in"))

    def test_completionExact(self):
        """
        Correct completions are generated for an exact match.
        """
        c = set(loader.getCompletions("green").split("\n"))
        self.assertIn("green", c)
        self.assertIn("green.test", c)
        self.assertIn("green.test.test_loader", c)
        self.assertIn("green.test.test_loader.TestCompletions", c)
        self.assertIn("green.test.test_loader.TestCompletions.test_completionExact", c)

    def test_completionPartialShort(self):
        """
        Correct completions generated for short partial match.
        """
        cwd = os.getcwd()
        green_parent = dirname(dirname(dirname(os.path.abspath(__file__))))
        os.chdir(green_parent)
        self.addCleanup(os.chdir, cwd)
        c = set(loader.getCompletions("gre").split("\n"))
        self.assertIn("green", c)
        self.assertIn("green.test", c)
        self.assertIn("green.test.test_loader", c)
        self.assertIn("green.test.test_loader.TestCompletions", c)
        self.assertIn(
            "green.test.test_loader.TestCompletions.test_completionPartialShort", c
        )

    def test_completionPartial(self):
        """
        Correct completions generated for partial match.  2nd target ignored.
        """
        c = set(loader.getCompletions(["green.te", "green"]).split("\n"))
        self.assertIn("green.test", c)
        self.assertIn("green.test.test_loader", c)
        self.assertIn("green.test.test_loader.TestCompletions", c)
        self.assertIn(
            "green.test.test_loader.TestCompletions.test_completionPartial", c
        )
        self.assertNotIn("green", c)

    def test_completionEmpty(self):
        """
        An empty target generates completions for the whole directory
        """
        cwd = os.getcwd()
        tmpdir = tempfile.mkdtemp()
        self.addCleanup(shutil.rmtree, tmpdir)
        os.chdir(tmpdir)
        self.addCleanup(os.chdir, cwd)
        os.mkdir("the_pkg")
        fh = open(os.path.join("the_pkg", "__init__.py"), "w")
        fh.write("")
        fh.close()
        fh = open(os.path.join("the_pkg", "test_things.py"), "w")
        fh.write(
            dedent(
                """
            import unittest

            class A(unittest.TestCase):
                def testOne(self):
                    pass
                def testTwo(self):
                    pass
            """
            )
        )
        fh.close()
        c = set(loader.getCompletions("").split("\n"))
        self.assertIn("the_pkg", c)
        self.assertIn("the_pkg.test_things", c)
        self.assertIn("the_pkg.test_things.A.testOne", c)
        self.assertIn("the_pkg.test_things.A.testTwo", c)

    def test_completionDot(self):
        """
        A '.' target generates completions for the whole directory
        """
        cwd = os.getcwd()
        tmpdir = tempfile.mkdtemp()
        self.addCleanup(shutil.rmtree, tmpdir)
        os.chdir(tmpdir)
        self.addCleanup(os.chdir, cwd)
        os.mkdir("my_pkg")
        fh = open(os.path.join("my_pkg", "__init__.py"), "w")
        fh.write("")
        fh.close()
        fh = open(os.path.join("my_pkg", "test_things.py"), "w")
        fh.write(
            dedent(
                """
            import unittest

            class A(unittest.TestCase):
                def testOne(self):
                    pass
                def testTwo(self):
                    pass
            """
            )
        )
        fh.close()
        c = set(loader.getCompletions(".").split("\n"))
        self.assertIn("my_pkg", c)
        self.assertIn("my_pkg.test_things", c)
        self.assertIn("my_pkg.test_things.A.testOne", c)
        self.assertIn("my_pkg.test_things.A.testTwo", c)

    def test_completionIgnoresErrors(self):
        """
        Errors in one module don't block the remaining completions
        """
        cwd = os.getcwd()
        tmpdir = tempfile.mkdtemp()
        self.addCleanup(shutil.rmtree, tmpdir)
        os.chdir(tmpdir)
        self.addCleanup(os.chdir, cwd)
        os.mkdir("my_pkg2")
        fh = open(os.path.join("my_pkg2", "__init__.py"), "w")
        fh.write("")
        fh.close()
        fh = open(os.path.join("my_pkg2", "test_crash01.py"), "w")
        contents = dedent(
            """
            import unittest

            class A(unittest.TestCase):
                def testOne(self):
                    pass
                def testTwo(self):
                    pass
            """
        )
        fh.write(contents)
        fh.close()
        fh = open(os.path.join("my_pkg2", "test_crash02.py"), "w")
        fh.write("import moocow")
        fh.close()
        fh = open(os.path.join("my_pkg2", "test_crash03.py"), "w")
        fh.write(contents)
        fh.close()
        c = set(loader.getCompletions(".").split("\n"))
        self.assertIn("my_pkg2", c)
        self.assertIn("my_pkg2.test_crash01", c)
        self.assertIn("my_pkg2.test_crash01.A.testOne", c)
        self.assertIn("my_pkg2.test_crash01.A.testTwo", c)
        self.assertIn("my_pkg2.test_crash03", c)
        self.assertIn("my_pkg2.test_crash03.A.testOne", c)
        self.assertIn("my_pkg2.test_crash03.A.testTwo", c)


class TestIsPackage(unittest.TestCase):

    def setUp(self):
        tmpdir = tempfile.mkdtemp()
        self.tmp_path = pathlib.Path(tmpdir)
        self.addCleanup(shutil.rmtree, tmpdir)

    def test_yes(self):
        """
        A package is identified.
        """
        (self.tmp_path / "__init__.py").write_text("pass\n", encoding="utf-8")
        self.assertTrue(loader.isPackage(self.tmp_path))

    def test_no(self):
        """
        A non-package is identified
        """
        self.assertFalse(loader.isPackage(self.tmp_path))


class TestDottedModule(unittest.TestCase):
    def test_bad_path(self):
        """
        A bad path causes an exception
        """
        self.assertRaises(
            ValueError, loader.findDottedModuleAndParentDir, tempfile.tempdir
        )

    def test_good_path(self):
        """
        A good path gets (dotted_module, parent) properly returned
        """
        tmpdir = tempfile.mkdtemp()
        os.makedirs(os.path.join(tmpdir, "a", "b", "c", "d"))
        package_init = os.path.join(tmpdir, "a", "b", "c", "__init__.py")
        subpkg_init = os.path.join(tmpdir, "a", "b", "c", "d", "__init__.py")
        module_name = "stuff.py"
        module = os.path.join(tmpdir, "a", "b", "c", "d", module_name)
        for filename in [package_init, subpkg_init, module]:
            fh = open(filename, "w")
            fh.write("pass\n")
            fh.close()
        self.assertEqual(
            loader.findDottedModuleAndParentDir(module),
            ("c.d.stuff", os.path.join(tmpdir, "a", "b")),
        )


class TestLoadTestsFromTestCase(unittest.TestCase):
    def setUp(self):
        self.loader = GreenTestLoader()

    def test_runTest(self):
        """
        When a testcase has no matching method names, but does have a runTest,
        use that instead.
        """

        class MyTestCase(unittest.TestCase):
            def helper1(self):
                pass

            def helper2(self):
                pass

            def runTest(self):
                pass

        suite = self.loader.loadTestsFromTestCase(MyTestCase)
        self.assertEqual(suite.countTestCases(), 1)
        self.assertEqual(suite._tests[0]._testMethodName, "runTest")

    def test_normal(self):
        """
        Normal test methods get loaded
        """

        class Normal(unittest.TestCase):
            def test_method1(self):
                pass

            def test_method2(self):
                pass

        suite = self.loader.loadTestsFromTestCase(Normal)
        self.assertEqual(suite.countTestCases(), 2)
        self.assertEqual(
            {x._testMethodName for x in suite._tests},
            {"test_method1", "test_method2"},
        )

    def test_isTestCaseDisabled(self):
        """
        TestCases disabled by nose generators don't get loaded
        """

        class HasDisabled(unittest.TestCase):
            def test_method(self):
                pass

            test_method.__test__ = False

        suite = self.loader.loadTestsFromTestCase(HasDisabled)
        self.assertEqual(suite.countTestCases(), 0)


class TestLoadFromModuleFilename(unittest.TestCase):
    def setUp(self):
        self.loader = GreenTestLoader()

    def test_skipped_module(self):
        """
        A module that wants to be skipped gets skipped
        """
        tmpdir = tempfile.mkdtemp()
        self.addCleanup(shutil.rmtree, tmpdir)
        filename = os.path.join(tmpdir, "skipped_module.py")
        fh = open(filename, "w")
        fh.write(
            dedent(
                """
            import unittest
            raise unittest.case.SkipTest
            class NotReached(unittest.TestCase):
                def test_one(self):
                    pass
                def test_two(self):
                    pass
            """
            )
        )
        fh.close()
        suite = self.loader.loadFromModuleFilename(filename)
        self.assertEqual(suite.countTestCases(), 1)
        self.assertRaises(
            unittest.case.SkipTest,
            getattr(suite._tests[0], suite._tests[0]._testMethodName),
        )


class TestDiscover(unittest.TestCase):
    def setUp(self):
        self.loader = GreenTestLoader()

    @patch("green.loader.os.path.isdir")
    @patch("green.loader.debug")
    @patch("green.loader.os.listdir")
    def test_oserror(self, mock_listdir, mock_debug, mock_isdir):
        """
        discover() prints a debug message and moves on when ecountering an OSError
        """
        mock_isdir.return_value = True
        mock_listdir.side_effect = OSError()
        tmpdir = tempfile.mkdtemp()
        self.addCleanup(shutil.rmtree, tmpdir)
        self.loader.discover(os.path.join(tmpdir, "garbage_in"))
        self.assertEqual(len(mock_debug.mock_calls), 1)

    def test_bad_input(self):
        """
        discover() raises ImportError when passed a non-directory
        """
        tmpdir = tempfile.mkdtemp()
        self.addCleanup(shutil.rmtree, tmpdir)
        self.assertRaises(
            ImportError, self.loader.discover, os.path.join(tmpdir, "garbage_in")
        )
        filename = os.path.join(tmpdir, "some_file.py")
        fh = open(filename, "w")
        fh.write("pass\n")
        fh.close()
        self.assertRaises(ImportError, self.loader.discover, filename)

    def test_bad_pkg_name(self):
        """
        If the directory is an invalid package name, don't bother looking in
        it.
        """
        tmpdir = tempfile.mkdtemp()
        self.addCleanup(shutil.rmtree, tmpdir)
        startdir = os.getcwd()
        os.chdir(tmpdir)
        self.addCleanup(os.chdir, startdir)
        bad_pkg_name = "1badname"
        os.mkdir(bad_pkg_name)
        tmp_subdir = os.path.join(tmpdir, bad_pkg_name)
        fh = open(os.path.join(tmp_subdir, "__init__.py"), "w")
        fh.write("\n")
        fh.close()
        named_module = os.path.join(os.path.basename(tmp_subdir), "named_module.py")
        fh = open(named_module, "w")
        fh.write(
            dedent(
                """
            import unittest
            class A(unittest.TestCase):
                def testPass(self):
                    pass
            """
            )
        )
        fh.close()
        self.assertEqual(self.loader.discover(tmpdir), None)

    def test_symlink(self):
        """
        If the directory is a symlink, it should be skipped.
        """
        if platform.system() == "Windows":  # pragma: no cover
            self.skipTest("This test is for posix-specific behavior")
        tmpdir = tempfile.mkdtemp()
        tmpdir2 = tempfile.mkdtemp()
        os.symlink(tmpdir, os.path.join(tmpdir2, "link"))
        self.addCleanup(shutil.rmtree, tmpdir)
        startdir = os.getcwd()
        os.chdir(tmpdir)
        self.addCleanup(os.chdir, startdir)
        pkg_name = "realpkg"
        os.mkdir(pkg_name)
        tmp_subdir = os.path.join(tmpdir, pkg_name)
        fh = open(os.path.join(tmp_subdir, "__init__.py"), "w")
        fh.write("\n")
        fh.close()
        named_module = os.path.join(os.path.basename(tmp_subdir), "test_module.py")
        fh = open(named_module, "w")
        fh.write(
            dedent(
                """
            import unittest
            class A(unittest.TestCase):
                def testPass(self):
                    pass
            """
            )
        )
        fh.close()
        self.assertEqual(self.loader.discover(tmpdir2), None)


class TestLoadTargets(unittest.TestCase):
    # Setup
    @classmethod
    def setUpClass(cls):
        cls.startdir = os.getcwd()
        cls.container_dir = tempfile.mkdtemp()

    @classmethod
    def tearDownClass(cls):
        if os.getcwd() != cls.startdir:
            os.chdir(cls.startdir)
        cls.startdir = None
        shutil.rmtree(cls.container_dir)

    def setUp(self):
        os.chdir(self.container_dir)
        self.tmpdir = tempfile.mkdtemp(dir=self.container_dir)
        self.loader = GreenTestLoader()

    def tearDown(self):
        os.chdir(self.container_dir)
        shutil.rmtree(self.tmpdir)

    # Tests
    def test_returnIsLoadable(self):
        """
        Results returned by toParallelTargets should be loadable by
        loadTargets(), even if they aren't directly loadable through a package
        relative to the current working directory.
        """
        tests_dir = tempfile.mkdtemp(dir=self.tmpdir)
        # No __init__.py in the directory!
        fh = open(os.path.join(tests_dir, "test_not_in_pkg.py"), "w")
        fh.write(
            dedent(
                """
            import unittest
            class A(unittest.TestCase):
                def testPass(self):
                    pass
            """
            )
        )
        fh.close()
        # Discover stuff
        suite = self.loader.loadTargets(".")
        # This should resolve it to the module that's not importable from here
        test = loader.toParallelTargets(suite, [])[0]
        self.loader.loadTargets(test)

    def test_emptyDirAbsolute(self):
        """
        Absolute path to empty directory returns None
        """
        tests = self.loader.loadTargets(self.tmpdir)
        self.assertTrue(tests is None)

    def test_emptyDirRelative(self):
        """
        Relative path to empty directory returns None
        """
        os.chdir(self.tmpdir)
        os.chdir("..")
        tests = self.loader.loadTargets(os.path.dirname(self.tmpdir))
        self.assertEqual(tests, None)

    def test_emptyDirDot(self):
        """
        '.' while in an empty directory returns None
        """
        os.chdir(self.tmpdir)
        tests = self.loader.loadTargets(".")
        self.assertTrue(tests is None)

    def test_relativeDotDir(self):
        """
        Dotted relative path to empty directory returns None
        """
        os.chdir(self.tmpdir)
        os.chdir("..")
        target = os.path.join(".", os.path.basename(self.tmpdir))
        tests = self.loader.loadTargets(target)
        self.assertTrue(tests is None)

    def test_BigDirWithAbsoluteImports(self):
        """
        Big dir discovers tests and doesn't crash on absolute import
        """
        sub_tmpdir = tempfile.mkdtemp(dir=self.tmpdir)
        pkg_name = os.path.basename(sub_tmpdir)
        # Child setup
        # pkg/__init__.py
        fh = open(os.path.join(sub_tmpdir, "__init__.py"), "w")
        fh.write("\n")
        fh.close()
        # pkg/target_module.py
        fh = open(os.path.join(sub_tmpdir, "target_module.py"), "w")
        fh.write("a = 1\n")
        fh.close()
        # pkg/test/__init__.py
        os.mkdir(os.path.join(sub_tmpdir, "test"))
        fh = open(os.path.join(sub_tmpdir, "test", "__init__.py"), "w")
        fh.write("\n")
        fh.close()
        # pkg/test/test_target_module.py
        fh = open(os.path.join(sub_tmpdir, "test", "test_target_module.py"), "w")
        fh.write(
            dedent(
                """
            import unittest
            import {}.target_module
            class A(unittest.TestCase):
                def testPass(self):
                    pass
            """.format(
                    pkg_name
                )
            )
        )
        fh.close()
        # Load the tests
        os.chdir(self.tmpdir)
        test_suite = self.loader.loadTargets(pkg_name)
        self.assertEqual(test_suite.countTestCases(), 1)
        # Dotted name should start with the package!
        self.assertEqual(
            pkg_name + ".test.test_target_module.A.testPass",
            loader.toProtoTestList(test_suite)[0].dotted_name,
        )

    def test_DirWithInit(self):
        """
        Dir empty other than blank __init__.py returns None
        """
        # Parent directory setup
        os.chdir(self.tmpdir)
        os.chdir("..")
        # Child setup
        target = os.path.join(self.tmpdir, "__init__.py")
        fh = open(target, "w")
        fh.write("\n")
        fh.close()
        fh = open(os.path.join(self.tmpdir, "test_module_with_init.py"), "w")
        fh.write(
            dedent(
                """
            import unittest
            class A(unittest.TestCase):
                def testPass(self):
                    pass
            """
            )
        )
        fh.close()
        # Load the tests
        module_name = os.path.basename(self.tmpdir)
        tests = self.loader.loadTargets(module_name)
        self.assertEqual(tests.countTestCases(), 1)

    def test_DottedName(self):
        """
        Importing a module via dotted name loads the tests.
        """
        # Parent directory setup
        os.chdir(self.tmpdir)
        sub_tmpdir = tempfile.mkdtemp(dir=self.tmpdir)
        basename = os.path.basename(sub_tmpdir)
        # Child setup
        fh = open(os.path.join(basename, "__init__.py"), "w")
        fh.write("\n")
        fh.close()
        fh = open(os.path.join(basename, "test_module_dotted_name.py"), "w")
        fh.write(
            dedent(
                """
            import unittest
            class A(unittest.TestCase):
                def testPass(self):
                    pass
            """
            )
        )
        fh.close()
        # Load the tests
        module_name = basename + ".test_module_dotted_name"
        tests = self.loader.loadTargets(module_name)
        self.assertEqual(tests.countTestCases(), 1)

    def test_DottedNamePackageFromPath(self):
        """
        Importing a package from path loads the tests.
        """
        # Child setup

        tmp_subdir = tempfile.mkdtemp(dir=self.tmpdir)
        fh = open(os.path.join(tmp_subdir, "__init__.py"), "w")
        fh.write("\n")
        fh.close()
        fh = open(os.path.join(tmp_subdir, "test_module.py"), "w")
        fh.write(
            dedent(
                """
            import unittest
            class A(unittest.TestCase):
                def testPass(self):
                    pass
            """
            )
        )
        fh.close()
        # Go somewhere else, but setup the path
        os.chdir(self.startdir)
        sys.path.insert(0, self.tmpdir)
        # Load the tests
        tests = self.loader.loadTargets(os.path.basename(tmp_subdir))
        sys.path.remove(self.tmpdir)
        self.assertTrue(tests.countTestCases(), 1)

    def test_ModuleByName(self):
        """
        A module in a package can be loaded by filename.
        """
        os.chdir(self.tmpdir)
        tmp_subdir = tempfile.mkdtemp(dir=self.tmpdir)
        fh = open(os.path.join(tmp_subdir, "__init__.py"), "w")
        fh.write("\n")
        fh.close()
        named_module = os.path.join(os.path.basename(tmp_subdir), "named_module.py")
        fh = open(named_module, "w")
        fh.write(
            dedent(
                """
            import unittest
            class A(unittest.TestCase):
                def testPass(self):
                    pass
            """
            )
        )
        fh.close()
        # Load the tests
        tests = self.loader.loadTargets(named_module)
        try:
            self.assertEqual(tests.countTestCases(), 1)
        except:
            raise
        finally:
            shutil.rmtree(tmp_subdir)

    def test_MalformedModuleByName(self):
        """
        Importing malformed module by name creates test that raises
        ImportError.
        """
        fh = open(os.path.join(self.tmpdir, "__init__.py"), "w")
        fh.write("\n")
        fh.close()
        malformed_module = os.path.join(
            os.path.basename(self.tmpdir), "malformed_module.py"
        )
        fh = open(malformed_module, "w")
        fh.write("This is a malformed module.")
        fh.close()
        # Load the tests
        tests = self.loader.loadTargets(malformed_module)
        self.assertEqual(tests.countTestCases(), 1)
        test = tests._tests[0]
        test_method = getattr(test, test._testMethodName)
        self.assertRaises(ImportError, test_method)

    def test_partiallyGoodName(self):
        """
        Don't crash loading module.object with existing module but not object.
        """
        # Parent directory setup
        os.chdir(self.tmpdir)
        sub_tmpdir = tempfile.mkdtemp(dir=self.tmpdir)
        basename = os.path.basename(sub_tmpdir)
        # Child setup
        fh = open(os.path.join(basename, "__init__.py"), "w")
        fh.write("\n")
        fh.close()
        fh = open(os.path.join(basename, "existing_module.py"), "w")
        fh.write(
            dedent(
                """
            import unittest
            class A(unittest.TestCase):
                def testPass(self):
                    pass
            """
            )
        )
        fh.close()
        # Load the tests
        module_name = basename + ".existing_module.nonexistant_object"
        tests = self.loader.loadTargets(module_name)
        self.assertEqual(tests, None)

    def test_multiple_targets(self):
        """
        Specifying multiple targets causes them all to be tested.
        """
        sub_tmpdir = tempfile.mkdtemp(dir=self.tmpdir)
        # pkg/__init__.py
        fh = open(os.path.join(sub_tmpdir, "__init__.py"), "w")
        fh.write("\n")
        fh.close()
        # pkg/test/test_target1.py
        fh = open(os.path.join(sub_tmpdir, "test_target1.py"), "w")
        fh.write(
            dedent(
                """
            import unittest
            class A(unittest.TestCase):
                def testPasses(self):
                    pass
            """
            )
        )
        fh.close()
        # pkg/test/test_target2.py
        fh = open(os.path.join(sub_tmpdir, "test_target2.py"), "w")
        fh.write(
            dedent(
                """
            import unittest
            class A(unittest.TestCase):
                def testPasses(self):
                    pass
            """
            )
        )
        fh.close()
        # Load the tests
        os.chdir(self.tmpdir)
        pkg = os.path.basename(sub_tmpdir)
        tests = self.loader.loadTargets(
            [pkg + "." + "test_target1", pkg + "." + "test_target2"]
        )
        self.assertEqual(tests.countTestCases(), 2)

    def test_duplicate_targets(self):
        """
        Specifying duplicate targets does not cause duplicate loading.
        """
        sub_tmpdir = tempfile.mkdtemp(dir=self.tmpdir)
        fh = open(os.path.join(sub_tmpdir, "__init__.py"), "w")
        fh.write("\n")
        fh.close()
        fh = open(os.path.join(sub_tmpdir, "test_dupe_target.py"), "w")
        fh.write(
            dedent(
                """
            import unittest
            class A(unittest.TestCase):
                def testPasses(self):
                    pass
            """
            )
        )
        fh.close()

        os.chdir(self.tmpdir)
        pkg = os.path.basename(sub_tmpdir)
        tests = self.loader.loadTargets(
            [
                pkg + "." + "test_dupe_target",
                pkg + "." + "test_dupe_target",
                pkg + "." + "test_dupe_target",
            ]
        )
        self.assertEqual(tests.countTestCases(), 1)

    def test_syntax_error_by_filename(self):
        """
        Loading a module by file name with a syntax error produces a crash.
        """
        sub_tmpdir = tempfile.mkdtemp(dir=self.tmpdir)
        fh = open(os.path.join(sub_tmpdir, "mod_with_import_error.py"), "w")
        fh.write("this is a syntax error")
        fh.close()

        os.chdir(sub_tmpdir)
        hit_exception = False
        try:
            self.loader.loadTargets("mod_with_import_error.py")
        except Exception as e:
            self.assertIn("invalid syntax", str(e))
            self.assertIn("mod_with_import_error.py", str(e))
            hit_exception = True
        if not hit_exception:
            self.fail("An exception should have been raised. :-(")

    def test_syntax_error_by_dotname(self):
        """
        Loading a module by dotname with a syntax error produces a crash.
        """
        sub_tmpdir = tempfile.mkdtemp(dir=self.tmpdir)
        fh = open(os.path.join(sub_tmpdir, "mod_with_syntax_error.py"), "w")
        fh.write(
            dedent(
                """
            import unittest
            class TestSyntax(unittest.TestCase):
                def test_syntax(self):
                    syntax error
            """
            )
        )
        fh.close()

        os.chdir(sub_tmpdir)
        hit_exception = False
        try:
            self.loader.loadTargets("mod_with_syntax_error.TestSyntax.test_syntax")
        except Exception as e:
            self.assertIn("invalid syntax", str(e))
            self.assertIn("mod_with_syntax_error.TestSyntax.test_syntax", str(e))
            hit_exception = True
        if not hit_exception:
            self.fail(
                "An exception should have been raised about the syntax error. :-("
            )

    def test_file_pattern(self):
        """
        Specifying a file pattern causes only matching files to be loaded
        """
        sub_tmpdir = tempfile.mkdtemp(dir=self.tmpdir)
        # pkg/__init__.py
        fh = open(os.path.join(sub_tmpdir, "__init__.py"), "w")
        fh.write("\n")
        fh.close()
        # pkg/test/target1_tests.py
        fh = open(os.path.join(sub_tmpdir, "target1_tests.py"), "w")
        fh.write(
            dedent(
                """
            import unittest
            class A(unittest.TestCase):
                def testPasses(self):
                    pass
            """
            )
        )
        fh.close()
        # pkg/test/target2_tests.py
        fh = open(os.path.join(sub_tmpdir, "target2_tests.py"), "w")
        fh.write(
            dedent(
                """
            import unittest
            class A(unittest.TestCase):
                def testPasses(self):
                    pass
            """
            )
        )
        fh.close()
        # pkg/test/test_target999.py: NOT a match.
        fh = open(os.path.join(sub_tmpdir, "test_target999.py"), "w")
        fh.write(
            dedent(
                """
            import unittest
            class A(unittest.TestCase):
                def testPasses(self):
                    pass
            """
            )
        )
        fh.close()
        # Load the tests
        os.chdir(self.tmpdir)
        pkg = os.path.basename(sub_tmpdir)
        tests = self.loader.loadTargets(pkg, file_pattern="*_tests.py")
        self.assertEqual(tests.countTestCases(), 2)


class TestFlattenTestSuite(unittest.TestCase):
    # Setup
    @classmethod
    def setUpClass(cls):
        cls.startdir = os.getcwd()
        cls.container_dir = tempfile.mkdtemp()

    @classmethod
    def tearDownClass(cls):
        if os.getcwd() != cls.startdir:
            os.chdir(cls.startdir)
        cls.startdir = None
        shutil.rmtree(cls.container_dir)

    def setUp(self):
        os.chdir(self.container_dir)
        self.tmpdir = tempfile.mkdtemp(dir=self.container_dir)
        self.loader = GreenTestLoader()

    def tearDown(self):
        os.chdir(self.container_dir)
        shutil.rmtree(self.tmpdir)

    @patch("green.loader.GreenTestLoader.suiteClass")
    @patch("green.loader.DocTestSuite")
    def test_docTests(self, mock_doc_test_suite, mock_suite_class):
        """
        flattenTestSuite injects the test module name into the doctest's .__module__
        """
        mock_test = MagicMock()
        mock_iter = MagicMock(return_value=iter([mock_test]))
        mock_suite = MagicMock()
        mock_suite.__iter__ = mock_iter
        mock_doc_test_suite.return_value = mock_suite
        module = MagicMock()
        test_module_name = "test.module"
        module.__name__ = test_module_name
        module.doctest_modules = ["real.module"]
        flattenTestSuite((), module)

        self.assertEqual(mock_test.__module__, test_module_name)
