package amneziawgnet

import (
	"context"
	"fmt"
	"io"
	"net"
	"net/netip"
	"strings"
	"testing"
	"time"

	awgconn "github.com/amnezia-vpn/amneziawg-go/v3/conn"
	"github.com/amnezia-vpn/amneziawg-go/v3/device"
	"github.com/amnezia-vpn/amneziawg-go/v3/tun/netstack"
	"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"

	"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
	"github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
)

// TestNewDeviceHandshakeForwarderAndIdentity is Phase 1's real end-to-end
// proof, not just a compile check: a genuine amneziawg-go client (via that
// project's own tun/netstack.CreateNetTUN -- the client side doesn't need a
// forwarder or peer-identity resolution, only this package's server side
// does) completes a real 3-way handshake against a Device built by
// NewDevice, dials a destination that was never configured anywhere on the
// server, and the test verifies AttachTCPForwarder recovers that exact
// destination *and* PeerIndex.Lookup resolves the connection's source back
// to the right peer's Email -- Phase 1a/1b/1c working together, the same
// mechanism Phase 0's throwaway spike validated, now as a real, repo-owned,
// repeatable test instead of scratch code.
func TestNewDeviceHandshakeForwarderAndIdentity(t *testing.T) {
	serverPriv, serverPub, err := wireguard.GenerateWireguardKeypair()
	if err != nil {
		t.Fatalf("generate server keypair: %v", err)
	}
	clientPriv, clientPub, err := wireguard.GenerateWireguardKeypair()
	if err != nil {
		t.Fatalf("generate client keypair: %v", err)
	}

	const listenPort = 58712 // fixed loopback test port, matches the validated Phase 0 spike approach
	const wantEmail = "test-peer@example.com"

	inst := amneziawg.Instance{
		Id:            1,
		InterfaceName: "awgtest1",
		ListenPort:    listenPort,
		PrivateKey:    serverPriv,
		PublicKey:     serverPub,
		Address:       []string{"10.201.0.1/24"},
		MTU:           1420,
		Obfuscation: amneziawg.Obfuscation31{
			Jc: 4, Jmin: 40, Jmax: 70,
			S1: 20, S2: 30, S3: 20, S4: 20,
		},
		Peers: []amneziawg.Peer{{
			Email:      wantEmail,
			PublicKey:  clientPub,
			AllowedIPs: []string{"10.201.0.2/32"},
		}},
	}

	dev, err := newUnconfiguredDevice(inst, DeviceOptions{})
	if err != nil {
		t.Fatalf("newUnconfiguredDevice: %v", err)
	}
	defer dev.Close()

	idx := NewPeerIndex(inst.Peers)

	type recovered struct {
		email string
		ok    bool
		dest  netip.AddrPort
	}
	got := make(chan recovered, 1)

	// Never configured anywhere server-side: the forwarder must recover it
	// purely from the decapsulated packet, not from any routing table.
	wantDest := netip.MustParseAddrPort("10.201.9.9:9999")

	AttachTCPForwarder(dev.Stack, func(conn *gonet.TCPConn, dest netip.AddrPort) {
		defer conn.Close()
		srcAddrPort, parseErr := netip.ParseAddrPort(conn.RemoteAddr().String())
		var peer amneziawg.Peer
		var ok bool
		if parseErr == nil {
			peer, ok = idx.Lookup(srcAddrPort.Addr().Unmap())
		}
		got <- recovered{email: peer.Email, ok: ok, dest: dest}
		io.Copy(io.Discard, conn)
	})

	// Configure (IpcSet) must come after AttachTCPForwarder -- see
	// newUnconfiguredDevice's doc comment: IpcSet is what starts the peer's
	// receive goroutine, which must never be able to run before the
	// forwarder is registered on the stack.
	if err := dev.Configure(inst, DeviceOptions{}); err != nil {
		t.Fatalf("Configure: %v", err)
	}

	clientTun, clientNet, err := netstack.CreateNetTUN(
		[]netip.Addr{netip.MustParseAddr("10.201.0.2")},
		[]netip.Addr{netip.MustParseAddr("1.1.1.1")}, 1420)
	if err != nil {
		t.Fatalf("client CreateNetTUN: %v", err)
	}
	clientDev := device.NewDevice(clientTun, awgconn.NewDefaultBind(), device.NewLogger(device.LogLevelSilent, ""))
	defer clientDev.Close()

	clientPrivHex, err := wireguard.KeyToHex(clientPriv)
	if err != nil {
		t.Fatalf("client key to hex: %v", err)
	}
	serverPubHex, err := wireguard.KeyToHex(serverPub)
	if err != nil {
		t.Fatalf("server key to hex: %v", err)
	}
	// allowed_ip=0.0.0.0/0 on the client matches a real VPN client's own
	// config (route everything through the tunnel) -- it's also what makes
	// dialing an arbitrary, never-configured destination like wantDest
	// actually get routed to the server peer at all: a narrower AllowedIPs
	// here would make the client's own Device drop the packet as
	// non-matching before it ever reached the wire.
	clientConf := fmt.Sprintf(
		"private_key=%s\njc=4\njmin=40\njmax=70\ns1=20\ns2=30\ns3=20\ns4=20\npublic_key=%s\nendpoint=127.0.0.1:%d\nallowed_ip=0.0.0.0/0\n",
		clientPrivHex, serverPubHex, listenPort)
	if err := clientDev.IpcSet(clientConf); err != nil {
		t.Fatalf("client IpcSet: %v", err)
	}
	if err := clientDev.Up(); err != nil {
		t.Fatalf("client Up: %v", err)
	}

	// Retry the dial rather than guessing a fixed handshake delay: the
	// first attempts may race the handshake, later ones should succeed
	// once it completes.
	dialCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	var lastErr error
	for {
		conn, dialErr := clientNet.DialContext(dialCtx, "tcp", wantDest.String())
		if dialErr == nil {
			conn.Close()
			break
		}
		lastErr = dialErr
		select {
		case <-dialCtx.Done():
			t.Fatalf("client dial never succeeded: %v", lastErr)
		case <-time.After(100 * time.Millisecond):
		}
	}

	select {
	case r := <-got:
		if !r.ok {
			t.Fatal("forwarder: peer identity lookup failed for the recovered connection")
		}
		if r.email != wantEmail {
			t.Errorf("resolved peer email = %q, want %q", r.email, wantEmail)
		}
		if r.dest != wantDest {
			t.Errorf("recovered destination = %v, want %v", r.dest, wantDest)
		}
	case <-time.After(5 * time.Second):
		t.Fatal("timed out waiting for the forwarder to hand back the recovered connection")
	}
}

