// 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 (
	"path/filepath"
	"runtime"
	"strings"
	"testing"

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

func pathEqualsTo(t *testing.T, expected string, actual *Path) {
	require.Equal(t, expected, filepath.ToSlash(actual.String()))
}

func TestPathNew(t *testing.T) {
	test1 := New("path")
	require.Equal(t, "path", test1.String())

	test2 := New("path", "path")
	require.Equal(t, filepath.Join("path", "path"), test2.String())

	test3 := New()
	require.Nil(t, test3)

	test4 := New("")
	require.Nil(t, test4)
}

func TestPath(t *testing.T) {
	testPath := New("testdata", "fileset")
	pathEqualsTo(t, "testdata/fileset", testPath)
	isDir, err := testPath.IsDirCheck()
	require.True(t, isDir)
	require.NoError(t, err)
	require.True(t, testPath.IsDir())
	require.False(t, testPath.IsNotDir())
	exist, err := testPath.ExistCheck()
	require.True(t, exist)
	require.NoError(t, err)
	require.True(t, testPath.Exist())
	require.False(t, testPath.NotExist())

	folderPath := testPath.Join("folder")
	pathEqualsTo(t, "testdata/fileset/folder", folderPath)
	isDir, err = folderPath.IsDirCheck()
	require.True(t, isDir)
	require.NoError(t, err)
	require.True(t, folderPath.IsDir())
	require.False(t, folderPath.IsNotDir())

	exist, err = folderPath.ExistCheck()
	require.True(t, exist)
	require.NoError(t, err)
	require.True(t, folderPath.Exist())
	require.False(t, folderPath.NotExist())

	filePath := testPath.Join("file")
	pathEqualsTo(t, "testdata/fileset/file", filePath)
	isDir, err = filePath.IsDirCheck()
	require.False(t, isDir)
	require.NoError(t, err)
	require.False(t, filePath.IsDir())
	require.True(t, filePath.IsNotDir())
	exist, err = filePath.ExistCheck()
	require.True(t, exist)
	require.NoError(t, err)
	require.True(t, filePath.Exist())
	require.False(t, filePath.NotExist())

	anotherFilePath := filePath.Join("notexistent")
	pathEqualsTo(t, "testdata/fileset/file/notexistent", anotherFilePath)
	isDir, err = anotherFilePath.IsDirCheck()
	require.False(t, isDir)
	require.Error(t, err)
	require.False(t, anotherFilePath.IsDir())
	require.False(t, anotherFilePath.IsNotDir())
	exist, err = anotherFilePath.ExistCheck()
	require.False(t, exist)
	require.NoError(t, err)
	require.False(t, anotherFilePath.Exist())
	require.True(t, anotherFilePath.NotExist())

	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])

	list2 := list.Clone()
	list2.FilterDirs()
	require.Len(t, list2, 1)
	pathEqualsTo(t, "testdata/fileset/folder/subfolder", list2[0])

	list2 = list.Clone()
	list2.FilterOutHiddenFiles()
	require.Len(t, list2, 3)
	pathEqualsTo(t, "testdata/fileset/folder/file2", list2[0])
	pathEqualsTo(t, "testdata/fileset/folder/file3", list2[1])
	pathEqualsTo(t, "testdata/fileset/folder/subfolder", list2[2])

	list2 = list.Clone()
	list2.FilterOutPrefix("file")
	require.Len(t, list2, 2)
	pathEqualsTo(t, "testdata/fileset/folder/.hidden", list2[0])
	pathEqualsTo(t, "testdata/fileset/folder/subfolder", list2[1])
}

func TestResetStatCacheWhenFollowingSymlink(t *testing.T) {
	testdata := New("testdata", "fileset")
	files, err := testdata.ReadDir()
	require.NoError(t, err)
	for _, file := range files {
		if file.Base() == "symlinktofolder" {
			err = file.FollowSymLink()
			require.NoError(t, err)
			isDir, err := file.IsDirCheck()
			require.NoError(t, err)
			require.True(t, isDir)
			break
		}
	}
}

func TestIsInsideDir(t *testing.T) {
	notInside := func(a, b *Path) {
		isInside, err := a.IsInsideDir(b)
		require.NoError(t, err)
		require.False(t, isInside, "%s is inside %s", a, b)
	}

	inside := func(a, b *Path) {
		isInside, err := a.IsInsideDir(b)
		require.NoError(t, err)
		require.True(t, isInside, "%s is inside %s", a, b)
		notInside(b, a)
	}

	f1 := New("/a/b/c")
	f2 := New("/a/b/c/d")
	f3 := New("/a/b/c/d/e")

	notInside(f1, f1)
	notInside(f1, f2)
	inside(f2, f1)
	notInside(f1, f3)
	inside(f3, f1)

	r1 := New("a/b/c")
	r2 := New("a/b/c/d")
	r3 := New("a/b/c/d/e")
	r4 := New("f/../a/b/c/d/e")
	r5 := New("a/b/c/d/e/f/..")

	notInside(r1, r1)
	notInside(r1, r2)
	inside(r2, r1)
	notInside(r1, r3)
	inside(r3, r1)
	inside(r4, r1)
	notInside(r1, r4)
	inside(r5, r1)
	notInside(r1, r5)

	f4 := New("/home/megabug/aide/arduino-1.8.6/hardware/arduino/avr")
	f5 := New("/home/megabug/a15/packages")
	notInside(f5, f4)
	notInside(f4, f5)

	if runtime.GOOS == "windows" {
		f6 := New("C:\\", "A")
		f7 := New("C:\\", "A", "B", "C")
		f8 := New("E:\\", "A", "B", "C")
		inside(f7, f6)
		notInside(f8, f6)
	}
}

