"""Tests for distutils.filelist."""

import logging
import os
import re
import sys
from distutils import debug, filelist
from distutils.errors import DistutilsTemplateError
from distutils.filelist import FileList, glob_to_re, translate_pattern

import jaraco.path
import pytest

from .compat import py39 as os_helper

MANIFEST_IN = """\
include ok
include xo
exclude xo
include foo.tmp
include buildout.cfg
global-include *.x
global-include *.txt
global-exclude *.tmp
recursive-include f *.oo
recursive-exclude global *.x
graft dir
prune dir3
"""


def make_local_path(s):
    """Converts '/' in a string to os.sep"""
    return s.replace('/', os.sep)


class TestFileList:
    def assertNoWarnings(self, caplog):
        warnings = [rec for rec in caplog.records if rec.levelno == logging.WARNING]
        assert not warnings
        caplog.clear()

    def assertWarnings(self, caplog):
        warnings = [rec for rec in caplog.records if rec.levelno == logging.WARNING]
        assert warnings
        caplog.clear()

    def test_glob_to_re(self):
        sep = re.escape(os.sep)
        # https://docs.python.org/3/whatsnew/3.14.html#re
        end_of_str_metachar = r"\z" if sys.version_info >= (3, 14) else r"\Z"

        for glob, regex in (
            # simple cases
            ('foo*', r'(?s:foo[^%(sep)s]*)%(eos)s'),
            ('foo?', r'(?s:foo[^%(sep)s])%(eos)s'),
            ('foo??', r'(?s:foo[^%(sep)s][^%(sep)s])%(eos)s'),
            # special cases
            (r'foo\\*', r'(?s:foo\\\\[^%(sep)s]*)%(eos)s'),
            (r'foo\\\*', r'(?s:foo\\\\\\[^%(sep)s]*)%(eos)s'),
            ('foo????', r'(?s:foo[^%(sep)s][^%(sep)s][^%(sep)s][^%(sep)s])%(eos)s'),
            (r'foo\\??', r'(?s:foo\\\\[^%(sep)s][^%(sep)s])%(eos)s'),
        ):
            regex = regex % {'sep': sep, 'eos': end_of_str_metachar}
            assert glob_to_re(glob) == regex

    def test_process_template_line(self):
        # testing  all MANIFEST.in template patterns
        file_list = FileList()
        mlp = make_local_path

        # simulated file list
        file_list.allfiles = [
            'foo.tmp',
            'ok',
            'xo',
            'four.txt',
            'buildout.cfg',
            # filelist does not filter out VCS directories,
            # it's sdist that does
            mlp('.hg/last-message.txt'),
            mlp('global/one.txt'),
            mlp('global/two.txt'),
            mlp('global/files.x'),
            mlp('global/here.tmp'),
            mlp('f/o/f.oo'),
            mlp('dir/graft-one'),
            mlp('dir/dir2/graft2'),
            mlp('dir3/ok'),
            mlp('dir3/sub/ok.txt'),
        ]

        for line in MANIFEST_IN.split('\n'):
            if line.strip() == '':
                continue
            file_list.process_template_line(line)

        wanted = [
            'ok',
            'buildout.cfg',
            'four.txt',
            mlp('.hg/last-message.txt'),
            mlp('global/one.txt'),
            mlp('global/two.txt'),
            mlp('f/o/f.oo'),
            mlp('dir/graft-one'),
            mlp('dir/dir2/graft2'),
        ]

        assert file_list.files == wanted

    def test_debug_print(self, capsys, monkeypatch):
        file_list = FileList()
        file_list.debug_print('xxx')
        assert capsys.readouterr().out == ''

        monkeypatch.setattr(debug, 'DEBUG', True)
        file_list.debug_print('xxx')
        assert capsys.readouterr().out == 'xxx\n'

    def test_set_allfiles(self):
        file_list = FileList()
        files = ['a', 'b', 'c']
        file_list.set_allfiles(files)
        assert file_list.allfiles == files

    def test_remove_duplicates(self):
        file_list = FileList()
        file_list.files = ['a', 'b', 'a', 'g', 'c', 'g']
        # files must be sorted beforehand (sdist does it)
        file_list.sort()
        file_list.remove_duplicates()
        assert file_list.files == ['a', 'b', 'c', 'g']

    def test_translate_pattern(self):
        # not regex
        assert hasattr(translate_pattern('a', anchor=True, is_regex=False), 'search')

        # is a regex
        regex = re.compile('a')
        assert translate_pattern(regex, anchor=True, is_regex=True) == regex

        # plain string flagged as regex
        assert hasattr(translate_pattern('a', anchor=True, is_regex=True), 'search')

        # glob support
        assert translate_pattern('*.py', anchor=True, is_regex=False).search(
            'filelist.py'
        )

    def test_exclude_pattern(self):
        # return False if no match
        file_list = FileList()
        assert not file_list.exclude_pattern('*.py')

        # return True if files match
        file_list = FileList()
        file_list.files = ['a.py', 'b.py']
        assert file_list.exclude_pattern('*.py')

        # test excludes
        file_list = FileList()
        file_list.files = ['a.py', 'a.txt']
        file_list.exclude_pattern('*.py')
        assert file_list.files == ['a.txt']

    def test_include_pattern(self):
        # return False if no match
        file_list = FileList()
        file_list.set_allfiles([])
        assert not file_list.include_pattern('*.py')

        # return True if files match
        file_list = FileList()
        file_list.set_allfiles(['a.py', 'b.txt'])
        assert file_list.include_pattern('*.py')

        # test * matches all files
        file_list = FileList()
        assert file_list.allfiles is None
        file_list.set_allfiles(['a.py', 'b.txt'])
        file_list.include_pattern('*')
        assert file_list.allfiles == ['a.py', 'b.txt']

    def test_process_template(self, caplog):
        mlp = make_local_path
        # invalid lines
        file_list = FileList()
        for action in (
            'include',
            'exclude',
            'global-include',
            'global-exclude',
            'recursive-include',
            'recursive-exclude',
            'graft',
            'prune',
            'blarg',
        ):
            with pytest.raises(DistutilsTemplateError):
                file_list.process_template_line(action)

        # include
        file_list = FileList()
        file_list.set_allfiles(['a.py', 'b.txt', mlp('d/c.py')])

        file_list.process_template_line('include *.py')
        assert file_list.files == ['a.py']
        self.assertNoWarnings(caplog)

        file_list.process_template_line('include *.rb')
        assert file_list.files == ['a.py']
        self.assertWarnings(caplog)

        # exclude
        file_list = FileList()
        file_list.files = ['a.py', 'b.txt', mlp('d/c.py')]

        file_list.process_template_line('exclude *.py')
        assert file_list.files == ['b.txt', mlp('d/c.py')]
        self.assertNoWarnings(caplog)

        file_list.process_template_line('exclude *.rb')
        assert file_list.files == ['b.txt', mlp('d/c.py')]
        self.assertWarnings(caplog)

        # global-include
        file_list = FileList()
        file_list.set_allfiles(['a.py', 'b.txt', mlp('d/c.py')])

        file_list.process_template_line('global-include *.py')
        assert file_list.files == ['a.py', mlp('d/c.py')]
        self.assertNoWarnings(caplog)

        file_list.process_template_line('global-include *.rb')
        assert file_list.files == ['a.py', mlp('d/c.py')]
        self.assertWarnings(caplog)

        # global-exclude
        file_list = FileList()
        file_list.files = ['a.py', 'b.txt', mlp('d/c.py')]

        file_list.process_template_line('global-exclude *.py')
        assert file_list.files == ['b.txt']
        self.assertNoWarnings(caplog)

        file_list.process_template_line('global-exclude *.rb')
        assert file_list.files == ['b.txt']
        self.assertWarnings(caplog)

        # recursive-include
        file_list = FileList()
        file_list.set_allfiles(['a.py', mlp('d/b.py'), mlp('d/c.txt'), mlp('d/d/e.py')])

        file_list.process_template_line('recursive-include d *.py')
        assert file_list.files == [mlp('d/b.py'), mlp('d/d/e.py')]
        self.assertNoWarnings(caplog)

        file_list.process_template_line('recursive-include e *.py')
        assert file_list.files == [mlp('d/b.py'), mlp('d/d/e.py')]
        self.assertWarnings(caplog)

        # recursive-exclude
        file_list = FileList()
        file_list.files = ['a.py', mlp('d/b.py'), mlp('d/c.txt'), mlp('d/d/e.py')]

        file_list.process_template_line('recursive-exclude d *.py')
        assert file_list.files == ['a.py', mlp('d/c.txt')]
        self.assertNoWarnings(caplog)

        file_list.process_template_line('recursive-exclude e *.py')
        assert file_list.files == ['a.py', mlp('d/c.txt')]
        self.assertWarnings(caplog)

        # graft
        file_list = FileList()
        file_list.set_allfiles(['a.py', mlp('d/b.py'), mlp('d/d/e.py'), mlp('f/f.py')])

        file_list.process_template_line('graft d')
        assert file_list.files == [mlp('d/b.py'), mlp('d/d/e.py')]
        self.assertNoWarnings(caplog)

        file_list.process_template_line('graft e')
        assert file_list.files == [mlp('d/b.py'), mlp('d/d/e.py')]
        self.assertWarnings(caplog)

        # prune
        file_list = FileList()
        file_list.files = ['a.py', mlp('d/b.py'), mlp('d/d/e.py'), mlp('f/f.py')]

        file_list.process_template_line('prune d')
        assert file_list.files == ['a.py', mlp('f/f.py')]
        self.assertNoWarnings(caplog)

        file_list.process_template_line('prune e')
        assert file_list.files == ['a.py', mlp('f/f.py')]
        self.assertWarnings(caplog)


class TestFindAll:
    @os_helper.skip_unless_symlink
    def test_missing_symlink(self, temp_cwd):
        os.symlink('foo', 'bar')
        assert filelist.findall() == []

    def test_basic_discovery(self, temp_cwd):
        """
        When findall is called with no parameters or with
        '.' as the parameter, the dot should be omitted from
        the results.
        """
        jaraco.path.build({'foo': {'file1.txt': ''}, 'bar': {'file2.txt': ''}})
        file1 = os.path.join('foo', 'file1.txt')
        file2 = os.path.join('bar', 'file2.txt')
        expected = [file2, file1]
        assert sorted(filelist.findall()) == expected

    def test_non_local_discovery(self, tmp_path):
        """
        When findall is called with another path, the full
        path name should be returned.
        """
        jaraco.path.build({'file1.txt': ''}, tmp_path)
        expected = [str(tmp_path / 'file1.txt')]
        assert filelist.findall(tmp_path) == expected

    @os_helper.skip_unless_symlink
    def test_symlink_loop(self, tmp_path):
        jaraco.path.build(
            {
                'link-to-parent': jaraco.path.Symlink('.'),
                'somefile': '',
            },
            tmp_path,
        )
        files = filelist.findall(tmp_path)
        assert len(files) == 1
