# Copyright 2013-2019 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)

import os
import fnmatch

import six
import pytest

from llnl.util.filesystem import LibraryList, HeaderList
from llnl.util.filesystem import find_libraries, find_headers, find

import spack.paths


@pytest.fixture()
def library_list():
    """Returns an instance of LibraryList."""
    # Test all valid extensions: ['.a', '.dylib', '.so']
    libs = [
        '/dir1/liblapack.a',
        '/dir2/libpython3.6.dylib',  # name may contain periods
        '/dir1/libblas.a',
        '/dir3/libz.so',
        'libmpi.so.20.10.1',  # shared object libraries may be versioned
    ]

    return LibraryList(libs)


@pytest.fixture()
def header_list():
    """Returns an instance of header list"""
    # Test all valid extensions: ['.h', '.hpp', '.hh', '.cuh']
    headers = [
        '/dir1/Python.h',
        '/dir2/date.time.h',
        '/dir1/pyconfig.hpp',
        '/dir3/core.hh',
        'pymem.cuh',
    ]
    h = HeaderList(headers)
    h.add_macro('-DBOOST_LIB_NAME=boost_regex')
    h.add_macro('-DBOOST_DYN_LINK')
    return h


class TestLibraryList(object):

    def test_repr(self, library_list):
        x = eval(repr(library_list))
        assert library_list == x

    def test_joined_and_str(self, library_list):

        s1 = library_list.joined()
        expected = '/dir1/liblapack.a /dir2/libpython3.6.dylib /dir1/libblas.a /dir3/libz.so libmpi.so.20.10.1'  # noqa: E501
        assert s1 == expected

        s2 = str(library_list)
        assert s1 == s2

        s3 = library_list.joined(';')
        expected = '/dir1/liblapack.a;/dir2/libpython3.6.dylib;/dir1/libblas.a;/dir3/libz.so;libmpi.so.20.10.1'  # noqa: E501
        assert s3 == expected

    def test_flags(self, library_list):

        search_flags = library_list.search_flags
        assert '-L/dir1' in search_flags
        assert '-L/dir2' in search_flags
        assert '-L/dir3' in search_flags
        assert isinstance(search_flags, str)
        assert search_flags == '-L/dir1 -L/dir2 -L/dir3'

        link_flags = library_list.link_flags
        assert '-llapack' in link_flags
        assert '-lpython3.6' in link_flags
        assert '-lblas' in link_flags
        assert '-lz' in link_flags
        assert '-lmpi' in link_flags
        assert isinstance(link_flags, str)
        assert link_flags == '-llapack -lpython3.6 -lblas -lz -lmpi'

        ld_flags = library_list.ld_flags
        assert isinstance(ld_flags, str)
        assert ld_flags == search_flags + ' ' + link_flags

    def test_paths_manipulation(self, library_list):
        names = library_list.names
        assert names == ['lapack', 'python3.6', 'blas', 'z', 'mpi']

        directories = library_list.directories
        assert directories == ['/dir1', '/dir2', '/dir3']

    def test_get_item(self, library_list):
        a = library_list[0]
        assert a == '/dir1/liblapack.a'

        b = library_list[:]
        assert type(b) == type(library_list)
        assert library_list == b
        assert library_list is not b

    def test_add(self, library_list):
        pylist = [
            '/dir1/liblapack.a',  # removed from the final list
            '/dir2/libmpi.so',
            '/dir4/libnew.a'
        ]
        another = LibraryList(pylist)
        both = library_list + another
        assert len(both) == 7

        # Invariant
        assert both == both + both

        # Always produce an instance of LibraryList
        assert type(library_list + pylist) == type(library_list)
        assert type(pylist + library_list) == type(library_list)


