# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import collections
import unittest

import TestUnit

import SCons.compat
import SCons.Scanner
from SCons.Scanner import ScannerBase, Selector, Classic, ClassicCPP, Current, FindPathDirs

class DummyFS:
    def File(self, name):
        return DummyNode(name)

class DummyEnvironment(collections.UserDict):
    def __init__(self, mapping=None, **kwargs) -> None:
        super().__init__(mapping)
        self.data.update(kwargs)
        self.fs = DummyFS()
    def subst(self, strSubst, target=None, source=None, conv=None):
        if strSubst[0] == '$':
            return self.data[strSubst[1:]]
        return strSubst
    def subst_list(self, strSubst, target=None, source=None, conv=None):
        if strSubst[0] == '$':
            return [self.data[strSubst[1:]]]
        return [[strSubst]]
    def subst_path(self, path, target=None, source=None, conv=None):
        if not isinstance(path, list):
            path = [path]
        return list(map(self.subst, path))
    def get_factory(self, factory):
        return factory or self.fs.File

class DummyNode:
    def __init__(self, name, search_result=()) -> None:
        self.name = name
        self.search_result = tuple(search_result)

    def rexists(self) -> bool:
        return True

    def __str__(self) -> str:
        return self.name

    def Rfindalldirs(self, pathlist):
        return self.search_result + pathlist
    def __repr__(self) -> str:
        return self.name
    def __eq__(self, other):
        return self.name == other.name

    def __repr__(self) -> str:
        return self.name

    def __eq__(self, other):
        return self.name == other.name

class FindPathDirsTestCase(unittest.TestCase):
    def test_FindPathDirs(self) -> None:
        """Test the FindPathDirs callable class"""

        env = DummyEnvironment(LIBPATH = [ 'foo' ])
        env.fs = DummyFS()
        env.fs._cwd = DummyNode('cwd')

        dir = DummyNode('dir', ['xxx'])
        fpd = FindPathDirs('LIBPATH')
        result = fpd(env)
        assert str(result) == "('foo',)", result
        result = fpd(env, dir)
        assert str(result) == "('xxx', 'foo')", result

class ScannerTestCase(unittest.TestCase):

    def test_creation(self) -> None:
        """Test creation of Scanner objects"""
        def func(self) -> None:
            pass
        s = ScannerBase(func)
        assert isinstance(s, ScannerBase), s
        s = ScannerBase({})
        assert isinstance(s, ScannerBase), s

        s = ScannerBase(func, name='fooscan')
        assert str(s) == 'fooscan', str(s)
        s = ScannerBase({}, name='barscan')
        assert str(s) == 'barscan', str(s)

        s = ScannerBase(func, name='fooscan', argument=9)
        assert str(s) == 'fooscan', str(s)
        assert s.argument == 9, s.argument
        s = ScannerBase({}, name='fooscan', argument=888)
        assert str(s) == 'fooscan', str(s)
        assert s.argument == 888, s.argument


