package selinux

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"os/user"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"testing"

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

func TestSetFileLabel(t *testing.T) {
	if !GetEnabled() {
		t.Skip("SELinux not enabled, skipping.")
	}

	const (
		tmpFile = "selinux_test"
		tmpLink = "selinux_test_link"
		con     = "system_u:object_r:bin_t:s0:c1,c2"
		con2    = "system_u:object_r:bin_t:s0:c3,c4"
	)

	_ = os.Remove(tmpFile)
	out, err := os.OpenFile(tmpFile, os.O_WRONLY|os.O_CREATE, 0)
	if err != nil {
		t.Fatal(err)
	}
	out.Close()
	defer os.Remove(tmpFile)

	_ = os.Remove(tmpLink)
	if err := os.Symlink(tmpFile, tmpLink); err != nil {
		t.Fatal(err)
	}
	defer os.Remove(tmpLink)

	if err := SetFileLabel(tmpLink, con); err != nil {
		t.Fatalf("SetFileLabel failed: %s", err)
	}
	filelabel, err := FileLabel(tmpLink)
	if err != nil {
		t.Fatalf("FileLabel failed: %s", err)
	}
	if filelabel != con {
		t.Fatalf("FileLabel failed, returned %s expected %s", filelabel, con)
	}

	// Using LfileLabel to verify that the symlink itself is not labeled.
	linkLabel, err := LfileLabel(tmpLink)
	if err != nil {
		t.Fatalf("LfileLabel failed: %s", err)
	}
	if linkLabel == con {
		t.Fatalf("Label on symlink should not be set, got: %q", linkLabel)
	}

	// Use LsetFileLabel to set a label on the symlink itself.
	if err := LsetFileLabel(tmpLink, con2); err != nil {
		t.Fatalf("LsetFileLabel failed: %s", err)
	}
	filelabel, err = FileLabel(tmpFile)
	if err != nil {
		t.Fatalf("FileLabel failed: %s", err)
	}
	if filelabel != con {
		t.Fatalf("FileLabel was updated, returned %s expected %s", filelabel, con)
	}

	linkLabel, err = LfileLabel(tmpLink)
	if err != nil {
		t.Fatalf("LfileLabel failed: %s", err)
	}
	if linkLabel != con2 {
		t.Fatalf("LfileLabel failed: returned %s expected %s", linkLabel, con2)
	}
}

func TestSetProcessKind(t *testing.T) {
	// These cases do not depend on SELinux being enabled.
	t.Run("empty label", func(t *testing.T) {
		out, err := SetProcessKind("", ProcessKindRegular)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if out != "" {
			t.Errorf("got %q, want empty string", out)
		}
	})

	t.Run("invalid ProcessKind", func(t *testing.T) {
		if _, err := SetProcessKind("system_u:system_r:container_t:s0", ProcessKind(0)); err == nil {
			t.Error("expected error for ProcessKind(0), got nil")
		}
		if _, err := SetProcessKind("system_u:system_r:container_t:s0", ProcessKind(42)); err == nil {
			t.Error("expected error for ProcessKind(42), got nil")
		}
	})

	if !GetEnabled() {
		t.Skip("SELinux not enabled, skipping policy-dependent cases")
	}

	t.Run("invalid input label", func(t *testing.T) {
		if _, err := SetProcessKind("not-a-valid-label", ProcessKindRegular); err == nil {
			t.Error("expected error for malformed label, got nil")
		}
	})

	t.Run("ProcessKindKVM", func(t *testing.T) {
		// Pick a base label we can mutate. Use the process label from policy as a
		// donor for user/role/level, but swap its type with something distinct so we
		// can observe SetProcessKind replacing it.
		base := label("process")
		if base == "" {
			t.Skip("no process label in policy, skipping.")
		}
		baseCtx, err := NewContext(base)
		if err != nil {
			t.Fatalf("NewContext(%q): %v", base, err)
		}
		baseCtx["type"] = "foo_bar_baz_t"
		if baseCtx["level"] == "" {
			baseCtx["level"] = "s0"
		}
		input := baseCtx.Get()
		res, err := SetProcessKind(input, ProcessKindKVM)
		t.Logf("SetProcessKind(%q): %q", input, res)
		if err != nil {
			t.Fatalf("expected no error, got %v", err)
		}
		if _, err := CanonicalizeContext(res); err != nil {
			t.Error(err)
		}
		// Check the type has changed.
		if strings.Contains(res, baseCtx["type"]) {
			t.Errorf("want %q to not contain %q", res, baseCtx["type"])
		}
	})
}

