//go:build linux

package bridge

import (
	"encoding/json"
	"net"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/duffy/usb-server/internal/usb"
	"golang.org/x/sys/unix"
)

// A minimal but valid descriptor blob: device descriptor, one configuration,
// one HID interface, one interrupt IN endpoint.
func testDescriptors() []byte {
	dev := []byte{
		18, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 64,
		0x6d, 0x04, // idVendor  046d
		0x1c, 0xc0, // idProduct c01c
		0x00, 0x01, // bcdDevice
		1, 2, 3, 1,
	}
	iface := []byte{9, 0x04, 0, 0, 1, 0x03, 0x01, 0x02, 0}
	ep := []byte{7, 0x05, 0x81, 0x03, 8, 0, 10}
	body := append(iface, ep...)
	cfg := append([]byte{9, 0x02, byte(9 + len(body)), 0, 1, 1, 0, 0x80, 250}, body...)
	return append(dev, cfg...)
}

// send delivers one request, attaching fd if it is non-negative.
func send(t *testing.T, conn *net.UnixConn, req Request, fd int) Response {
	t.Helper()

	data, err := json.Marshal(req)
	if err != nil {
		t.Fatalf("marshalling request: %v", err)
	}

	var oob []byte
	if fd >= 0 {
		oob = unix.UnixRights(fd)
	}

	if _, _, err := conn.WriteMsgUnix(data, oob, nil); err != nil {
		t.Fatalf("sending request: %v", err)
	}

	conn.SetReadDeadline(time.Now().Add(3 * time.Second))
	buf := make([]byte, 4096)
	n, err := conn.Read(buf)
	if err != nil {
		t.Fatalf("reading response: %v", err)
	}

	var resp Response
	if err := json.Unmarshal(buf[:n], &resp); err != nil {
		t.Fatalf("parsing response %q: %v", buf[:n], err)
	}
	return resp
}

func startServer(t *testing.T) (*Server, *net.UnixConn) {
	t.Helper()

	path := filepath.Join(t.TempDir(), "bridge.sock")
	srv, err := Listen(path)
	if err != nil {
		t.Fatalf("Listen: %v", err)
	}
	t.Cleanup(func() { srv.Close() })

	conn, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: path, Net: "unix"})
	if err != nil {
		t.Fatalf("dialling bridge: %v", err)
	}
	t.Cleanup(func() { conn.Close() })

	return srv, conn
}

// openTestFD returns a real descriptor to hand over. Its contents do not
// matter — nothing in the bridge reads from it — only that it is open.
func openTestFD(t *testing.T) int {
	t.Helper()

	f, err := os.CreateTemp(t.TempDir(), "fd")
	if err != nil {
		t.Fatalf("creating temp file: %v", err)
	}
	defer f.Close()

	fd, err := unix.Dup(int(f.Fd()))
	if err != nil {
		t.Fatalf("dup: %v", err)
	}
	return fd
}

func TestAddRegistersDeviceAndDescriptor(t *testing.T) {
	usb.UnregisterExternalDevice("9-9")
	srv, conn := startServer(t)

	changed := make(chan struct{}, 1)
	srv.OnChange = func() {
		select {
		case changed <- struct{}{}:
		default:
		}
	}

	resp := send(t, conn, Request{
		Action:       "add",
		BusID:        "9-9",
		Descriptors:  testDescriptors(),
		BusNum:       9,
		DevNum:       9,
		Speed:        3,
		ConfigValue:  1,
		Manufacturer: "Test",
		Product:      "Keyboard",
	}, openTestFD(t))

	if !resp.OK {
		t.Fatalf("add failed: %s", resp.Error)
	}
	t.Cleanup(func() { usb.UnregisterExternalDevice("9-9") })

	select {
	case <-changed:
	case <-time.After(2 * time.Second):
		t.Error("OnChange did not fire after a device was added")
	}

	// The device must show up in enumeration, parsed from the blob.
	var found *usb.Device
	for _, d := range usb.ExternalDevices() {
		if d.BusID == "9-9" {
			cp := d
			found = &cp
		}
	}
	if found == nil {
		t.Fatal("device was not registered")
	}
	if found.VendorID != 0x046d || found.ProductID != 0xc01c {
		t.Errorf("got %04x:%04x, want 046d:c01c", found.VendorID, found.ProductID)
	}
	if found.Product != "Keyboard" {
		t.Errorf("product = %q, want %q", found.Product, "Keyboard")
	}
	// The endpoint has to survive with its real transfer type, which is the
	// whole reason the descriptors are sent along.
	ep, ok := found.Endpoints[0x81]
	if !ok {
		t.Fatal("endpoint 0x81 missing from the parsed descriptors")
	}
	if ep.TransferType != usb.TransferTypeInterrupt {
		t.Errorf("endpoint type = %d, want interrupt", ep.TransferType)
	}

	if !usb.HasAdoptedFD("9-9") {
		t.Error("the file descriptor was not adopted")
	}
}

