// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0

package assertions

import (
	"fmt"
	"io/fs"
	"os"
)

// FileExists checks whether a file exists in the given path. It also fails if
// the path points to a directory or there is an error when trying to check the file.
//
// # Usage
//
//	assertions.FileExists(t, "path/to/file")
//
// # Examples
//
//	success: filepath.Join(testDataPath(),"existing_file")
//	failure: filepath.Join(testDataPath(),"non_existing_file")
func FileExists(t T, path string, msgAndArgs ...any) bool {
	// Domain: file
	// Opposite: FileNotExists
	if h, ok := t.(H); ok {
		h.Helper()
	}

	info, err := lstat(path, "file")
	if err != nil {
		return Fail(t, err.Error(), msgAndArgs...)
	}

	if info.IsDir() {
		return Fail(t, fmt.Sprintf("%q is a directory", path), msgAndArgs...)
	}

	return true
}

// FileNotExists checks whether a file does not exist in a given path. It fails
// if the path points to an existing _file_ only.
//
// # Usage
//
//	assertions.FileNotExists(t, "path/to/file")
//
// # Examples
//
//	success: filepath.Join(testDataPath(),"non_existing_file")
//	failure: filepath.Join(testDataPath(),"existing_file")
func FileNotExists(t T, path string, msgAndArgs ...any) bool {
	// Domain: file
	if h, ok := t.(H); ok {
		h.Helper()
	}

	info, err := os.Lstat(path)
	if err != nil {
		return true
	}

	if info.IsDir() {
		return true
	}

	return Fail(t, fmt.Sprintf("file %q exists", path), msgAndArgs...)
}

// DirExists checks whether a directory exists in the given path. It also fails
// if the path is a file rather a directory or there is an error checking whether it exists.
//
// # Usage
//
//	assertions.DirExists(t, "path/to/directory")
//
// # Examples
//
//	success: filepath.Join(testDataPath(),"existing_dir")
//	failure: filepath.Join(testDataPath(),"non_existing_dir")
func DirExists(t T, path string, msgAndArgs ...any) bool {
	// Domain: file
	// Opposite: DirNotExists
	if h, ok := t.(H); ok {
		h.Helper()
	}

	info, err := lstat(path, "directory")
	if err != nil {
		return Fail(t, err.Error(), msgAndArgs...)
	}

	if !info.IsDir() {
		return Fail(t, fmt.Sprintf("%q is a file", path), msgAndArgs...)
	}

	return true
}

// DirNotExists checks whether a directory does not exist in the given path.
// It fails if the path points to an existing _directory_ only.
//
// # Usage
//
//	assertions.DirNotExists(t, "path/to/directory")
//
// # Examples
//
//	success: filepath.Join(testDataPath(),"non_existing_dir")
//	failure: filepath.Join(testDataPath(),"existing_dir")
func DirNotExists(t T, path string, msgAndArgs ...any) bool {
	// Domain: file
	if h, ok := t.(H); ok {
		h.Helper()
	}

	info, err := os.Lstat(path)
	if err != nil {
		return true
	}

	if !info.IsDir() {
		return true
	}

	return Fail(t, fmt.Sprintf("directory %q exists", path), msgAndArgs...)
}

// FileEmpty checks whether a file exists in the given path and is empty.
// It fails if the file is not empty, if the path points to a directory or there is an error when trying to check the file.
//
// # Usage
//
//	assertions.FileEmpty(t, "path/to/file")
//
// # Examples
//
//	success: filepath.Join(testDataPath(),"empty_file")
//	failure: filepath.Join(testDataPath(),"existing_file")
func FileEmpty(t T, path string, msgAndArgs ...any) bool {
	// Domain: file
	// Opposite: FileNotEmpty
	if h, ok := t.(H); ok {
		h.Helper()
	}

	return fileEmpty(t, path, 0, msgAndArgs...)
}

func fileEmpty(t T, path string, depth int, msgAndArgs ...any) bool {
	if depth > maxDepth {
		return Fail(t, fmt.Sprintf("chain of symlinks too long, stopped at %d nested symlinks", maxDepth), msgAndArgs...)
	}

	info, err := lstat(path, "file")
	if err != nil {
		return Fail(t, err.Error(), msgAndArgs...)
	}

	if info.IsDir() {
		return Fail(t, fmt.Sprintf("%q is a directory", path), msgAndArgs...)
	}

	target, isSymlink, err := isSymlink(path, info)
	if err != nil {
		return Fail(t, err.Error(), msgAndArgs...)
	}
	if isSymlink {
		return fileEmpty(t, target, depth+1, msgAndArgs...)
	}

	if info.Size() > 0 {
		return Fail(t, fmt.Sprintf("%q is not empty", path), msgAndArgs...)
	}

	return true
}

// FileNotEmpty checks whether a file exists in the given path and is not empty.
// It fails if the file is empty, if the path points to a directory or there is an error when trying to check the file.
//
// # Usage
//
//	assertions.FileNotEmpty(t, "path/to/file")
//
// # Examples
//
//	success: filepath.Join(testDataPath(),"existing_file")
//	failure: filepath.Join(testDataPath(),"empty_file")
func FileNotEmpty(t T, path string, msgAndArgs ...any) bool {
	// Domain: file
	if h, ok := t.(H); ok {
		h.Helper()
	}

	return fileNotEmpty(t, path, 0, msgAndArgs...)
}

func fileNotEmpty(t T, path string, depth int, msgAndArgs ...any) bool {
	if depth > maxDepth {
		return Fail(t, fmt.Sprintf("chain of symlinks too long, stopped at %d nested symlinks", maxDepth), msgAndArgs...)
	}

	info, err := lstat(path, "file")
	if err != nil {
		return Fail(t, err.Error(), msgAndArgs...)
	}

	if info.IsDir() {
		return Fail(t, fmt.Sprintf("%q is a directory", path), msgAndArgs...)
	}

	target, isSymlink, err := isSymlink(path, info)
	if err != nil {
		return Fail(t, err.Error(), msgAndArgs...)
	}
	if isSymlink {
		return fileNotEmpty(t, target, depth+1, msgAndArgs...)
	}

	if info.Size() == 0 {
		return Fail(t, fmt.Sprintf("%q is empty", path), msgAndArgs...)
	}

	return true
}

func lstat(path, kind string) (info os.FileInfo, err error) {
	info, err = os.Lstat(path)
	if err != nil {
		if os.IsNotExist(err) {
			return info, fmt.Errorf("unable to find %s %q: %w", kind, path, err)
		}

		return info, fmt.Errorf("error when running os.Lstat(%q): %w", path, err)
	}

	return info, nil
}

func isSymlink(path string, info os.FileInfo) (target string, isSymlink bool, err error) {
	if info.Mode()&fs.ModeSymlink == 0 {
		return path, false, nil
	}

	target, err = os.Readlink(path)
	if err != nil {
		// This is not reachable on linux, but windows has different semantics for symlinks
		return target, true, fmt.Errorf("could not resolve symlink %q", path)
	}

	return target, true, nil
}
