package proxy

import (
	"net"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// buildClientHello constructs a minimal TLS ClientHello with the given SNI.
func buildClientHello(serverName string) []byte {
	// Build SNI extension data.
	var sniExt []byte
	if serverName != "" {
		nameBytes := []byte(serverName)
		nameLen := len(nameBytes)
		// server_name list entry: type(1) + length(2) + name
		entry := []byte{sniHostNameType, byte(nameLen >> 8), byte(nameLen)}
		entry = append(entry, nameBytes...)
		// server_name list: length(2) + entries
		listLen := len(entry)
		sniList := []byte{byte(listLen >> 8), byte(listLen)}
		sniList = append(sniList, entry...)
		// extension header: type(2) + length(2) + data
		extData := sniList
		sniExt = []byte{0x00, 0x00, byte(len(extData) >> 8), byte(len(extData))}
		sniExt = append(sniExt, extData...)
	}

	// Extensions block: length(2) + extensions
	var extensions []byte
	if len(sniExt) > 0 {
		extLen := len(sniExt)
		extensions = []byte{byte(extLen >> 8), byte(extLen)}
		extensions = append(extensions, sniExt...)
	}

	// ClientHello body:
	// version(2) + random(32) + sessionID len(1) + ciphers len(2) + one cipher(2) + comp len(1) + comp(1)
	hello := make([]byte, 0, 256)
	hello = append(hello, 0x03, 0x03)             // client version TLS 1.2
	hello = append(hello, make([]byte, 32)...)    // random
	hello = append(hello, 0x00)                   // session ID length = 0
	hello = append(hello, 0x00, 0x02, 0x00, 0x2f) // cipher suites: length=2, TLS_RSA_WITH_AES_128_CBC_SHA
	hello = append(hello, 0x01, 0x00)             // compression: length=1, null
	hello = append(hello, extensions...)

	// Handshake message: type(1) + length(3) + body
	helloLen := len(hello)
	handshake := []byte{handshakeClientHello, 0x00, byte(helloLen >> 8), byte(helloLen)}
	handshake = append(handshake, hello...)

	// TLS record: type(1) + version(2) + length(2) + body
	recordLen := len(handshake)
	record := []byte{tlsRecordHandshake, 0x03, 0x01, byte(recordLen >> 8), byte(recordLen)}
	record = append(record, handshake...)

	return record
}

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

	hello := buildClientHello("github.com")

	go func() {
		_, _ = client.Write(hello)
	}()

	peeked, name, err := PeekClientHello(server)

	require.NoError(t, err)
	assert.Equal(t, "github.com", name)
	assert.Equal(t, hello, peeked)
}

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

	hello := buildClientHello("")

	go func() {
		_, _ = client.Write(hello)
	}()

	peeked, name, err := PeekClientHello(server)

	require.NoError(t, err)
	assert.Empty(t, name)
	assert.Equal(t, hello, peeked)
}

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

	go func() {
		_, _ = client.Write([]byte("GET / HTTP/1.1\r\n"))
	}()

	_, _, err := PeekClientHello(server)

	require.Error(t, err)
	assert.Contains(t, err.Error(), "not a TLS handshake record")
}

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

	go func() {
		_, _ = client.Write([]byte{0x16, 0x03})
		_ = client.Close()
	}()

	_, _, err := PeekClientHello(server)

	require.Error(t, err)
	assert.Contains(t, err.Error(), "reading TLS record header")
}

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

	// Valid header claiming 100 bytes, but only send 10.
	header := []byte{tlsRecordHandshake, 0x03, 0x01, 0x00, 100}

	go func() {
		_, _ = client.Write(header)
		_, _ = client.Write(make([]byte, 10))
		_ = client.Close()
	}()

	_, _, err := PeekClientHello(server)

	require.Error(t, err)
	assert.Contains(t, err.Error(), "reading TLS record body")
}

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

	hello := buildClientHello("very-long-subdomain.example.github.com")

	go func() {
		_, _ = client.Write(hello)
	}()

	peeked, name, err := PeekClientHello(server)

	require.NoError(t, err)
	assert.Equal(t, "very-long-subdomain.example.github.com", name)
	assert.Equal(t, hello, peeked)
}