func TestKVMContainerLabel(t *testing.T) {
	if !GetEnabled() {
		t.Skip("SELinux not enabled, skipping.")
	}

	plabel, err := KVMContainerLabel()
	if plabel == "" || err != nil {
		t.Fatalf("KVMContainerLabel: got empty label (error %v)", err)
	}
	t.Log(plabel)
	if _, err := CanonicalizeContext(plabel); err != nil {
		t.Fatal(err)
	}

	ReleaseLabel(plabel)
}

func TestInitContainerLabel(t *testing.T) {
	if !GetEnabled() {
		t.Skip("SELinux not enabled, skipping.")
	}

	plabel, err := InitContainerLabel()
	if plabel == "" || err != nil {
		t.Fatalf("InitContainerLabel: got empty label (error %v)", err)
	}
	t.Log(plabel)
	if _, err := CanonicalizeContext(plabel); err != nil {
		t.Fatal(err)
	}

	ReleaseLabel(plabel)
}

func TestDuplicateLabel(t *testing.T) {
	secopt, err := DupSecOpt("system_u:system_r:container_t:s0:c1,c2")
	if err != nil {
		t.Fatalf("DupSecOpt: %v", err)
	}
	for _, opt := range secopt {
		key, val, _ := strings.Cut(opt, ":")
		switch key {
		case "user":
			if val != "system_u" {
				t.Errorf("DupSecOpt Failed user incorrect")
			}
		case "role":
			if val != "system_r" {
				t.Errorf("DupSecOpt Failed role incorrect")
			}
		case "type":
			if val != "container_t" {
				t.Errorf("DupSecOpt Failed type incorrect")
			}
		case "level":
			if val != "s0:c1,c2" {
				t.Errorf("DupSecOpt Failed level incorrect")
			}
		default:
			t.Errorf("DupSecOpt failed: invalid field %q", key)
		}
	}
	secopt = DisableSecOpt()
	if secopt[0] != "disable" {
		t.Errorf(`DisableSecOpt failed: want "disable", got %q`, secopt[0])
	}
}

func TestSELinuxNoLevel(t *testing.T) {
	if !GetEnabled() {
		t.Skip("SELinux not enabled, skipping.")
	}

	tlabel := "system_u:system_r:container_t"
	dup, err := DupSecOpt(tlabel)
	if err != nil {
		t.Fatal(err)
	}

	if len(dup) != 3 {
		t.Errorf("DupSecOpt failed on non mls label: want 3, got %d", len(dup))
	}
	con, err := NewContext(tlabel)
	if err != nil {
		t.Fatal(err)
	}
	if con.Get() != tlabel {
		t.Errorf("NewContext and con.Get() failed on non mls label: want %q, got %q", tlabel, con.Get())
	}
}

func TestSocketLabel(t *testing.T) {
	if !GetEnabled() {
		t.Skip("SELinux not enabled, skipping.")
	}

	// Ensure the thread stays the same for duration of the test.
	// Otherwise Go runtime can switch this to a different thread,
	// which results in EACCES in call to SetSocketLabel.
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	label := "system_u:object_r:container_t:s0:c1,c2"
	if err := SetSocketLabel(label); err != nil {
		t.Fatal(err)
	}
	nlabel, err := SocketLabel()
	if err != nil {
		t.Fatal(err)
	}
	if label != nlabel {
		t.Errorf("SocketLabel %s != %s", nlabel, label)
	}
}

func TestExecLabel(t *testing.T) {
	if !GetEnabled() {
		t.Skip("SELinux not enabled, skipping.")
	}

	// Ensure the thread stays the same for duration of the test.
	// Otherwise Go runtime can switch this to a different thread,
	// which results in EACCES in call to SetExecLabel.
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	const label = "system_u:object_r:container_t:s0:c1,c2"
	if err := SetExecLabel(label); err != nil {
		t.Fatal(err)
	}
	defer func() {
		if err := SetExecLabel(""); err != nil {
			t.Fatal(err)
		}
	}()
	nlabel, err := ExecLabel()
	if err != nil {
		t.Fatal(err)
	}
	if label != nlabel {
		t.Errorf("ExecLabel: want %q, got %q", label, nlabel)
	}
}