class TestHeaderList(object):

    def test_repr(self, header_list):
        x = eval(repr(header_list))
        assert header_list == x

    def test_joined_and_str(self, header_list):
        s1 = header_list.joined()
        expected = '/dir1/Python.h /dir2/date.time.h /dir1/pyconfig.hpp /dir3/core.hh pymem.cuh'  # noqa: E501
        assert s1 == expected

        s2 = str(header_list)
        assert s1 == s2

        s3 = header_list.joined(';')
        expected = '/dir1/Python.h;/dir2/date.time.h;/dir1/pyconfig.hpp;/dir3/core.hh;pymem.cuh'  # noqa: E501
        assert s3 == expected

    def test_flags(self, header_list):
        include_flags = header_list.include_flags
        assert '-I/dir1' in include_flags
        assert '-I/dir2' in include_flags
        assert '-I/dir3' in include_flags
        assert isinstance(include_flags, str)
        assert include_flags == '-I/dir1 -I/dir2 -I/dir3'

        macros = header_list.macro_definitions
        assert '-DBOOST_LIB_NAME=boost_regex' in macros
        assert '-DBOOST_DYN_LINK' in macros
        assert isinstance(macros, str)
        assert macros == '-DBOOST_LIB_NAME=boost_regex -DBOOST_DYN_LINK'

        cpp_flags = header_list.cpp_flags
        assert isinstance(cpp_flags, str)
        assert cpp_flags == include_flags + ' ' + macros

    def test_paths_manipulation(self, header_list):
        names = header_list.names
        assert names == ['Python', 'date.time', 'pyconfig', 'core', 'pymem']

        directories = header_list.directories
        assert directories == ['/dir1', '/dir2', '/dir3']

    def test_get_item(self, header_list):
        a = header_list[0]
        assert a == '/dir1/Python.h'

        b = header_list[:]
        assert type(b) == type(header_list)
        assert header_list == b
        assert header_list is not b

    def test_add(self, header_list):
        pylist = [
            '/dir1/Python.h',  # removed from the final list
            '/dir2/pyconfig.hpp',
            '/dir4/date.time.h'
        ]
        another = HeaderList(pylist)
        h = header_list + another
        assert len(h) == 7

        # Invariant : l == l + l
        assert h == h + h

        # Always produce an instance of HeaderList
        assert type(header_list + pylist) == type(header_list)
        assert type(pylist + header_list) == type(header_list)


#: Directory where the data for the test below is stored
search_dir = os.path.join(spack.paths.test_path, 'data', 'directory_search')


@pytest.mark.parametrize('search_fn,search_list,root,kwargs', [
    (find_libraries, 'liba', search_dir, {'recursive': True}),
    (find_libraries, ['liba'], search_dir, {'recursive': True}),
    (find_libraries, 'libb', search_dir, {'recursive': True}),
    (find_libraries, ['libc'], search_dir, {'recursive': True}),
    (find_libraries, ['libc', 'liba'], search_dir, {'recursive': True}),
    (find_libraries, ['liba', 'libc'], search_dir, {'recursive': True}),
    (find_libraries,
     ['libc', 'libb', 'liba'],
     search_dir,
     {'recursive': True}
     ),
    (find_libraries, ['liba', 'libc'], search_dir, {'recursive': True}),
    (find_libraries,
     ['libc', 'libb', 'liba'],
     search_dir,
     {'recursive': True, 'shared': False}
     ),
    (find_headers, 'a', search_dir, {'recursive': True}),
    (find_headers, ['a'], search_dir, {'recursive': True}),
    (find_headers, 'b', search_dir, {'recursive': True}),
    (find_headers, ['c'], search_dir, {'recursive': True}),
    (find_headers, ['c', 'a'], search_dir, {'recursive': True}),
    (find_headers, ['a', 'c'], search_dir, {'recursive': True}),
    (find_headers, ['c', 'b', 'a'], search_dir, {'recursive': True}),
    (find_headers, ['a', 'c'], search_dir, {'recursive': True}),
    (find_libraries,
     ['liba', 'libd'],
     os.path.join(search_dir, 'b'),
     {'recursive': False}
     ),
    (find_headers,
     ['b', 'd'],
     os.path.join(search_dir, 'b'),
     {'recursive': False}
     ),
])
def test_searching_order(search_fn, search_list, root, kwargs):

    # Test search
    result = search_fn(search_list, root, **kwargs)

    # The tests are set-up so that something is always found
    assert len(result) != 0

    # Now reverse the result and start discarding things
    # as soon as you have matches. In the end the list should
    # be emptied.
    rlist = list(reversed(result))

    # At this point make sure the search list is a sequence
    if isinstance(search_list, six.string_types):
        search_list = [search_list]

    # Discard entries in the order they appear in search list
    for x in search_list:
        try:
            while fnmatch.fnmatch(rlist[-1], x) or x in rlist[-1]:
                rlist.pop()
        except IndexError:
            # List is empty
            pass

    # List should be empty here
    assert len(rlist) == 0


@pytest.mark.parametrize('root,search_list,kwargs,expected', [
    (search_dir, '*/*bar.tx?', {'recursive': False}, [
        os.path.join(search_dir, 'a/foobar.txt'),
        os.path.join(search_dir, 'b/bar.txp'),
        os.path.join(search_dir, 'c/bar.txt'),
    ]),
    (search_dir, '*/*bar.tx?', {'recursive': True}, [
        os.path.join(search_dir, 'a/foobar.txt'),
        os.path.join(search_dir, 'b/bar.txp'),
        os.path.join(search_dir, 'c/bar.txt'),
    ])
])
def test_find_with_globbing(root, search_list, kwargs, expected):
    matches = find(root, search_list, **kwargs)
    assert sorted(matches) == sorted(expected)
