// Copyright 2015, 2018 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package dbus

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"reflect"
	"syscall"
	"testing"
	"time"

	"github.com/godbus/dbus/v5"
)

type TrUnitProp struct {
	name  string
	props []Property
}

func setupConn(t *testing.T) *Conn {
	conn, err := New()
	if err != nil {
		t.Fatal(err)
	}

	return conn
}

func findFixture(target string, t *testing.T) string {
	abs, err := filepath.Abs("../fixtures/" + target)
	if err != nil {
		t.Fatal(err)
	}
	return abs
}

func setupUnit(target string, conn *Conn, t *testing.T) {
	t.Helper()
	// Blindly stop the unit in case it is running
	_, _ = conn.StopUnit(target, "replace", nil)

	// Blindly remove the symlink in case it exists
	targetRun := filepath.Join("/run/systemd/system/", target)
	if err := os.Remove(targetRun); err != nil && !errors.Is(err, os.ErrNotExist) {
		t.Fatal(err)
	}
}

func linkUnit(target string, conn *Conn, t *testing.T) {
	abs := findFixture(target, t)
	fixture := []string{abs}

	changes, err := conn.LinkUnitFiles(fixture, true, true)
	if err != nil {
		t.Fatal(err)
	}

	if len(changes) < 1 {
		t.Fatalf("Expected one change, got %v", changes)
	}

	runPath := filepath.Join("/run/systemd/system/", target)
	if changes[0].Filename != runPath {
		t.Fatal("Unexpected target filename")
	}
}

func getUnitStatus(units []UnitStatus, name string) *UnitStatus {
	for _, u := range units {
		if u.Name == name {
			return &u
		}
	}
	return nil
}

func getUnitStatusSingle(conn *Conn, name string) *UnitStatus {
	units, err := conn.ListUnits()
	if err != nil {
		return nil
	}
	return getUnitStatus(units, name)
}

func getUnitFile(units []UnitFile, name string) *UnitFile {
	for _, u := range units {
		if path.Base(u.Path) == name {
			return &u
		}
	}
	return nil
}

func runStartTrUnit(t *testing.T, conn *Conn, trTarget TrUnitProp) error {
	reschan := make(chan string)
	_, err := conn.StartTransientUnit(trTarget.name, "replace", trTarget.props, reschan)
	if err != nil {
		return err
	}

	job := <-reschan
	if job != "done" {
		return fmt.Errorf("Job is not done: %s", job)
	}

	return nil
}

func runStopUnit(t *testing.T, conn *Conn, trTarget TrUnitProp) error {
	reschan := make(chan string)
	_, err := conn.StopUnit(trTarget.name, "replace", reschan)
	if err != nil {
		return err
	}

	// wait for StopUnit job to complete
	<-reschan

	return nil
}

func getJobStatusIfExists(jobs []JobStatus, jobName string) *JobStatus {
	for _, j := range jobs {
		if j.Unit == jobName {
			return &j
		}
	}
	return nil
}

func isJobStatusEmpty(job JobStatus) bool {
	return job.Id == 0 && job.Unit == "" && job.JobType == "" && job.Status == "" && job.JobPath == "" && job.UnitPath == ""
}

// Ensure that basic unit starting and stopping works.
func TestStartStopUnit(t *testing.T) {
	target := "start-stop.service"
	conn := setupConn(t)
	defer conn.Close()

	setupUnit(target, conn, t)
	linkUnit(target, conn, t)

	// 2. Start the unit
	reschan := make(chan string)
	_, err := conn.StartUnit(target, "replace", reschan)
	if err != nil {
		t.Fatal(err)
	}

	job := <-reschan
	if job != "done" {
		t.Fatal("Job is not done:", job)
	}

	units, err := conn.ListUnits()
	if err != nil {
		t.Fatal(err)
	}

	unit := getUnitStatus(units, target)

	if unit == nil {
		t.Fatalf("Test unit not found in list")
	} else if unit.ActiveState != "active" {
		t.Fatalf("Test unit not active")
	}

	// 3. Stop the unit
	_, err = conn.StopUnit(target, "replace", reschan)
	if err != nil {
		t.Fatal(err)
	}

	// wait for StopUnit job to complete
	<-reschan

	units, err = conn.ListUnits()
	if err != nil {
		t.Fatal(err)
	}

	unit = getUnitStatus(units, target)

	if unit != nil {
		t.Fatalf("Test unit found in list, should be stopped")
	}
}

// Ensure that basic unit restarting works.
func TestRestartUnit(t *testing.T) {
	target := "start-stop.service"
	conn := setupConn(t)
	defer conn.Close()

	setupUnit(target, conn, t)
	linkUnit(target, conn, t)

	// Start the unit
	reschan := make(chan string)
	_, err := conn.StartUnit(target, "replace", reschan)
	if err != nil {
		t.Fatal(err)
	}

	job := <-reschan
	if job != "done" {
		t.Fatal("Job is not done:", job)
	}

	units, err := conn.ListUnits()
	if err != nil {
		t.Fatal(err)
	}

	unit := getUnitStatus(units, target)
	if unit == nil {
		t.Fatalf("Test unit not found in list")
	} else if unit.ActiveState != "active" {
		t.Fatalf("Test unit not active")
	}

	// Restart the unit
	reschan = make(chan string)
	_, err = conn.RestartUnit(target, "replace", reschan)
	if err != nil {
		t.Fatal(err)
	}

	job = <-reschan
	if job != "done" {
		t.Fatal("Job is not done:", job)
	}

	// Stop the unit
	_, err = conn.StopUnit(target, "replace", reschan)
	if err != nil {
		t.Fatal(err)
	}

	// wait for StopUnit job to complete
	<-reschan

	units, err = conn.ListUnits()
	if err != nil {
		t.Fatal(err)
	}

	unit = getUnitStatus(units, target)
	if unit != nil {
		t.Fatalf("Test unit found in list, should be stopped")
	}

	// Try to restart the unit.
	// It should still succeed, even if the unit is inactive.
	reschan = make(chan string)
	_, err = conn.TryRestartUnit(target, "replace", reschan)
	if err != nil {
		t.Fatal(err)
	}

	// wait for StopUnit job to complete
	<-reschan

	units, err = conn.ListUnits()
	if err != nil {
		t.Fatal(err)
	}

	unit = getUnitStatus(units, target)
	if unit != nil {
		t.Fatalf("Test unit found in list, should be stopped")
	}
}