func TestKeyLabel(t *testing.T) {
	if !GetEnabled() {
		t.Skip("SELinux not enabled, skipping.")
	}

	// Ensure the thread stays the same for duration of the test.
	// Otherwise Go runtime can switch this to a different thread,
	// which results in EACCES in call to SetKeyLabel.
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	if unix.Getpid() != unix.Gettid() {
		t.Skip(ErrNotTGLeader)
	}

	label := "system_u:object_r:container_t:s0:c1,c2"
	if err := SetKeyLabel(label); err != nil {
		t.Fatal(err)
	}
	nlabel, err := KeyLabel()
	if err != nil {
		t.Fatal(err)
	}
	if label != nlabel {
		t.Errorf("KeyLabel: want %q, got %q", label, nlabel)
	}
}

func BenchmarkContextGet(b *testing.B) {
	ctx, err := NewContext("system_u:object_r:container_file_t:s0:c1022,c1023")
	if err != nil {
		b.Fatal(err)
	}
	str := ""
	for i := 0; i < b.N; i++ {
		str = ctx.get()
	}
	b.Log(str)
}

func TestSELinux(t *testing.T) {
	if !GetEnabled() {
		t.Skip("SELinux not enabled, skipping.")
	}

	// Ensure the thread stays the same for duration of the test.
	// Otherwise Go runtime can switch this to a different thread,
	// which results in EACCES in call to SetFSCreateLabel.
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	var (
		err            error
		plabel, flabel string
	)

	plabel, flabel = ContainerLabels()
	t.Log(plabel)
	t.Log(flabel)
	plabel, flabel = ContainerLabels()
	t.Log(plabel)
	t.Log(flabel)
	ReleaseLabel(plabel)

	plabel, flabel = ContainerLabels()
	t.Log(plabel)
	t.Log(flabel)
	ClearLabels()
	t.Log("ClearLabels")
	plabel, flabel = ContainerLabels()
	t.Log(plabel)
	t.Log(flabel)
	ReleaseLabel(plabel)

	err = SetFSCreateLabel("unconfined_u:unconfined_r:unconfined_t:s0")
	if err != nil {
		t.Fatal("SetFSCreateLabel failed:", err)
	}
	t.Log(FSCreateLabel())
	err = SetFSCreateLabel("")
	if err != nil {
		t.Fatal("SetFSCreateLabel failed:", err)
	}
	t.Log(FSCreateLabel())
	t.Log(PidLabel(1))
}

func TestSetEnforceMode(t *testing.T) {
	if !GetEnabled() {
		t.Skip("SELinux not enabled, skipping.")
	}
	if os.Geteuid() != 0 {
		t.Skip("root required, skipping")
	}

	t.Log("Enforcing Mode:", EnforceMode())
	mode := DefaultEnforceMode()
	t.Log("Default Enforce Mode:", mode)
	defer func() {
		_ = SetEnforceMode(mode)
	}()

	if err := SetEnforceMode(Enforcing); err != nil {
		t.Fatalf("setting selinux mode to enforcing failed: %v", err)
	}
	if err := SetEnforceMode(Permissive); err != nil {
		t.Fatalf("setting selinux mode to permissive failed: %v", err)
	}
}

func TestCanonicalizeContext(t *testing.T) {
	if !GetEnabled() {
		t.Skip("SELinux not enabled, skipping.")
	}

	con := "system_u:object_r:bin_t:s0:c1,c2,c3"
	checkcon := "system_u:object_r:bin_t:s0:c1.c3"
	newcon, err := CanonicalizeContext(con)
	if err != nil {
		t.Fatal(err)
	}
	if newcon != checkcon {
		t.Fatalf("CanonicalizeContext(%s) returned %s expected %s", con, newcon, checkcon)
	}
	con = "system_u:object_r:bin_t:s0:c5,c2"
	checkcon = "system_u:object_r:bin_t:s0:c2,c5"
	newcon, err = CanonicalizeContext(con)
	if err != nil {
		t.Fatal(err)
	}
	if newcon != checkcon {
		t.Fatalf("CanonicalizeContext(%s) returned %s expected %s", con, newcon, checkcon)
	}
}