func TestAddRejectsMissingPieces(t *testing.T) {
	_, conn := startServer(t)

	tests := []struct {
		name string
		req  Request
		fd   bool
	}{
		{"no bus id", Request{Action: "add", Descriptors: testDescriptors()}, true},
		{"no descriptors", Request{Action: "add", BusID: "8-8"}, true},
		{"no file descriptor", Request{Action: "add", BusID: "8-8", Descriptors: testDescriptors()}, false},
		{"garbage descriptors", Request{Action: "add", BusID: "8-8", Descriptors: []byte{1, 2, 3}}, true},
		{"unknown action", Request{Action: "frobnicate", BusID: "8-8"}, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fd := -1
			if tt.fd {
				fd = openTestFD(t)
			}
			resp := send(t, conn, tt.req, fd)
			if resp.OK {
				t.Error("request was accepted but should have been rejected")
			}
			if resp.Error == "" {
				t.Error("rejection carried no explanation")
			}
		})
	}

	if usb.HasAdoptedFD("8-8") {
		t.Error("a rejected request left a descriptor behind")
		usb.ReleaseAdoptedFDs()
	}
}

func TestRemoveWithdrawsDevice(t *testing.T) {
	_, conn := startServer(t)

	resp := send(t, conn, Request{
		Action: "add", BusID: "7-7", Descriptors: testDescriptors(), ConfigValue: 1,
	}, openTestFD(t))
	if !resp.OK {
		t.Fatalf("add failed: %s", resp.Error)
	}

	resp = send(t, conn, Request{Action: "remove", BusID: "7-7"}, -1)
	if !resp.OK {
		t.Fatalf("remove failed: %s", resp.Error)
	}

	for _, d := range usb.ExternalDevices() {
		if d.BusID == "7-7" {
			t.Fatal("device is still registered after removal")
		}
	}
}

// The socket lets its holder make this client share arbitrary devices, so it
// must not be world-writable.
func TestSocketIsPrivate(t *testing.T) {
	path := filepath.Join(t.TempDir(), "bridge.sock")
	srv, err := Listen(path)
	if err != nil {
		t.Fatalf("Listen: %v", err)
	}
	defer srv.Close()

	info, err := os.Stat(path)
	if err != nil {
		t.Fatalf("stat: %v", err)
	}
	if perm := info.Mode().Perm(); perm != 0600 {
		t.Errorf("socket permissions are %04o, want 0600", perm)
	}
}

// Restarting must not fail because the previous socket file is still there.
func TestListenReplacesStaleSocket(t *testing.T) {
	path := filepath.Join(t.TempDir(), "bridge.sock")

	first, err := Listen(path)
	if err != nil {
		t.Fatalf("first Listen: %v", err)
	}
	first.listener.Close() // simulate a crash: socket file survives

	second, err := Listen(path)
	if err != nil {
		t.Fatalf("second Listen failed on a leftover socket: %v", err)
	}
	second.Close()
}

func TestCloseRemovesSocket(t *testing.T) {
	path := filepath.Join(t.TempDir(), "bridge.sock")

	srv, err := Listen(path)
	if err != nil {
		t.Fatalf("Listen: %v", err)
	}
	srv.Close()

	if _, err := os.Stat(path); !os.IsNotExist(err) {
		t.Error("the socket file outlived the server")
	}
}