// Ensure that basic unit reloading works.
func TestReloadUnit(t *testing.T) {
	target := "reload.service"
	conn := setupConn(t)
	defer conn.Close()

	err := conn.Subscribe()
	if err != nil {
		t.Fatal(err)
	}

	subSet := conn.NewSubscriptionSet()
	evChan, errChan := subSet.Subscribe()

	subSet.Add(target)

	setupUnit(target, conn, t)
	linkUnit(target, conn, t)

	// Start the unit
	reschan := make(chan string)
	_, err = conn.StartUnit(target, "replace", reschan)
	if err != nil {
		t.Fatal(err)
	}

	job := <-reschan
	if job != "done" {
		t.Fatal("Job is not done:", job)
	}

	units, err := conn.ListUnits()
	if err != nil {
		t.Fatal(err)
	}

	unit := getUnitStatus(units, target)
	if unit == nil {
		t.Fatalf("Test unit not found in list")
	} else if unit.ActiveState != "active" {
		t.Fatalf("Test unit not active")
	}

	// Reload the unit
	reschan = make(chan string)

	_, err = conn.ReloadUnit(target, "replace", reschan)
	if err != nil {
		t.Fatal(err)
	}

	job = <-reschan
	if job != "done" {
		t.Fatal("Job is not done:", job)
	}

	timeout := make(chan bool, 1)
	go func() {
		time.Sleep(3 * time.Second)
		close(timeout)
	}()

	// Wait for the event, expecting the target UnitStatus meets all of the
	// following conditions:
	//  * target is non-nil
	//  * target's ActiveState is active.
waitevent:
	for {
		select {
		case changes := <-evChan:
			tch, ok := changes[target]
			if !ok {
				continue waitevent
			}
			if tch != nil && tch.Name == target && tch.ActiveState == "active" {
				break waitevent
			}
		case err = <-errChan:
			t.Fatal(err)
		case <-timeout:
			t.Fatal("Reached timeout")
		}
	}
}

// Ensure that basic unit reload-or-restarting works.
func TestReloadOrRestartUnit(t *testing.T) {
	target := "reload.service"
	conn := setupConn(t)
	defer conn.Close()

	setupUnit(target, conn, t)
	linkUnit(target, conn, t)

	// Start the unit
	reschan := make(chan string)
	_, err := conn.StartUnit(target, "replace", reschan)
	if err != nil {
		t.Fatal(err)
	}

	job := <-reschan
	if job != "done" {
		t.Fatal("Job is not done:", job)
	}

	units, err := conn.ListUnits()
	if err != nil {
		t.Fatal(err)
	}

	unit := getUnitStatus(units, target)
	if unit == nil {
		t.Fatalf("Test unit not found in list")
	} else if unit.ActiveState != "active" {
		t.Fatalf("Test unit not active")
	}

	// Reload or restart the unit
	reschan = make(chan string)
	_, err = conn.ReloadOrRestartUnit(target, "replace", reschan)
	if err != nil {
		t.Fatal(err)
	}

	job = <-reschan
	if job != "done" {
		t.Fatal("Job is not done:", job)
	}

	// Stop the unit
	_, err = conn.StopUnit(target, "replace", reschan)
	if err != nil {
		t.Fatal(err)
	}

	// wait for StopUnit job to complete
	<-reschan

	units, err = conn.ListUnits()
	if err != nil {
		t.Fatal(err)
	}

	unit = getUnitStatus(units, target)
	if unit != nil && unit.ActiveState == "active" {
		t.Fatalf("Test unit still active, should be inactive.")
	}

	// Reload or try to restart the unit
	// It should still succeed, even if the unit is inactive.
	reschan = make(chan string)
	_, err = conn.ReloadOrTryRestartUnit(target, "replace", reschan)
	if err != nil {
		t.Fatal(err)
	}

	job = <-reschan
	if job != "done" {
		t.Fatal("Job is not done:", job)
	}
}

// Ensure that GetUnitByPID works.
func TestGetUnitByPID(t *testing.T) {
	conn := setupConn(t)
	defer conn.Close()

	path, err := conn.GetUnitByPID(context.Background(), 1)
	if err != nil {
		t.Error(err)
	}

	if path == "" {
		t.Fatal("path is empty")
	}
}

// Ensure that GetUnitNameByPID works.
func TestGetUnitNameByPID(t *testing.T) {
	conn := setupConn(t)
	defer conn.Close()

	name, err := conn.GetUnitNameByPID(context.Background(), 1)
	if err != nil {
		t.Error(err)
	}

	if name == "" {
		t.Fatal("name is empty")
	}
}

// Ensure that ListUnitsByNames works.
func TestListUnitsByNames(t *testing.T) {
	target1 := "systemd-journald.service"
	target2 := "unexisting.service"

	conn := setupConn(t)
	defer conn.Close()

	units, err := conn.ListUnitsByNames([]string{target1, target2})
	if err != nil {
		t.Skip(err)
	}

	unit := getUnitStatus(units, target1)

	if unit == nil {
		t.Fatalf("%s unit not found in list", target1)
	} else if unit.ActiveState != "active" {
		t.Fatalf("%s unit should be active but it is %s", target1, unit.ActiveState)
	}

	unit = getUnitStatus(units, target2)

	if unit == nil {
		t.Fatalf("Unexisting test unit not found in list")
	} else if unit.ActiveState != "inactive" {
		t.Fatalf("Test unit should be inactive")
	}
}