func TestFindSELinuxfsInMountinfo(t *testing.T) {
	//nolint:dupword // ignore duplicate words (sysfs sysfs)
	const mountinfo = `18 62 0:17 / /sys rw,nosuid,nodev,noexec,relatime shared:6 - sysfs sysfs rw,seclabel
19 62 0:3 / /proc rw,nosuid,nodev,noexec,relatime shared:5 - proc proc rw
20 62 0:5 / /dev rw,nosuid shared:2 - devtmpfs devtmpfs rw,seclabel,size=3995472k,nr_inodes=998868,mode=755
21 18 0:16 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:7 - securityfs securityfs rw
22 20 0:18 / /dev/shm rw,nosuid,nodev shared:3 - tmpfs tmpfs rw,seclabel
23 20 0:11 / /dev/pts rw,nosuid,noexec,relatime shared:4 - devpts devpts rw,seclabel,gid=5,mode=620,ptmxmode=000
24 62 0:19 / /run rw,nosuid,nodev shared:23 - tmpfs tmpfs rw,seclabel,mode=755
25 18 0:20 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:8 - tmpfs tmpfs ro,seclabel,mode=755
26 25 0:21 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:9 - cgroup cgroup rw,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd
27 18 0:22 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime shared:20 - pstore pstore rw
28 25 0:23 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:10 - cgroup cgroup rw,perf_event
29 25 0:24 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:11 - cgroup cgroup rw,devices
30 25 0:25 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:12 - cgroup cgroup rw,cpuacct,cpu
31 25 0:26 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:13 - cgroup cgroup rw,freezer
32 25 0:27 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:14 - cgroup cgroup rw,net_prio,net_cls
33 25 0:28 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:15 - cgroup cgroup rw,cpuset
34 25 0:29 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:16 - cgroup cgroup rw,memory
35 25 0:30 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:17 - cgroup cgroup rw,pids
36 25 0:31 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:18 - cgroup cgroup rw,hugetlb
37 25 0:32 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:19 - cgroup cgroup rw,blkio
59 18 0:33 / /sys/kernel/config rw,relatime shared:21 - configfs configfs rw
62 1 253:1 / / rw,relatime shared:1 - ext4 /dev/vda1 rw,seclabel,data=ordered
38 18 0:15 / /sys/fs/selinux rw,relatime shared:22 - selinuxfs selinuxfs rw
39 19 0:35 / /proc/sys/fs/binfmt_misc rw,relatime shared:24 - autofs systemd-1 rw,fd=29,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=11601
40 20 0:36 / /dev/hugepages rw,relatime shared:25 - hugetlbfs hugetlbfs rw,seclabel
41 20 0:14 / /dev/mqueue rw,relatime shared:26 - mqueue mqueue rw,seclabel
42 18 0:6 / /sys/kernel/debug rw,relatime shared:27 - debugfs debugfs rw
112 62 253:1 /var/lib/docker/plugins /var/lib/docker/plugins rw,relatime - ext4 /dev/vda1 rw,seclabel,data=ordered
115 62 253:1 /var/lib/docker/overlay2 /var/lib/docker/overlay2 rw,relatime - ext4 /dev/vda1 rw,seclabel,data=ordered
118 62 7:0 / /root/mnt rw,relatime shared:66 - ext4 /dev/loop0 rw,seclabel,data=ordered
121 115 0:38 / /var/lib/docker/overlay2/8cdbabf81bc89b14ea54eaf418c1922068f06917fff57e184aa26541ff291073/merged rw,relatime - overlay overlay rw,seclabel,lowerdir=/var/lib/docker/overlay2/l/CPD4XI7UD4GGTGSJVPQSHWZKTK:/var/lib/docker/overlay2/l/NQKORR3IS7KNQDER35AZECLH4Z,upperdir=/var/lib/docker/overlay2/8cdbabf81bc89b14ea54eaf418c1922068f06917fff57e184aa26541ff291073/diff,workdir=/var/lib/docker/overlay2/8cdbabf81bc89b14ea54eaf418c1922068f06917fff57e184aa26541ff291073/work
125 62 0:39 / /var/lib/docker/containers/5e3fce422957c291a5b502c2cf33d512fc1fcac424e4113136c808360e5b7215/shm rw,nosuid,nodev,noexec,relatime shared:68 - tmpfs shm rw,seclabel,size=65536k
186 24 0:3 / /run/docker/netns/0a08e7496c6d rw,nosuid,nodev,noexec,relatime shared:5 - proc proc rw
130 62 0:15 / /root/chroot/selinux rw,relatime shared:22 - selinuxfs selinuxfs rw
109 24 0:37 / /run/user/0 rw,nosuid,nodev,relatime shared:62 - tmpfs tmpfs rw,seclabel,size=801032k,mode=700
`
	s := bufio.NewScanner(bytes.NewBuffer([]byte(mountinfo)))
	for _, expected := range []string{"/sys/fs/selinux", "/root/chroot/selinux", ""} {
		mnt := findSELinuxfsMount(s)
		t.Logf("found %q", mnt)
		if mnt != expected {
			t.Fatalf("expected %q, got %q", expected, mnt)
		}
	}
}

