package conn

import (
	"bytes"
	"errors"
	"io"
	"testing"

	gogotypes "github.com/gogo/protobuf/types"
	"github.com/gtank/merlin"
	"github.com/stretchr/testify/assert"
	"golang.org/x/crypto/chacha20poly1305"

	"github.com/tendermint/tendermint/crypto"
	"github.com/tendermint/tendermint/crypto/ed25519"
	cryptoenc "github.com/tendermint/tendermint/crypto/encoding"
	"github.com/tendermint/tendermint/libs/protoio"
	tmp2p "github.com/tendermint/tendermint/proto/tendermint/p2p"
)

type buffer struct {
	next bytes.Buffer
}

func (b *buffer) Read(data []byte) (n int, err error) {
	return b.next.Read(data)
}

func (b *buffer) Write(data []byte) (n int, err error) {
	return b.next.Write(data)
}

func (b *buffer) Bytes() []byte {
	return b.next.Bytes()
}

func (b *buffer) Close() error {
	return nil
}

type evilConn struct {
	secretConn *SecretConnection
	buffer     *buffer

	locEphPub  *[32]byte
	locEphPriv *[32]byte
	remEphPub  *[32]byte
	privKey    crypto.PrivKey

	readStep   int
	writeStep  int
	readOffset int

	shareEphKey        bool
	badEphKey          bool
	shareAuthSignature bool
	badAuthSignature   bool
}

func newEvilConn(shareEphKey, badEphKey, shareAuthSignature, badAuthSignature bool) *evilConn {
	privKey := ed25519.GenPrivKey()
	locEphPub, locEphPriv := genEphKeys()
	var rep [32]byte
	c := &evilConn{
		locEphPub:  locEphPub,
		locEphPriv: locEphPriv,
		remEphPub:  &rep,
		privKey:    privKey,

		shareEphKey:        shareEphKey,
		badEphKey:          badEphKey,
		shareAuthSignature: shareAuthSignature,
		badAuthSignature:   badAuthSignature,
	}

	return c
}

func (c *evilConn) Read(data []byte) (n int, err error) {
	if !c.shareEphKey {
		return 0, io.EOF
	}

	switch c.readStep {
	case 0:
		if !c.badEphKey {
			lc := *c.locEphPub
			bz, err := protoio.MarshalDelimited(&gogotypes.BytesValue{Value: lc[:]})
			if err != nil {
				panic(err)
			}
			copy(data, bz[c.readOffset:])
			n = len(data)
		} else {
			bz, err := protoio.MarshalDelimited(&gogotypes.BytesValue{Value: []byte("drop users;")})
			if err != nil {
				panic(err)
			}
			copy(data, bz)
			n = len(data)
		}
		c.readOffset += n

		if n >= 32 {
			c.readOffset = 0
			c.readStep = 1
			if !c.shareAuthSignature {
				c.readStep = 2
			}
		}

		return n, nil
	case 1:
		signature := c.signChallenge()
		if !c.badAuthSignature {
			pkpb, err := cryptoenc.PubKeyToProto(c.privKey.PubKey())
			if err != nil {
				panic(err)
			}
			bz, err := protoio.MarshalDelimited(&tmp2p.AuthSigMessage{PubKey: pkpb, Sig: signature})
			if err != nil {
				panic(err)
			}
			n, err = c.secretConn.Write(bz)
			if err != nil {
				panic(err)
			}
			if c.readOffset > len(c.buffer.Bytes()) {
				return len(data), nil
			}
			copy(data, c.buffer.Bytes()[c.readOffset:])
		} else {
			bz, err := protoio.MarshalDelimited(&gogotypes.BytesValue{Value: []byte("select * from users;")})
			if err != nil {
				panic(err)
			}
			n, err = c.secretConn.Write(bz)
			if err != nil {
				panic(err)
			}
			if c.readOffset > len(c.buffer.Bytes()) {
				return len(data), nil
			}
			copy(data, c.buffer.Bytes())
		}
		c.readOffset += len(data)
		return n, nil
	default:
		return 0, io.EOF
	}
}