// Ensure that ListUnitsByPatterns works.
func TestListUnitsByPatterns(t *testing.T) {
	target1 := "systemd-journald.service"
	target2 := "unexisting.service"

	conn := setupConn(t)
	defer conn.Close()

	units, err := conn.ListUnitsByPatterns([]string{}, []string{"systemd-journald*", target2})
	if err != nil {
		t.Skip(err)
	}

	unit := getUnitStatus(units, target1)

	if unit == nil {
		t.Fatalf("%s unit not found in list", target1)
	} else if unit.ActiveState != "active" {
		t.Fatalf("Test unit should be active")
	}

	unit = getUnitStatus(units, target2)

	if unit != nil {
		t.Fatalf("Unexisting test unit found in list")
	}
}

// Ensure that ListUnitsFiltered works.
func TestListUnitsFiltered(t *testing.T) {
	target := "systemd-journald.service"

	conn := setupConn(t)
	defer conn.Close()

	units, err := conn.ListUnitsFiltered([]string{"active"})
	if err != nil {
		t.Fatal(err)
	}

	unit := getUnitStatus(units, target)

	if unit == nil {
		t.Fatalf("%s unit not found in list", target)
	} else if unit.ActiveState != "active" {
		t.Fatalf("Test unit should be active")
	}

	units, err = conn.ListUnitsFiltered([]string{"inactive"})
	if err != nil {
		t.Fatal(err)
	}

	unit = getUnitStatus(units, target)

	if unit != nil {
		t.Fatalf("Inactive unit should not be found in list")
	}
}

// Ensure that ListUnitFilesByPatterns works.
func TestListUnitFilesByPatterns(t *testing.T) {
	target1 := "systemd-journald.service"
	target2 := "exit.target"

	conn := setupConn(t)
	defer conn.Close()

	units, err := conn.ListUnitFilesByPatterns([]string{"static"}, []string{"systemd-journald*", target2})
	if err != nil {
		t.Skip(err)
	}

	unit := getUnitFile(units, target1)

	if unit == nil {
		t.Fatalf("%s unit not found in list", target1)
	} else if unit.Type != "static" {
		t.Fatalf("Test unit file should be static")
	}

	units, err = conn.ListUnitFilesByPatterns([]string{"disabled"}, []string{"systemd-journald*", target2})
	if err != nil {
		t.Fatal(err)
	}

	unit = getUnitFile(units, target2)

	if unit == nil {
		t.Fatalf("%s unit not found in list", target2)
	} else if unit.Type != "disabled" {
		t.Fatalf("%s unit file should be disabled", target2)
	}
}

func TestListUnitFiles(t *testing.T) {
	target1 := "systemd-journald.service"
	target2 := "exit.target"

	conn := setupConn(t)
	defer conn.Close()

	units, err := conn.ListUnitFiles()
	if err != nil {
		t.Fatal(err)
	}

	unit := getUnitFile(units, target1)

	if unit == nil {
		t.Fatalf("%s unit not found in list", target1)
	} else if unit.Type != "static" {
		t.Fatalf("Test unit file should be static")
	}

	unit = getUnitFile(units, target2)

	if unit == nil {
		t.Fatalf("%s unit not found in list", target2)
	} else if unit.Type != "disabled" {
		t.Fatalf("%s unit file should be disabled", target2)
	}
}

// Enables a unit and then immediately tears it down
func TestEnableDisableUnit(t *testing.T) {
	target := "enable-disable.service"
	conn := setupConn(t)
	defer conn.Close()

	setupUnit(target, conn, t)
	abs := findFixture(target, t)
	runPath := filepath.Join("/run/systemd/system/", target)

	// 1. Enable the unit
	install, changes, err := conn.EnableUnitFiles([]string{abs}, true, true)
	if err != nil {
		t.Fatal(err)
	}

	if install {
		t.Log("Install was true")
	}

	if len(changes) < 1 {
		t.Fatalf("Expected one change, got %v", changes)
	}

	if changes[0].Filename != runPath {
		t.Fatal("Unexpected target filename")
	}

	// 2. Disable the unit
	dChanges, err := conn.DisableUnitFiles([]string{target}, true)
	if err != nil {
		t.Fatal(err)
	}

	if len(dChanges) != 1 {
		t.Fatalf("Changes should include the path, %v", dChanges)
	}
	if dChanges[0].Filename != runPath {
		t.Fatalf("Change should include correct filename, %+v", dChanges[0])
	}
	if dChanges[0].Destination != "" {
		t.Fatalf("Change destination should be empty, %+v", dChanges[0])
	}
}

// Ensure that ListJobs works.
func TestListJobs(t *testing.T) {
	service := "oneshot.service"

	conn := setupConn(t)

	setupUnit(service, conn, t)
	linkUnit(service, conn, t)

	_, err := conn.StartUnit(service, "replace", nil)
	if err != nil {
		t.Fatal(err)
	}

	jobs, err := conn.ListJobs()
	if err != nil {
		t.Skip(err)
	}

	found := getJobStatusIfExists(jobs, service)
	if found == nil {
		t.Fatalf("%s job not found in list", service)
	}

	if isJobStatusEmpty(*found) {
		t.Fatalf("empty %s job found in list", service)
	}

	reschan := make(chan string)
	_, err = conn.StopUnit(service, "replace", reschan)
	if err != nil {
		t.Fatal(err)
	}

	<-reschan

	jobs, err = conn.ListJobs()

	found = getJobStatusIfExists(jobs, service)
	if err != nil {
		t.Fatal(err)
	}

	if found != nil {
		t.Fatalf("%s job found in list when it shouldn't", service)
	}
}

