package dbus

import (
	"context"
	"testing"
	"time"
)

type objectGoContextServer struct {
	t     *testing.T
	sleep time.Duration
}

func (o objectGoContextServer) Sleep() *Error {
	o.t.Log("Got object call and sleeping for ", o.sleep)
	time.Sleep(o.sleep)
	o.t.Log("Completed sleeping for ", o.sleep)
	return nil
}

func TestObjectGoWithContextTimeout(t *testing.T) {
	bus, err := ConnectSessionBus()
	if err != nil {
		t.Fatalf("Unexpected error connecting to session bus: %s", err)
	}
	defer bus.Close()

	name := bus.Names()[0]
	bus.Export(objectGoContextServer{t, time.Second}, "/org/dannin/DBus/Test", "org.dannin.DBus.Test")
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	select {
	case call := <-bus.Object(name, "/org/dannin/DBus/Test").GoWithContext(ctx, "org.dannin.DBus.Test.Sleep", 0, nil).Done:
		if call.Err != ctx.Err() {
			t.Fatal("Expected ", ctx.Err(), " but got ", call.Err)
		}
	case <-time.After(2 * time.Second):
		t.Fatal("Expected call to not respond in time")
	}
}

func TestObjectGoWithContext(t *testing.T) {
	bus, err := ConnectSessionBus()
	if err != nil {
		t.Fatalf("Unexpected error connecting to session bus: %s", err)
	}
	defer bus.Close()

	name := bus.Names()[0]
	bus.Export(objectGoContextServer{t, time.Millisecond}, "/org/dannin/DBus/Test", "org.dannin.DBus.Test")
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	select {
	case call := <-bus.Object(name, "/org/dannin/DBus/Test").GoWithContext(ctx, "org.dannin.DBus.Test.Sleep", 0, nil).Done:
		if call.Err != ctx.Err() {
			t.Fatal("Expected ", ctx.Err(), " but got ", call.Err)
		}
	case <-time.After(time.Second):
		t.Fatal("Expected call to respond in 1 Millisecond")
	}
}

type nopServer struct{}

func (_ nopServer) Nop() *Error {
	return nil
}

func TestObjectSignalHandling(t *testing.T) {
	bus, err := ConnectSessionBus()
	if err != nil {
		t.Fatalf("Unexpected error connecting to session bus: %s", err)
	}
	defer bus.Close()

	name := bus.Names()[0]
	path := ObjectPath("/org/godbus/DBus/TestSignals")
	otherPath := ObjectPath("/org/other/godbus/DBus/TestSignals")
	iface := "org.godbus.DBus.TestSignals"
	otherIface := "org.godbus.DBus.OtherTestSignals"
	err = bus.Export(nopServer{}, path, iface)
	if err != nil {
		t.Fatalf("Unexpected error registering nop server: %v", err)
	}

	obj := bus.Object(name, path)
	if err := bus.AddMatchSignal(
		WithMatchInterface(iface),
		WithMatchMember("Heartbeat"),
		WithMatchObjectPath(path),
	); err != nil {
		t.Fatal(err)
	}

	ch := make(chan *Signal, 5)
	bus.Signal(ch)

	go func() {
		defer func() {
			if err := recover(); err != nil {
				t.Errorf("Caught panic in emitter goroutine: %v", err)
			}
		}()

		emit := func(path ObjectPath, name string, values ...interface{}) {
			t.Helper()
			if err := bus.Emit(path, name, values...); err != nil {
				t.Error("Emit:", err)
			}
		}

		// desired signals
		emit(path, iface+".Heartbeat", uint32(1))
		emit(path, iface+".Heartbeat", uint32(2))
		// undesired signals
		emit(otherPath, iface+".Heartbeat", uint32(3))
		emit(otherPath, otherIface+".Heartbeat", uint32(4))
		emit(path, iface+".Updated", false)
		// sentinel
		emit(path, iface+".Heartbeat", uint32(5))

		time.Sleep(100 * time.Millisecond)
		emit(path, iface+".Heartbeat", uint32(6))
	}()

	checkSignal := func(ch chan *Signal, value uint32) {
		t.Helper()

		const timeout = 50 * time.Millisecond
		var sig *Signal

		select {
		case sig = <-ch:
			// do nothing
		case <-time.After(timeout):
			t.Fatalf("Failed to fetch signal in specified timeout %s", timeout)
		}

		if sig.Path != path {
			t.Errorf("signal.Path mismatch: %s != %s", path, sig.Path)
		}

		name := iface + ".Heartbeat"
		if sig.Name != name {
			t.Errorf("signal.Name mismatch: %s != %s", name, sig.Name)
		}

		if len(sig.Body) != 1 {
			t.Errorf("Invalid signal body length: %d", len(sig.Body))
			return
		}

		if sig.Body[0] != interface{}(value) {
			t.Errorf("signal value mismatch: %d != %d", value, sig.Body[0])
		}
	}

	checkSignal(ch, 1)
	checkSignal(ch, 2)
	checkSignal(ch, 5)

	obj.RemoveMatchSignal(iface, "Heartbeat", WithMatchObjectPath(obj.Path()))
	select {
	case sig := <-ch:
		t.Errorf("Got signal after removing subscription: %v", sig)
	case <-time.After(200 * time.Millisecond):
	}
}