class ScannerBaseTestCase(unittest.TestCase):

    class skey_node:
        def __init__(self, key) -> None:
            self.key = key
        def scanner_key(self):
            return self.key
        def rexists(self) -> bool:
            return True

    def func(self, filename, env, target, *args):
        self.filename = filename
        self.env = env
        self.target = target

        if len(args) > 0:
            self.arg = args[0]

        return self.deps

    def test(self, scanner, env, filename, deps, *args) -> None:
        self.deps = deps
        path = scanner.path(env)
        scanned = scanner(filename, env, path)
        scanned_strs = [str(x) for x in scanned]

        self.assertTrue(self.filename == filename, "the filename was passed incorrectly")
        self.assertTrue(self.env == env, "the environment was passed incorrectly")
        self.assertTrue(scanned_strs == deps, "the dependencies were returned incorrectly")
        for d in scanned:
            self.assertTrue(not isinstance(d, str), "got a string in the dependencies")

        if len(args) > 0:
            self.assertTrue(self.arg == args[0], "the argument was passed incorrectly")
        else:
            self.assertFalse(hasattr(self, "arg"), "an argument was given when it shouldn't have been")

    def test___call__dict(self) -> None:
        """Test calling ScannerBase objects with a dictionary"""
        called = []
        def s1func(node, env, path, called=called):
            called.append('s1func')
            called.append(node)
            return []
        def s2func(node, env, path, called=called):
            called.append('s2func')
            called.append(node)
            return []
        s1 = ScannerBase(s1func)
        s2 = ScannerBase(s2func)
        selector = ScannerBase({'.x' : s1, '.y' : s2})
        nx = self.skey_node('.x')
        env = DummyEnvironment()
        selector(nx, env, [])
        assert called == ['s1func', nx], called
        del called[:]
        ny = self.skey_node('.y')
        selector(ny, env, [])
        assert called == ['s2func', ny], called

    def test_path(self) -> None:
        """Test the ScannerBase path() method"""
        def pf(env, cwd, target, source, argument=None) -> str:
            return "pf: %s %s %s %s %s" % \
                        (env.VARIABLE, cwd, target[0], source[0], argument)

        env = DummyEnvironment()
        env.VARIABLE = 'v1'
        target = DummyNode('target')
        source = DummyNode('source')

        s = ScannerBase(self.func, path_function=pf)
        p = s.path(env, 'here', [target], [source])
        assert p == "pf: v1 here target source None", p

        s = ScannerBase(self.func, path_function=pf, argument="xyz")
        p = s.path(env, 'here', [target], [source])
        assert p == "pf: v1 here target source xyz", p

    def test_positional(self) -> None:
        """Test the ScannerBase class using positional arguments"""
        s = ScannerBase(self.func, "Pos")
        env = DummyEnvironment()
        env.VARIABLE = "var1"
        self.test(s, env, DummyNode('f1.cpp'), ['f1.h', 'f1.hpp'])

        env = DummyEnvironment()
        env.VARIABLE = "i1"
        self.test(s, env, DummyNode('i1.cpp'), ['i1.h', 'i1.hpp'])

    def test_keywords(self) -> None:
        """Test the ScannerBase class using keyword arguments"""
        s = ScannerBase(function = self.func, name = "Key")
        env = DummyEnvironment()
        env.VARIABLE = "var2"
        self.test(s, env, DummyNode('f2.cpp'), ['f2.h', 'f2.hpp'])

        env = DummyEnvironment()
        env.VARIABLE = "i2"

        self.test(s, env, DummyNode('i2.cpp'), ['i2.h', 'i2.hpp'])

    def test_pos_opt(self) -> None:
        """Test the ScannerBase class using both position and optional arguments"""
        arg = "this is the argument"
        s = ScannerBase(self.func, "PosArg", arg)
        env = DummyEnvironment()
        env.VARIABLE = "var3"
        self.test(s, env, DummyNode('f3.cpp'), ['f3.h', 'f3.hpp'], arg)

        env = DummyEnvironment()
        env.VARIABLE = "i3"
        self.test(s, env, DummyNode('i3.cpp'), ['i3.h', 'i3.hpp'], arg)

    def test_key_opt(self) -> None:
        """Test the ScannerBase class using both keyword and optional arguments"""
        arg = "this is another argument"
        s = ScannerBase(function = self.func, name = "KeyArg", argument = arg)
        env = DummyEnvironment()
        env.VARIABLE = "var4"
        self.test(s, env, DummyNode('f4.cpp'), ['f4.h', 'f4.hpp'], arg)

        env = DummyEnvironment()
        env.VARIABLE = "i4"
        self.test(s, env, DummyNode('i4.cpp'), ['i4.h', 'i4.hpp'], arg)

    def test___cmp__(self) -> None:
        """Test the ScannerBase class __cmp__() method"""
        s = ScannerBase(self.func, "Cmp")
        assert s is not None

    def test_hash(self) -> None:
        """Test the ScannerBase class __hash__() method"""
        s = ScannerBase(self.func, "Hash")
        mapping = {}
        mapping[s] = 777
        i = hash(id(s))
        h = hash(list(mapping)[0])
        self.assertTrue(h == i,
                        "hash Scanner base class expected %s, got %s" % (i, h))

    def test_scan_check(self) -> None:
        """Test the ScannerBase class scan_check() method"""
        def my_scan(filename, env, target, *args):
            return []
        def check(node, env, s=self) -> int:
            s.checked[str(node)] = 1
            return 1
        env = DummyEnvironment()
        s = ScannerBase(my_scan, "Check", scan_check = check)
        self.checked = {}
        path = s.path(env)
        scanned = s(DummyNode('x'), env, path)
        self.assertTrue(self.checked['x'] == 1,
                        "did not call check function")

    def test_recursive(self) -> None:
        """Test the ScannerBase class recursive flag"""
        nodes = [1, 2, 3, 4]

        s = ScannerBase(function = self.func)
        n = s.recurse_nodes(nodes)
        self.assertTrue(n == [], "default behavior returned nodes: %s" % n)

        s = ScannerBase(function = self.func, recursive = None)
        n = s.recurse_nodes(nodes)
        self.assertTrue(n == [], "recursive = None returned nodes: %s" % n)

        s = ScannerBase(function = self.func, recursive = 1)
        n = s.recurse_nodes(nodes)
        self.assertTrue(n == n, "recursive = 1 didn't return all nodes: %s" % n)

        def odd_only(nodes):
            return [n for n in nodes if n % 2]
        s = ScannerBase(function = self.func, recursive = odd_only)
        n = s.recurse_nodes(nodes)
        self.assertTrue(n == [1, 3], "recursive = 1 didn't return all nodes: %s" % n)

    def test_get_skeys(self) -> None:
        """Test the ScannerBase get_skeys() method"""
        s = ScannerBase(function = self.func)
        sk = s.get_skeys()
        self.assertTrue(sk == [], "did not initialize to expected []")

        s = ScannerBase(function = self.func, skeys = ['.1', '.2'])
        sk = s.get_skeys()
        self.assertTrue(sk == ['.1', '.2'], "sk was %s, not ['.1', '.2']")

        s = ScannerBase(function = self.func, skeys = '$LIST')
        env = DummyEnvironment(LIST = ['.3', '.4'])
        sk = s.get_skeys(env)
        self.assertTrue(sk == ['.3', '.4'], "sk was %s, not ['.3', '.4']")

    def test_select(self) -> None:
        """Test the ScannerBase select() method"""
        scanner = ScannerBase(function = self.func)
        s = scanner.select('.x')
        assert s is scanner, s

        selector = ScannerBase({'.x' : 1, '.y' : 2})
        s = selector.select(self.skey_node('.x'))
        assert s == 1, s
        s = selector.select(self.skey_node('.y'))
        assert s == 2, s
        s = selector.select(self.skey_node('.z'))
        assert s is None, s

    def test_add_scanner(self) -> None:
        """Test the ScannerBase add_scanner() method"""
        selector = ScannerBase({'.x' : 1, '.y' : 2})
        s = selector.select(self.skey_node('.z'))
        assert s is None, s
        selector.add_scanner('.z', 3)
        s = selector.select(self.skey_node('.z'))
        assert s == 3, s

    def test___str__(self) -> None:
        """Test the ScannerBase __str__() method"""
        scanner = ScannerBase(function = self.func)
        s = str(scanner)
        assert s == 'NONE', s
        scanner = ScannerBase(function = self.func, name = 'xyzzy')
        s = str(scanner)
        assert s == 'xyzzy', s