// TestBuildUAPIConfigHeaderProtectionAndContentPaddingLines is a cheap,
// network-free companion to the real round-trip test below: confirms the 2
// AWG 3.0 UAPI lines only appear when set, and that a malformed
// HeaderProtectionKey surfaces a clear, wrapped error instead of silently
// producing a UAPI string amneziawg-go's own IpcSet would reject uselessly.
func TestBuildUAPIConfigHeaderProtectionAndContentPaddingLines(t *testing.T) {
	priv, _, err := wireguard.GenerateWireguardKeypair()
	if err != nil {
		t.Fatalf("generate keypair: %v", err)
	}
	inst := amneziawg.Instance{
		PrivateKey: priv,
		Obfuscation: amneziawg.Obfuscation31{
			S1: 20, S2: 20, S3: 20, S4: 20,
		},
	}

	conf, err := buildUAPIConfig(inst, DeviceOptions{})
	if err != nil {
		t.Fatalf("buildUAPIConfig with empty options: %v", err)
	}
	if strings.Contains(conf, "header_protection_key=") || strings.Contains(conf, "content_padding_addition=") {
		t.Fatalf("empty DeviceOptions must not emit AWG 3.0 lines, got:\n%s", conf)
	}

	key, err := wireguard.GenerateWireguardPSK()
	if err != nil {
		t.Fatalf("generate header protection key: %v", err)
	}
	conf, err = buildUAPIConfig(inst, DeviceOptions{HeaderProtectionKey: key, ContentPaddingAddition: "20-40"})
	if err != nil {
		t.Fatalf("buildUAPIConfig with AWG 3.0 options: %v", err)
	}
	if !strings.Contains(conf, "header_protection_key=") {
		t.Errorf("expected a header_protection_key= line, got:\n%s", conf)
	}
	if !strings.Contains(conf, "content_padding_addition=20-40\n") {
		t.Errorf("expected a content_padding_addition=20-40 line, got:\n%s", conf)
	}

	if _, err := buildUAPIConfig(inst, DeviceOptions{HeaderProtectionKey: "not-a-valid-base64-key"}); err == nil {
		t.Fatal("a malformed HeaderProtectionKey must be rejected, not silently passed through")
	}
}