func TestSecurityCheckContext(t *testing.T) {
	if !GetEnabled() {
		t.Skip("SELinux not enabled, skipping.")
	}

	// check with valid context
	context, err := CurrentLabel()
	if err != nil {
		t.Fatalf("CurrentLabel() error: %v", err)
	}
	if context != "" {
		t.Logf("SecurityCheckContext(%q)", context)
		err = SecurityCheckContext(context)
		if err != nil {
			t.Errorf("SecurityCheckContext(%q) error: %v", context, err)
		}
	}

	context = "not-syntactically-valid"
	err = SecurityCheckContext(context)
	if err == nil {
		t.Errorf("SecurityCheckContext(%q) succeeded, expected to fail", context)
	}
}

func TestClassIndex(t *testing.T) {
	if !GetEnabled() {
		t.Skip("SELinux not enabled, skipping.")
	}

	idx, err := ClassIndex("process")
	if err != nil {
		t.Errorf("Classindex error: %v", err)
	}
	// Every known policy has process as index 2, but it isn't guaranteed
	if idx != 2 {
		t.Errorf("ClassIndex unexpected answer %d, possibly not reference policy", idx)
	}

	_, err = ClassIndex("foobar")
	if err == nil {
		t.Errorf("ClassIndex(\"foobar\") succeeded, expected to fail:")
	}
}

func TestComputeCreateContext(t *testing.T) {
	if !GetEnabled() {
		t.Skip("SELinux not enabled, skipping.")
	}

	// This may or may not be in the loaded policy but any refpolicy based policy should have it
	init := "system_u:system_r:init_t:s0"
	tmp := "system_u:object_r:tmp_t:s0"
	file := "file"
	t.Logf("ComputeCreateContext(%s, %s, %s)", init, tmp, file)
	context, err := ComputeCreateContext(init, tmp, file)
	if err != nil {
		t.Errorf("ComputeCreateContext error: %v", err)
	}
	if context != "system_u:object_r:init_tmp_t:s0" {
		t.Errorf("ComputeCreateContext unexpected answer %s, possibly not reference policy", context)
	}

	badcon := "badcon"
	process := "process"
	// Test to ensure that a bad context returns an error
	t.Logf("ComputeCreateContext(%s, %s, %s)", badcon, tmp, process)
	_, err = ComputeCreateContext(badcon, tmp, process)
	if err == nil {
		t.Errorf("ComputeCreateContext(%s, %s, %s) succeeded, expected failure", badcon, tmp, process)
	}
}