class SelectorTestCase(unittest.TestCase):

    class skey_node:
        def __init__(self, key) -> None:
            self.key = key
        def scanner_key(self):
            return self.key
        def rexists(self) -> bool:
            return True

    def test___init__(self) -> None:
        """Test creation of Scanner.Selector object"""
        s = Selector({})
        assert isinstance(s, Selector), s
        assert s.mapping == {}, s.mapping

    def test___call__(self) -> None:
        """Test calling Scanner.Selector objects"""
        called = []
        def s1func(node, env, path, called=called):
            called.append('s1func')
            called.append(node)
            return []
        def s2func(node, env, path, called=called):
            called.append('s2func')
            called.append(node)
            return []
        s1 = ScannerBase(s1func)
        s2 = ScannerBase(s2func)
        selector = Selector({'.x' : s1, '.y' : s2})
        nx = self.skey_node('.x')
        env = DummyEnvironment()
        selector(nx, env, [])
        assert called == ['s1func', nx], called
        del called[:]
        ny = self.skey_node('.y')
        selector(ny, env, [])
        assert called == ['s2func', ny], called

    def test_select(self) -> None:
        """Test the Scanner.Selector select() method"""
        selector = Selector({'.x' : 1, '.y' : 2})
        s = selector.select(self.skey_node('.x'))
        assert s == 1, s
        s = selector.select(self.skey_node('.y'))
        assert s == 2, s
        s = selector.select(self.skey_node('.z'))
        assert s is None, s

    def test_add_scanner(self) -> None:
        """Test the Scanner.Selector add_scanner() method"""
        selector = Selector({'.x' : 1, '.y' : 2})
        s = selector.select(self.skey_node('.z'))
        assert s is None, s
        selector.add_scanner('.z', 3)
        s = selector.select(self.skey_node('.z'))
        assert s == 3, s

