//go:build windows
// +build windows

package security

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"testing"
)

const (
	vmAccountName = `NT VIRTUAL MACHINE\\Virtual Machines`
	vmAccountSID  = "S-1-5-83-0"
)

// TestGrantVmGroupAccess verifies for the three case of a file, a directory,
// and a file in a directory that the appropriate ACEs are set, including
// inheritance in the second two examples. These are the expected ACES. Is
// verified by running icacls and comparing output.
//
// File:
// S-1-15-3-1024-2268835264-3721307629-241982045-173645152-1490879176-104643441-2915960892-1612460704:(R,W)
// S-1-5-83-1-3166535780-1122986932-343720105-43916321:(R,W)
//
// Directory:
// S-1-15-3-1024-2268835264-3721307629-241982045-173645152-1490879176-104643441-2915960892-1612460704:(OI)(CI)(R,W)
// S-1-5-83-1-3166535780-1122986932-343720105-43916321:(OI)(CI)(R,W)
//
// File in directory (inherited):
// S-1-15-3-1024-2268835264-3721307629-241982045-173645152-1490879176-104643441-2915960892-1612460704:(I)(R,W)
// S-1-5-83-1-3166535780-1122986932-343720105-43916321:(I)(R,W)

func TestGrantVmGroupAccessDefault(t *testing.T) {
	f1Path := filepath.Join(t.TempDir(), "gvmgafile")
	f, err := os.Create(f1Path)
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		_ = f.Close()
		_ = os.Remove(f1Path)
	}()

	dir2 := t.TempDir()
	f2Path := filepath.Join(dir2, "find.txt")
	find, err := os.Create(f2Path)
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		_ = find.Close()
		_ = os.Remove(f2Path)
	}()

	if err := GrantVmGroupAccess(f1Path); err != nil {
		t.Fatal(err)
	}

	if err := GrantVmGroupAccess(dir2); err != nil {
		t.Fatal(err)
	}

	verifyVMAccountDACLs(t,
		f1Path,
		[]string{`(R)`},
	)

	// Two items here:
	//  - One explicit read only.
	//  - Other applies to this folder, subfolders and files
	//      (OI): object inherit
	//      (CI): container inherit
	//      (IO): inherit only
	//      (GR): generic read
	//
	// In properties for the directory, advanced security settings, this will
	// show as a single line "Allow/Virtual Machines/Read/Inherited from none/This folder, subfolder and files
	verifyVMAccountDACLs(t,
		dir2,
		[]string{`(R)`, `(OI)(CI)(IO)(GR)`},
	)

	verifyVMAccountDACLs(t,
		f2Path,
		[]string{`(I)(R)`},
	)
}

func TestGrantVMGroupAccess_File_DesiredPermissions(t *testing.T) {
	type config struct {
		name                string
		desiredAccess       accessMask
		expectedPermissions []string
	}

	for _, cfg := range []config{
		{
			name:                "Read",
			desiredAccess:       AccessMaskRead,
			expectedPermissions: []string{`(R)`},
		},
		{
			name:                "Write",
			desiredAccess:       AccessMaskWrite,
			expectedPermissions: []string{`(W,Rc)`},
		},
		{
			name:                "Execute",
			desiredAccess:       AccessMaskExecute,
			expectedPermissions: []string{`(Rc,S,X,RA)`},
		},
		{
			name:                "ReadWrite",
			desiredAccess:       AccessMaskRead | AccessMaskWrite,
			expectedPermissions: []string{`(R,W)`},
		},
		{
			name:                "ReadExecute",
			desiredAccess:       AccessMaskRead | AccessMaskExecute,
			expectedPermissions: []string{`(RX)`},
		},
		{
			name:                "WriteExecute",
			desiredAccess:       AccessMaskWrite | AccessMaskExecute,
			expectedPermissions: []string{`(W,Rc,X,RA)`},
		},
		{
			name:                "ReadWriteExecute",
			desiredAccess:       AccessMaskRead | AccessMaskWrite | AccessMaskExecute,
			expectedPermissions: []string{`(RX,W)`},
		},
		{
			name:                "All",
			desiredAccess:       AccessMaskAll,
			expectedPermissions: []string{`(F)`},
		},
	} {
		t.Run(cfg.name, func(t *testing.T) {
			dir := t.TempDir()
			fd, err := os.Create(filepath.Join(dir, "test.txt"))
			if err != nil {
				t.Fatalf("failed to create temporary file: %s", err)
			}
			defer func() {
				_ = fd.Close()
				_ = os.Remove(fd.Name())
			}()

			if err := GrantVmGroupAccessWithMask(fd.Name(), cfg.desiredAccess); err != nil {
				t.Fatal(err)
			}
			verifyVMAccountDACLs(t, fd.Name(), cfg.expectedPermissions)
		})
	}
}

