//go:build windows
// +build windows

package winio

import (
	"os"
	"testing"
	"unsafe"

	"golang.org/x/sys/windows"
)

// Checks if current matches expected. Note that AllocationSize is filesystem-specific,
// so we check that the current.AllocationSize is >= expected.AllocationSize.
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/5afa7f66-619c-48f3-955f-68c4ece704ae
func checkFileStandardInfo(t *testing.T, current, expected *FileStandardInfo) {
	t.Helper()

	if current.AllocationSize < expected.AllocationSize {
		t.Fatalf("FileStandardInfo unexpectedly had AllocationSize %d, expecting >=%d", current.AllocationSize, expected.AllocationSize)
	}

	if current.EndOfFile != expected.EndOfFile {
		t.Fatalf("FileStandardInfo unexpectedly had EndOfFile %d, expecting %d", current.EndOfFile, expected.EndOfFile)
	}

	if current.NumberOfLinks != expected.NumberOfLinks {
		t.Fatalf("FileStandardInfo unexpectedly had NumberOfLinks %d, expecting %d", current.NumberOfLinks, expected.NumberOfLinks)
	}

	if current.DeletePending != expected.DeletePending {
		if current.DeletePending {
			t.Fatalf("FileStandardInfo unexpectedly DeletePending")
		} else {
			t.Fatalf("FileStandardInfo unexpectedly not DeletePending")
		}
	}

	if current.Directory != expected.Directory {
		if current.Directory {
			t.Fatalf("FileStandardInfo unexpectedly Directory")
		} else {
			t.Fatalf("FileStandardInfo unexpectedly not Directory")
		}
	}
}

func TestGetFileStandardInfo_File(t *testing.T) {
	f, err := os.CreateTemp("", "tst")
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()
	defer os.Remove(f.Name())

	expectedFileInfo := &FileStandardInfo{
		AllocationSize: 0,
		EndOfFile:      0,
		NumberOfLinks:  1,
		DeletePending:  false,
		Directory:      false,
	}

	info, err := GetFileStandardInfo(f)
	if err != nil {
		t.Fatal(err)
	}
	checkFileStandardInfo(t, info, expectedFileInfo)

	bytesWritten, err := f.Write([]byte("0123456789"))
	if err != nil {
		t.Fatal(err)
	}

	expectedFileInfo.EndOfFile = int64(bytesWritten)
	expectedFileInfo.AllocationSize = int64(bytesWritten)

	info, err = GetFileStandardInfo(f)
	if err != nil {
		t.Fatal(err)
	}
	checkFileStandardInfo(t, info, expectedFileInfo)

	linkName := f.Name() + ".link"

	if err = os.Link(f.Name(), linkName); err != nil {
		t.Fatal(err)
	}
	defer os.Remove(linkName)

	expectedFileInfo.NumberOfLinks = 2

	info, err = GetFileStandardInfo(f)
	if err != nil {
		t.Fatal(err)
	}
	checkFileStandardInfo(t, info, expectedFileInfo)

	os.Remove(linkName)

	expectedFileInfo.NumberOfLinks = 1

	info, err = GetFileStandardInfo(f)
	if err != nil {
		t.Fatal(err)
	}
	checkFileStandardInfo(t, info, expectedFileInfo)
}

func TestGetFileStandardInfo_Directory(t *testing.T) {
	tempDir := t.TempDir()
	// os.Open returns the Search Handle, not the Directory Handle
	// See https://github.com/golang/go/issues/13738
	f, err := OpenForBackup(tempDir, windows.GENERIC_READ, 0, windows.OPEN_EXISTING)
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()

	expectedFileInfo := &FileStandardInfo{
		AllocationSize: 0,
		EndOfFile:      0,
		NumberOfLinks:  1,
		DeletePending:  false,
		Directory:      true,
	}

	info, err := GetFileStandardInfo(f)
	if err != nil {
		t.Fatal(err)
	}
	checkFileStandardInfo(t, info, expectedFileInfo)
}

// TestFileInfoStructAlignment checks that the alignment of Go fileinfo structs
// match what is expected by the Windows API.
func TestFileInfoStructAlignment(t *testing.T) {
	//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
	const (
		// The alignment of various types, as named in the Windows APIs. When
		// deciding on an expectedAlignment for a struct's test case, use the
		// type of the largest field in the struct as written in the Windows
		// docs. This is intended to help reviewers by allowing them to first
		// check that a new align* const is correct, then independently check
		// that the test case is correct, rather than all at once.
		alignLARGE_INTEGER = unsafe.Alignof(uint64(0))
		alignULONGLONG     = unsafe.Alignof(uint64(0))
	)
	tests := []struct {
		name              string
		actualAlign       uintptr
		actualSize        uintptr
		expectedAlignment uintptr
	}{
		{
			// alignedFileBasicInfo is passed to the Windows API rather than FileBasicInfo.
			"alignedFileBasicInfo", unsafe.Alignof(alignedFileBasicInfo{}), unsafe.Sizeof(alignedFileBasicInfo{}),
			// https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_basic_info
			alignLARGE_INTEGER,
		},
		{
			"FileStandardInfo", unsafe.Alignof(FileStandardInfo{}), unsafe.Sizeof(FileStandardInfo{}),
			// https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_standard_info
			alignLARGE_INTEGER,
		},
		{
			"FileIDInfo", unsafe.Alignof(FileIDInfo{}), unsafe.Sizeof(FileIDInfo{}),
			// https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_id_info
			alignULONGLONG,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if tt.actualAlign != tt.expectedAlignment {
				t.Errorf("alignment mismatch: actual %d, expected %d", tt.actualAlign, tt.expectedAlignment)
			}
			if r := tt.actualSize % tt.expectedAlignment; r != 0 {
				t.Errorf(
					"size is not a multiple of alignment: size %% alignment (%d %% %d) is %d, expected 0",
					tt.actualSize, tt.expectedAlignment, r)
			}
		})
	}
}
