// This file is part of PathsHelper library.
//
// Copyright 2018-2025 Arduino AG (http://www.arduino.cc/)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
// You can be released from the requirements of the above licenses by purchasing
// a commercial license. Buying such a license is mandatory if you want to
// modify or otherwise use the software for commercial activities involving the
// Arduino software without disclosing the source code of your own applications.
// To purchase a commercial license, send an email to license@arduino.cc.

package paths

import (
	"fmt"
	"io/fs"
	"os"
	"runtime"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

func TestReadDirRecursive(t *testing.T) {
	testPath := New("testdata", "fileset")

	list, err := testPath.ReadDirRecursive()
	require.NoError(t, err)
	require.Len(t, list, 16)

	pathEqualsTo(t, "testdata/fileset/anotherFile", list[0])
	pathEqualsTo(t, "testdata/fileset/file", list[1])
	pathEqualsTo(t, "testdata/fileset/folder", list[2])
	pathEqualsTo(t, "testdata/fileset/folder/.hidden", list[3])
	pathEqualsTo(t, "testdata/fileset/folder/file2", list[4])
	pathEqualsTo(t, "testdata/fileset/folder/file3", list[5])
	pathEqualsTo(t, "testdata/fileset/folder/subfolder", list[6])
	pathEqualsTo(t, "testdata/fileset/folder/subfolder/file4", list[7])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder", list[8])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/.hidden", list[9])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/file2", list[10])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/file3", list[11])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/subfolder", list[12])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/subfolder/file4", list[13])
	pathEqualsTo(t, "testdata/fileset/test.txt", list[14])
	pathEqualsTo(t, "testdata/fileset/test.txt.gz", list[15])
}

func TestReadDirRecursiveSymLinkLoop(t *testing.T) {
	// Test symlink loop
	tmp, err := MkTempDir("", "")
	require.NoError(t, err)
	defer tmp.RemoveAll()

	folder := tmp.Join("folder")
	err = os.Symlink(tmp.String(), folder.String())
	require.NoError(t, err)

	l, err := tmp.ReadDirRecursive()
	require.Error(t, err)
	fmt.Println(err)
	require.Nil(t, l)

	l, err = tmp.ReadDirRecursiveFiltered(nil)
	require.Error(t, err)
	fmt.Println(err)
	require.Nil(t, l)
}

func TestReadDirFiltered(t *testing.T) {
	folderPath := New("testdata/fileset/folder")
	list, err := folderPath.ReadDir()
	require.NoError(t, err)
	require.Len(t, list, 4)
	pathEqualsTo(t, "testdata/fileset/folder/.hidden", list[0])
	pathEqualsTo(t, "testdata/fileset/folder/file2", list[1])
	pathEqualsTo(t, "testdata/fileset/folder/file3", list[2])
	pathEqualsTo(t, "testdata/fileset/folder/subfolder", list[3])

	list, err = folderPath.ReadDir(FilterDirectories())
	require.NoError(t, err)
	require.Len(t, list, 1)
	pathEqualsTo(t, "testdata/fileset/folder/subfolder", list[0])

	list, err = folderPath.ReadDir(FilterOutPrefixes("file"))
	require.NoError(t, err)
	require.Len(t, list, 2)
	pathEqualsTo(t, "testdata/fileset/folder/.hidden", list[0])
	pathEqualsTo(t, "testdata/fileset/folder/subfolder", list[1])
}