func TestReadFileAsLines(t *testing.T) {
	lines, err := New("testdata/fileset/anotherFile").ReadFileAsLines()
	require.NoError(t, err)
	require.Len(t, lines, 4)
	require.Equal(t, "line 1", lines[0])
	require.Equal(t, "line 2", lines[1])
	require.Equal(t, "", lines[2])
	require.Equal(t, "line 3", lines[3])
}

func TestCanonicaTempDir(t *testing.T) {
	require.Equal(t, TempDir().String(), TempDir().Canonical().String())
}

func TestCopyDir(t *testing.T) {
	tmp, err := MkTempDir("", "")
	require.NoError(t, err)
	defer tmp.RemoveAll()

	src := New("testdata", "fileset")
	err = src.CopyDirTo(tmp.Join("dest"))
	require.NoError(t, err, "copying dir")

	exist, err := tmp.Join("dest", "folder", "subfolder", "file4").ExistCheck()
	require.True(t, exist)
	require.NoError(t, err)

	isdir, err := tmp.Join("dest", "folder", "subfolder", "file4").IsDirCheck()
	require.False(t, isdir)
	require.NoError(t, err)

	err = src.CopyDirTo(tmp.Join("dest"))
	require.Error(t, err, "copying dir to already existing")

	err = src.Join("file").CopyDirTo(tmp.Join("dest2"))
	require.Error(t, err, "copying file as dir")
}

func TestParents(t *testing.T) {
	parents := New("/a/very/long/path").Parents()
	require.Len(t, parents, 5)
	pathEqualsTo(t, "/a/very/long/path", parents[0])
	pathEqualsTo(t, "/a/very/long", parents[1])
	pathEqualsTo(t, "/a/very", parents[2])
	pathEqualsTo(t, "/a", parents[3])
	pathEqualsTo(t, "/", parents[4])

	parents2 := New("a/very/relative/path").Parents()
	require.Len(t, parents, 5)
	pathEqualsTo(t, "a/very/relative/path", parents2[0])
	pathEqualsTo(t, "a/very/relative", parents2[1])
	pathEqualsTo(t, "a/very", parents2[2])
	pathEqualsTo(t, "a", parents2[3])
	pathEqualsTo(t, ".", parents2[4])
}

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

	list, err := testPath.ReadDir()
	require.NoError(t, err)
	require.Len(t, list, 6)

	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/symlinktofolder", list[3])
	pathEqualsTo(t, "testdata/fileset/test.txt", list[4])
	pathEqualsTo(t, "testdata/fileset/test.txt.gz", list[5])

	list.FilterDirs()
	require.Len(t, list, 2)
	pathEqualsTo(t, "testdata/fileset/folder", list[0])
	pathEqualsTo(t, "testdata/fileset/symlinktofolder", list[1])
}

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

		list, err := testPath.ReadDir()
		require.NoError(t, err)
		require.Len(t, list, 6)

		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/symlinktofolder", list[3])
		pathEqualsTo(t, "testdata/fileset/test.txt", list[4])
		pathEqualsTo(t, "testdata/fileset/test.txt.gz", list[5])

		list.FilterOutDirs()
		require.Len(t, list, 4)
		pathEqualsTo(t, "testdata/fileset/anotherFile", list[0])
		pathEqualsTo(t, "testdata/fileset/file", list[1])
		pathEqualsTo(t, "testdata/fileset/test.txt", list[2])
		pathEqualsTo(t, "testdata/fileset/test.txt.gz", list[3])
	}

	{
		list, err := New("testdata", "broken_symlink", "dir_1").ReadDirRecursive()
		require.NoError(t, err)

		require.Len(t, list, 7)
		pathEqualsTo(t, "testdata/broken_symlink/dir_1/broken_link", list[0])
		pathEqualsTo(t, "testdata/broken_symlink/dir_1/file2", list[1])
		pathEqualsTo(t, "testdata/broken_symlink/dir_1/linked_dir", list[2])
		pathEqualsTo(t, "testdata/broken_symlink/dir_1/linked_dir/file1", list[3])
		pathEqualsTo(t, "testdata/broken_symlink/dir_1/linked_file", list[4])
		pathEqualsTo(t, "testdata/broken_symlink/dir_1/real_dir", list[5])
		pathEqualsTo(t, "testdata/broken_symlink/dir_1/real_dir/file1", list[6])

		list.FilterOutDirs()
		require.Len(t, list, 5)
		pathEqualsTo(t, "testdata/broken_symlink/dir_1/broken_link", list[0])
		pathEqualsTo(t, "testdata/broken_symlink/dir_1/file2", list[1])
		pathEqualsTo(t, "testdata/broken_symlink/dir_1/linked_dir/file1", list[2])
		pathEqualsTo(t, "testdata/broken_symlink/dir_1/linked_file", list[3])
		pathEqualsTo(t, "testdata/broken_symlink/dir_1/real_dir/file1", list[4])
	}
}

