package ike

import (
	"bytes"
	"crypto/cipher"
	"encoding/binary"
	"encoding/hex"
	"errors"
	"math"
	"net"
	"testing"
)

type espRepeatingReader byte

func (value espRepeatingReader) Read(destination []byte) (int, error) {
	for index := range destination {
		destination[index] = byte(value)
	}
	return len(destination), nil
}

func TestESPTunnelRoundTripNegotiatedSuites(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name       string
		encryption string
		integrity  string
		encKey     []byte
		authKey    []byte
	}{
		{
			name:       "AES128-SHA1",
			encryption: "aes-cbc-128",
			integrity:  "hmac-sha1-96",
			encKey:     bytes.Repeat([]byte{0x11}, 16),
			authKey:    bytes.Repeat([]byte{0x22}, 20),
		},
		{
			name:       "AES256-SHA256",
			encryption: "aes-cbc-256",
			integrity:  "hmac-sha2-256-128",
			encKey:     bytes.Repeat([]byte{0x33}, 32),
			authKey:    bytes.Repeat([]byte{0x44}, 32),
		},
	}
	for _, test := range tests {
		test := test
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			tunnel := mustTestESPTunnel(t, test.encryption, test.integrity, test.encKey, test.authKey)
			outbound := testIPv4UDPPacket(
				net.IPv4(10, 0, 0, 2),
				net.IPv4(10, 0, 0, 9),
				40123,
				50600,
				[]byte("REGISTER"),
			)
			protected, err := tunnel.seal(outbound)
			if err != nil {
				t.Fatalf("seal: %v", err)
			}
			if got := binary.BigEndian.Uint32(protected[0:4]); got != 0x11223344 {
				t.Fatalf("SPI = %#x, want %#x", got, uint32(0x11223344))
			}
			if got := binary.BigEndian.Uint32(protected[4:8]); got != 1 {
				t.Fatalf("sequence = %d, want 1", got)
			}

			// A peer opens the outbound SA with the same SPI and key material.
			peer := mustTestESPTunnel(t, test.encryption, test.integrity, test.encKey, test.authKey)
			peer.initiatorSelectors, peer.responderSelectors =
				peer.responderSelectors, peer.initiatorSelectors
			cleartext, err := peer.open(protected)
			if err != nil {
				t.Fatalf("open: %v", err)
			}
			if !bytes.Equal(cleartext, outbound) {
				t.Fatalf("round trip changed the inner packet")
			}
		})
	}
}

func TestESPEncryptionMatchesRFC3602TunnelModeVector(t *testing.T) {
	t.Parallel()
	// RFC 3602 section 4, case #7. Authentication is intentionally outside
	// that RFC vector, so this assertion covers ESP SPI/sequence/IV layout,
	// tunnel-mode padding/trailer, and all 96 AES-CBC ciphertext octets.
	key := mustDecodeHex(t, "0123456789abcdef0123456789abcdef")
	iv := mustDecodeHex(t, "f4e765244f6407adf13dc1380f673f37")
	innerPacket := mustDecodeHex(t,
		"45000054090400004001f988c0a87b03c0a87bc8"+
			"08009f76a90a0100b49c083d02a20400"+
			"08090a0b0c0d0e0f1011121314151617"+
			"18191a1b1c1d1e1f2021222324252627"+
			"28292a2b2c2d2e2f3031323334353637",
	)
	expectedCiphertext := mustDecodeHex(t,
		"773b5241a4c449225e4f3ce5ed611b0c"+
			"237ca96cf74a93013c1b0ea1a0cf70f8"+
			"e4ecaec78ac53aad7a0f022b859243c6"+
			"47752e94a859352b8a4d4d2decd136e5"+
			"c177f132ad3fbfb2201ac9904c74ee0a"+
			"109e0ca1e4dfe9d5a100b842f1c22f0d",
	)
	direction, err := newESPDirection(
		0x8765,
		key,
		bytes.Repeat([]byte{0x5a}, 20),
		"aes-cbc-128",
		"hmac-sha1-96",
		bytes.NewReader(iv),
	)
	if err != nil {
		t.Fatal(err)
	}
	direction.sequence = 1
	packet, err := direction.seal(innerPacket, 4)
	if err != nil {
		t.Fatal(err)
	}
	if got := packet[:8]; !bytes.Equal(got, mustDecodeHex(t, "0000876500000002")) {
		t.Fatalf("ESP header = %x", got)
	}
	if got := packet[8:24]; !bytes.Equal(got, iv) {
		t.Fatalf("ESP IV = %x", got)
	}
	if got := packet[24 : 24+len(expectedCiphertext)]; !bytes.Equal(got, expectedCiphertext) {
		t.Fatalf("ESP ciphertext = %x, want %x", got, expectedCiphertext)
	}
}