func TestReadDirRecursiveFiltered(t *testing.T) {
	testdata := New("testdata", "fileset")
	l, err := testdata.ReadDirRecursiveFiltered(nil)
	require.NoError(t, err)
	l.Sort()
	require.Len(t, l, 16)
	pathEqualsTo(t, "testdata/fileset/anotherFile", l[0])
	pathEqualsTo(t, "testdata/fileset/file", l[1])
	pathEqualsTo(t, "testdata/fileset/folder", l[2])
	pathEqualsTo(t, "testdata/fileset/folder/.hidden", l[3])
	pathEqualsTo(t, "testdata/fileset/folder/file2", l[4])
	pathEqualsTo(t, "testdata/fileset/folder/file3", l[5])
	pathEqualsTo(t, "testdata/fileset/folder/subfolder", l[6])
	pathEqualsTo(t, "testdata/fileset/folder/subfolder/file4", l[7])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder", l[8])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/.hidden", l[9])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/file2", l[10])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/file3", l[11])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/subfolder", l[12])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/subfolder/file4", l[13])
	pathEqualsTo(t, "testdata/fileset/test.txt", l[14])
	pathEqualsTo(t, "testdata/fileset/test.txt.gz", l[15])

	l, err = testdata.ReadDirRecursiveFiltered(FilterOutDirectories())
	require.NoError(t, err)
	l.Sort()
	require.Len(t, l, 6)
	pathEqualsTo(t, "testdata/fileset/anotherFile", l[0])
	pathEqualsTo(t, "testdata/fileset/file", l[1])
	pathEqualsTo(t, "testdata/fileset/folder", l[2])          // <- this is listed but not traversed
	pathEqualsTo(t, "testdata/fileset/symlinktofolder", l[3]) // <- this is listed but not traversed
	pathEqualsTo(t, "testdata/fileset/test.txt", l[4])
	pathEqualsTo(t, "testdata/fileset/test.txt.gz", l[5])

	l, err = testdata.ReadDirRecursiveFiltered(nil, FilterOutDirectories())
	require.NoError(t, err)
	l.Sort()
	require.Len(t, l, 12)
	pathEqualsTo(t, "testdata/fileset/anotherFile", l[0])
	pathEqualsTo(t, "testdata/fileset/file", l[1])
	pathEqualsTo(t, "testdata/fileset/folder/.hidden", l[2])
	pathEqualsTo(t, "testdata/fileset/folder/file2", l[3])
	pathEqualsTo(t, "testdata/fileset/folder/file3", l[4])
	pathEqualsTo(t, "testdata/fileset/folder/subfolder/file4", l[5])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/.hidden", l[6])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/file2", l[7])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/file3", l[8])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/subfolder/file4", l[9])
	pathEqualsTo(t, "testdata/fileset/test.txt", l[10])
	pathEqualsTo(t, "testdata/fileset/test.txt.gz", l[11])

	l, err = testdata.ReadDirRecursiveFiltered(FilterOutDirectories(), FilterOutDirectories())
	require.NoError(t, err)
	l.Sort()
	require.Len(t, l, 4)
	pathEqualsTo(t, "testdata/fileset/anotherFile", l[0])
	pathEqualsTo(t, "testdata/fileset/file", l[1])
	pathEqualsTo(t, "testdata/fileset/test.txt", l[2])
	pathEqualsTo(t, "testdata/fileset/test.txt.gz", l[3])

	l, err = testdata.ReadDirRecursiveFiltered(FilterOutPrefixes("sub"), FilterOutSuffixes("3"))
	require.NoError(t, err)
	l.Sort()
	require.Len(t, l, 12)
	pathEqualsTo(t, "testdata/fileset/anotherFile", l[0])
	pathEqualsTo(t, "testdata/fileset/file", l[1])
	pathEqualsTo(t, "testdata/fileset/folder", l[2])
	pathEqualsTo(t, "testdata/fileset/folder/.hidden", l[3])
	pathEqualsTo(t, "testdata/fileset/folder/file2", l[4])
	pathEqualsTo(t, "testdata/fileset/folder/subfolder", l[5]) // <- subfolder skipped by Prefix("sub")
	pathEqualsTo(t, "testdata/fileset/symlinktofolder", l[6])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/.hidden", l[7])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/file2", l[8])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/subfolder", l[9]) // <- subfolder skipped by Prefix("sub")
	pathEqualsTo(t, "testdata/fileset/test.txt", l[10])
	pathEqualsTo(t, "testdata/fileset/test.txt.gz", l[11])

	l, err = testdata.ReadDirRecursiveFiltered(FilterOutPrefixes("sub"), AndFilter(FilterOutSuffixes("3"), FilterOutPrefixes("fil")))
	require.NoError(t, err)
	l.Sort()
	require.Len(t, l, 9)
	pathEqualsTo(t, "testdata/fileset/anotherFile", l[0])
	pathEqualsTo(t, "testdata/fileset/folder", l[1])
	pathEqualsTo(t, "testdata/fileset/folder/.hidden", l[2])
	pathEqualsTo(t, "testdata/fileset/folder/subfolder", l[3])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder", l[4])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/.hidden", l[5])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/subfolder", l[6])
	pathEqualsTo(t, "testdata/fileset/test.txt", l[7])
	pathEqualsTo(t, "testdata/fileset/test.txt.gz", l[8])

	l, err = testdata.ReadDirRecursiveFiltered(FilterOutPrefixes("sub"), AndFilter(FilterOutSuffixes("3"), FilterOutPrefixes("fil"), FilterOutSuffixes(".gz")))
	require.NoError(t, err)
	l.Sort()
	require.Len(t, l, 8)
	pathEqualsTo(t, "testdata/fileset/anotherFile", l[0])
	pathEqualsTo(t, "testdata/fileset/folder", l[1])
	pathEqualsTo(t, "testdata/fileset/folder/.hidden", l[2])
	pathEqualsTo(t, "testdata/fileset/folder/subfolder", l[3])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder", l[4])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/.hidden", l[5])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/subfolder", l[6])
	pathEqualsTo(t, "testdata/fileset/test.txt", l[7])

	l, err = testdata.ReadDirRecursiveFiltered(OrFilter(FilterPrefixes("sub"), FilterSuffixes("tofolder")))
	require.NoError(t, err)
	l.Sort()
	require.Len(t, l, 11)
	pathEqualsTo(t, "testdata/fileset/anotherFile", l[0])
	pathEqualsTo(t, "testdata/fileset/file", l[1])
	pathEqualsTo(t, "testdata/fileset/folder", l[2])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder", l[3])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/.hidden", l[4])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/file2", l[5])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/file3", l[6])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/subfolder", l[7])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/subfolder/file4", l[8])
	pathEqualsTo(t, "testdata/fileset/test.txt", l[9])
	pathEqualsTo(t, "testdata/fileset/test.txt.gz", l[10])

	l, err = testdata.ReadDirRecursiveFiltered(nil, FilterNames("folder"))
	require.NoError(t, err)
	l.Sort()
	require.Len(t, l, 1)
	pathEqualsTo(t, "testdata/fileset/folder", l[0])

	l, err = testdata.ReadDirRecursiveFiltered(FilterNames("symlinktofolder"), FilterOutNames(".hidden"))
	require.NoError(t, err)
	require.Len(t, l, 9)
	l.Sort()
	pathEqualsTo(t, "testdata/fileset/anotherFile", l[0])
	pathEqualsTo(t, "testdata/fileset/file", l[1])
	pathEqualsTo(t, "testdata/fileset/folder", l[2])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder", l[3])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/file2", l[4])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/file3", l[5])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder/subfolder", l[6])
	pathEqualsTo(t, "testdata/fileset/test.txt", l[7])
	pathEqualsTo(t, "testdata/fileset/test.txt.gz", l[8])
}

