// Copyright 2024 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 windows_test

import (
	"internal/syscall/windows"
	"os"
	"path/filepath"
	"syscall"
	"testing"
	"unsafe"
)

func TestOpen(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	file := filepath.Join(dir, "a")
	f, err := os.Create(file)
	if err != nil {
		t.Fatal(err)
	}
	f.Close()

	tests := []struct {
		path string
		flag int
		err  error
	}{
		{dir, syscall.O_RDONLY, nil},
		{dir, syscall.O_CREAT, nil},
		{dir, syscall.O_RDONLY | syscall.O_CREAT, nil},
		{file, syscall.O_APPEND | syscall.O_WRONLY | os.O_CREATE, nil},
		{file, syscall.O_APPEND | syscall.O_WRONLY | os.O_CREATE | os.O_TRUNC, nil},
		{dir, syscall.O_RDONLY | syscall.O_TRUNC, syscall.ERROR_ACCESS_DENIED},
		{dir, syscall.O_WRONLY | syscall.O_RDWR, nil}, // TODO: syscall.Open returns EISDIR here, we should reconcile this
		{dir, syscall.O_WRONLY, syscall.EISDIR},
		{dir, syscall.O_RDWR, syscall.EISDIR},
	}
	for i, tt := range tests {
		dir := filepath.Dir(tt.path)
		dirfd, err := syscall.Open(dir, syscall.O_RDONLY, 0)
		if err != nil {
			t.Error(err)
			continue
		}
		base := filepath.Base(tt.path)
		h, err := windows.Openat(dirfd, base, uint64(tt.flag), 0o660)
		syscall.CloseHandle(dirfd)
		if err == nil {
			syscall.CloseHandle(h)
		}
		if err != tt.err {
			t.Errorf("%d: Open got %q, want %q", i, err, tt.err)
		}
	}
}

func TestDeleteAt(t *testing.T) {
	testCases := []struct {
		name     string
		modifier func(t *testing.T, path string)
	}{
		{"DeleteAt removes normal file", func(t *testing.T, name string) {}},
		{"DeleteAt removes file with no read permission", makeFileNotReadable},
		{"DeleteAt removes readonly file", makeFileReadonly},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			dir := t.TempDir()
			file := filepath.Join(dir, "a")
			f, err := os.Create(file)
			if err != nil {
				t.Fatal(err)
			}
			f.Close()

			// Remove all permissions from the file.
			// Do not use os.Chmod it sets only readonly attribute on Windows.
			tc.modifier(t, file)

			// delete file using DeleteAt
			dirfd, err := syscall.Open(dir, syscall.O_RDONLY, 0)
			if err != nil {
				t.Fatal(err)
			}
			base := filepath.Base(file)
			err = windows.Deleteat(dirfd, base, 0)
			syscall.CloseHandle(dirfd)
			if err != nil {
				t.Fatalf("Deleteat failed: %v", err)
			}

			// Verify the file has been deleted.
			if _, err := os.Stat(file); !os.IsNotExist(err) {
				t.Fatalf("file still exists after DeleteAt")
			}
		})
	}
}

func makeFileReadonly(t *testing.T, name string) {
	if err := os.Chmod(name, 0); err != nil {
		t.Fatal(err)
	}
}

func makeFileNotReadable(t *testing.T, name string) {
	creatorOwnerSID, err := syscall.StringToSid("S-1-3-0")
	if err != nil {
		t.Fatal(err)
	}
	creatorGroupSID, err := syscall.StringToSid("S-1-3-1")
	if err != nil {
		t.Fatal(err)
	}
	everyoneSID, err := syscall.StringToSid("S-1-1-0")
	if err != nil {
		t.Fatal(err)
	}

	entryForSid := func(sid *syscall.SID) windows.EXPLICIT_ACCESS {
		return windows.EXPLICIT_ACCESS{
			AccessPermissions: 0,
			AccessMode:        windows.GRANT_ACCESS,
			Inheritance:       windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
			Trustee: windows.TRUSTEE{
				TrusteeForm: windows.TRUSTEE_IS_SID,
				Name:        (uintptr)(unsafe.Pointer(sid)),
			},
		}
	}

	entries := []windows.EXPLICIT_ACCESS{
		entryForSid(creatorOwnerSID),
		entryForSid(creatorGroupSID),
		entryForSid(everyoneSID),
	}

	var oldAcl, newAcl *windows.ACL
	if err := windows.SetEntriesInAcl(
		uint32(len(entries)),
		&entries[0],
		oldAcl,
		&newAcl,
	); err != nil {
		t.Fatal(err)
	}

	defer syscall.LocalFree((syscall.Handle)(unsafe.Pointer(newAcl)))
	if err := windows.SetNamedSecurityInfo(
		name,
		windows.SE_FILE_OBJECT,
		windows.DACL_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION,
		nil,
		nil,
		newAcl,
		nil,
	); err != nil {
		t.Fatal(err)
	}
}