// TestSystemState tests if system state is one of the valid states
func TestSystemState(t *testing.T) {
	conn := setupConn(t)
	defer conn.Close()

	prop, err := conn.SystemState()
	if err != nil {
		t.Fatal(err)
	}

	if prop.Name != "SystemState" {
		t.Fatalf("unexpected property name: %v", prop.Name)
	}

	val := prop.Value.Value().(string)

	switch val {
	case "initializing":
	case "starting":
	case "running":
	case "degraded":
	case "maintenance":
	case "stopping":
	case "offline":
	case "unknown":
		// valid systemd state - do nothing

	default:
		t.Fatalf("unexpected property value: %v", val)
	}
}

// TestGetUnitProperties reads the `-.mount` which should exist on all systemd
// systems and ensures that one of its properties is valid.
func TestGetUnitProperties(t *testing.T) {
	conn := setupConn(t)
	defer conn.Close()

	unit := "-.mount"

	info, err := conn.GetUnitProperties(unit)
	if err != nil {
		t.Fatal(err)
	}

	desc, _ := info["Description"].(string)

	prop, err := conn.GetUnitProperty(unit, "Description")
	if err != nil {
		t.Fatal(err)
	}

	if prop.Name != "Description" {
		t.Fatal("unexpected property name")
	}

	val := prop.Value.Value().(string)
	if !reflect.DeepEqual(val, desc) {
		t.Fatal("unexpected property value")
	}
}

// TestGetUnitPropertiesRejectsInvalidName attempts to get the properties for a
// unit with an invalid name. This test should be run with --test.timeout set,
// as a fail will manifest as GetUnitProperties hanging indefinitely.
func TestGetUnitPropertiesRejectsInvalidName(t *testing.T) {
	conn := setupConn(t)
	defer conn.Close()

	unit := "//invalid#$^/"

	_, err := conn.GetUnitProperties(unit)
	if err == nil {
		t.Fatal("Expected an error, got nil")
	}

	_, err = conn.GetUnitProperty(unit, "Wants")
	if err == nil {
		t.Fatal("Expected an error, got nil")
	}
}

// TestGetServiceProperty reads the `systemd-udevd.service` which should exist
// on all systemd systems and ensures that one of its property is valid.
func TestGetServiceProperty(t *testing.T) {
	conn := setupConn(t)
	defer conn.Close()

	service := "systemd-udevd.service"

	prop, err := conn.GetServiceProperty(service, "Type")
	if err != nil {
		t.Fatal(err)
	}

	if prop.Name != "Type" {
		t.Fatal("unexpected property name")
	}

	if _, ok := prop.Value.Value().(string); !ok {
		t.Fatal("invalid property value")
	}
}

// TestSetUnitProperties changes a cgroup setting on the `-.mount`
// which should exist on all systemd systems and ensures that the
// property was set.
func TestSetUnitProperties(t *testing.T) {
	conn := setupConn(t)
	defer conn.Close()

	unit := "-.mount"

	if err := conn.SetUnitProperties(unit, true, Property{"CPUShares", dbus.MakeVariant(uint64(1023))}); err != nil {
		t.Fatal(err)
	}

	info, err := conn.GetUnitTypeProperties(unit, "Mount")
	if err != nil {
		t.Fatal(err)
	}

	value, _ := info["CPUShares"].(uint64)
	if value != 1023 {
		t.Fatal("CPUShares of unit is not 1023:", value)
	}
}