func TestGlbLub(t *testing.T) {
	tests := []struct {
		expectedErr   error
		sourceRange   string
		targetRange   string
		expectedRange string
	}{
		{
			sourceRange:   "s0:c0.c100-s10:c0.c150",
			targetRange:   "s5:c50.c100-s15:c0.c149",
			expectedRange: "s5:c50.c100-s10:c0.c149",
		},
		{
			sourceRange:   "s5:c50.c100-s15:c0.c149",
			targetRange:   "s0:c0.c100-s10:c0.c150",
			expectedRange: "s5:c50.c100-s10:c0.c149",
		},
		{
			sourceRange:   "s0:c0.c100-s10:c0.c150",
			targetRange:   "s0",
			expectedRange: "s0",
		},
		{
			sourceRange:   "s6:c0.c1023",
			targetRange:   "s6:c0,c2,c11,c201.c429,c431.c511",
			expectedRange: "s6:c0,c2,c11,c201.c429,c431.c511",
		},
		{
			sourceRange:   "s0-s15:c0.c1023",
			targetRange:   "s6:c0,c2,c11,c201.c429,c431.c511",
			expectedRange: "s6-s6:c0,c2,c11,c201.c429,c431.c511",
		},
		{
			sourceRange:   "s0:c0.c100,c125,c140,c150-s10",
			targetRange:   "s4:c0.c50,c140",
			expectedRange: "s4:c0.c50,c140-s4",
		},
		{
			sourceRange:   "s5:c512.c550,c552.c1023-s5:c0.c550,c552.c1023",
			targetRange:   "s5:c512.c550,c553.c1023-s5:c0,c1,c4,c5,c6,c512.c550,c553.c1023",
			expectedRange: "s5:c512.c550,c553.c1023-s5:c0,c1,c4.c6,c512.c550,c553.c1023",
		},
		{
			sourceRange:   "s5:c512.c540,c542,c543,c552.c1023-s5:c0.c550,c552.c1023",
			targetRange:   "s5:c512.c550,c553.c1023-s5:c0,c1,c4,c5,c6,c512.c550,c553.c1023",
			expectedRange: "s5:c512.c540,c542,c543,c553.c1023-s5:c0,c1,c4.c6,c512.c550,c553.c1023",
		},
		{
			sourceRange:   "s5:c50.c100-s15:c0.c149",
			targetRange:   "s5:c512.c550,c552.c1023-s5:c0.c550,c552.c1023",
			expectedRange: "s5-s5:c0.c149",
		},
		{
			sourceRange:   "s5-s15",
			targetRange:   "s6-s7",
			expectedRange: "s6-s7",
		},
		{
			sourceRange: "s5:c50.c100-s15:c0.c149",
			targetRange: "s4-s4:c0.c1023",
			expectedErr: ErrIncomparable,
		},
		{
			sourceRange: "s4-s4:c0.c1023",
			targetRange: "s5:c50.c100-s15:c0.c149",
			expectedErr: ErrIncomparable,
		},
		{
			sourceRange: "s4-s4:c0.c1023.c10000",
			targetRange: "s5:c50.c100-s15:c0.c149",
			expectedErr: strconv.ErrSyntax,
		},
		{
			sourceRange: "s4-s4:c0.c1023.c10000-s4",
			targetRange: "s5:c50.c100-s15:c0.c149-s5",
			expectedErr: strconv.ErrSyntax,
		},
		{
			sourceRange: "4-4",
			targetRange: "s5:c50.c100-s15:c0.c149",
			expectedErr: ErrLevelSyntax,
		},
		{
			sourceRange: "t4-t4",
			targetRange: "s5:c50.c100-s15:c0.c149",
			expectedErr: ErrLevelSyntax,
		},
		{
			sourceRange: "s5:x50.x100-s15:c0.c149",
			targetRange: "s5:c50.c100-s15:c0.c149",
			expectedErr: ErrLevelSyntax,
		},
	}

	for _, tt := range tests {
		got, err := CalculateGlbLub(tt.sourceRange, tt.targetRange)
		if !errors.Is(err, tt.expectedErr) {
			t.Fatalf("want %q got %q: src: %q tgt: %q", tt.expectedErr, err, tt.sourceRange, tt.targetRange)
		}

		if got != tt.expectedRange {
			t.Errorf("want %q got %q", tt.expectedRange, got)
		}
	}
}

