/*
Copyright 2015 The Kubernetes Authors.

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 (
	"fmt"
	"os"
	"testing"

	godbus "github.com/godbus/dbus"
)

const (
	DBusNameFlagDoNotQueue uint32 = 1 << (iota + 1)
)

const (
	DBusRequestNameReplyPrimaryOwner uint32 = iota + 1
	DBusRequestNameReplyAlreadyOwner
)

const (
	DBusReleaseNameReplyReleased uint32 = iota + 1
	DBusReleaseNameReplyNotOwner
)

func doDBusTest(t *testing.T, dbus Interface, real bool) {
	bus, err := dbus.SystemBus()
	if err != nil {
		if !real {
			t.Errorf("dbus.SystemBus() failed with fake Interface")
		}
		t.Skipf("D-Bus is not running: %v", err)
	}
	busObj := bus.BusObject()

	id := ""
	err = busObj.Call("org.freedesktop.DBus.GetId", 0).Store(&id)
	if err != nil {
		t.Errorf("expected success, got %v", err)
	}
	if len(id) == 0 {
		t.Errorf("expected non-empty Id, got \"\"")
	}

	// Switch to the session bus for the rest, since the system bus is more
	// locked down (and thus harder to trick into emitting signals).

	bus, err = dbus.SessionBus()
	if err != nil {
		if !real {
			t.Errorf("dbus.SystemBus() failed with fake Interface")
		}
		t.Skipf("D-Bus session bus is not available: %v", err)
	}
	busObj = bus.BusObject()

	name := fmt.Sprintf("io.kubernetes.dbus_test_%d", os.Getpid())
	owner := ""
	err = busObj.Call("org.freedesktop.DBus.GetNameOwner", 0, name).Store(&owner)
	if err == nil {
		t.Errorf("expected '%s' to be un-owned, but found owner %s", name, owner)
	}
	dbuserr, ok := err.(godbus.Error)
	if !ok {
		t.Errorf("expected godbus.Error, but got %#v", err)
	}
	if dbuserr.Name != "org.freedesktop.DBus.Error.NameHasNoOwner" {
		t.Errorf("expected NameHasNoOwner error but got %v", err)
	}

	sigchan := make(chan *godbus.Signal, 10)
	bus.Signal(sigchan)

	rule := fmt.Sprintf("type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',path='/org/freedesktop/DBus',sender='org.freedesktop.DBus',arg0='%s'", name)
	err = busObj.Call("org.freedesktop.DBus.AddMatch", 0, rule).Store()
	if err != nil {
		t.Errorf("expected success, got %v", err)
	}

	var ret uint32
	err = busObj.Call("org.freedesktop.DBus.RequestName", 0, name, DBusNameFlagDoNotQueue).Store(&ret)
	if err != nil {
		t.Errorf("expected success, got %v", err)
	}
	if ret != DBusRequestNameReplyPrimaryOwner {
		t.Errorf("expected %v, got %v", DBusRequestNameReplyPrimaryOwner, ret)
	}

	err = busObj.Call("org.freedesktop.DBus.GetNameOwner", 0, name).Store(&owner)
	if err != nil {
		t.Errorf("expected success, got %v", err)
	}

	var changedSignal, acquiredSignal, lostSignal *godbus.Signal

	sig1 := <-sigchan
	sig2 := <-sigchan
	// We get two signals, but the order isn't guaranteed
	if sig1.Name == "org.freedesktop.DBus.NameOwnerChanged" {
		changedSignal = sig1
		acquiredSignal = sig2
	} else {
		acquiredSignal = sig1
		changedSignal = sig2
	}

	if acquiredSignal.Sender != "org.freedesktop.DBus" || acquiredSignal.Name != "org.freedesktop.DBus.NameAcquired" {
		t.Errorf("expected NameAcquired signal, got %v", acquiredSignal)
	}
	acquiredName := acquiredSignal.Body[0].(string)
	if acquiredName != name {
		t.Errorf("unexpected NameAcquired arguments: %v", acquiredSignal)
	}

	if changedSignal.Sender != "org.freedesktop.DBus" || changedSignal.Name != "org.freedesktop.DBus.NameOwnerChanged" {
		t.Errorf("expected NameOwnerChanged signal, got %v", changedSignal)
	}

	changedName := changedSignal.Body[0].(string)
	oldOwner := changedSignal.Body[1].(string)
	newOwner := changedSignal.Body[2].(string)
	if changedName != name || oldOwner != "" || newOwner != owner {
		t.Errorf("unexpected NameOwnerChanged arguments: %v", changedSignal)
	}

	err = busObj.Call("org.freedesktop.DBus.ReleaseName", 0, name).Store(&ret)
	if err != nil {
		t.Errorf("expected success, got %v", err)
	}
	if ret != DBusReleaseNameReplyReleased {
		t.Errorf("expected %v, got %v", DBusReleaseNameReplyReleased, ret)
	}

	sig1 = <-sigchan
	sig2 = <-sigchan
	if sig1.Name == "org.freedesktop.DBus.NameOwnerChanged" {
		changedSignal = sig1
		lostSignal = sig2
	} else {
		lostSignal = sig1
		changedSignal = sig2
	}

	if lostSignal.Sender != "org.freedesktop.DBus" || lostSignal.Name != "org.freedesktop.DBus.NameLost" {
		t.Errorf("expected NameLost signal, got %v", lostSignal)
	}
	lostName := lostSignal.Body[0].(string)
	if lostName != name {
		t.Errorf("unexpected NameLost arguments: %v", lostSignal)
	}

	if changedSignal.Sender != "org.freedesktop.DBus" || changedSignal.Name != "org.freedesktop.DBus.NameOwnerChanged" {
		t.Errorf("expected NameOwnerChanged signal, got %v", changedSignal)
	}

	changedName = changedSignal.Body[0].(string)
	oldOwner = changedSignal.Body[1].(string)
	newOwner = changedSignal.Body[2].(string)
	if changedName != name || oldOwner != owner || newOwner != "" {
		t.Errorf("unexpected NameOwnerChanged arguments: %v", changedSignal)
	}

	if len(sigchan) != 0 {
		t.Errorf("unexpected extra signals (%d)", len(sigchan))
	}

	// Unregister sigchan
	bus.Signal(sigchan)
}

func TestRealDBus(t *testing.T) {
	dbus := New()
	doDBusTest(t, dbus, true)
}

func TestFakeDBus(t *testing.T) {
	uniqueName := ":1.1"
	ownedName := ""

	fakeSystem := NewFakeConnection()
	fakeSystem.SetBusObject(
		func(method string, args ...interface{}) ([]interface{}, error) {
			if method == "org.freedesktop.DBus.GetId" {
				return []interface{}{"foo"}, nil
			}
			return nil, fmt.Errorf("unexpected method call '%s'", method)
		},
	)

	fakeSession := NewFakeConnection()
	fakeSession.SetBusObject(
		func(method string, args ...interface{}) ([]interface{}, error) {
			if method == "org.freedesktop.DBus.GetNameOwner" {
				checkName := args[0].(string)
				if checkName != ownedName {
					return nil, godbus.Error{Name: "org.freedesktop.DBus.Error.NameHasNoOwner", Body: nil}
				}
				return []interface{}{uniqueName}, nil
			} else if method == "org.freedesktop.DBus.RequestName" {
				reqName := args[0].(string)
				_ = args[1].(uint32)
				if ownedName != "" {
					return []interface{}{DBusRequestNameReplyAlreadyOwner}, nil
				}
				ownedName = reqName
				fakeSession.EmitSignal("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "NameAcquired", reqName)
				fakeSession.EmitSignal("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "NameOwnerChanged", reqName, "", uniqueName)
				return []interface{}{DBusRequestNameReplyPrimaryOwner}, nil
			} else if method == "org.freedesktop.DBus.ReleaseName" {
				reqName := args[0].(string)
				if reqName != ownedName {
					return []interface{}{DBusReleaseNameReplyNotOwner}, nil
				}
				ownedName = ""
				fakeSession.EmitSignal("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "NameOwnerChanged", reqName, uniqueName, "")
				fakeSession.EmitSignal("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "NameLost", reqName)
				return []interface{}{DBusReleaseNameReplyReleased}, nil
			} else if method == "org.freedesktop.DBus.AddMatch" {
				return nil, nil
			} else {
				return nil, fmt.Errorf("unexpected method call '%s'", method)
			}
		},
	)

	dbus := NewFake(fakeSystem, fakeSession)
	doDBusTest(t, dbus, false)
}