func TestGrantVMGroupAccess_Directory_Permissions(t *testing.T) {
	type config struct {
		name            string
		access          accessMask
		filePermissions []string
		dirPermissions  []string
	}

	for _, cfg := range []config{
		{
			name:            "Read",
			access:          AccessMaskRead,
			filePermissions: []string{`(I)(R)`},
			dirPermissions:  []string{`(R)`, `(OI)(CI)(IO)(GR)`},
		},
		{
			name:            "Write",
			access:          AccessMaskWrite,
			filePermissions: []string{`(I)(W,Rc)`},
			dirPermissions:  []string{`(W,Rc)`, `(OI)(CI)(IO)(GW)`},
		},
		{
			name:            "Execute",
			access:          AccessMaskExecute,
			filePermissions: []string{`(I)(Rc,S,X,RA)`},
			dirPermissions:  []string{`(Rc,S,X,RA)`, `(OI)(CI)(IO)(GE)`},
		},
		{
			name:            "ReadWrite",
			access:          AccessMaskRead | AccessMaskWrite,
			filePermissions: []string{`(I)(R,W)`},
			dirPermissions:  []string{`(R,W)`, `(OI)(CI)(IO)(GR,GW)`},
		},
		{
			name:            "ReadExecute",
			access:          AccessMaskRead | AccessMaskExecute,
			filePermissions: []string{`(I)(RX)`},
			dirPermissions:  []string{`(RX)`, `(OI)(CI)(IO)(GR,GE)`},
		},
		{
			name:            "WriteExecute",
			access:          AccessMaskWrite | AccessMaskExecute,
			filePermissions: []string{`(I)(W,Rc,X,RA)`},
			dirPermissions:  []string{`(W,Rc,X,RA)`, `(OI)(CI)(IO)(GW,GE)`},
		},
		{
			name:            "ReadWriteExecute",
			access:          AccessMaskRead | AccessMaskWrite | AccessMaskExecute,
			filePermissions: []string{`(I)(RX,W)`},
			dirPermissions:  []string{`(RX,W)`, `(OI)(CI)(IO)(GR,GW,GE)`},
		},
		{
			name:            "All",
			access:          AccessMaskAll,
			filePermissions: []string{`(I)(F)`},
			dirPermissions:  []string{`(F)`, `(OI)(CI)(IO)(F)`},
		}} {
		t.Run(cfg.name, func(t *testing.T) {
			dir := t.TempDir()
			fd, err := os.Create(filepath.Join(dir, "test.txt"))
			if err != nil {
				t.Fatalf("failed to create temporary file: %s", err)
			}
			defer func() {
				_ = fd.Close()
				_ = os.Remove(fd.Name())
			}()

			if err := GrantVmGroupAccessWithMask(dir, cfg.access); err != nil {
				t.Fatal(err)
			}
			verifyVMAccountDACLs(t, dir, cfg.dirPermissions)
			verifyVMAccountDACLs(t, fd.Name(), cfg.filePermissions)
		})
	}
}

func TestGrantVmGroupAccess_Invalid_AccessMask(t *testing.T) {
	t.Helper()
	for _, access := range []accessMask{
		0,          // no bits set
		1,          // invalid bit set
		0x02000001, // invalid extra bit set
	} {
		t.Run(fmt.Sprintf("AccessMask_0x%x", access), func(t *testing.T) {
			dir := t.TempDir()
			fd, err := os.Create(filepath.Join(dir, "test.txt"))
			if err != nil {
				t.Fatalf("failed to create temporary file: %s", err)
			}
			defer func() {
				_ = fd.Close()
				_ = os.Remove(fd.Name())
			}()

			if err := GrantVmGroupAccessWithMask(fd.Name(), access); err == nil {
				t.Fatalf("expected an error for mask: %x", access)
			}
		})
	}
}

func verifyVMAccountDACLs(t *testing.T, name string, permissions []string) {
	t.Helper()
	cmd := exec.Command("icacls", name)
	outb, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatal(err)
	}
	out := string(outb)

	for _, p := range permissions {
		// Avoid '(' and ')' being part of match groups
		p = regexp.QuoteMeta(p)

		nameToCheck := vmAccountName + ":" + p
		sidToCheck := vmAccountSID + ":" + p

		rxName := regexp.MustCompile(nameToCheck)
		rxSID := regexp.MustCompile(sidToCheck)

		matchesName := rxName.FindAllStringIndex(out, -1)
		matchesSID := rxSID.FindAllStringIndex(out, -1)

		if len(matchesName) != 1 && len(matchesSID) != 1 {
			t.Fatalf("expected one match for %s or %s\n%s\n", nameToCheck, sidToCheck, out)
		}
	}
}