func TestGetSeUser(t *testing.T) {
	lookupGroup := func(string) (*user.Group, error) {
		return &user.Group{
			Gid:  "42",
			Name: "group",
		}, nil
	}

	//nolint:govet // ignore field alignment
	tests := []struct {
		name        string
		username    string
		gids        []string
		seUserBuf   string
		seUser      string
		level       string
		expectedErr string
	}{
		{
			name:      "one entry match",
			username:  "bob",
			seUserBuf: "bob:staff_u:s0",
			seUser:    "staff_u",
			level:     "s0",
		},
		{
			name:      "match with no level",
			username:  "bob",
			seUserBuf: "bob:staff_u",
			seUser:    "staff_u",
		},
		{
			name:     "match",
			username: "bob",
			seUserBuf: `
system_u:system_u:s0-s15:c0.c255
root:root:s0-s15:c0.c255
bob:staff_u:s0-s15:c0.c255
`,
			seUser: "staff_u",
			level:  "s0-s15:c0.c255",
		},
		{
			name:     "match with comments",
			username: "bob",
			seUserBuf: `# bazboom
system_u:system_u:s0-s15:c0.c255
# foobar
root:root:s0-s15:c0.c255
bob:staff_u:s0-s15:c0.c255 #baz`,
			seUser: "staff_u",
			level:  "s0-s15:c0.c255",
		},
		{
			name:     "no match",
			username: "bob",
			seUserBuf: `
system_u:system_u:s0-s15:c0.c255
root:root:s0-s15:c0.c255
`,
			expectedErr: `could not find SELinux user for "bob" login`,
		},
		{
			name:     "group match",
			username: "bob",
			gids:     []string{"42"},
			seUserBuf: `
system_u:system_u:s0-s15:c0.c255
root:root:s0-s15:c0.c255
%group:staff_u:s0`,
			seUser: "staff_u",
			level:  "s0",
		},
		{
			name:     "no group match",
			username: "bob",
			gids:     []string{"99"},
			seUserBuf: `
system_u:system_u:s0-s15:c0.c255
root:root:s0-s15:c0.c255
%group:staff_u:s0`,
			expectedErr: `could not find SELinux user for "bob" login`,
		},
		{
			name:     "malformed line",
			username: "bob",
			seUserBuf: `
system_u:system_u:s0-s15:c0.c255
root:root:s0-s15:c0.c255
foobar
bob:staff_u:s0-s15:c0.c255`,
			expectedErr: "line 3: malformed line",
		},
		{
			name:     "empty user",
			username: "bob",
			seUserBuf: `
system_u:system_u:s0-s15:c0.c255
root:root:s0-s15:c0.c255
:seuser_u
bob:staff_u:s0-s15:c0.c255`,
			expectedErr: "line 3: user_id or group_id is empty",
		},
		{
			name:     "empty seuser",
			username: "bob",
			seUserBuf: `
system_u:system_u:s0-s15:c0.c255
root:root:s0-s15:c0.c255
user::s0
bob:staff_u:s0-s15:c0.c255`,
			expectedErr: "line 3: seuser_id is empty",
		},
		{
			name:      "one entry match with whitespace",
			username:  "bob",
			seUserBuf: "  bob:staff_u:s0 ",
			seUser:    "staff_u",
			level:     "s0",
		},
		{
			name:      "one entry match with trailing comment",
			username:  "bob",
			seUserBuf: "bob:staff_u:s0#comment",
			seUser:    "staff_u",
			level:     "s0",
		},
		{
			name:      "one entry match with whitespace and trailing comment",
			username:  "bob",
			seUserBuf: " bob:staff_u:s0  #comment   ",
			seUser:    "staff_u",
			level:     "s0",
		},
		{
			name:     "one entry match with leading comment",
			username: "bob",
			seUserBuf: `# this is a comment
bob:staff_u:s0
`,
			seUser: "staff_u",
			level:  "s0",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			r := bytes.NewBufferString(tt.seUserBuf)
			seUser, level, err := getSeUserFromReader(tt.username, tt.gids, r, lookupGroup)
			if tt.expectedErr != "" {
				if err == nil {
					t.Fatal("expected an error but got nil")
				} else if err.Error() != tt.expectedErr {
					t.Fatalf("got error: %q but expected %q", err.Error(), tt.expectedErr)
				}
			} else if tt.expectedErr == "" && err != nil {
				t.Fatalf("err should not exist but is: %v", err)
			}

			if seUser != tt.seUser {
				t.Fatalf("got seUser: %q but expected %q", seUser, tt.seUser)
			}
			if level != tt.level {
				t.Fatalf("got level: %q but expected %q", level, tt.level)
			}
		})
	}
}

