// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package os_test

import (
	"errors"
	"internal/testenv"
	"io/fs"
	"os"
	"path/filepath"
	"runtime"
	"testing"
)

type testStatAndLstatParams struct {
	isLink     bool
	statCheck  func(*testing.T, string, fs.FileInfo)
	lstatCheck func(*testing.T, string, fs.FileInfo)
}

// testStatAndLstat verifies that all os.Stat, os.Lstat os.File.Stat and os.Readdir work.
func testStatAndLstat(t *testing.T, path string, params testStatAndLstatParams) {
	// test os.Stat
	sfi, err := os.Stat(path)
	if err != nil {
		t.Error(err)
		return
	}
	params.statCheck(t, path, sfi)

	// test os.Lstat
	lsfi, err := os.Lstat(path)
	if err != nil {
		t.Error(err)
		return
	}
	params.lstatCheck(t, path, lsfi)

	if params.isLink {
		if os.SameFile(sfi, lsfi) {
			t.Errorf("stat and lstat of %q should not be the same", path)
		}
	} else {
		if !os.SameFile(sfi, lsfi) {
			t.Errorf("stat and lstat of %q should be the same", path)
		}
	}

	// test os.File.Stat
	f, err := os.Open(path)
	if err != nil {
		t.Error(err)
		return
	}
	defer f.Close()

	sfi2, err := f.Stat()
	if err != nil {
		t.Error(err)
		return
	}
	params.statCheck(t, path, sfi2)

	if !os.SameFile(sfi, sfi2) {
		t.Errorf("stat of open %q file and stat of %q should be the same", path, path)
	}

	if params.isLink {
		if os.SameFile(sfi2, lsfi) {
			t.Errorf("stat of opened %q file and lstat of %q should not be the same", path, path)
		}
	} else {
		if !os.SameFile(sfi2, lsfi) {
			t.Errorf("stat of opened %q file and lstat of %q should be the same", path, path)
		}
	}

	parentdir, base := filepath.Split(path)
	if parentdir == "" || base == "" {
		// skip os.Readdir test of files without directory or file name component,
		// such as directories with slash at the end or Windows device names.
		return
	}

	parent, err := os.Open(parentdir)
	if err != nil {
		t.Error(err)
		return
	}
	defer parent.Close()

	fis, err := parent.Readdir(-1)
	if err != nil {
		t.Error(err)
		return
	}
	var lsfi2 fs.FileInfo
	for _, fi2 := range fis {
		if fi2.Name() == base {
			lsfi2 = fi2
			break
		}
	}
	if lsfi2 == nil {
		t.Errorf("failed to find %q in its parent", path)
		return
	}
	params.lstatCheck(t, path, lsfi2)

	if !os.SameFile(lsfi, lsfi2) {
		t.Errorf("lstat of %q file in %q directory and %q should be the same", lsfi2.Name(), parentdir, path)
	}
}

// testIsDir verifies that fi refers to directory.
func testIsDir(t *testing.T, path string, fi fs.FileInfo) {
	t.Helper()
	if !fi.IsDir() {
		t.Errorf("%q should be a directory", path)
	}
	if fi.Mode()&fs.ModeSymlink != 0 {
		t.Errorf("%q should not be a symlink", path)
	}
}

// testIsSymlink verifies that fi refers to symlink.
func testIsSymlink(t *testing.T, path string, fi fs.FileInfo) {
	t.Helper()
	if fi.IsDir() {
		t.Errorf("%q should not be a directory", path)
	}
	if fi.Mode()&fs.ModeSymlink == 0 {
		t.Errorf("%q should be a symlink", path)
	}
}

// testIsFile verifies that fi refers to file.
func testIsFile(t *testing.T, path string, fi fs.FileInfo) {
	t.Helper()
	if fi.IsDir() {
		t.Errorf("%q should not be a directory", path)
	}
	if fi.Mode()&fs.ModeSymlink != 0 {
		t.Errorf("%q should not be a symlink", path)
	}
}

func testDirStats(t *testing.T, path string) {
	params := testStatAndLstatParams{
		isLink:     false,
		statCheck:  testIsDir,
		lstatCheck: testIsDir,
	}
	testStatAndLstat(t, path, params)
}

func testFileStats(t *testing.T, path string) {
	params := testStatAndLstatParams{
		isLink:     false,
		statCheck:  testIsFile,
		lstatCheck: testIsFile,
	}
	testStatAndLstat(t, path, params)
}

func testSymlinkStats(t *testing.T, path string, isdir bool) {
	params := testStatAndLstatParams{
		isLink:     true,
		lstatCheck: testIsSymlink,
	}
	if isdir {
		params.statCheck = testIsDir
	} else {
		params.statCheck = testIsFile
	}
	testStatAndLstat(t, path, params)
}

func testSymlinkSameFile(t *testing.T, path, link string) {
	pathfi, err := os.Stat(path)
	if err != nil {
		t.Error(err)
		return
	}

	linkfi, err := os.Stat(link)
	if err != nil {
		t.Error(err)
		return
	}
	if !os.SameFile(pathfi, linkfi) {
		t.Errorf("os.Stat(%q) and os.Stat(%q) are not the same file", path, link)
	}

	linkfi, err = os.Lstat(link)
	if err != nil {
		t.Error(err)
		return
	}
	if os.SameFile(pathfi, linkfi) {
		t.Errorf("os.Stat(%q) and os.Lstat(%q) are the same file", path, link)
	}
}