func TestEquivalentPaths(t *testing.T) {
	wd, err := Getwd()
	require.NoError(t, err)
	require.True(t, New("file1").EquivalentTo(New("file1", "somethingelse", "..")))
	require.True(t, New("file1", "abc").EquivalentTo(New("file1", "abc", "def", "..")))
	require.True(t, wd.Join("file1").EquivalentTo(New("file1")))
	require.True(t, wd.Join("file1").EquivalentTo(New("file1", "abc", "..")))

	if runtime.GOOS == "windows" {
		q := New("testdata", "fileset", "anotherFile")
		r := New(shorten(t, q.String()))
		t.Log("SHORTENED PATH:", r.String())
		require.True(t, q.EquivalentTo(r))
		require.True(t, r.EquivalentTo(q))
	}
}

func TestCanonicalize(t *testing.T) {
	wd, err := Getwd()
	require.NoError(t, err)

	p := New("testdata", "fileset", "anotherFile").Canonical()
	require.Equal(t, wd.Join("testdata", "fileset", "anotherFile").String(), p.String())

	p = New("testdata", "fileset", "nonexistentFile").Canonical()
	require.Equal(t, wd.Join("testdata", "fileset", "nonexistentFile").String(), p.String())

	if runtime.GOOS == "windows" {
		qshort := New(shorten(t, New("testdata", "fileset", "anotherFile").String()))
		t.Log("SHORTENED PATH:", qshort.String())
		q := qshort.Canonical()
		require.Equal(t, wd.Join("testdata", "fileset", "anotherFile").String(), q.String())

		r := New("c:\\").Canonical()
		require.Equal(t, "C:\\", r.String())

		tmp, err := MkTempDir("", "pref")
		require.NoError(t, err)
		require.Equal(t, tmp.String(), tmp.Canonical().String())
	}
}

func TestRelativeTo(t *testing.T) {
	res, err := New("/my/abs/path/123/456").RelTo(New("/my/abs/path"))
	require.NoError(t, err)
	pathEqualsTo(t, "../..", res)

	res, err = New("/my/abs/path").RelTo(New("/my/abs/path/123/456"))
	require.NoError(t, err)
	pathEqualsTo(t, "123/456", res)

	res, err = New("my/path").RelTo(New("/other/path"))
	require.Error(t, err)
	require.Nil(t, res)

	res, err = New("/my/abs/path/123/456").RelFrom(New("/my/abs/path"))
	pathEqualsTo(t, "123/456", res)
	require.NoError(t, err)

	res, err = New("/my/abs/path").RelFrom(New("/my/abs/path/123/456"))
	require.NoError(t, err)
	pathEqualsTo(t, "../..", res)

	res, err = New("my/path").RelFrom(New("/other/path"))
	require.Error(t, err)
	require.Nil(t, res)
}

func TestWriteToTempFile(t *testing.T) {
	tmpDir := New("testdata", "fileset", "tmp")
	err := tmpDir.MkdirAll()
	require.NoError(t, err)
	defer tmpDir.RemoveAll()

	tmpData := []byte("test")
	tmp, err := WriteToTempFile(tmpData, tmpDir, "prefix")
	defer tmp.Remove()
	require.NoError(t, err)
	require.True(t, strings.HasPrefix(tmp.Base(), "prefix"))
	isInside, err := tmp.IsInsideDir(tmpDir)
	require.NoError(t, err)
	require.True(t, isInside)
	data, err := tmp.ReadFile()
	require.NoError(t, err)
	require.Equal(t, tmpData, data)
}

func TestCopyToSamePath(t *testing.T) {
	tmpDir := New(t.TempDir())
	srcFile := tmpDir.Join("test_file")
	dstFile := srcFile

	// create the source file in tmp dir
	err := srcFile.WriteFile([]byte("hello"))
	require.NoError(t, err)
	content, err := srcFile.ReadFile()
	require.NoError(t, err)
	require.Equal(t, []byte("hello"), content)

	// cannot copy the same file
	err = srcFile.CopyTo(dstFile)
	require.Error(t, err)
	require.Contains(t, err.Error(), "are the same file")
}