func TestContextWithLevel(t *testing.T) {
	want := "bob:sysadm_r:sysadm_t:SystemLow-SystemHigh"

	goodDefaultBuff := `
foo_r:foo_t:s0     sysadm_r:sysadm_t:s0
staff_r:staff_t:s0                 baz_r:baz_t:s0   sysadm_r:sysadm_t:s0
`
	goodFailsafeBuff := "unconfined_r:unconfined_t:s0"

	verifier := func(con string) error {
		if con != want {
			return fmt.Errorf("invalid context %s", con)
		}

		return nil
	}

	tests := []struct {
		name, userBuff, defaultBuff, failsafeBuff string
	}{
		{
			name: "match exists in user context file",
			userBuff: `# COMMENT
foo_r:foo_t:s0     sysadm_r:sysadm_t:s0

staff_r:staff_t:s0                 baz_r:baz_t:s0   sysadm_r:sysadm_t:s0
`,
			defaultBuff:  goodDefaultBuff,
			failsafeBuff: goodFailsafeBuff,
		},
		{
			name: "match exists in default context file, but not in user file",
			userBuff: `# COMMENT
foo_r:foo_t:s0     sysadm_r:sysadm_t:s0
fake_r:fake_t:s0                 baz_r:baz_t:s0   sysadm_r:sysadm_t:s0
`,
			defaultBuff:  goodDefaultBuff,
			failsafeBuff: goodFailsafeBuff,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c := defaultSECtx{
				user:  "bob",
				level: "SystemLow-SystemHigh",
				scon:  "system_u:staff_r:staff_t:s0",
				openUserRdr: func() (io.ReadCloser, error) {
					return io.NopCloser(bytes.NewBufferString(tt.userBuff)), nil
				},
				openDefaultRdr: func() (io.ReadCloser, error) {
					return io.NopCloser(bytes.NewBufferString(tt.defaultBuff)), nil
				},
				verifier: verifier,
			}

			got, err := getDefaultContextFromReaders(&c)
			if err != nil {
				t.Fatalf("err should not exist but is: %v", err)
			}

			if got != want {
				t.Fatalf("got context: %q but expected %q", got, want)
			}
		})
	}

	t.Run("no match in user or default context files", func(t *testing.T) {
		badUserBuff := ""

		badDefaultBuff := `
		foo_r:foo_t:s0     sysadm_r:sysadm_t:s0
		dne_r:dne_t:s0                 baz_r:baz_t:s0   sysadm_r:sysadm_t:s0
		`
		c := defaultSECtx{
			user:  "bob",
			level: "SystemLow-SystemHigh",
			scon:  "system_u:staff_r:staff_t:s0",
			openUserRdr: func() (io.ReadCloser, error) {
				return io.NopCloser(bytes.NewBufferString(badUserBuff)), nil
			},
			openDefaultRdr: func() (io.ReadCloser, error) {
				return io.NopCloser(bytes.NewBufferString(badDefaultBuff)), nil
			},
			openFailsafeRdr: func() (io.ReadCloser, error) {
				return io.NopCloser(bytes.NewBufferString(goodFailsafeBuff)), nil
			},
			verifier: func(_ string) error {
				return nil
			},
		}

		got, err := getDefaultContextFromReaders(&c)
		if err != nil {
			t.Fatalf("err should not exist but is: %v", err)
		}

		const want string = "bob:unconfined_r:unconfined_t:SystemLow-SystemHigh"
		if got != want {
			t.Fatalf("got context: %q but expected %q", got, want)
		}
	})
}

func TestReserveLabelNoMCS(t *testing.T) {
	const label = "system_u:system_r:container_runtime_t:s0"

	_ = ReserveLabelV2(label)
	if err := ReserveLabelV2(label); err != nil {
		t.Errorf("repeated ReserveLabelV2(%q): want nil, got %v", label, err)
	}
}

func BenchmarkChcon(b *testing.B) {
	file, err := filepath.Abs(os.Args[0])
	if err != nil {
		b.Fatalf("filepath.Abs: %v", err)
	}
	dir := filepath.Dir(file)
	con, err := FileLabel(file)
	if err != nil {
		b.Fatalf("FileLabel(%q): %v", file, err)
	}
	b.Logf("Chcon(%q, %q)", dir, con)
	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		if err := Chcon(dir, con, true); err != nil {
			b.Fatal(err)
		}
	}
}

func BenchmarkCurrentLabel(b *testing.B) {
	var (
		l   string
		err error
	)
	for n := 0; n < b.N; n++ {
		l, err = CurrentLabel()
		if err != nil {
			b.Fatal(err)
		}
	}
	b.Log(l)
}

func BenchmarkReadConfig(b *testing.B) {
	str := ""
	for n := 0; n < b.N; n++ {
		str = readConfig(selinuxTypeTag)
	}
	b.Log(str)
}

func BenchmarkLoadLabels(b *testing.B) {
	for n := 0; n < b.N; n++ {
		loadLabels()
	}
}