class CurrentTestCase(unittest.TestCase):
    def test_class(self) -> None:
        """Test the Scanner.Current class"""
        class MyNode:
            def __init__(self) -> None:
                self.called_has_builder = None
                self.called_is_up_to_date = None
                self.func_called = None
            def rexists(self) -> bool:
                return True
        class HasNoBuilder(MyNode):
            def has_builder(self) -> bool:
                self.called_has_builder = True
                return False
        class IsNotCurrent(MyNode):
            def has_builder(self) -> bool:
                self.called_has_builder = True
                return True
            def is_up_to_date(self) -> bool:
                self.called_is_up_to_date = True
                return False
        class IsCurrent(MyNode):
            def has_builder(self) -> bool:
                self.called_has_builder = True
                return True
            def is_up_to_date(self) -> bool:
                self.called_is_up_to_date = True
                return True
        def func(node, env, path):
            node.func_called = True
            return []
        env = DummyEnvironment()
        s = Current(func)
        path = s.path(env)
        hnb = HasNoBuilder()
        s(hnb, env, path)
        self.assertTrue(hnb.called_has_builder, "did not call has_builder()")
        self.assertTrue(not hnb.called_is_up_to_date, "did call is_up_to_date()")
        self.assertTrue(hnb.func_called, "did not call func()")
        inc = IsNotCurrent()
        s(inc, env, path)
        self.assertTrue(inc.called_has_builder, "did not call has_builder()")
        self.assertTrue(inc.called_is_up_to_date, "did not call is_up_to_date()")
        self.assertTrue(not inc.func_called, "did call func()")
        ic = IsCurrent()
        s(ic, env, path)
        self.assertTrue(ic.called_has_builder, "did not call has_builder()")
        self.assertTrue(ic.called_is_up_to_date, "did not call is_up_to_date()")
        self.assertTrue(ic.func_called, "did not call func()")

