package testutils

import (
	"bytes"

	"github.com/lucas-clemente/quic-go/internal/handshake"
	"github.com/lucas-clemente/quic-go/internal/protocol"
	"github.com/lucas-clemente/quic-go/internal/wire"
)

// Utilities for simulating packet injection and man-in-the-middle (MITM) attacker tests.
// Do not use for non-testing purposes.

// CryptoFrameType uses same types as messageType in crypto_setup.go
type CryptoFrameType uint8

// writePacket returns a new raw packet with the specified header and payload
func writePacket(hdr *wire.ExtendedHeader, data []byte) []byte {
	buf := &bytes.Buffer{}
	hdr.Write(buf, protocol.VersionTLS)
	return append(buf.Bytes(), data...)
}

// packRawPayload returns a new raw payload containing given frames
func packRawPayload(version protocol.VersionNumber, frames []wire.Frame) []byte {
	buf := new(bytes.Buffer)
	for _, cf := range frames {
		cf.Write(buf, version)
	}
	return buf.Bytes()
}

// ComposeCryptoFrame returns a new empty crypto frame of the specified
// type padded to size bytes with zeroes
func ComposeCryptoFrame(cft CryptoFrameType, size int) *wire.CryptoFrame {
	data := make([]byte, size)
	data[0] = byte(cft)
	return &wire.CryptoFrame{
		Offset: 0,
		Data:   data,
	}
}

// ComposeConnCloseFrame returns a new Connection Close frame with a generic error
func ComposeConnCloseFrame() *wire.ConnectionCloseFrame {
	return &wire.ConnectionCloseFrame{
		IsApplicationError: true,
		ErrorCode:          0,
		ReasonPhrase:       "mitm attacker",
	}
}

// ComposeAckFrame returns a new Ack Frame that acknowledges all packets between smallest and largest
func ComposeAckFrame(smallest protocol.PacketNumber, largest protocol.PacketNumber) *wire.AckFrame {
	ackRange := wire.AckRange{
		Smallest: smallest,
		Largest:  largest,
	}
	return &wire.AckFrame{
		AckRanges: []wire.AckRange{ackRange},
		DelayTime: 0,
	}
}

// ComposeInitialPacket returns an Initial packet encrypted under key
// (the original destination connection ID) containing specified frames
func ComposeInitialPacket(srcConnID protocol.ConnectionID, destConnID protocol.ConnectionID, version protocol.VersionNumber, key protocol.ConnectionID, frames []wire.Frame) []byte {
	sealer, _ := handshake.NewInitialAEAD(key, protocol.PerspectiveServer)

	// compose payload
	var payload []byte
	if len(frames) == 0 {
		payload = make([]byte, protocol.MinInitialPacketSize)
	} else {
		payload = packRawPayload(version, frames)
	}

	// compose Initial header
	payloadSize := len(payload)
	pnLength := protocol.PacketNumberLen4
	length := payloadSize + int(pnLength) + sealer.Overhead()
	hdr := &wire.ExtendedHeader{
		Header: wire.Header{
			IsLongHeader:     true,
			Type:             protocol.PacketTypeInitial,
			SrcConnectionID:  srcConnID,
			DestConnectionID: destConnID,
			Length:           protocol.ByteCount(length),
			Version:          version,
		},
		PacketNumberLen: pnLength,
		PacketNumber:    0x0,
	}

	raw := writePacket(hdr, payload)

	// encrypt payload and header
	payloadOffset := len(raw) - payloadSize
	var encrypted []byte
	encrypted = sealer.Seal(encrypted, payload, hdr.PacketNumber, raw[:payloadOffset])
	hdrBytes := raw[0:payloadOffset]
	encrypted = append(hdrBytes, encrypted...)
	pnOffset := payloadOffset - int(pnLength) // packet number offset
	sealer.EncryptHeader(
		encrypted[payloadOffset:payloadOffset+16], // first 16 bytes of payload (sample)
		&encrypted[0],                     // first byte of header
		encrypted[pnOffset:payloadOffset], // packet number bytes
	)
	return encrypted
}

// ComposeRetryPacket returns a new raw Retry Packet
func ComposeRetryPacket(
	srcConnID protocol.ConnectionID,
	destConnID protocol.ConnectionID,
	origDestConnID protocol.ConnectionID,
	token []byte,
	version protocol.VersionNumber,
) []byte {
	hdr := &wire.ExtendedHeader{
		Header: wire.Header{
			IsLongHeader:     true,
			Type:             protocol.PacketTypeRetry,
			SrcConnectionID:  srcConnID,
			DestConnectionID: destConnID,
			Token:            token,
			Version:          version,
		},
	}
	data := writePacket(hdr, nil)
	return append(data, handshake.GetRetryIntegrityTag(data, origDestConnID)[:]...)
}