// Ensure that oneshot transient unit starting and stopping works.
func TestStartStopTransientUnitAll(t *testing.T) {
	testCases := []struct {
		trTarget  TrUnitProp
		trDep     TrUnitProp
		checkFunc func(t *testing.T, trTarget TrUnitProp, trDep TrUnitProp) error
	}{
		{
			trTarget: TrUnitProp{
				name: "testing-transient.service",
				props: []Property{
					PropExecStart([]string{"/bin/sleep", "400"}, false),
				},
			},
			trDep:     TrUnitProp{"", nil},
			checkFunc: checkTransientUnit,
		},
		{
			trTarget: TrUnitProp{
				name: "testing-transient-oneshot.service",
				props: []Property{
					PropExecStart([]string{"/bin/true"}, false),
					PropType("oneshot"),
					PropRemainAfterExit(true),
				},
			},
			trDep:     TrUnitProp{"", nil},
			checkFunc: checkTransientUnitOneshot,
		},
		{
			trTarget: TrUnitProp{
				name: "testing-transient-requires.service",
				props: []Property{
					PropExecStart([]string{"/bin/sleep", "400"}, false),
					PropRequires("testing-transient-requiresdep.service"),
				},
			},
			trDep: TrUnitProp{
				name: "testing-transient-requiresdep.service",
				props: []Property{
					PropExecStart([]string{"/bin/sleep", "400"}, false),
				},
			},
			checkFunc: checkTransientUnitRequires,
		},
		{
			trTarget: TrUnitProp{
				name: "testing-transient-requires-ov.service",
				props: []Property{
					PropExecStart([]string{"/bin/sleep", "400"}, false),
					PropRequires("testing-transient-requiresdep-ov.service"),
				},
			},
			trDep: TrUnitProp{
				name: "testing-transient-requiresdep-ov.service",
				props: []Property{
					PropExecStart([]string{"/bin/sleep", "400"}, false),
				},
			},
			checkFunc: checkTransientUnitRequiresOv,
		},
		{
			trTarget: TrUnitProp{
				name: "testing-transient-requisite.service",
				props: []Property{
					PropExecStart([]string{"/bin/sleep", "400"}, false),
					PropRequisite("testing-transient-requisitedep.service"),
				},
			},
			trDep: TrUnitProp{
				name: "testing-transient-requisitedep.service",
				props: []Property{
					PropExecStart([]string{"/bin/sleep", "400"}, false),
				},
			},
			checkFunc: checkTransientUnitRequisite,
		},
		{
			trTarget: TrUnitProp{
				name: "testing-transient-requisite-ov.service",
				props: []Property{
					PropExecStart([]string{"/bin/sleep", "400"}, false),
					PropRequisiteOverridable("testing-transient-requisitedep-ov.service"),
				},
			},
			trDep: TrUnitProp{
				name: "testing-transient-requisitedep-ov.service",
				props: []Property{
					PropExecStart([]string{"/bin/sleep", "400"}, false),
				},
			},
			checkFunc: checkTransientUnitRequisiteOv,
		},
		{
			trTarget: TrUnitProp{
				name: "testing-transient-wants.service",
				props: []Property{
					PropExecStart([]string{"/bin/sleep", "400"}, false),
					PropWants("testing-transient-wantsdep.service"),
				},
			},
			trDep: TrUnitProp{
				name: "testing-transient-wantsdep.service",
				props: []Property{
					PropExecStart([]string{"/bin/sleep", "400"}, false),
				},
			},
			checkFunc: checkTransientUnitWants,
		},
		{
			trTarget: TrUnitProp{
				name: "testing-transient-bindsto.service",
				props: []Property{
					PropExecStart([]string{"/bin/sleep", "400"}, false),
					PropBindsTo("testing-transient-bindstodep.service"),
				},
			},
			trDep: TrUnitProp{
				name: "testing-transient-bindstodep.service",
				props: []Property{
					PropExecStart([]string{"/bin/sleep", "400"}, false),
				},
			},
			checkFunc: checkTransientUnitBindsTo,
		},
		{
			trTarget: TrUnitProp{
				name: "testing-transient-conflicts.service",
				props: []Property{
					PropExecStart([]string{"/bin/sleep", "400"}, false),
					PropConflicts("testing-transient-conflictsdep.service"),
				},
			},
			trDep: TrUnitProp{
				name: "testing-transient-conflictsdep.service",
				props: []Property{
					PropExecStart([]string{"/bin/sleep", "400"}, false),
				},
			},
			checkFunc: checkTransientUnitConflicts,
		},
	}

	for i, tt := range testCases {
		if err := tt.checkFunc(t, tt.trTarget, tt.trDep); err != nil {
			t.Errorf("case %d: failed test with unit %s. err: %v", i, tt.trTarget.name, err)
		}
	}
}

// Ensure that basic transient unit starting and stopping works.
func checkTransientUnit(t *testing.T, trTarget TrUnitProp, trDep TrUnitProp) error {
	conn := setupConn(t)
	defer conn.Close()

	// Start the unit
	err := runStartTrUnit(t, conn, trTarget)
	if err != nil {
		return err
	}

	unit := getUnitStatusSingle(conn, trTarget.name)
	if unit == nil {
		return fmt.Errorf("Test unit not found in list")
	} else if unit.ActiveState != "active" {
		return fmt.Errorf("Test unit not active")
	}

	// Stop the unit
	err = runStopUnit(t, conn, trTarget)
	if err != nil {
		return err
	}

	unit = getUnitStatusSingle(conn, trTarget.name)
	if unit != nil {
		return fmt.Errorf("Test unit found in list, should be stopped")
	}

	return nil
}

func checkTransientUnitOneshot(t *testing.T, trTarget TrUnitProp, trDep TrUnitProp) error {
	conn := setupConn(t)
	defer conn.Close()

	// Start the unit
	err := runStartTrUnit(t, conn, trTarget)
	if err != nil {
		return err
	}

	unit := getUnitStatusSingle(conn, trTarget.name)
	if unit == nil {
		return fmt.Errorf("Test unit not found in list")
	} else if unit.ActiveState != "active" {
		return fmt.Errorf("Test unit not active")
	}

	// Stop the unit
	err = runStopUnit(t, conn, trTarget)
	if err != nil {
		return err
	}

	unit = getUnitStatusSingle(conn, trTarget.name)
	if unit != nil {
		return fmt.Errorf("Test unit found in list, should be stopped")
	}

	return nil
}

// Ensure that transient unit with Requires starting and stopping works.
func checkTransientUnitRequires(t *testing.T, trTarget TrUnitProp, trDep TrUnitProp) error {
	conn := setupConn(t)
	defer conn.Close()

	// Start the dependent unit
	err := runStartTrUnit(t, conn, trDep)
	if err != nil {
		return err
	}

	// Start the target unit
	err = runStartTrUnit(t, conn, trTarget)
	if err != nil {
		return err
	}

	unit := getUnitStatusSingle(conn, trTarget.name)
	if unit == nil {
		return fmt.Errorf("Test unit not found in list")
	} else if unit.ActiveState != "active" {
		return fmt.Errorf("Test unit not active")
	}

	// Stop the unit
	err = runStopUnit(t, conn, trTarget)
	if err != nil {
		return err
	}

	unit = getUnitStatusSingle(conn, trTarget.name)
	if unit != nil {
		return fmt.Errorf("Test unit found in list, should be stopped")
	}

	// Stop the dependent unit
	err = runStopUnit(t, conn, trDep)
	if err != nil {
		return err
	}

	unit = getUnitStatusSingle(conn, trDep.name)
	if unit != nil {
		return fmt.Errorf("Test unit found in list, should be stopped")
	}

	return nil
}