func testSymlinkSameFileOpen(t *testing.T, link string) {
	f, err := os.Open(link)
	if err != nil {
		t.Error(err)
		return
	}
	defer f.Close()

	fi, err := f.Stat()
	if err != nil {
		t.Error(err)
		return
	}

	fi2, err := os.Stat(link)
	if err != nil {
		t.Error(err)
		return
	}

	if !os.SameFile(fi, fi2) {
		t.Errorf("os.Open(%q).Stat() and os.Stat(%q) are not the same file", link, link)
	}
}

func TestDirAndSymlinkStats(t *testing.T) {
	testenv.MustHaveSymlink(t)
	t.Parallel()

	tmpdir := t.TempDir()
	dir := filepath.Join(tmpdir, "dir")
	if err := os.Mkdir(dir, 0777); err != nil {
		t.Fatal(err)
	}
	testDirStats(t, dir)

	dirlink := filepath.Join(tmpdir, "link")
	if err := os.Symlink(dir, dirlink); err != nil {
		t.Fatal(err)
	}
	testSymlinkStats(t, dirlink, true)
	testSymlinkSameFile(t, dir, dirlink)
	testSymlinkSameFileOpen(t, dirlink)

	linklink := filepath.Join(tmpdir, "linklink")
	if err := os.Symlink(dirlink, linklink); err != nil {
		t.Fatal(err)
	}
	testSymlinkStats(t, linklink, true)
	testSymlinkSameFile(t, dir, linklink)
	testSymlinkSameFileOpen(t, linklink)
}

func TestFileAndSymlinkStats(t *testing.T) {
	testenv.MustHaveSymlink(t)
	t.Parallel()

	tmpdir := t.TempDir()
	file := filepath.Join(tmpdir, "file")
	if err := os.WriteFile(file, []byte(""), 0644); err != nil {
		t.Fatal(err)
	}
	testFileStats(t, file)

	filelink := filepath.Join(tmpdir, "link")
	if err := os.Symlink(file, filelink); err != nil {
		t.Fatal(err)
	}
	testSymlinkStats(t, filelink, false)
	testSymlinkSameFile(t, file, filelink)
	testSymlinkSameFileOpen(t, filelink)

	linklink := filepath.Join(tmpdir, "linklink")
	if err := os.Symlink(filelink, linklink); err != nil {
		t.Fatal(err)
	}
	testSymlinkStats(t, linklink, false)
	testSymlinkSameFile(t, file, linklink)
	testSymlinkSameFileOpen(t, linklink)
}

// see issue 27225 for details
func TestSymlinkWithTrailingSlash(t *testing.T) {
	testenv.MustHaveSymlink(t)
	t.Parallel()

	tmpdir := t.TempDir()
	dir := filepath.Join(tmpdir, "dir")
	if err := os.Mkdir(dir, 0777); err != nil {
		t.Fatal(err)
	}
	dirlink := filepath.Join(tmpdir, "link")
	if err := os.Symlink(dir, dirlink); err != nil {
		t.Fatal(err)
	}
	dirlinkWithSlash := dirlink + string(os.PathSeparator)

	testDirStats(t, dirlinkWithSlash)

	fi1, err := os.Stat(dir)
	if err != nil {
		t.Error(err)
		return
	}
	fi2, err := os.Stat(dirlinkWithSlash)
	if err != nil {
		t.Error(err)
		return
	}
	if !os.SameFile(fi1, fi2) {
		t.Errorf("os.Stat(%q) and os.Stat(%q) are not the same file", dir, dirlinkWithSlash)
	}
}

func TestStatConsole(t *testing.T) {
	if runtime.GOOS != "windows" {
		t.Skip("skipping on non-Windows")
	}
	t.Parallel()
	consoleNames := []string{
		"CONIN$",
		"CONOUT$",
		"CON",
	}
	for _, name := range consoleNames {
		params := testStatAndLstatParams{
			isLink:     false,
			statCheck:  testIsFile,
			lstatCheck: testIsFile,
		}
		testStatAndLstat(t, name, params)
		testStatAndLstat(t, `\\.\`+name, params)
	}
}

func TestClosedStat(t *testing.T) {
	// Historically we do not seem to match ErrClosed on non-Unix systems.
	switch runtime.GOOS {
	case "windows", "plan9":
		t.Skipf("skipping on %s", runtime.GOOS)
	}

	t.Parallel()
	f, err := os.Open("testdata/hello")
	if err != nil {
		t.Fatal(err)
	}
	if err := f.Close(); err != nil {
		t.Fatal(err)
	}
	_, err = f.Stat()
	if err == nil {
		t.Error("Stat succeeded on closed File")
	} else if !errors.Is(err, os.ErrClosed) {
		t.Errorf("error from Stat on closed file did not match ErrClosed: %q, type %T", err, err)
	}
}