// TestNewDeviceHeaderProtectionAndContentPaddingRoundTrip is the real proof
// behind AmneziaWG 3.0's admin-facing HeaderProtectionKey/
// ContentPaddingAddition fields: a genuine amneziawg-go client, configured
// with matching header_protection_key/content_padding_addition UAPI lines
// (S1-S4 all >= 12, the hard requirement amneziawg-go's own IpcSet enforces
// for header protection), completes a real handshake against a Device built
// via NewDevice/DeviceOptions and exchanges real application data both
// directions through it. This is more than a handshake-completed check --
// it also confirms actual payload bytes survive content padding on both the
// send and receive sides, the specific area a third-party AmneziaWG
// installer project's docs flagged a past interop concern for (see the
// migration plan's own risk note); it is not a substitute for real-VPS
// verification against the official client, but it is the cheapest
// available local check against a regression in either engine's own padding
// handling.
func TestNewDeviceHeaderProtectionAndContentPaddingRoundTrip(t *testing.T) {
	serverPriv, serverPub, err := wireguard.GenerateWireguardKeypair()
	if err != nil {
		t.Fatalf("generate server keypair: %v", err)
	}
	clientPriv, clientPub, err := wireguard.GenerateWireguardKeypair()
	if err != nil {
		t.Fatalf("generate client keypair: %v", err)
	}
	headerProtectionKey, err := wireguard.GenerateWireguardPSK()
	if err != nil {
		t.Fatalf("generate header protection key: %v", err)
	}

	const listenPort = 58713 // fixed loopback test port, distinct from the handshake test above
	const contentPaddingAddition = "20-40"

	inst := amneziawg.Instance{
		Id:            2,
		InterfaceName: "awgtest2",
		ListenPort:    listenPort,
		PrivateKey:    serverPriv,
		PublicKey:     serverPub,
		Address:       []string{"10.202.0.1/24"},
		MTU:           1420,
		Obfuscation: amneziawg.Obfuscation31{
			Jc: 4, Jmin: 40, Jmax: 70,
			S1: 20, S2: 30, S3: 20, S4: 20, // all >= 12, required for header protection
		},
		Peers: []amneziawg.Peer{{
			Email:      "hp-peer@example.com",
			PublicKey:  clientPub,
			AllowedIPs: []string{"10.202.0.2/32"},
		}},
	}

	opts := DeviceOptions{
		HeaderProtectionKey:    headerProtectionKey,
		ContentPaddingAddition: contentPaddingAddition,
	}
	dev, err := newUnconfiguredDevice(inst, opts)
	if err != nil {
		t.Fatalf("newUnconfiguredDevice: %v", err)
	}
	defer dev.Close()

	const wantRequest = "hello from client"
	const wantReply = "hello from server"
	serverDone := make(chan error, 1)
	AttachTCPForwarder(dev.Stack, func(conn *gonet.TCPConn, dest netip.AddrPort) {
		defer conn.Close()
		buf := make([]byte, len(wantRequest))
		if _, err := io.ReadFull(conn, buf); err != nil {
			serverDone <- fmt.Errorf("server read: %w", err)
			return
		}
		if string(buf) != wantRequest {
			serverDone <- fmt.Errorf("server got %q, want %q", buf, wantRequest)
			return
		}
		if _, err := conn.Write([]byte(wantReply)); err != nil {
			serverDone <- fmt.Errorf("server write: %w", err)
			return
		}
		serverDone <- nil
	})

	// Configure (IpcSet) must come after AttachTCPForwarder -- see
	// newUnconfiguredDevice's doc comment.
	if err := dev.Configure(inst, opts); err != nil {
		t.Fatalf("Configure: %v", err)
	}

	clientTun, clientNet, err := netstack.CreateNetTUN(
		[]netip.Addr{netip.MustParseAddr("10.202.0.2")},
		[]netip.Addr{netip.MustParseAddr("1.1.1.1")}, 1420)
	if err != nil {
		t.Fatalf("client CreateNetTUN: %v", err)
	}
	clientDev := device.NewDevice(clientTun, awgconn.NewDefaultBind(), device.NewLogger(device.LogLevelSilent, ""))
	defer clientDev.Close()

	clientPrivHex, err := wireguard.KeyToHex(clientPriv)
	if err != nil {
		t.Fatalf("client key to hex: %v", err)
	}
	serverPubHex, err := wireguard.KeyToHex(serverPub)
	if err != nil {
		t.Fatalf("server key to hex: %v", err)
	}
	headerProtectionKeyHex, err := wireguard.KeyToHex(headerProtectionKey)
	if err != nil {
		t.Fatalf("header protection key to hex: %v", err)
	}
	clientConf := fmt.Sprintf(
		"private_key=%s\njc=4\njmin=40\njmax=70\ns1=20\ns2=30\ns3=20\ns4=20\nheader_protection_key=%s\ncontent_padding_addition=%s\npublic_key=%s\nendpoint=127.0.0.1:%d\nallowed_ip=0.0.0.0/0\n",
		clientPrivHex, headerProtectionKeyHex, contentPaddingAddition, serverPubHex, listenPort)
	if err := clientDev.IpcSet(clientConf); err != nil {
		t.Fatalf("client IpcSet: %v", err)
	}
	if err := clientDev.Up(); err != nil {
		t.Fatalf("client Up: %v", err)
	}

	dialCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	var conn net.Conn
	for {
		c, dialErr := clientNet.DialContext(dialCtx, "tcp", "10.202.9.9:9999")
		if dialErr == nil {
			conn = c
			break
		}
		select {
		case <-dialCtx.Done():
			t.Fatalf("client dial never succeeded: %v", dialErr)
		case <-time.After(100 * time.Millisecond):
		}
	}
	defer conn.Close()

	if _, err := conn.Write([]byte(wantRequest)); err != nil {
		t.Fatalf("client write: %v", err)
	}
	if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
		t.Fatalf("SetReadDeadline: %v", err)
	}
	reply := make([]byte, len(wantReply))
	if _, err := io.ReadFull(conn, reply); err != nil {
		t.Fatalf("client read reply: %v", err)
	}
	if string(reply) != wantReply {
		t.Fatalf("client got reply %q, want %q", reply, wantReply)
	}

	select {
	case err := <-serverDone:
		if err != nil {
			t.Fatalf("server side: %v", err)
		}
	case <-time.After(5 * time.Second):
		t.Fatal("timed out waiting for the server side to finish")
	}
}