// Ensure that transient unit with RequiresOverridable starting and stopping works.
func checkTransientUnitRequiresOv(t *testing.T, trTarget TrUnitProp, trDep TrUnitProp) error {
	conn := setupConn(t)
	defer conn.Close()

	// Start the dependent unit
	err := runStartTrUnit(t, conn, trDep)
	if err != nil {
		return err
	}

	// Start the target unit
	err = runStartTrUnit(t, conn, trTarget)
	if err != nil {
		return err
	}

	unit := getUnitStatusSingle(conn, trTarget.name)
	if unit == nil {
		return fmt.Errorf("Test unit not found in list")
	} else if unit.ActiveState != "active" {
		return fmt.Errorf("Test unit not active")
	}

	// Stop the unit
	err = runStopUnit(t, conn, trTarget)
	if err != nil {
		return err
	}

	unit = getUnitStatusSingle(conn, trTarget.name)
	if unit != nil {
		return fmt.Errorf("Test unit found in list, should be stopped")
	}

	// Stop the dependent unit
	err = runStopUnit(t, conn, trDep)
	if err != nil {
		return err
	}

	unit = getUnitStatusSingle(conn, trDep.name)
	if unit != nil {
		return fmt.Errorf("Test unit found in list, should be stopped")
	}

	return nil
}

// Ensure that transient unit with Requisite starting and stopping works.
// It's expected for target unit to fail, as its child is not started at all.
func checkTransientUnitRequisite(t *testing.T, trTarget TrUnitProp, trDep TrUnitProp) error {
	conn := setupConn(t)
	defer conn.Close()

	// Start the target unit
	err := runStartTrUnit(t, conn, trTarget)
	if err == nil {
		return fmt.Errorf("Unit %s is expected to fail, but succeeded", trTarget.name)
	}

	unit := getUnitStatusSingle(conn, trTarget.name)
	if unit != nil && unit.ActiveState == "active" {
		return fmt.Errorf("Test unit %s is active, should be inactive", trTarget.name)
	}

	return nil
}

// Ensure that transient unit with RequisiteOverridable starting and stopping works.
// It's expected for target unit to fail, as its child is not started at all.
func checkTransientUnitRequisiteOv(t *testing.T, trTarget TrUnitProp, trDep TrUnitProp) error {
	conn := setupConn(t)
	defer conn.Close()

	// Start the target unit
	err := runStartTrUnit(t, conn, trTarget)
	if err == nil {
		return fmt.Errorf("Unit %s is expected to fail, but succeeded", trTarget.name)
	}

	unit := getUnitStatusSingle(conn, trTarget.name)
	if unit != nil && unit.ActiveState == "active" {
		return fmt.Errorf("Test unit %s is active, should be inactive", trTarget.name)
	}

	return nil
}

// Ensure that transient unit with Wants starting and stopping works.
// It's expected for target to successfully start, even when its child is not started.
func checkTransientUnitWants(t *testing.T, trTarget TrUnitProp, trDep TrUnitProp) error {
	conn := setupConn(t)
	defer conn.Close()

	// Start the target unit
	err := runStartTrUnit(t, conn, trTarget)
	if err != nil {
		return err
	}

	unit := getUnitStatusSingle(conn, trTarget.name)
	if unit == nil {
		return fmt.Errorf("Test unit not found in list")
	} else if unit.ActiveState != "active" {
		return fmt.Errorf("Test unit not active")
	}

	// Stop the unit
	err = runStopUnit(t, conn, trTarget)
	if err != nil {
		return err
	}

	unit = getUnitStatusSingle(conn, trTarget.name)
	if unit != nil {
		return fmt.Errorf("Test unit found in list, should be stopped")
	}

	return nil
}

// Ensure that transient unit with BindsTo starting and stopping works.
// Stopping its child should result in stopping target unit.
func checkTransientUnitBindsTo(t *testing.T, trTarget TrUnitProp, trDep TrUnitProp) error {
	conn := setupConn(t)
	defer conn.Close()

	// Start the dependent unit
	err := runStartTrUnit(t, conn, trDep)
	if err != nil {
		return err
	}

	// Start the target unit
	err = runStartTrUnit(t, conn, trTarget)
	if err != nil {
		return err
	}

	unit := getUnitStatusSingle(conn, trTarget.name)
	if unit == nil {
		t.Fatalf("Test unit not found in list")
	} else if unit.ActiveState != "active" {
		t.Fatalf("Test unit not active")
	}

	// Stop the dependent unit
	err = runStopUnit(t, conn, trDep)
	if err != nil {
		return err
	}

	unit = getUnitStatusSingle(conn, trDep.name)
	if unit != nil {
		t.Fatalf("Test unit found in list, should be stopped")
	}

	// Then the target unit should be gone
	unit = getUnitStatusSingle(conn, trTarget.name)
	if unit != nil {
		t.Fatalf("Test unit found in list, should be stopped")
	}

	return nil
}

// Ensure that transient unit with Conflicts starting and stopping works.
func checkTransientUnitConflicts(t *testing.T, trTarget TrUnitProp, trDep TrUnitProp) error {
	conn := setupConn(t)
	defer conn.Close()

	// Start the dependent unit
	err := runStartTrUnit(t, conn, trDep)
	if err != nil {
		return err
	}

	// Start the target unit
	err = runStartTrUnit(t, conn, trTarget)
	if err != nil {
		return err
	}

	isTargetActive := false
	unit := getUnitStatusSingle(conn, trTarget.name)
	if unit != nil && unit.ActiveState == "active" {
		isTargetActive = true
	}

	isReqDepActive := false
	unit = getUnitStatusSingle(conn, trDep.name)
	if unit != nil && unit.ActiveState == "active" {
		isReqDepActive = true
	}

	if isTargetActive && isReqDepActive {
		return fmt.Errorf("Conflicts didn't take place")
	}

	// Stop the target unit
	if isTargetActive {
		err = runStopUnit(t, conn, trTarget)
		if err != nil {
			return err
		}

		unit = getUnitStatusSingle(conn, trTarget.name)
		if unit != nil {
			return fmt.Errorf("Test unit %s found in list, should be stopped", trTarget.name)
		}
	}

	// Stop the dependent unit
	if isReqDepActive {
		err = runStopUnit(t, conn, trDep)
		if err != nil {
			return err
		}

		unit = getUnitStatusSingle(conn, trDep.name)
		if unit != nil {
			return fmt.Errorf("Test unit %s found in list, should be stopped", trDep.name)
		}
	}

	return nil
}