// Absolute byte offsets into the wire record produced by buildClientHello.
// Derived from the layout at the top of this file:
//
//	5  TLS record header
//	+ 4  handshake header
//	+ 2  client version
//	+ 32 random
//	+ 1  sessionID len (=0)
//	+ 4  ciphers (len 2 + one 2-byte cipher)
//	+ 2  compression (len 1 + one 1-byte method)
//	= 50 bytes, so extensions length starts at wire offset 50.
const (
	sniExtensionsLenOffset = 50 // first byte of the 2-byte extensions length
	sniFirstExtDataLen     = 54 // first byte of the first extension's data length
	sniExtDataStart        = 56 // first byte of the first extension's data
)

// An extensions-length field that claims to exceed the record is the
// classic buffer-overrun vector for TLS record parsers. The guard at
// sni.go:89 rejects it; this test pins that behavior.
func TestPeekClientHello_extensionsLengthExceedsRecord(t *testing.T) {
	hello := buildClientHello("example.com")
	// Overwrite the extensions length with the maximum 16-bit value.
	hello[sniExtensionsLenOffset] = 0xFF
	hello[sniExtensionsLenOffset+1] = 0xFF

	client, server := net.Pipe()
	defer func() { _ = client.Close() }()
	defer func() { _ = server.Close() }()
	go func() { _, _ = client.Write(hello) }()

	_, _, err := PeekClientHello(server)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "extensions length exceeds record")
}

// An individual extension's data length larger than the remaining
// extensions block would let a crafted ClientHello read past the
// allocated buffer. The guard at sni.go:98 rejects it.
func TestPeekClientHello_extensionDataLengthExceedsBlock(t *testing.T) {
	hello := buildClientHello("x")
	// Overwrite the first extension's data length.
	hello[sniFirstExtDataLen] = 0xFF
	hello[sniFirstExtDataLen+1] = 0xFF

	client, server := net.Pipe()
	defer func() { _ = client.Close() }()
	defer func() { _ = server.Close() }()
	go func() { _, _ = client.Write(hello) }()

	_, _, err := PeekClientHello(server)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "extension data exceeds record")
}

// An SNI extension with listLen == 0 is a protocol violation — the
// guard at sni.go:122 rejects it so parseSNIExtension cannot then
// iterate an empty list and return a garbage empty hostname.
func TestPeekClientHello_emptySNIList(t *testing.T) {
	hello := buildClientHello("x")
	// Overwrite the SNI listLen (first 2 bytes of extension data).
	hello[sniExtDataStart] = 0
	hello[sniExtDataStart+1] = 0

	client, server := net.Pipe()
	defer func() { _ = client.Close() }()
	defer func() { _ = server.Close() }()
	go func() { _, _ = client.Write(hello) }()

	_, _, err := PeekClientHello(server)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "SNI list length mismatch")
}

// The SNI RFC reserves name types other than 0x00 (host_name) for
// future use. Current clients only send host_name, but the parser
// must tolerate a non-DNS name type by treating it as "no SNI" rather
// than returning garbled bytes as the hostname.
func TestPeekClientHello_nonDNSNameType(t *testing.T) {
	hello := buildClientHello("x")
	// nameType sits two bytes into the extension data (after listLen).
	hello[sniExtDataStart+2] = 0x01

	client, server := net.Pipe()
	defer func() { _ = client.Close() }()
	defer func() { _ = server.Close() }()
	go func() { _, _ = client.Write(hello) }()

	_, name, err := PeekClientHello(server)
	require.NoError(t, err)
	assert.Empty(t, name, "non-DNS name type must be skipped and return an empty SNI")
}

// Fuzz the parser with a seed corpus drawn from buildClientHello plus
// a handful of known-bad inputs. The parser must never panic — error
// returns and empty hostnames are both acceptable outcomes.
func FuzzPeekClientHello(f *testing.F) {
	f.Add(buildClientHello("github.com"))
	f.Add(buildClientHello("x"))
	f.Add(buildClientHello(""))
	f.Add([]byte{0x16, 0x03, 0x01, 0x00, 0x00})
	f.Add([]byte("GET / HTTP/1.1\r\n"))

	f.Fuzz(func(_ *testing.T, data []byte) {
		client, server := net.Pipe()
		defer func() { _ = client.Close() }()
		defer func() { _ = server.Close() }()
		go func() {
			_, _ = client.Write(data)
			_ = client.Close()
		}()
		_, _, _ = PeekClientHello(server)
	})
}