func TestESPRejectsTamperWrongSPIAndReplay(t *testing.T) {
	t.Parallel()
	tunnel := mustTestESPTunnel(
		t,
		"aes-cbc-128",
		"hmac-sha1-96",
		bytes.Repeat([]byte{0x51}, 16),
		bytes.Repeat([]byte{0x61}, 20),
	)
	peer := mustTestESPTunnel(
		t,
		"aes-cbc-128",
		"hmac-sha1-96",
		bytes.Repeat([]byte{0x51}, 16),
		bytes.Repeat([]byte{0x61}, 20),
	)
	peer.initiatorSelectors, peer.responderSelectors =
		peer.responderSelectors, peer.initiatorSelectors
	inner := testIPv4UDPPacket(
		net.IPv4(10, 0, 0, 2),
		net.IPv4(10, 0, 0, 9),
		42000,
		50600,
		[]byte("payload"),
	)
	protected, err := tunnel.seal(inner)
	if err != nil {
		t.Fatal(err)
	}

	tampered := append([]byte(nil), protected...)
	tampered[len(tampered)-1] ^= 0x80
	if _, err := peer.open(tampered); !errors.Is(err, errESPAuthentication) {
		t.Fatalf("tampered packet error = %v, want authentication failure", err)
	}

	wrongSPI := append([]byte(nil), protected...)
	wrongSPI[0] ^= 0x01
	if _, err := peer.open(wrongSPI); err == nil {
		t.Fatal("packet with wrong SPI was accepted")
	}

	if _, err := peer.open(protected); err != nil {
		t.Fatalf("first authenticated packet: %v", err)
	}
	if _, err := peer.open(protected); !errors.Is(err, errESPReplay) {
		t.Fatalf("replayed packet error = %v, want replay rejection", err)
	}
}

func TestESPReplayWindowAcceptsAuthenticatedReordering(t *testing.T) {
	t.Parallel()
	sender := mustDefaultESPTunnel(t)
	receiver := mustDefaultESPTunnel(t)
	receiver.initiatorSelectors, receiver.responderSelectors =
		receiver.responderSelectors, receiver.initiatorSelectors
	var protected [][]byte
	for index := 0; index < 3; index++ {
		packet := testIPv4UDPPacket(
			net.IPv4(10, 0, 0, 2),
			net.IPv4(10, 0, 0, 9),
			uint16(40000+index),
			50600,
			[]byte{byte(index)},
		)
		value, err := sender.seal(packet)
		if err != nil {
			t.Fatal(err)
		}
		protected = append(protected, value)
	}
	for _, index := range []int{2, 0, 1} {
		if _, err := receiver.open(protected[index]); err != nil {
			t.Fatalf("open sequence %d: %v", index+1, err)
		}
	}
	if _, err := receiver.open(protected[0]); !errors.Is(err, errESPReplay) {
		t.Fatalf("duplicate reordered packet error = %v", err)
	}
}

func TestESPRejectsAuthenticatedInvalidPaddingWithoutConsumingSequence(t *testing.T) {
	t.Parallel()
	sender := mustDefaultESPTunnel(t)
	receiver := mustDefaultESPTunnel(t)
	receiver.initiatorSelectors, receiver.responderSelectors =
		receiver.responderSelectors, receiver.initiatorSelectors
	inner := testIPv4UDPPacket(
		net.IPv4(10, 0, 0, 2),
		net.IPv4(10, 0, 0, 9),
		40000,
		50600,
		[]byte("one"),
	)
	protected, err := sender.seal(inner)
	if err != nil {
		t.Fatal(err)
	}
	malformed := append([]byte(nil), protected...)
	rewriteESPPlaintext(t, receiver.inbound, malformed, func(plaintext []byte) {
		paddingLength := int(plaintext[len(plaintext)-2])
		if paddingLength == 0 {
			plaintext[len(plaintext)-2] = 1
			plaintext[len(plaintext)-3] = 0xff
			return
		}
		plaintext[len(plaintext)-2-paddingLength] ^= 0xff
	})
	if _, err := receiver.open(malformed); err == nil {
		t.Fatal("authenticated packet with invalid padding was accepted")
	}
	if _, err := receiver.open(protected); err != nil {
		t.Fatalf("invalid packet consumed the sequence number: %v", err)
	}
}

func TestESPTrafficSelectorsAreEnforcedInBothDirections(t *testing.T) {
	t.Parallel()
	tunnel := mustDefaultESPTunnel(t)
	disallowed := testIPv4UDPPacket(
		net.IPv4(10, 0, 0, 2),
		net.IPv4(203, 0, 113, 10),
		40000,
		50600,
		nil,
	)
	if _, err := tunnel.seal(disallowed); err == nil {
		t.Fatal("outbound packet outside responder selector was accepted")
	}

	sender := mustDefaultESPTunnel(t)
	receiver := mustDefaultESPTunnel(t)
	receiver.initiatorSelectors, receiver.responderSelectors =
		receiver.responderSelectors, receiver.initiatorSelectors
	allowed := testIPv4UDPPacket(
		net.IPv4(10, 0, 0, 2),
		net.IPv4(10, 0, 0, 9),
		40000,
		50600,
		nil,
	)
	protected, err := sender.seal(allowed)
	if err != nil {
		t.Fatal(err)
	}
	rewriteESPPlaintext(t, receiver.inbound, protected, func(plaintext []byte) {
		copy(plaintext[16:20], net.IPv4(203, 0, 113, 10).To4())
	})
	if _, err := receiver.open(protected); err == nil {
		t.Fatal("authenticated inbound packet outside selectors was accepted")
	}
}