func TestBuildUAPIConfigRandomTrailersAndDisableCookiesLines(t *testing.T) {
	priv, _, err := wireguard.GenerateWireguardKeypair()
	if err != nil {
		t.Fatalf("generate keypair: %v", err)
	}
	inst := amneziawg.Instance{PrivateKey: priv}

	// Unlike HeaderProtectionKey/ContentPaddingAddition, these two lines
	// must always be present -- see DeviceOptions.RandomTrailers's own doc
	// comment on why an absent line (instead of an explicit "false") would
	// break the reconfigure-in-place diff for a true->false edit.
	conf, err := buildUAPIConfig(inst, DeviceOptions{})
	if err != nil {
		t.Fatalf("buildUAPIConfig with empty options: %v", err)
	}
	if !strings.Contains(conf, "random_trailers=false\n") {
		t.Errorf("expected an explicit random_trailers=false line even when unset, got:\n%s", conf)
	}
	if !strings.Contains(conf, "disable_cookies=false\n") {
		t.Errorf("expected an explicit disable_cookies=false line even when unset, got:\n%s", conf)
	}

	conf, err = buildUAPIConfig(inst, DeviceOptions{RandomTrailers: true, DisableCookies: true})
	if err != nil {
		t.Fatalf("buildUAPIConfig with both enabled: %v", err)
	}
	if !strings.Contains(conf, "random_trailers=true\n") {
		t.Errorf("expected a random_trailers=true line, got:\n%s", conf)
	}
	if !strings.Contains(conf, "disable_cookies=true\n") {
		t.Errorf("expected a disable_cookies=true line, got:\n%s", conf)
	}
}