func (c *evilConn) Write(data []byte) (n int, err error) {
	switch c.writeStep {
	case 0:
		var (
			bytes     gogotypes.BytesValue
			remEphPub [32]byte
		)
		err := protoio.UnmarshalDelimited(data, &bytes)
		if err != nil {
			panic(err)
		}
		copy(remEphPub[:], bytes.Value)
		c.remEphPub = &remEphPub
		c.writeStep = 1
		if !c.shareAuthSignature {
			c.writeStep = 2
		}
		return len(data), nil
	case 1:
		// Signature is not needed, therefore skipped.
		return len(data), nil
	default:
		return 0, io.EOF
	}
}

func (c *evilConn) Close() error {
	return nil
}

func (c *evilConn) signChallenge() []byte {
	// Sort by lexical order.
	loEphPub, hiEphPub := sort32(c.locEphPub, c.remEphPub)

	transcript := merlin.NewTranscript("TENDERMINT_SECRET_CONNECTION_TRANSCRIPT_HASH")

	transcript.AppendMessage(labelEphemeralLowerPublicKey, loEphPub[:])
	transcript.AppendMessage(labelEphemeralUpperPublicKey, hiEphPub[:])

	// Check if the local ephemeral public key was the least, lexicographically
	// sorted.
	locIsLeast := bytes.Equal(c.locEphPub[:], loEphPub[:])

	// Compute common diffie hellman secret using X25519.
	dhSecret, err := computeDHSecret(c.remEphPub, c.locEphPriv)
	if err != nil {
		panic(err)
	}

	transcript.AppendMessage(labelDHSecret, dhSecret[:])

	// Generate the secret used for receiving, sending, challenge via HKDF-SHA2
	// on the transcript state (which itself also uses HKDF-SHA2 to derive a key
	// from the dhSecret).
	recvSecret, sendSecret := deriveSecrets(dhSecret, locIsLeast)

	const challengeSize = 32
	var challenge [challengeSize]byte
	challengeSlice := transcript.ExtractBytes(labelSecretConnectionMac, challengeSize)

	copy(challenge[:], challengeSlice[0:challengeSize])

	sendAead, err := chacha20poly1305.New(sendSecret[:])
	if err != nil {
		panic(errors.New("invalid send SecretConnection Key"))
	}
	recvAead, err := chacha20poly1305.New(recvSecret[:])
	if err != nil {
		panic(errors.New("invalid receive SecretConnection Key"))
	}

	b := &buffer{}
	c.secretConn = &SecretConnection{
		conn:       b,
		recvBuffer: nil,
		recvNonce:  new([aeadNonceSize]byte),
		sendNonce:  new([aeadNonceSize]byte),
		recvAead:   recvAead,
		sendAead:   sendAead,
	}
	c.buffer = b

	// Sign the challenge bytes for authentication.
	locSignature, err := signChallenge(&challenge, c.privKey)
	if err != nil {
		panic(err)
	}

	return locSignature
}

// TestMakeSecretConnection creates an evil connection and tests that
// MakeSecretConnection errors at different stages.
func TestMakeSecretConnection(t *testing.T) {
	testCases := []struct {
		name   string
		conn   *evilConn
		errMsg string
	}{
		{"refuse to share ethimeral key", newEvilConn(false, false, false, false), "EOF"},
		{"share bad ethimeral key", newEvilConn(true, true, false, false), "wrong wireType"},
		{"refuse to share auth signature", newEvilConn(true, false, false, false), "EOF"},
		{"share bad auth signature", newEvilConn(true, false, true, true), "failed to decrypt SecretConnection"},
		{"all good", newEvilConn(true, false, true, false), ""},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			privKey := ed25519.GenPrivKey()
			_, err := MakeSecretConnection(tc.conn, privKey)
			if tc.errMsg != "" {
				if assert.Error(t, err) {
					assert.Contains(t, err.Error(), tc.errMsg)
				}
			} else {
				assert.NoError(t, err)
			}
		})
	}
}
