package system

import (
	"fmt"
	"os"
	"path/filepath"
	"syscall"
	"testing"
	"time"

	"hakurei.app/internal/acl"
	"hakurei.app/internal/pipewire"
	"hakurei.app/internal/stub"
)

func TestPipeWireOp(t *testing.T) {
	t.Parallel()

	checkOpBehaviour(t, checkNoParallel, []opBehaviourTestCase{
		{"success", 0xbeef, 0xff, &pipewireOp{nil,
			m(filepath.Join(t.TempDir(), "pipewire")),
			"org.chromium.Chromium",
			"ebf083d1b175911782d413369b64ce7c",
		}, []stub.Call{
			call("pipewireConnect", stub.ExpectArgs{}, func() *pipewire.Context {
				if ctx, err := pipewire.New(&stubPipeWireConn{sendmsg: []string{

					/* roundtrip 0 */

					string([]byte{
						// header: Core::Hello
						0, 0, 0, 0,
						0x18, 0, 0, 1,
						0, 0, 0, 0,
						0, 0, 0, 0,
						// Struct
						0x10, 0, 0, 0,
						0xe, 0, 0, 0,
						// Int: version = 4
						4, 0, 0, 0,
						4, 0, 0, 0,
						4, 0, 0, 0,
						0, 0, 0, 0,

						// header: Client::UpdateProperties
						1, 0, 0, 0,
						0x50, 0, 0, 2,
						1, 0, 0, 0,
						0, 0, 0, 0,
						// Struct: spa_dict
						0x48, 0, 0, 0,
						0xe, 0, 0, 0,
						// Struct: spa_dict_item
						0x40, 0, 0, 0,
						0xe, 0, 0, 0,
						// Int: n_items
						4, 0, 0, 0,
						4, 0, 0, 0,
						1, 0, 0, 0,
						0, 0, 0, 0,
						// String: key = "remote.intention"
						0x11, 0, 0, 0,
						8, 0, 0, 0,
						0x72, 0x65, 0x6d, 0x6f,
						0x74, 0x65, 0x2e, 0x69,
						0x6e, 0x74, 0x65, 0x6e,
						0x74, 0x69, 0x6f, 0x6e,
						0, 0, 0, 0,
						0, 0, 0, 0,
						// String: value = "manager"
						8, 0, 0, 0,
						8, 0, 0, 0,
						0x6d, 0x61, 0x6e, 0x61,
						0x67, 0x65, 0x72, 0,

						// header: Core::GetRegistry
						0, 0, 0, 0,
						0x28, 0, 0, 5,
						2, 0, 0, 0,
						0, 0, 0, 0,
						// Struct
						0x20, 0, 0, 0,
						0xe, 0, 0, 0,
						// Int: version = 3
						4, 0, 0, 0,
						4, 0, 0, 0,
						3, 0, 0, 0,
						0, 0, 0, 0,
						// Int: new_id = 2
						4, 0, 0, 0,
						4, 0, 0, 0,
						2, 0, 0, 0,
						0, 0, 0, 0,
						// header: Core::Sync
						0, 0, 0, 0,
						0x28, 0, 0, 2,
						3, 0, 0, 0,
						0, 0, 0, 0,
						// Struct
						0x20, 0, 0, 0,
						0xe, 0, 0, 0,
						// Int: id = 0
						4, 0, 0, 0,
						4, 0, 0, 0,
						0, 0, 0, 0,
						0, 0, 0, 0,
						// Int: seq = 0x40000003
						4, 0, 0, 0,
						4, 0, 0, 0,
						3, 0, 0, 0x40,
						0, 0, 0, 0,
					}),

					/* roundtrip 1 */

					string([]byte{
						// header: Registry::Bind
						2, 0, 0, 0,
						0x68, 0, 0, 1,
						4, 0, 0, 0,
						0, 0, 0, 0,
						// Struct
						0x60, 0, 0, 0,
						0xe, 0, 0, 0,
						// Int: id = 3
						4, 0, 0, 0,
						4, 0, 0, 0,
						3, 0, 0, 0,
						0, 0, 0, 0,
						// String: type = "PipeWire:Interface:SecurityContext"
						0x23, 0, 0, 0,
						8, 0, 0, 0,
						0x50, 0x69, 0x70, 0x65,
						0x57, 0x69, 0x72, 0x65,
						0x3a, 0x49, 0x6e, 0x74,
						0x65, 0x72, 0x66, 0x61,
						0x63, 0x65, 0x3a, 0x53,
						0x65, 0x63, 0x75, 0x72,
						0x69, 0x74, 0x79, 0x43,
						0x6f, 0x6e, 0x74, 0x65,
						0x78, 0x74, 0, 0,
						0, 0, 0, 0,
						// Int: version = 3
						4, 0, 0, 0,
						4, 0, 0, 0,
						3, 0, 0, 0,
						0, 0, 0, 0,
						// Int: new_id = 3
						4, 0, 0, 0,
						4, 0, 0, 0,
						3, 0, 0, 0,
						0, 0, 0, 0,
					}),

					/* roundtrip 2 */

					string([]byte{
						// header: SecurityContext::Create
						3, 0, 0, 0,
						0x40, 1, 0, 1,
						5, 0, 0, 0,
						2, 0, 0, 0,
						// Struct
						0x38, 1, 0, 0,
						0xe, 0, 0, 0,
						// Fd: listen_fd = 1
						8, 0, 0, 0,
						0x12, 0, 0, 0,
						1, 0, 0, 0,
						0, 0, 0, 0,
						// Fd: close_fd = 0
						8, 0, 0, 0,
						0x12, 0, 0, 0,
						0, 0, 0, 0,
						0, 0, 0, 0,
						// Struct: spa_dict
						0x10, 1, 0, 0,
						0xe, 0, 0, 0,
						// Int: n_items = 4
						4, 0, 0, 0,
						4, 0, 0, 0,
						4, 0, 0, 0,
						0, 0, 0, 0,
						// String: key = "pipewire.sec.engine"
						0x14, 0, 0, 0,
						8, 0, 0, 0,
						0x70, 0x69, 0x70, 0x65,
						0x77, 0x69, 0x72, 0x65,
						0x2e, 0x73, 0x65, 0x63,
						0x2e, 0x65, 0x6e, 0x67,
						0x69, 0x6e, 0x65, 0,
						0, 0, 0, 0,
						// String: value = "app.hakurei"
						0xc, 0, 0, 0,
						8, 0, 0, 0,
						0x61, 0x70, 0x70, 0x2e,
						0x68, 0x61, 0x6b, 0x75,
						0x72, 0x65, 0x69, 0,
						0, 0, 0, 0,
						// String: key = "pipewire.sec.app-id"
						0x14, 0, 0, 0,
						8, 0, 0, 0,
						0x70, 0x69, 0x70, 0x65,
						0x77, 0x69, 0x72, 0x65,
						0x2e, 0x73, 0x65, 0x63,
						0x2e, 0x61, 0x70, 0x70,
						0x2d, 0x69, 0x64, 0,
						0, 0, 0, 0,
						// String: value = "org.chromium.Chromium"
						0x16, 0, 0, 0,
						8, 0, 0, 0,
						0x6f, 0x72, 0x67, 0x2e,
						0x63, 0x68, 0x72, 0x6f,
						0x6d, 0x69, 0x75, 0x6d,
						0x2e, 0x43, 0x68, 0x72,
						0x6f, 0x6d, 0x69, 0x75,
						// String: key = "pipewire.sec.instance-id"
						0x6d, 0, 0, 0,
						0x19, 0, 0, 0,
						8, 0, 0, 0,
						0x70, 0x69, 0x70, 0x65,
						0x77, 0x69, 0x72, 0x65,
						0x2e, 0x73, 0x65, 0x63,
						0x2e, 0x69, 0x6e, 0x73,
						0x74, 0x61, 0x6e, 0x63,
						0x65, 0x2d, 0x69, 0x64,
						0, 0, 0, 0,
						0, 0, 0, 0,
						// String: value = "ebf083d1b175911782d413369b64ce7c"
						0x21, 0, 0, 0,
						8, 0, 0, 0,
						0x65, 0x62, 0x66, 0x30,
						0x38, 0x33, 0x64, 0x31,
						0x62, 0x31, 0x37, 0x35,
						0x39, 0x31, 0x31, 0x37,
						0x38, 0x32, 0x64, 0x34,
						0x31, 0x33, 0x33, 0x36,
						0x39, 0x62, 0x36, 0x34,
						0x63, 0x65, 0x37, 0x63,
						0, 0, 0, 0,
						0, 0, 0, 0,
						// String: key = "pipewire.access"
						0x10, 0, 0, 0,
						8, 0, 0, 0,
						0x70, 0x69, 0x70, 0x65,
						0x77, 0x69, 0x72, 0x65,
						0x2e, 0x61, 0x63, 0x63,
						0x65, 0x73, 0x73, 0,
						// String: value = "restricted"
						0xb, 0, 0, 0,
						8, 0, 0, 0,
						0x72, 0x65, 0x73, 0x74,
						0x72, 0x69, 0x63, 0x74,
						0x65, 0x64, 0, 0,
						0, 0, 0, 0,
						// header: Core::Sync
						0, 0, 0, 0,
						0x28, 0, 0, 2,
						6, 0, 0, 0,
						0, 0, 0, 0,
						// Struct
						0x20, 0, 0, 0,
						0xe, 0, 0, 0,
						// Int: id = 0
						4, 0, 0, 0,
						4, 0, 0, 0,
						0, 0, 0, 0,
						0, 0, 0, 0,
						// Int: seq = 0x40000006
						4, 0, 0, 0,
						4, 0, 0, 0,
						6, 0, 0, 0x40,
						0, 0, 0, 0,
					}),
				}, recvmsg: []*stubMessage{

					/* roundtrip 0 */

					{pipewire.PW_ID_CORE, nil, &pipewire.CoreInfo{
						ID:         pipewire.PW_ID_CORE,
						Cookie:     -2069267610,
						UserName:   "alice",
						HostName:   "nixos",
						Version:    "1.4.7",
						Name:       "pipewire-0",
						ChangeMask: pipewire.PW_CORE_CHANGE_MASK_PROPS,
						Properties: &pipewire.SPADict{
							{Key: pipewire.PW_KEY_CONFIG_NAME, Value: "pipewire.conf"},
							{Key: pipewire.PW_KEY_APP_NAME, Value: "pipewire"},
							{Key: pipewire.PW_KEY_APP_PROCESS_BINARY, Value: "pipewire"},
							{Key: pipewire.PW_KEY_APP_LANGUAGE, Value: "en_US.UTF-8"},
							{Key: pipewire.PW_KEY_APP_PROCESS_ID, Value: "1446"},
							{Key: pipewire.PW_KEY_APP_PROCESS_USER, Value: "alice"},
							{Key: pipewire.PW_KEY_APP_PROCESS_HOST, Value: "nixos"},
							{Key: pipewire.PW_KEY_CORE_DAEMON, Value: "true"},
							{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-0"},
							{Key: pipewire.PW_KEY_OBJECT_ID, Value: "0"},
							{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "0"},
						},
					}},
					{pipewire.PW_ID_CORE, nil, &pipewire.CoreBoundProps{
						ID:       pipewire.PW_ID_CLIENT,
						GlobalID: 34,
						Properties: &pipewire.SPADict{
							{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "34"},
							{Key: pipewire.PW_KEY_MODULE_ID, Value: "2"},
							{Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"},
							{Key: pipewire.PW_KEY_SEC_PID, Value: "1443"},
							{Key: pipewire.PW_KEY_SEC_UID, Value: "1000"},
							{Key: pipewire.PW_KEY_SEC_GID, Value: "100"},
							{Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"},
						},
					}},
					{pipewire.PW_ID_CLIENT, nil, &pipewire.ClientInfo{
						ID:         34,
						ChangeMask: pipewire.PW_CLIENT_CHANGE_MASK_PROPS,
						Properties: &pipewire.SPADict{
							{Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"},
							{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-alice-1443"},
							{Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"},
							{Key: pipewire.PW_KEY_SEC_PID, Value: "1443"},
							{Key: pipewire.PW_KEY_SEC_UID, Value: "1000"},
							{Key: pipewire.PW_KEY_SEC_GID, Value: "100"},
							{Key: pipewire.PW_KEY_MODULE_ID, Value: "2"},
							{Key: pipewire.PW_KEY_OBJECT_ID, Value: "34"},
							{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "34"},
							{Key: pipewire.PW_KEY_REMOTE_INTENTION, Value: "manager"},
							{Key: pipewire.PW_KEY_ACCESS, Value: "unrestricted"},
						},
					}},
					{pipewire.PW_ID_CORE, nil, &pipewire.CoreDone{
						ID:       -1,
						Sequence: 0,
					}},
					{2, nil, &pipewire.RegistryGlobal{
						ID:          pipewire.PW_ID_CORE,
						Permissions: pipewire.PW_CORE_PERM_MASK,
						Type:        pipewire.PW_TYPE_INTERFACE_Core,
						Version:     pipewire.PW_VERSION_CORE,
						Properties: &pipewire.SPADict{
							{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "0"},
							{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-0"},
						},
					}},
					{2, nil, &pipewire.RegistryGlobal{
						ID:          1,
						Permissions: pipewire.PW_MODULE_PERM_MASK,
						Type:        pipewire.PW_TYPE_INTERFACE_Module,
						Version:     pipewire.PW_VERSION_MODULE,
						Properties: &pipewire.SPADict{
							{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "1"},
							{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-rt"},
						},
					}},
					{2, nil, &pipewire.RegistryGlobal{
						ID:          3,
						Permissions: pipewire.PW_SECURITY_CONTEXT_PERM_MASK,
						Type:        pipewire.PW_TYPE_INTERFACE_SecurityContext,
						Version:     pipewire.PW_VERSION_SECURITY_CONTEXT,
						Properties: &pipewire.SPADict{
							{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "3"},
						},
					}},
					{2, nil, &pipewire.RegistryGlobal{
						ID:          2,
						Permissions: pipewire.PW_MODULE_PERM_MASK,
						Type:        pipewire.PW_TYPE_INTERFACE_Module,
						Version:     pipewire.PW_VERSION_MODULE,
						Properties: &pipewire.SPADict{
							{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "2"},
							{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-protocol-native"},
						},
					}},
					{pipewire.PW_ID_CORE, nil, &pipewire.CoreDone{
						ID:       0,
						Sequence: pipewire.CoreSyncSequenceOffset + 3,
					}},
					{2, nil, &pipewire.RegistryGlobal{
						ID:          4,
						Permissions: pipewire.PW_CLIENT_PERM_MASK,
						Type:        pipewire.PW_TYPE_INTERFACE_Client,
						Version:     pipewire.PW_VERSION_CLIENT,
						Properties: &pipewire.SPADict{
							{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "4"},
							{Key: pipewire.PW_KEY_MODULE_ID, Value: "2"},
							{Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"},
							{Key: pipewire.PW_KEY_SEC_PID, Value: "1447"},
							{Key: pipewire.PW_KEY_SEC_UID, Value: "1000"},
							{Key: pipewire.PW_KEY_SEC_GID, Value: "100"},
							{Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"},
							{Key: pipewire.PW_KEY_ACCESS, Value: "unrestricted"},
							{Key: pipewire.PW_KEY_APP_NAME, Value: "WirePlumber"},
						},
					}},
					nil,

					/* roundtrip 1 */

					{pipewire.PW_ID_CORE, nil, &pipewire.CoreBoundProps{
						ID:       3,
						GlobalID: 3,
						Properties: &pipewire.SPADict{
							{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "3"},
						},
					}},
					nil,

					/* roundtrip 2 */

					{pipewire.PW_ID_CORE, nil, &pipewire.CoreDone{
						ID:       0,
						Sequence: pipewire.CoreSyncSequenceOffset + 6,
					}},
					nil,
				}}, pipewire.SPADict{
					{Key: pipewire.PW_KEY_REMOTE_INTENTION, Value: "manager"},
				}); err != nil {
					panic(err)
				} else {
					return ctx
				}
			}, nil),
			call("verbosef", stub.ExpectArgs{"pipewire pathname socket on %q", []any{ignoreValue{}}}, nil, nil),

			call("chmod", stub.ExpectArgs{ignoreValue{}, os.FileMode(0)}, nil, nil),
			call("aclUpdate", stub.ExpectArgs{ignoreValue{}, 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
		}, nil, []stub.Call{
			call("verbosef", stub.ExpectArgs{"hanging up pipewire socket on %q", []any{ignoreValue{}}}, nil, nil),
		}, nil},
	})

	checkOpsBuilder(t, "PipeWire", []opsBuilderTestCase{
		{"sample", 0xcafe, func(_ *testing.T, sys *I) {
			sys.PipeWire(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
				"org.chromium.Chromium",
				"ebf083d1b175911782d413369b64ce7c")
		}, []Op{&pipewireOp{nil,
			m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
			"org.chromium.Chromium",
			"ebf083d1b175911782d413369b64ce7c",
		}}, stub.Expect{}},
	})

	checkOpIs(t, []opIsTestCase{
		{"dst differs", &pipewireOp{nil,
			m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7d/pipewire"),
			"org.chromium.Chromium",
			"ebf083d1b175911782d413369b64ce7c",
		}, &pipewireOp{nil,
			m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
			"org.chromium.Chromium",
			"ebf083d1b175911782d413369b64ce7c",
		}, false},

		{"equals", &pipewireOp{nil,
			m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
			"org.chromium.Chromium",
			"ebf083d1b175911782d413369b64ce7c",
		}, &pipewireOp{nil,
			m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
			"org.chromium.Chromium",
			"ebf083d1b175911782d413369b64ce7c",
		}, true},
	})

	checkOpMeta(t, []opMetaTestCase{
		{"sample", &pipewireOp{nil,
			m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
			"org.chromium.Chromium",
			"ebf083d1b175911782d413369b64ce7c",
		}, Process, "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire",
			`pipewire socket at "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"`},
	})
}

// stubMessage is a [pipewire.Message] prepared ahead of time.
type stubMessage struct {
	// Proxy Id included in the [pipewire.Header].
	id pipewire.Int
	// Footer optionally appended after the message body.
	footer pipewire.KnownSize
	// Known-good message prepared ahead of time.
	m pipewire.Message
}

// stubPipeWireConn implements [pipewire.Conn] and checks the behaviour of [pipewire.Context].
type stubPipeWireConn struct {
	// Marshaled and sent for Recvmsg.
	recvmsg []*stubMessage
	// Current position in recvmsg.
	curRecvmsg int
	// Current server seq number.
	sequence pipewire.Int

	// Compared against calls to Sendmsg.
	sendmsg []string
	// Current position in sendmsg.
	curSendmsg int
}

func (conn *stubPipeWireConn) MightBlock(timeout time.Duration) {
	if timeout != pipewire.RoundtripTimeout {
		panic("unexpected timeout " + timeout.String())
	}
}

// Recvmsg marshals and copies a stubMessage prepared ahead of time.
func (conn *stubPipeWireConn) Recvmsg(p, _ []byte, _ int) (n, _, recvflags int, err error) {
	defer func() { conn.curRecvmsg++ }()
	recvflags = syscall.MSG_CMSG_CLOEXEC

	if conn.recvmsg[conn.curRecvmsg] == nil {
		err = syscall.EAGAIN
		return
	}
	defer func() { conn.sequence++ }()

	if data, marshalErr := (pipewire.MessageEncoder{Message: conn.recvmsg[conn.curRecvmsg].m}).AppendMessage(nil,
		conn.recvmsg[conn.curRecvmsg].id,
		conn.sequence,
		conn.recvmsg[conn.curRecvmsg].footer,
	); marshalErr != nil {
		panic(marshalErr)
	} else {
		n = copy(p, data)
	}
	return
}

// Sendmsg checks a client message against a known-good sample.
func (conn *stubPipeWireConn) Sendmsg(p, _ []byte, _ int) (n int, err error) {
	defer func() { conn.curSendmsg++ }()

	n = len(p)
	if string(p) != conn.sendmsg[conn.curSendmsg] {
		err = fmt.Errorf("%#v, want %#v", p, []byte(conn.sendmsg[conn.curSendmsg]))
	}
	return
}

// Close checks whether Recvmsg and Sendmsg has depleted all samples.
func (conn *stubPipeWireConn) Close() error {
	if conn.curRecvmsg != len(conn.recvmsg) {
		return fmt.Errorf("consumed %d recvmsg samples, want %d", conn.curRecvmsg, len(conn.recvmsg))
	}

	if conn.curSendmsg != len(conn.sendmsg) {
		return fmt.Errorf("consumed %d sendmsg samples, want %d", conn.curSendmsg, len(conn.sendmsg))
	}

	return nil
}