// TestNewDeviceRandomTrailersAndDisableCookiesRoundTrip is the real proof
// behind AmneziaWG 3.1's two new device-wide toggles: a genuine amneziawg-go
// client with matching random_trailers=true/disable_cookies=true UAPI lines
// completes a real handshake against a Device built via NewDevice/
// DeviceOptions and exchanges real application data both directions through
// it. This specifically exercises amneziawg-go's receive.go size-matching
// path for RandomTrailers (device_test.go's HeaderProtection test doesn't
// enable it), which only accepts a message when
// `size == expectedSize || randomTrailers && size > expectedSize` -- proof
// that setting it on both ends really does interoperate, not just that
// IpcSet accepts the value.
func TestNewDeviceRandomTrailersAndDisableCookiesRoundTrip(t *testing.T) {
	serverPriv, serverPub, err := wireguard.GenerateWireguardKeypair()
	if err != nil {
		t.Fatalf("generate server keypair: %v", err)
	}
	clientPriv, clientPub, err := wireguard.GenerateWireguardKeypair()
	if err != nil {
		t.Fatalf("generate client keypair: %v", err)
	}

	const listenPort = 58721 // fixed loopback test port, distinct from every other test in this package

	inst := amneziawg.Instance{
		Id:            3,
		InterfaceName: "awgtest3",
		ListenPort:    listenPort,
		PrivateKey:    serverPriv,
		PublicKey:     serverPub,
		Address:       []string{"10.203.0.1/24"},
		MTU:           1420,
		Peers: []amneziawg.Peer{{
			Email:      "trailer-peer@example.com",
			PublicKey:  clientPub,
			AllowedIPs: []string{"10.203.0.2/32"},
		}},
	}

	opts := DeviceOptions{RandomTrailers: true, DisableCookies: true}
	dev, err := newUnconfiguredDevice(inst, opts)
	if err != nil {
		t.Fatalf("newUnconfiguredDevice: %v", err)
	}
	defer dev.Close()

	const wantRequest = "hello from client, with a trailer"
	const wantReply = "hello from server, with a trailer"
	serverDone := make(chan error, 1)
	AttachTCPForwarder(dev.Stack, func(conn *gonet.TCPConn, dest netip.AddrPort) {
		defer conn.Close()
		buf := make([]byte, len(wantRequest))
		if _, err := io.ReadFull(conn, buf); err != nil {
			serverDone <- fmt.Errorf("server read: %w", err)
			return
		}
		if string(buf) != wantRequest {
			serverDone <- fmt.Errorf("server got %q, want %q", buf, wantRequest)
			return
		}
		if _, err := conn.Write([]byte(wantReply)); err != nil {
			serverDone <- fmt.Errorf("server write: %w", err)
			return
		}
		serverDone <- nil
	})

	// Configure (IpcSet) must come after AttachTCPForwarder -- see
	// newUnconfiguredDevice's doc comment.
	if err := dev.Configure(inst, opts); err != nil {
		t.Fatalf("Configure: %v", err)
	}

	clientTun, clientNet, err := netstack.CreateNetTUN(
		[]netip.Addr{netip.MustParseAddr("10.203.0.2")},
		[]netip.Addr{netip.MustParseAddr("1.1.1.1")}, 1420)
	if err != nil {
		t.Fatalf("client CreateNetTUN: %v", err)
	}
	clientDev := device.NewDevice(clientTun, awgconn.NewDefaultBind(), device.NewLogger(device.LogLevelSilent, ""))
	defer clientDev.Close()

	clientPrivHex, err := wireguard.KeyToHex(clientPriv)
	if err != nil {
		t.Fatalf("client key to hex: %v", err)
	}
	serverPubHex, err := wireguard.KeyToHex(serverPub)
	if err != nil {
		t.Fatalf("server key to hex: %v", err)
	}
	clientConf := fmt.Sprintf(
		"private_key=%s\nrandom_trailers=true\ndisable_cookies=true\npublic_key=%s\nendpoint=127.0.0.1:%d\nallowed_ip=0.0.0.0/0\n",
		clientPrivHex, serverPubHex, listenPort)
	if err := clientDev.IpcSet(clientConf); err != nil {
		t.Fatalf("client IpcSet: %v", err)
	}
	if err := clientDev.Up(); err != nil {
		t.Fatalf("client Up: %v", err)
	}

	dialCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	var conn net.Conn
	for {
		c, dialErr := clientNet.DialContext(dialCtx, "tcp", "10.203.9.9:9999")
		if dialErr == nil {
			conn = c
			break
		}
		select {
		case <-dialCtx.Done():
			t.Fatalf("client dial never succeeded: %v", dialErr)
		case <-time.After(100 * time.Millisecond):
		}
	}
	defer conn.Close()

	if _, err := conn.Write([]byte(wantRequest)); err != nil {
		t.Fatalf("client write: %v", err)
	}
	if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
		t.Fatalf("SetReadDeadline: %v", err)
	}
	reply := make([]byte, len(wantReply))
	if _, err := io.ReadFull(conn, reply); err != nil {
		t.Fatalf("client read reply: %v", err)
	}
	if string(reply) != wantReply {
		t.Fatalf("client got reply %q, want %q", reply, wantReply)
	}

	select {
	case err := <-serverDone:
		if err != nil {
			t.Fatalf("server side: %v", err)
		}
	case <-time.After(5 * time.Second):
		t.Fatal("timed out waiting for the server side to finish")
	}
}