func TestESPSequenceExhaustionRequiresRekey(t *testing.T) {
	t.Parallel()
	tunnel := mustDefaultESPTunnel(t)
	tunnel.outbound.sequence = math.MaxUint32
	packet := testIPv4UDPPacket(
		net.IPv4(10, 0, 0, 2),
		net.IPv4(10, 0, 0, 9),
		40000,
		50600,
		nil,
	)
	if _, err := tunnel.seal(packet); err == nil {
		t.Fatal("ESP sequence wrapped instead of requiring rekey")
	}
}

func TestParseInnerIPv6ESP(t *testing.T) {
	t.Parallel()
	packet := make([]byte, 40+8)
	packet[0] = 0x60
	binary.BigEndian.PutUint16(packet[4:6], 8)
	packet[6] = 50
	packet[7] = 64
	copy(packet[8:24], net.ParseIP("2001:db8::1").To16())
	copy(packet[24:40], net.ParseIP("2001:db8::2").To16())
	metadata, err := parseInnerPacket(packet)
	if err != nil {
		t.Fatal(err)
	}
	if metadata.protocol != 50 || metadata.nextHeader != 41 {
		t.Fatalf("metadata = %+v", metadata)
	}
}

func mustDefaultESPTunnel(t *testing.T) *espTunnel {
	t.Helper()
	return mustTestESPTunnel(
		t,
		"aes-cbc-128",
		"hmac-sha1-96",
		bytes.Repeat([]byte{0x31}, 16),
		bytes.Repeat([]byte{0x41}, 20),
	)
}

func mustTestESPTunnel(
	t *testing.T,
	encryption string,
	integrity string,
	encryptionKey []byte,
	authenticationKey []byte,
) *espTunnel {
	t.Helper()
	selector := func(ip net.IP) trafficSelector {
		return trafficSelector{
			StartPort: 0,
			EndPort:   65535,
			StartIP:   append(net.IP(nil), ip.To4()...),
			EndIP:     append(net.IP(nil), ip.To4()...),
		}
	}
	tunnel, err := newESPTunnel(ChildSAConfig{
		InboundSPI:         0x11223344,
		OutboundSPI:        0x11223344,
		Encryption:         encryption,
		Integrity:          integrity,
		InboundEncKey:      encryptionKey,
		InboundAuthKey:     authenticationKey,
		OutboundEncKey:     encryptionKey,
		OutboundAuthKey:    authenticationKey,
		InitiatorSelectors: []trafficSelector{selector(net.IPv4(10, 0, 0, 2))},
		ResponderSelectors: []trafficSelector{selector(net.IPv4(10, 0, 0, 9))},
	}, espRepeatingReader(0xa5))
	if err != nil {
		t.Fatal(err)
	}
	return tunnel
}

func testIPv4UDPPacket(
	source net.IP,
	destination net.IP,
	sourcePort uint16,
	destinationPort uint16,
	payload []byte,
) []byte {
	packet := make([]byte, 20+8+len(payload))
	packet[0] = 0x45
	binary.BigEndian.PutUint16(packet[2:4], uint16(len(packet)))
	packet[8] = 64
	packet[9] = 17
	copy(packet[12:16], source.To4())
	copy(packet[16:20], destination.To4())
	binary.BigEndian.PutUint16(packet[20:22], sourcePort)
	binary.BigEndian.PutUint16(packet[22:24], destinationPort)
	binary.BigEndian.PutUint16(packet[24:26], uint16(8+len(payload)))
	copy(packet[28:], payload)
	return packet
}

func rewriteESPPlaintext(
	t *testing.T,
	direction *espDirection,
	packet []byte,
	rewrite func([]byte),
) {
	t.Helper()
	blockSize := direction.block.BlockSize()
	authenticatedLength := len(packet) - direction.icvLength
	iv := packet[espHeaderLength : espHeaderLength+blockSize]
	ciphertext := packet[espHeaderLength+blockSize : authenticatedLength]
	plaintext := make([]byte, len(ciphertext))
	cipher.NewCBCDecrypter(direction.block, iv).CryptBlocks(plaintext, ciphertext)
	rewrite(plaintext)
	cipher.NewCBCEncrypter(direction.block, iv).CryptBlocks(ciphertext, plaintext)
	copy(packet[authenticatedLength:], direction.authenticationCode(packet[:authenticatedLength]))
}

func mustDecodeHex(t *testing.T, value string) []byte {
	t.Helper()
	decoded, err := hex.DecodeString(value)
	if err != nil {
		t.Fatal(err)
	}
	return decoded
}