func TestReadDirRecursiveLoopDetection(t *testing.T) {
	loopsPath := New("testdata", "loops")
	unbuondedReaddir := func(testdir string) (PathList, error) {
		var files PathList
		var err error
		done := make(chan bool)
		go func() {
			files, err = loopsPath.Join(testdir).ReadDirRecursive()
			done <- true
		}()
		require.Eventually(
			t,
			func() bool {
				select {
				case <-done:
					return true
				default:
					return false
				}
			},
			5*time.Second,
			10*time.Millisecond,
			"Infinite symlink loop while loading sketch",
		)
		return files, err
	}

	for _, dir := range []string{"loop_1", "loop_2", "loop_3", "loop_4"} {
		l, err := unbuondedReaddir(dir)
		require.EqualError(t, err, "directories symlink loop detected", "loop not detected in %s", dir)
		require.Nil(t, l)
	}

	{
		l, err := unbuondedReaddir("regular_1")
		require.NoError(t, err)
		require.Len(t, l, 4)
		l.Sort()
		pathEqualsTo(t, "testdata/loops/regular_1/dir1", l[0])
		pathEqualsTo(t, "testdata/loops/regular_1/dir1/file1", l[1])
		pathEqualsTo(t, "testdata/loops/regular_1/dir2", l[2])
		pathEqualsTo(t, "testdata/loops/regular_1/dir2/file1", l[3])
	}

	{
		l, err := unbuondedReaddir("regular_2")
		require.NoError(t, err)
		require.Len(t, l, 6)
		l.Sort()
		pathEqualsTo(t, "testdata/loops/regular_2/dir1", l[0])
		pathEqualsTo(t, "testdata/loops/regular_2/dir1/file1", l[1])
		pathEqualsTo(t, "testdata/loops/regular_2/dir2", l[2])
		pathEqualsTo(t, "testdata/loops/regular_2/dir2/dir1", l[3])
		pathEqualsTo(t, "testdata/loops/regular_2/dir2/dir1/file1", l[4])
		pathEqualsTo(t, "testdata/loops/regular_2/dir2/file2", l[5])
	}

	{
		l, err := unbuondedReaddir("regular_3")
		require.NoError(t, err)
		require.Len(t, l, 7)
		l.Sort()
		pathEqualsTo(t, "testdata/loops/regular_3/dir1", l[0])
		pathEqualsTo(t, "testdata/loops/regular_3/dir1/file1", l[1])
		pathEqualsTo(t, "testdata/loops/regular_3/dir2", l[2])
		pathEqualsTo(t, "testdata/loops/regular_3/dir2/dir1", l[3])
		pathEqualsTo(t, "testdata/loops/regular_3/dir2/dir1/file1", l[4])
		pathEqualsTo(t, "testdata/loops/regular_3/dir2/file2", l[5])
		pathEqualsTo(t, "testdata/loops/regular_3/link", l[6]) // broken symlink is reported in files
	}

	if runtime.GOOS != "windows" {
		dir1 := loopsPath.Join("regular_4_with_permission_error", "dir1")

		l, err := unbuondedReaddir("regular_4_with_permission_error")
		require.NoError(t, err)
		require.NotEmpty(t, l)

		dir1Stat, err := dir1.Stat()
		require.NoError(t, err)
		err = dir1.Chmod(fs.FileMode(0)) // Enforce permission error
		require.NoError(t, err)
		t.Cleanup(func() {
			// Restore normal permission after the test
			dir1.Chmod(dir1Stat.Mode())
		})

		l, err = unbuondedReaddir("regular_4_with_permission_error")
		require.Error(t, err)
		require.Nil(t, l)
	}
}