func runSleep(t *testing.T) uint32 {
	cmd := exec.Command("/bin/sleep", "400")
	err := cmd.Start()
	if err != nil {
		t.Fatal(err)
	}

	t.Cleanup(func() {
		err := cmd.Process.Kill()
		if err != nil {
			t.Fatal(err)
		}
		_ = cmd.Wait()
	})

	return uint32(cmd.Process.Pid)
}

// Ensure that putting running programs into scopes works
func TestStartStopTransientScope(t *testing.T) {
	conn := setupConn(t)
	defer conn.Close()

	pid := runSleep(t)
	props := []Property{
		PropPids(pid),
	}
	target := fmt.Sprintf("testing-transient-%d.scope", pid)

	// Start the unit
	reschan := make(chan string)
	_, err := conn.StartTransientUnit(target, "replace", props, reschan)
	if err != nil {
		t.Fatal(err)
	}

	job := <-reschan
	if job != "done" {
		t.Fatal("Job is not done:", job)
	}

	units, err := conn.ListUnits()
	if err != nil {
		t.Fatal(err)
	}

	unit := getUnitStatus(units, target)

	if unit == nil {
		t.Fatalf("Test unit not found in list")
	} else if unit.ActiveState != "active" {
		t.Fatalf("Test unit not active")
	}

	// maybe check if pid is really a member of the just created scope
	//   systemd uses the following api which does not use dbus, but directly
	//   accesses procfs for cgroup information.
	//     int sd_pid_get_unit(pid_t pid, char **session)
}

// Ensure that basic unit gets killed by SIGTERM
func TestKillUnit(t *testing.T) {
	target := "start-stop.service"
	conn := setupConn(t)
	defer conn.Close()

	err := conn.Subscribe()
	if err != nil {
		t.Fatal(err)
	}

	subSet := conn.NewSubscriptionSet()
	evChan, errChan := subSet.Subscribe()

	subSet.Add(target)

	setupUnit(target, conn, t)
	linkUnit(target, conn, t)

	// Start the unit
	reschan := make(chan string)
	_, err = conn.StartUnit(target, "replace", reschan)
	if err != nil {
		t.Fatal(err)
	}

	job := <-reschan
	if job != "done" {
		t.Fatal("Job is not done:", job)
	}

	// send SIGTERM
	conn.KillUnit(target, int32(syscall.SIGTERM))

	timeout := make(chan bool, 1)
	go func() {
		time.Sleep(3 * time.Second)
		close(timeout)
	}()

	// Wait for the event, expecting the target UnitStatus meets one of the
	// following conditions:
	//  * target is nil, meaning the unit has completely gone.
	//  * target is non-nil, and its ActiveState is not active.
waitevent:
	for {
		select {
		case changes := <-evChan:
			tch, ok := changes[target]
			if !ok {
				continue waitevent
			}
			if tch == nil || (tch.Name == target && tch.ActiveState != "active") {
				break waitevent
			}
		case err = <-errChan:
			t.Fatal(err)
		case <-timeout:
			t.Fatal("Reached timeout")
		}
	}
}

// Ensure that a failed unit gets reset
func TestResetFailedUnit(t *testing.T) {
	target := "start-failed.service"
	conn := setupConn(t)
	defer conn.Close()

	setupUnit(target, conn, t)
	linkUnit(target, conn, t)

	// Start the unit
	reschan := make(chan string)
	_, err := conn.StartUnit(target, "replace", reschan)
	if err != nil {
		t.Fatal(err)
	}

	job := <-reschan
	if job != "failed" {
		t.Fatal("Job is not failed:", job)
	}

	units, err := conn.ListUnits()
	if err != nil {
		t.Fatal(err)
	}

	unit := getUnitStatus(units, target)
	if unit == nil {
		t.Fatalf("Test unit not found in list")
	}

	// reset the failed unit
	err = conn.ResetFailedUnit(target)
	if err != nil {
		t.Fatal(err)
	}

	// Ensure that the target unit is actually gone
	units, err = conn.ListUnits()
	if err != nil {
		t.Fatal(err)
	}

	found := false
	for _, u := range units {
		if u.Name == target {
			found = true
			break
		}
	}
	if found {
		t.Fatalf("Test unit still found in list. units = %v", units)
	}
}

func TestConnJobListener(t *testing.T) {
	target := "start-stop.service"
	conn := setupConn(t)
	defer conn.Close()

	setupUnit(target, conn, t)
	linkUnit(target, conn, t)

	jobSize := len(conn.jobListener.jobs)

	reschan := make(chan string)
	_, err := conn.StartUnit(target, "replace", reschan)
	if err != nil {
		t.Fatal(err)
	}

	<-reschan

	_, err = conn.StopUnit(target, "replace", reschan)
	if err != nil {
		t.Fatal(err)
	}

	<-reschan

	currentJobSize := len(conn.jobListener.jobs)
	if jobSize != currentJobSize {
		t.Fatal("JobListener jobs leaked")
	}
}