class ClassicTestCase(unittest.TestCase):

    def func(self, filename, env, target, *args):
        self.filename = filename
        self.env = env
        self.target = target

        if len(args) > 0:
            self.arg = args[0]

        return self.deps

    def test_find_include(self) -> None:
        """Test the Scanner.Classic find_include() method"""
        env = DummyEnvironment()
        s = Classic("t", ['.suf'], 'MYPATH', r'^my_inc (\S+)')

        def _find_file(filename, paths):
            return paths[0]+'/'+filename

        save = SCons.Node.FS.find_file
        SCons.Node.FS.find_file = _find_file

        try:
            n, i = s.find_include('aaa', 'foo', ('path',))
            assert n == 'foo/aaa', n
            assert i == 'aaa', i

        finally:
            SCons.Node.FS.find_file = save

    def test_name(self) -> None:
        """Test setting the Scanner.Classic name"""
        s = Classic("my_name", ['.s'], 'MYPATH', r'^my_inc (\S+)')
        assert s.name == "my_name", s.name

    def test_scan(self) -> None:
        """Test the Scanner.Classic scan() method"""
        class MyNode:
            def __init__(self, name) -> None:
                self.name = name
                self._rfile = self
                self.includes = None
            def rfile(self):
                return self._rfile
            def exists(self):
                return self._exists
            def get_contents(self):
                return self._contents
            def get_text_contents(self):
                return self._contents
            def get_dir(self):
                return self._dir

        class MyScanner(Classic):
            def find_include(self, include, source_dir, path):
                return include, include

        env = DummyEnvironment()
        s = MyScanner("t", ['.suf'], 'MYPATH', r'^my_inc (\S+)')

        # This set of tests is intended to test the scanning operation
        # of the Classic scanner.

        # Note that caching has been added for not just the includes
        # but the entire scan call.  The caching is based on the
        # arguments, so we will fiddle with the path parameter to
        # defeat this caching for the purposes of these tests.

        # If the node doesn't exist, scanning turns up nothing.
        n1 = MyNode("n1")
        n1._exists = None
        ret = s.function(n1, env)
        assert ret == [], ret

        # Verify that it finds includes from the contents.
        n = MyNode("n")
        n._exists = 1
        n._dir = MyNode("n._dir")
        n._contents = 'my_inc abc\n'
        ret = s.function(n, env, ('foo',))
        assert ret == ['abc'], ret

        # Verify that it uses the cached include info.
        n._contents = 'my_inc def\n'
        ret = s.function(n, env, ('foo2',))
        assert ret == ['abc'], ret

        # Verify that if we wipe the cache, it uses the new contents.
        n.includes = None
        ret = s.function(n, env, ('foo3',))
        assert ret == ['def'], ret

        # We no longer cache overall scan results, which would be returned
        # if individual results are de-cached.  If we ever restore that
        # functionality, this test goes back here.
        #ret = s.function(n, env, ('foo2',))
        #assert ret == ['abc'], 'caching inactive; got: %s'%ret

        # Verify that it sorts what it finds.
        n.includes = ['xyz', 'uvw']
        ret = s.function(n, env, ('foo4',))
        assert ret == ['uvw', 'xyz'], ret

        # Verify that we use the rfile() node.
        nr = MyNode("nr")
        nr._exists = 1
        nr._dir = MyNode("nr._dir")
        nr.includes = ['jkl', 'mno']
        n._rfile = nr
        ret = s.function(n, env, ('foo5',))
        assert ret == ['jkl', 'mno'], ret

    def test_recursive(self) -> None:
        """Test the Scanner.Classic class recursive flag"""
        nodes = [1, 2, 3, 4]


        s = Classic("Test", [], None, "", function=self.func, recursive=1)
        n = s.recurse_nodes(nodes)
        self.assertTrue(n == n,
                        "recursive = 1 didn't return all nodes: %s" % n)

        def odd_only(nodes):
            return [n for n in nodes if n % 2]

        s = Classic("Test", [], None, "", function=self.func, recursive=odd_only)
        n = s.recurse_nodes(nodes)
        self.assertTrue(n == [1, 3],
                        "recursive = 1 didn't return all nodes: %s" % n)


class ClassicCPPTestCase(unittest.TestCase):
    def test_find_include(self) -> None:
        """Test the Scanner.ClassicCPP find_include() method"""
        env = DummyEnvironment()
        s = ClassicCPP("Test", [], None, "")

        def _find_file(filename, paths):
            return paths[0]+'/'+filename

        save = SCons.Node.FS.find_file
        SCons.Node.FS.find_file = _find_file

        try:
            n, i = s.find_include(('"', 'aaa'), 'foo', ('path',))
            assert n == 'foo/aaa', n
            assert i == 'aaa', i

            n, i = s.find_include(('<', 'bbb'), 'foo', ('path',))
            assert n == 'path/bbb', n
            assert i == 'bbb', i

            n, i = s.find_include(('<', 'ccc'), 'foo', ('path',))
            assert n == 'path/ccc', n
            assert i == 'ccc', i

        finally:
            SCons.Node.FS.find_file = save

def suite():
    suite = unittest.TestSuite()
    tclasses = [
                 FindPathDirsTestCase,
                 ScannerTestCase,
                 ScannerBaseTestCase,
                 SelectorTestCase,
                 CurrentTestCase,
                 ClassicTestCase,
                 ClassicCPPTestCase,
               ]
    for tclass in tclasses:
        loader = unittest.TestLoader()
        loader.testMethodPrefix = 'test_'
        names = loader.getTestCaseNames(tclass)
        suite.addTests(list(map(tclass, names)))
    return suite

if __name__ == "__main__":
    TestUnit.run(suite())

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:
