package client

import (
	"bytes"
	"net"
	"strconv"
	"testing"
	"time"

	"github.com/duffy/usb-server/internal/crypto"
	"github.com/duffy/usb-server/internal/protocol"
)

const (
	testTok1 = "111111111111111111111111111111111111111111="
	testTok2 = "222222222222222222222222222222222222222222="
	testTok3 = "333333333333333333333333333333333333333333="
)

func testSecret(t *testing.T) *crypto.TunnelSecret {
	t.Helper()
	s, err := crypto.DeriveTunnelSecret(testTok1, testTok2, testTok3)
	if err != nil {
		t.Fatalf("DeriveTunnelSecret: %v", err)
	}
	return s
}

const testTunnelID = "0123456789abcdef" // exactly TunnelHeaderSize

func TestHandshakeRoundTrip(t *testing.T) {
	s := testSecret(t)
	token := s.PeerToken(testTunnelID)

	greeting, err := buildHandshake(testTunnelID, token)
	if err != nil {
		t.Fatalf("buildHandshake: %v", err)
	}
	if len(greeting) != handshakeSize {
		t.Fatalf("greeting is %d bytes, want %d", len(greeting), handshakeSize)
	}

	gotID, gotToken, err := parseHandshake(greeting)
	if err != nil {
		t.Fatalf("parseHandshake: %v", err)
	}
	if gotID != testTunnelID {
		t.Errorf("tunnel ID = %q, want %q", gotID, testTunnelID)
	}
	if gotToken != token {
		t.Errorf("token = %q, want %q", gotToken, token)
	}
}

func TestParseHandshakeRejectsMalformed(t *testing.T) {
	s := testSecret(t)
	valid, _ := buildHandshake(testTunnelID, s.PeerToken(testTunnelID))

	tests := []struct {
		name string
		data []byte
	}{
		{"empty", nil},
		{"truncated", valid[:handshakeSize-1]},
		{"too long", append(append([]byte{}, valid...), 0x00)},
		{"bad magic", func() []byte {
			b := append([]byte{}, valid...)
			b[0] = 'X'
			return b
		}()},
		{"unsupported version", func() []byte {
			b := append([]byte{}, valid...)
			b[4] = 99
			return b
		}()},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if _, _, err := parseHandshake(tt.data); err == nil {
				t.Error("malformed handshake was accepted")
			}
		})
	}
}

func TestHandshakeReplyStatuses(t *testing.T) {
	if err := parseHandshakeReply(buildHandshakeReply(directAccepted)); err != nil {
		t.Errorf("accepted reply reported an error: %v", err)
	}
	for _, status := range []byte{directUnknownTun, directBadToken, directWrongVerson, 99} {
		if err := parseHandshakeReply(buildHandshakeReply(status)); err == nil {
			t.Errorf("status %d was treated as success", status)
		}
	}
}

func TestBuildHandshakeRejectsBadInput(t *testing.T) {
	s := testSecret(t)
	good := s.PeerToken(testTunnelID)

	if _, err := buildHandshake("short", good); err == nil {
		t.Error("a wrong-length tunnel ID was accepted")
	}
	if _, err := buildHandshake(testTunnelID, "not-hex"); err == nil {
		t.Error("a non-hex token was accepted")
	}
	if _, err := buildHandshake(testTunnelID, "abcd"); err == nil {
		t.Error("a short token was accepted")
	}
}

// A listener must only hand over connections whose peer proves group
// membership. The relay knows tunnel IDs, so the token is what stops it — or
// anyone else who reaches the port — from taking a device over.
func TestListenerAcceptsOnlyValidToken(t *testing.T) {
	s := testSecret(t)

	dl, err := newDirectListener(0, s)
	if err != nil {
		t.Fatalf("newDirectListener: %v", err)
	}
	defer dl.Close()

	accepted, err := dl.Expect(testTunnelID)
	if err != nil {
		t.Fatalf("Expect: %v", err)
	}

	addr := net.JoinHostPort("127.0.0.1", strconv.Itoa(dl.Port()))

	t.Run("wrong token is rejected", func(t *testing.T) {
		other, _ := crypto.DeriveTunnelSecret(testTok1, testTok2, "different")
		greeting, _ := buildHandshake(testTunnelID, other.PeerToken(testTunnelID))

		conn, err := net.DialTimeout("tcp", addr, time.Second)
		if err != nil {
			t.Fatalf("dial: %v", err)
		}
		defer conn.Close()

		conn.Write(greeting)
		reply := make([]byte, handshakeReplySize)
		conn.SetReadDeadline(time.Now().Add(2 * time.Second))
		if _, err := readFull(conn, reply); err != nil {
			t.Fatalf("reading reply: %v", err)
		}
		if err := parseHandshakeReply(reply); err == nil {
			t.Fatal("listener accepted a connection with the wrong token")
		}
	})

	t.Run("unknown tunnel is rejected", func(t *testing.T) {
		greeting, _ := buildHandshake("fedcba9876543210", s.PeerToken("fedcba9876543210"))

		conn, err := net.DialTimeout("tcp", addr, time.Second)
		if err != nil {
			t.Fatalf("dial: %v", err)
		}
		defer conn.Close()

		conn.Write(greeting)
		reply := make([]byte, handshakeReplySize)
		conn.SetReadDeadline(time.Now().Add(2 * time.Second))
		readFull(conn, reply)
		if err := parseHandshakeReply(reply); err == nil {
			t.Fatal("listener accepted a connection for an unregistered tunnel")
		}
	})

	t.Run("valid token is accepted", func(t *testing.T) {
		conn, _, err := dialDirect([]string{addr}, testTunnelID, s.PeerToken(testTunnelID))
		if err != nil {
			t.Fatalf("dialDirect: %v", err)
		}
		defer conn.Close()

		select {
		case got := <-accepted:
			if got == nil {
				t.Fatal("listener delivered a nil connection")
			}
			got.Close()
		case <-time.After(2 * time.Second):
			t.Fatal("listener never delivered the accepted connection")
		}
	})
}

// End-to-end over a real socket pair: the two ends must agree on framing and
// on which direction each encrypts in.
func TestDirectTunnelCarriesTrafficBothWays(t *testing.T) {
	s := testSecret(t)

	dl, err := newDirectListener(0, s)
	if err != nil {
		t.Fatalf("newDirectListener: %v", err)
	}
	defer dl.Close()

	accepted, err := dl.Expect(testTunnelID)
	if err != nil {
		t.Fatalf("Expect: %v", err)
	}

	addr := net.JoinHostPort("127.0.0.1", strconv.Itoa(dl.Port()))
	useConn, _, err := dialDirect([]string{addr}, testTunnelID, s.PeerToken(testTunnelID))
	if err != nil {
		t.Fatalf("dialDirect: %v", err)
	}
	defer useConn.Close()

	var shareConn *directConn
	select {
	case shareConn = <-accepted:
	case <-time.After(2 * time.Second):
		t.Fatal("listener never delivered the connection")
	}
	defer shareConn.Close()

	shareCodec, err := newTunnelCodec(s, testTunnelID, crypto.DirShareToUse)
	if err != nil {
		t.Fatalf("share codec: %v", err)
	}
	useCodec, err := newTunnelCodec(s, testTunnelID, crypto.DirUseToShare)
	if err != nil {
		t.Fatalf("use codec: %v", err)
	}

	// use -> share
	want := []byte("USBIP CMD_SUBMIT payload")
	if err := send(useCodec, directSender(useConn), want); err != nil {
		t.Fatalf("sending use->share: %v", err)
	}
	frame, err := shareConn.ReadFrame()
	if err != nil {
		t.Fatalf("share reading frame: %v", err)
	}
	got, err := shareCodec.decode(frame)
	if err != nil {
		t.Fatalf("share decoding frame: %v", err)
	}
	if !bytes.Equal(got, want) {
		t.Errorf("share received %q, want %q", got, want)
	}

	// share -> use
	want2 := []byte("USBIP RET_SUBMIT payload")
	if err := send(shareCodec, directSender(shareConn), want2); err != nil {
		t.Fatalf("sending share->use: %v", err)
	}
	frame2, err := useConn.ReadFrame()
	if err != nil {
		t.Fatalf("use reading frame: %v", err)
	}
	got2, err := useCodec.decode(frame2)
	if err != nil {
		t.Fatalf("use decoding frame: %v", err)
	}
	if !bytes.Equal(got2, want2) {
		t.Errorf("use received %q, want %q", got2, want2)
	}
}

func TestDirectConnFramingPreservesBoundaries(t *testing.T) {
	client, server := net.Pipe()
	defer client.Close()
	defer server.Close()

	sender := newDirectConn(client)
	receiver := newDirectConn(server)

	payloads := [][]byte{
		[]byte("a"),
		bytes.Repeat([]byte("x"), 1000),
		[]byte("last one"),
	}

	go func() {
		for _, p := range payloads {
			if err := sender.WriteFrame(p); err != nil {
				return
			}
		}
	}()

	for i, want := range payloads {
		got, err := receiver.ReadFrame()
		if err != nil {
			t.Fatalf("frame %d: %v", i, err)
		}
		if !bytes.Equal(got, want) {
			t.Errorf("frame %d is %d bytes, want %d", i, len(got), len(want))
		}
	}
}

func TestDirectConnRejectsOversizedLength(t *testing.T) {
	client, server := net.Pipe()
	defer client.Close()
	defer server.Close()

	go func() {
		// A length prefix claiming far more than the cap must be refused
		// before anything is allocated.
		client.Write([]byte{0xFF, 0xFF, 0xFF, 0xFF})
	}()

	receiver := newDirectConn(server)
	if _, err := receiver.ReadFrame(); err == nil {
		t.Error("an oversized frame length was accepted")
	}
}

func TestDialDirectFailsWithoutReachableAddress(t *testing.T) {
	s := testSecret(t)

	// Port 1 on loopback refuses immediately, so this stays fast.
	_, _, err := dialDirect([]string{"127.0.0.1:1"}, testTunnelID, s.PeerToken(testTunnelID))
	if err == nil {
		t.Fatal("dialDirect succeeded against a closed port")
	}

	if _, _, err := dialDirect(nil, testTunnelID, s.PeerToken(testTunnelID)); err == nil {
		t.Error("dialDirect succeeded with no candidate addresses")
	}
}

// The dialer races candidates; an unreachable one alongside a good one must
// not stop the good one from winning.
func TestDialDirectPicksTheReachableAddress(t *testing.T) {
	s := testSecret(t)

	dl, err := newDirectListener(0, s)
	if err != nil {
		t.Fatalf("newDirectListener: %v", err)
	}
	defer dl.Close()

	accepted, _ := dl.Expect(testTunnelID)
	good := net.JoinHostPort("127.0.0.1", strconv.Itoa(dl.Port()))

	conn, addr, err := dialDirect(
		[]string{"127.0.0.1:1", good, "127.0.0.1:2"},
		testTunnelID, s.PeerToken(testTunnelID))
	if err != nil {
		t.Fatalf("dialDirect: %v", err)
	}
	defer conn.Close()

	if addr != good {
		t.Errorf("connected to %s, want %s", addr, good)
	}
	select {
	case c := <-accepted:
		c.Close()
	case <-time.After(2 * time.Second):
		t.Error("listener never saw the connection")
	}
}

func TestLocalEndpointsExcludeLoopback(t *testing.T) {
	if got := localEndpoints(0); got != nil {
		t.Errorf("localEndpoints(0) = %v, want nil — port 0 means no listener", got)
	}

	for _, ep := range localEndpoints(9000) {
		host, port, err := net.SplitHostPort(ep)
		if err != nil {
			t.Errorf("endpoint %q is not host:port: %v", ep, err)
			continue
		}
		if port != "9000" {
			t.Errorf("endpoint %q has port %q, want 9000", ep, port)
		}
		ip := net.ParseIP(host)
		if ip == nil {
			t.Errorf("endpoint %q has an unparseable host", ep)
			continue
		}
		if ip.IsLoopback() {
			t.Errorf("endpoint %q is loopback; a peer cannot reach that", ep)
		}
	}
}

func TestTunnelCodecNilPassesThrough(t *testing.T) {
	var codec *tunnelCodec

	if codec.encrypted() {
		t.Error("a nil codec reported itself as encrypted")
	}

	payload := []byte("cleartext")
	encoded, err := codec.encode(payload)
	if err != nil {
		t.Fatalf("encode: %v", err)
	}
	if !bytes.Equal(encoded, payload) {
		t.Error("a nil codec altered the payload")
	}

	decoded, err := codec.decode(encoded)
	if err != nil {
		t.Fatalf("decode: %v", err)
	}
	if !bytes.Equal(decoded, payload) {
		t.Error("round trip through a nil codec changed the payload")
	}
}

func TestTunnelCodecEncryptsWhenSecretPresent(t *testing.T) {
	s := testSecret(t)

	codec, err := newTunnelCodec(s, testTunnelID, crypto.DirShareToUse)
	if err != nil {
		t.Fatalf("newTunnelCodec: %v", err)
	}
	if !codec.encrypted() {
		t.Fatal("codec with a secret reported itself as unencrypted")
	}

	payload := []byte("this must not appear on the wire")
	encoded, err := codec.encode(payload)
	if err != nil {
		t.Fatalf("encode: %v", err)
	}
	if bytes.Contains(encoded, payload) {
		t.Error("the encoded frame contains its plaintext")
	}

	peer, _ := newTunnelCodec(s, testTunnelID, crypto.DirUseToShare)
	decoded, err := peer.decode(encoded)
	if err != nil {
		t.Fatalf("peer decode: %v", err)
	}
	if !bytes.Equal(decoded, payload) {
		t.Errorf("peer decoded %q, want %q", decoded, payload)
	}
}

func TestConstantTimeEqual(t *testing.T) {
	if !constantTimeEqual("abc", "abc") {
		t.Error("equal strings compared unequal")
	}
	if constantTimeEqual("abc", "abd") {
		t.Error("different strings compared equal")
	}
	if constantTimeEqual("abc", "abcd") {
		t.Error("strings of different length compared equal")
	}
	if !constantTimeEqual("", "") {
		t.Error("empty strings compared unequal")
	}
}

// Guards the assumption baked into the wire format.
func TestTunnelIDFitsHandshake(t *testing.T) {
	if protocol.TunnelHeaderSize != 16 {
		t.Fatalf("TunnelHeaderSize is %d; the handshake layout assumes 16", protocol.TunnelHeaderSize)
	}
	if len(testTunnelID) != protocol.TunnelHeaderSize {
		t.Fatalf("test tunnel ID is %d bytes, want %d", len(testTunnelID), protocol.TunnelHeaderSize)
	}
}

// --- helpers ---

func readFull(conn net.Conn, buf []byte) (int, error) {
	total := 0
	for total < len(buf) {
		n, err := conn.Read(buf[total:])
		total += n
		if err != nil {
			return total, err
		}
	}
	return total, nil
}