// TestValidatedObfuscationAlwaysApplies pins the contract ValidateObfuscation
// exists for: whatever it accepts, amneziawg-go's own IpcSet must accept too.
func TestValidatedObfuscationAlwaysApplies(t *testing.T) {
	priv, pub, err := wireguard.GenerateWireguardKeypair()
	if err != nil {
		t.Fatalf("server keypair: %v", err)
	}
	_, peerPub, err := wireguard.GenerateWireguardKeypair()
	if err != nil {
		t.Fatalf("peer keypair: %v", err)
	}
	base := amneziawg.Obfuscation31{Jc: 4, Jmin: 40, Jmax: 70, S1: 20, S2: 30, S3: 20, S4: 20}

	cases := []struct {
		name string
		mut  func(*amneziawg.Obfuscation31)
	}{
		{"generated defaults", func(o *amneziawg.Obfuscation31) { *o = amneziawg.GenerateObfuscation31() }},
		{"S1 over uint16", func(o *amneziawg.Obfuscation31) { o.S1 = 70000 }},
		{"S2 over uint16", func(o *amneziawg.Obfuscation31) { o.S2 = 70000 }},
		{"negative Jc", func(o *amneziawg.Obfuscation31) { o.Jc = -1 }},
		{"negative Jmin and Jmax", func(o *amneziawg.Obfuscation31) { o.Jmin, o.Jmax = -5, -1 }},
		{"Jc over uint32", func(o *amneziawg.Obfuscation31) { o.Jc = 5000000000 }},
		{"I1 unknown tag", func(o *amneziawg.Obfuscation31) { o.I1 = "<rand 100>" }},
		{"I1 missing close", func(o *amneziawg.Obfuscation31) { o.I1 = "<r 100" }},
		{"I1 empty tag", func(o *amneziawg.Obfuscation31) { o.I1 = "<>" }},
		// The specs validateObfChain deliberately accepts must really apply.
		{"I1 chained tags", func(o *amneziawg.Obfuscation31) { o.I1 = "<b ff00><r 10>" }},
		{"I1 valueless tag", func(o *amneziawg.Obfuscation31) { o.I1 = "<t><rc 5>" }},
		{"I1 no tags at all", func(o *amneziawg.Obfuscation31) { o.I1 = "plain text" }},
	}

	for i, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			o := base
			tc.mut(&o)
			if err := amneziawg.ValidateObfuscation(o); err != nil {
				return // rejected before saving, which is the whole point
			}
			inst := amneziawg.Instance{
				Id: 88, InterfaceName: "awgcontract", ListenPort: 58900 + i,
				PrivateKey: priv, PublicKey: pub,
				Address: []string{"10.198.0.1/24"}, MTU: 1420,
				Obfuscation: o,
				Peers: []amneziawg.Peer{{
					Email: "contract@example.com", PublicKey: peerPub,
					AllowedIPs: []string{"10.198.0.2/32"},
				}},
			}
			opts := DeviceOptions{
				HeaderProtectionKey:    o.HeaderProtectionKey,
				ContentPaddingAddition: o.ContentPaddingAddition,
				RekeyAfterTime:         o.RekeyAfterTime,
				RekeyTimeout:           o.RekeyTimeout,
				RejectAfterTime:        o.RejectAfterTime,
				KeepaliveTimeout:       o.KeepaliveTimeout,
				MaxHandshakeAttempts:   o.MaxHandshakeAttempts,
				RandomTrailers:         o.RandomTrailers,
				DisableCookies:         o.DisableCookies,
			}
			dev, err := newUnconfiguredDevice(inst, opts)
			if err != nil {
				t.Fatalf("newUnconfiguredDevice: %v", err)
			}
			defer dev.Close()
			conf, err := buildUAPIConfig(inst, opts)
			if err != nil {
				t.Fatalf("buildUAPIConfig: %v", err)
			}
			if err := dev.IpcSet(conf); err != nil {
				t.Fatalf("ValidateObfuscation accepted this config but amneziawg-go rejected it: %v", err)
			}
		})
	}
}