// Enables a unit and then masks/unmasks it
func TestMaskUnmask(t *testing.T) {
	target := "mask-unmask.service"
	conn := setupConn(t)
	defer conn.Close()

	setupUnit(target, conn, t)
	abs := findFixture(target, t)
	runPath := filepath.Join("/run/systemd/system/", target)

	// 1. Enable the unit
	install, changes, err := conn.EnableUnitFiles([]string{abs}, true, true)
	if err != nil {
		t.Fatal(err)
	}

	if install {
		t.Log("Install was true")
	}

	if len(changes) < 1 {
		t.Fatalf("Expected one change, got %v", changes)
	}

	if changes[0].Filename != runPath {
		t.Fatal("Unexpected target filename")
	}

	// 2. Mask the unit
	mChanges, err := conn.MaskUnitFiles([]string{target}, true, true)
	if err != nil {
		t.Fatal(err)
	}
	if mChanges[0].Filename != runPath {
		t.Fatalf("Change should include correct filename, %+v", mChanges[0])
	}
	if mChanges[0].Destination != "" {
		t.Fatalf("Change destination should be empty, %+v", mChanges[0])
	}

	// 3. Unmask the unit
	uChanges, err := conn.UnmaskUnitFiles([]string{target}, true)
	if err != nil {
		t.Fatal(err)
	}
	if uChanges[0].Filename != runPath {
		t.Fatalf("Change should include correct filename, %+v", uChanges[0])
	}
	if uChanges[0].Destination != "" {
		t.Fatalf("Change destination should be empty, %+v", uChanges[0])
	}
}

// Test a global Reload
func TestReload(t *testing.T) {
	conn := setupConn(t)
	defer conn.Close()

	err := conn.Reload()
	if err != nil {
		t.Fatal(err)
	}
}

func TestUnitName(t *testing.T) {
	for _, unit := range []string{
		"",
		"foo.service",
		"foobar",
		"woof@woof.service",
		"0123456",
		"account_db.service",
		"got-dashes",
	} {
		got := unitName(unitPath(unit))
		if got != unit {
			t.Errorf("bad result for unitName(%s): got %q, want %q", unit, got, unit)
		}
	}
}

func TestFreezer(t *testing.T) {
	target := "freeze.service"
	conn := setupConn(t)
	defer conn.Close()

	setupUnit(target, conn, t)
	linkUnit(target, conn, t)

	reschan := make(chan string)
	_, err := conn.StartUnit(target, "replace", reschan)
	if err != nil {
		t.Fatal(err)
	}

	job := <-reschan
	if job != "done" {
		t.Fatal("Job is not done:", job)
	}

	if err := conn.FreezeUnit(context.Background(), target); err != nil {
		// Don't fail the test if freezing units is not implemented at all (on older systemd versions) or
		// not supported (on systems running with cgroup v1).
		e, ok := err.(dbus.Error)
		if ok && (e.Name == "org.freedesktop.DBus.Error.UnknownMethod" || e.Name == "org.freedesktop.DBus.Error.NotSupported") {
			t.SkipNow()
		}
		t.Fatalf("failed to freeze unit %s: %s", target, err)
	}

	p, err := conn.GetUnitProperty(target, "FreezerState")
	if err != nil {
		t.Fatal(err)
	}

	v := p.Value.Value().(string)
	if v != "frozen" {
		t.Fatalf("unit is not frozen after calling FreezeUnit(), FreezerState=%s", v)
	}

	if err := conn.ThawUnit(context.Background(), target); err != nil {
		t.Fatalf("failed to thaw unit %s: %s", target, err)
	}

	p, err = conn.GetUnitProperty(target, "FreezerState")
	if err != nil {
		t.Fatal(err)
	}

	v = p.Value.Value().(string)
	if v != "running" {
		t.Fatalf("unit is not frozen after calling ThawUnit(), FreezerState=%s", v)
	}

	if err := runStopUnit(t, conn, TrUnitProp{target, nil}); err != nil {
		t.Fatal("StopUnit:", err)
	}
}

func testAttachProcessesToUnit(t *testing.T, subcgroup string) {
	target := "attach-processes.service"
	conn := setupConn(t)
	defer conn.Close()

	setupUnit(target, conn, t)
	linkUnit(target, conn, t)

	// Start the unit first.
	reschan := make(chan string)
	_, err := conn.StartUnit(target, "replace", reschan)
	if err != nil {
		t.Fatal(err)
	}
	job := <-reschan
	if job != "done" {
		t.Fatal("Job is not done:", job)
	}

	// Cleanup.
	defer func() {
		_, err = conn.StopUnit(target, "replace", reschan)
		if err != nil {
			t.Fatal(err)
		}
		<-reschan
	}()

	if subcgroup != "" {
		// Pre-create a sub-cgroup.
		prop, err := conn.GetServiceProperty(target, "ControlGroup")
		if err != nil {
			t.Fatal(err)
		}
		path := prop.Value.Value().(string)
		err = os.Mkdir(filepath.Join("/sys/fs/cgroup", path, subcgroup), 0o777)
		if err != nil {
			t.Fatal(err)
		}

	}

	// Start a test process that we can attach.
	pid := runSleep(t)

	// Test attaching the process to the unit.
	ctx := context.Background()
	err = conn.AttachProcessesToUnit(ctx, target, subcgroup, []uint32{pid})
	if err != nil {
		// AttachProcessesToUnit might not be supported on all systemd versions.
		e, ok := err.(dbus.Error)
		if ok && (e.Name == "org.freedesktop.DBus.Error.UnknownMethod" ||
			e.Name == "org.freedesktop.DBus.Error.NotSupported") {
			t.SkipNow()
		}
		t.Fatalf("failed to attach process %d to unit %s: %s", pid, target, err)
	}

	// Verify the process was attached by getting the unit it belongs to.
	attachedPath, err := conn.GetUnitByPID(ctx, pid)
	if err != nil {
		t.Fatal(err)
	}

	attachedUnit := unitName(attachedPath)
	if attachedUnit != target {
		t.Fatalf("process was not attached to correct unit: got %s, want %s", attachedUnit, target)
	}
}

func TestAttachProcessesToUnit(t *testing.T) {
	testAttachProcessesToUnit(t, "")
}

func TestAttachProcessesToUnitWithSubcgroup(t *testing.T) {
	testAttachProcessesToUnit(t, "/test-subcgroup")
}
