package spy

import (
	"bytes"
	"context"
	"encoding/hex"
	"fmt"
	"io"
	"net"
	"testing"
	"time"

	"github.com/certusone/wormhole/node/pkg/common"
	gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1"
	publicrpcv1 "github.com/certusone/wormhole/node/pkg/proto/publicrpc/v1"
	spyv1 "github.com/certusone/wormhole/node/pkg/proto/spy/v1"
	ipfslog "github.com/ipfs/go-log/v2"
	"github.com/wormhole-foundation/wormhole/sdk/vaa"
	"go.uber.org/zap"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/test/bufconn"
)

const vaaNonce = uint32(1)
const vaaSequence = uint64(1)
const tx = "39c5940431b1507c2a496e945dfb6b6760771fb3c19f2531c5976decc16814ca"

var govEmitter = vaa.Address{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4}

// govAddress is the string representation of the govEmitter Address.
// Leaving here in case it is helpful for future tests.
// const govAddress = "0000000000000000000000000000000000000000000000000000000000000004"

func getTxBytes(tx string) []byte {
	bytes, err := hex.DecodeString(tx)
	if err != nil {
		panic("failed decoding Tx string to bytes")
	}
	return bytes
}

// helper method for *vaa.VAA creation
func getVAA(chainID vaa.ChainID, emitterAddr vaa.Address, nonce uint32) *vaa.VAA {
	var payload = []byte{97, 97, 97, 97, 97, 97}

	vaa := &vaa.VAA{
		Version:          vaa.SupportedVAAVersion,
		GuardianSetIndex: uint32(1),
		Signatures:       nil,
		Timestamp:        time.Unix(0, 0),
		Nonce:            nonce,
		Sequence:         vaaSequence,
		ConsistencyLevel: uint8(32),
		EmitterChain:     chainID,
		EmitterAddress:   emitterAddr,
		Payload:          payload,
	}

	return vaa
}

// helper method for *vaa.BatchVAA creation
func getBatchVAA(chainID vaa.ChainID, txID []byte, nonce uint32, emitterAddr vaa.Address) *vaa.BatchVAA {
	v := getVAA(chainID, emitterAddr, nonce)
	obs := &vaa.Observation{
		Index:       uint8(0),
		Observation: v,
	}
	var obsList = []*vaa.Observation{}
	obsList = append(obsList, obs)

	batchVAA := &vaa.BatchVAA{
		Version:          vaa.BatchVAAVersion,
		GuardianSetIndex: uint32(1),
		Signatures:       nil,
		Observations:     obsList,
	}

	return batchVAA
}

// helper method for *gossipv1.SignedBatchVAAWithQuorum creation
func getBatchVAAQuorumMessage(chainID vaa.ChainID, txID []byte, nonce uint32, emitterAddr vaa.Address) *gossipv1.SignedBatchVAAWithQuorum {
	batchVaa := getBatchVAA(chainID, txID, nonce, emitterAddr)
	vaaBytes, err := batchVaa.Marshal()
	if err != nil {
		panic("failed marshaling batchVAA.")
	}

	msg := &gossipv1.SignedBatchVAAWithQuorum{
		BatchVaa: vaaBytes,
		ChainId:  uint32(chainID),
		TxId:     txID,
		Nonce:    nonce,
	}
	return msg
}

// wait for the server to establish a client subscription before returning.
func waitForClientSubscriptionInit(server *spyServer) {
	for {
		server.subsAllVaaMu.Lock()
		subs := len(server.subsAllVaa)
		server.subsAllVaaMu.Unlock()

		if subs > 0 {
			return
		}
		time.Sleep(time.Millisecond * 10)
	}
}

//
// unit tests
//

// returns a gossip message and the pertinant values for filtering
func batchMsg() (msg *gossipv1.SignedBatchVAAWithQuorum, chainID vaa.ChainID, txID []byte, nonce uint32, emitterAddr vaa.Address) {
	chainID = vaa.ChainIDEthereum
	txID = getTxBytes(tx)
	nonce = vaaNonce
	emitterAddr = govEmitter
	msg = getBatchVAAQuorumMessage(chainID, txID, nonce, emitterAddr)
	return msg, chainID, txID, nonce, emitterAddr
}

// happy path of VAA and spyv1.FilterEntry comparison
func TestSpyTransactionIdMatches(t *testing.T) {
	batchMsg, chainID, txID, _, _ := batchMsg()

	filter := &spyv1.BatchFilter{
		ChainId: publicrpcv1.ChainID(chainID),
		TxId:    txID,
	}
	matches := TransactionIdMatches(batchMsg, filter)
	if !matches {
		t.FailNow()
	}
}

// filter does not match the VAA
func TestSpyTransactionIdMatchesNoMatch(t *testing.T) {
	batchMsg, chainID, _, _, _ := batchMsg()
	filter := &spyv1.BatchFilter{
		ChainId: publicrpcv1.ChainID(chainID),
		TxId:    []byte{1, 1, 1, 1}, // anything that does not match txBytes
	}
	matches := TransactionIdMatches(batchMsg, filter)
	if matches {
		// should not return true
		t.FailNow()
	}
}

// happy path
func TestSpyBatchMatchesFilter(t *testing.T) {
	batchMsg, chainID, txID, nonce, _ := batchMsg()

	// filter without nonce
	filter := &spyv1.BatchFilter{
		ChainId: publicrpcv1.ChainID(chainID),
		TxId:    txID,
	}
	matches := BatchMatchesFilter(batchMsg, filter)
	if !matches {
		t.FailNow()
	}

	// filter with nonce
	nonceFilter := &spyv1.BatchFilter{
		ChainId: publicrpcv1.ChainID(chainID),
		TxId:    txID,
		Nonce:   nonce,
	}
	nonceMatches := BatchMatchesFilter(batchMsg, nonceFilter)
	if !nonceMatches {
		t.FailNow()
	}
}

// filter is valid, but does not match the VAA
func TestSpyBatchMatchesFilterNoMatch(t *testing.T) {
	batchMsg, chainID, txID, _, _ := batchMsg()

	// different chainID
	solFilter := &spyv1.BatchFilter{
		ChainId: publicrpcv1.ChainID(vaa.ChainIDSolana),
		TxId:    txID,
	}
	solMatches := BatchMatchesFilter(batchMsg, solFilter)
	if solMatches {
		// should not return true
		t.FailNow()
	}

	// different transaction identifier
	txFilter := &spyv1.BatchFilter{
		ChainId: publicrpcv1.ChainID(chainID),
		TxId:    []byte{1, 1, 1, 1}, // anything that does not match txBytes
	}
	txMatches := BatchMatchesFilter(batchMsg, txFilter)
	if txMatches {
		// should not return true
		t.FailNow()
	}
}

//
// gRPC server setup for e2e tests
//

// for protobuf gRPC testing
const bufSize = 1024 * 1024

var lis *bufconn.Listener
var mockedSpyServer *spyServer

// mock the rpc server so it can run in CI without needing ports and such.
func init() {
	// setup the spyServer as it is setup in prod, only mock what is necessary.
	logger := ipfslog.Logger("wormhole-spy-mocked-in-ci").Desugar()

	// only print PANIC logs from the server's logger
	_ = ipfslog.SetLogLevel("wormhole-spy-mocked-in-ci", "PANIC")

	lis = bufconn.Listen(bufSize)

	grpcServer := common.NewInstrumentedGRPCServer(logger)

	mockedSpyServer = newSpyServer(logger)
	spyv1.RegisterSpyRPCServiceServer(grpcServer, mockedSpyServer)
	go func() {
		if err := grpcServer.Serve(lis); err != nil {
			fmt.Println("server error", err)
			logger.Fatal("Server exited with error", zap.Error(err))
		}
	}()
}

// creates a network connection in memory rather than with the host's network
// stack, for CI. See https://pkg.go.dev/google.golang.org/grpc/test/bufconn for info.
func bufDialer(context.Context, string) (net.Conn, error) {
	return lis.Dial()
}

//
// e2e gRPC server tests
// assert clients can connect, supply filters, and responses are as expected
//

// subscription tests - make sure the spy allows subscribing with and without filters

// helper function that setups a gRPC client for spySever
func grpcClientSetup(t *testing.T) (context.Context, *grpc.ClientConn, spyv1.SpyRPCServiceClient) {
	ctx := context.Background()
	creds := grpc.WithTransportCredentials(insecure.NewCredentials())
	conn, err := grpc.DialContext(ctx, "bufnet", grpc.WithContextDialer(bufDialer), creds)
	if err != nil {
		t.Fatalf("Failed to dial bufnet: %v", err)
	}

	client := spyv1.NewSpyRPCServiceClient(conn)

	return ctx, conn, client
}

// Tests creating a subscription to spyServer with no filters succeeds
func TestSpySubscribeNoFilter(t *testing.T) {
	ctx, conn, client := grpcClientSetup(t)
	defer conn.Close()

	req := &spyv1.SubscribeSignedVAARequest{Filters: []*spyv1.FilterEntry{}}

	_, err := client.SubscribeSignedVAA(ctx, req)
	if err != nil {
		t.Fatalf("SubscribeSignedVAA failed: %v", err)
	}
	// just check the subscription can be created without returning an error
}

// Tests creating a subscription to spyServer with a spyv1.EmitterFilter succeeds
func TestSpySubscribeEmitterFilter(t *testing.T) {
	ctx, conn, client := grpcClientSetup(t)
	defer conn.Close()

	// create EmitterFilter
	emitterFilter := &spyv1.EmitterFilter{ChainId: publicrpcv1.ChainID(vaa.ChainIDEthereum), EmitterAddress: ""}
	filterEntryEmitter := &spyv1.FilterEntry_EmitterFilter{EmitterFilter: emitterFilter}
	filter := &spyv1.FilterEntry{Filter: filterEntryEmitter}

	req := &spyv1.SubscribeSignedVAARequest{Filters: []*spyv1.FilterEntry{filter}}

	_, err := client.SubscribeSignedVAA(ctx, req)
	if err != nil {
		t.Fatalf("SubscribeSignedVAA failed: %v", err)
	}
	// just check the subscription can be created without returning an error
}

// Tests creating a subscription to spyServer with a spyv1.BatchFilter succeeds
func TestSpySubscribeBatchFilter(t *testing.T) {
	ctx, conn, client := grpcClientSetup(t)
	defer conn.Close()

	batchFilter := &spyv1.BatchFilter{
		ChainId: publicrpcv1.ChainID(vaa.ChainIDEthereum),
		TxId:    getTxBytes(tx),
		Nonce:   vaaNonce,
	}
	filterEntryBatch := &spyv1.FilterEntry_BatchFilter{BatchFilter: batchFilter}
	filter := &spyv1.FilterEntry{Filter: filterEntryBatch}
	req := &spyv1.SubscribeSignedVAAByTypeRequest{Filters: []*spyv1.FilterEntry{filter}}

	_, err := client.SubscribeSignedVAAByType(ctx, req)
	if err != nil {
		t.Fatalf("SubscribeSignedVAAByType failed: %v", err)
	}
	// just check the subscription can be created without returning an error
}

// Tests creating a subscription to spyServer with a spyv1.BatchTransactionFilter succeeds
func TestSpySubscribeBatchTransactionFilter(t *testing.T) {

	ctx, conn, client := grpcClientSetup(t)
	defer conn.Close()

	batchTxFilter := &spyv1.BatchTransactionFilter{
		ChainId: publicrpcv1.ChainID(vaa.ChainIDEthereum),
		TxId:    getTxBytes(tx),
	}
	filterEntryBatchTx := &spyv1.FilterEntry_BatchTransactionFilter{
		BatchTransactionFilter: batchTxFilter,
	}
	filter := &spyv1.FilterEntry{Filter: filterEntryBatchTx}
	req := &spyv1.SubscribeSignedVAAByTypeRequest{Filters: []*spyv1.FilterEntry{filter}}

	_, err := client.SubscribeSignedVAAByType(ctx, req)
	if err != nil {
		t.Fatalf("SubscribeSignedVAAByType failed: %v", err)
	}
	// just check the subscription can be created without returning an error
}

// Tests a subscription to spySever with no filters will return message(s)
func TestSpyHandleGossipVAA(t *testing.T) {
	ctx, conn, client := grpcClientSetup(t)
	defer conn.Close()

	vaaToSend := getVAA(vaa.ChainIDEthereum, govEmitter, vaaNonce)

	req := &spyv1.SubscribeSignedVAAByTypeRequest{Filters: []*spyv1.FilterEntry{}}

	stream, err := client.SubscribeSignedVAAByType(ctx, req)
	if err != nil {
		t.Fatalf("SubscribeSignedVAA failed: %v", err)
	}

	doneCh := make(chan bool)
	go func() {
		defer close(doneCh)
		for {
			// recieve is a blocking call, it will keep recieving/looping until the pipe breaks.
			signedVAA, err := stream.Recv()
			if err == io.EOF {
				t.Log("the SignedVAA stream has closed, err == io.EOF. going to break.")
				t.Fail()
				return
			}
			if err != nil {
				t.Log("SubscribeSignedVAAByType returned an error.")
				t.Fail()
				return
			}

			vaaRes := signedVAA.GetVaaType()
			switch resType := vaaRes.(type) {
			case *spyv1.SubscribeSignedVAAByTypeResponse_SignedVaa:
				bytes := resType.SignedVaa.Vaa
				parsedRes, err := vaa.Unmarshal(bytes)
				if err != nil {
					t.Log("failed unmarshaling VAA from response")
					t.Fail()
					return
				}
				if parsedRes.MessageID() != vaaToSend.MessageID() {
					t.Log("parsedRes.MessageID() does not equal vaaToSend.MessageID()")
					t.Fail()
					return
				}
				// this test only expects one response, so return
				return
			}
		}
	}()
	waitForClientSubscriptionInit(mockedSpyServer)

	vaaBytes, err := vaaToSend.Marshal()
	if err != nil {
		t.Fatal("failed marshaling VAA to bytes")
	}
	msg := &gossipv1.SignedVAAWithQuorum{
		Vaa: vaaBytes,
	}
	err = mockedSpyServer.HandleGossipVAA(msg)
	if err != nil {
		t.Fatal("failed HandleGossipVAA")
	}

	<-doneCh
}

// Tests spySever's implementation of the spyv1.EmitterFilter
func TestSpyHandleGossipBatchVAAEmitterFilter(t *testing.T) {
	ctx, conn, client := grpcClientSetup(t)
	defer conn.Close()

	// This test creates a message (gossipMsg) that will be published, and a filter that will allow the
	// message through (emitterFilter). Then it subscribes to messages from the server (SubscribeSignedVAAByType),
	// and publishes the gossipMsg, along with some messages that do not pass the filter (msg1, msg2, etc).
	// This test passes if only the expected gossipMsg is recieved from the client stream (emitterFilterStream.Recv()).

	// gossipMsg will be published to the spyServer
	gossipMsg, chainID, txID, nonce, emitter := batchMsg()

	// create an EmitterFilter request
	emitterFilter := &spyv1.EmitterFilter{
		ChainId:        publicrpcv1.ChainID(chainID),
		EmitterAddress: emitter.String(),
	}
	filterEntryEmitter := &spyv1.FilterEntry_EmitterFilter{EmitterFilter: emitterFilter}
	emitterFilterEnvelope := &spyv1.FilterEntry{Filter: filterEntryEmitter}
	emitterFilterReq := &spyv1.SubscribeSignedVAAByTypeRequest{Filters: []*spyv1.FilterEntry{emitterFilterEnvelope}}

	emitterFilterStream, err := client.SubscribeSignedVAAByType(ctx, emitterFilterReq)
	if err != nil {
		t.Fatalf("SubscribeSignedVAAByType failed: %v", err)
	}

	doneCh := make(chan bool)
	go func() {
		defer close(doneCh)
		for {
			// recieve is a blocking call, it will keep recieving/looping until the pipe breaks.
			res, err := emitterFilterStream.Recv()
			if err == io.EOF {
				t.Log("the SignedVAA stream has closed, err == io.EOF. going to break.")
				t.Fail()
				return
			}
			if err != nil {
				t.Log("SubscribeSignedVAAByType returned an error.")
				t.Fail()
				return
			}
			vaaRes := res.GetVaaType()
			switch resType := vaaRes.(type) {
			case *spyv1.SubscribeSignedVAAByTypeResponse_SignedBatchVaa:
				vaaBytes := resType.SignedBatchVaa.BatchVaa

				// just unmarshal to smoke test
				_, err := vaa.UnmarshalBatch(vaaBytes)
				if err != nil {
					t.Log("failed unmarshaling BatchVAA from response")
					t.Fail()
					return
				}

				// check the response is the expected vaa (one that passes the supplied filter)
				if !bytes.Equal(resType.SignedBatchVaa.TxId, txID) {
					t.Log("Got a VAA from the server stream not matching the supplied filter.")
					t.Fail()
				}

				// check to make sure msg1 did not make it through
				if resType.SignedBatchVaa.ChainId != uint32(chainID) {
					t.Fail()
				}

				// check that the VAA we got is exactly what we expect
				if !bytes.Equal(vaaBytes, gossipMsg.BatchVaa) {
					t.Log("vaaBytes of the response does not match what was sent")
					t.Fail()
				}

				return
			case *spyv1.SubscribeSignedVAAByTypeResponse_SignedVaa:
				t.Log("got SubscribeSignedVAAByTypeResponse_SignedVaa")
				return
			}
		}
	}()
	waitForClientSubscriptionInit(mockedSpyServer)

	// should not be sent to us by the server
	// everything passes the filter except for chainID
	msg1 := getBatchVAAQuorumMessage(vaa.ChainIDSolana, txID, nonce, emitter)
	err = mockedSpyServer.HandleGossipBatchVAA(msg1)
	if err != nil {
		t.Fatal("failed HandleGossipBatchVAA for msg1")
	}

	// should not be sent to us by the server
	// everything passes the filter except except for emitterAddress
	differentEmitter := vaa.Address{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
	msg2 := getBatchVAAQuorumMessage(chainID, txID, nonce, differentEmitter)
	err = mockedSpyServer.HandleGossipBatchVAA(msg2)
	if err != nil {
		t.Fatal("failed HandleGossipBatchVAA for msg2")
	}

	// passes the filter - should be sent back to us by the server
	err = mockedSpyServer.HandleGossipBatchVAA(gossipMsg)
	if err != nil {
		t.Fatal("failed HandleGossipBatchVAA for gossipMsg")
	}

	<-doneCh
}

// Tests spySever's implementation of the spyv1.BatchTransactionFilter
func TestSpyHandleGossipBatchVAABatchTxFilter(t *testing.T) {
	ctx, conn, client := grpcClientSetup(t)
	defer conn.Close()

	// This test creates a message (gossipMsg) that will be published, and a filter that will allow the
	// message through (batchTxFilter). Then it subscribes to messages from the server (SubscribeSignedVAAByType),
	// and publishes the gossipMsg, along with some messages that do not pass the filter (msg1, msg2, etc).
	// This test passes if only the expected gossipMsg is recieved from the client stream (batchTxStream.Recv()).

	// gossipMsg will be published to the spyServer
	// the other values returned are used for setting up the filters we expect this message to pass
	gossipMsg, chainID, txID, nonce, emitter := batchMsg()

	// create a BatchTransactionFilter request
	batchTxFilter := &spyv1.BatchTransactionFilter{
		ChainId: publicrpcv1.ChainID(chainID),
		TxId:    txID,
	}
	filterEntryBatchTx := &spyv1.FilterEntry_BatchTransactionFilter{
		BatchTransactionFilter: batchTxFilter,
	}
	batchTxFilterEnvelope := &spyv1.FilterEntry{Filter: filterEntryBatchTx}
	batchTxReq := &spyv1.SubscribeSignedVAAByTypeRequest{Filters: []*spyv1.FilterEntry{batchTxFilterEnvelope}}

	batchTxStream, err := client.SubscribeSignedVAAByType(ctx, batchTxReq)
	if err != nil {
		t.Fatalf("SubscribeSignedVAAByType failed: %v", err)
	}

	doneCh := make(chan bool)
	go func() {
		defer close(doneCh)
		for {
			// recieve is a blocking call, it will keep recieving/looping until the pipe breaks.
			res, err := batchTxStream.Recv()
			if err == io.EOF {
				t.Log("the SignedVAA stream has closed, err == io.EOF. going to break.")
				t.Fail()
				return
			}
			if err != nil {
				t.Log("SubscribeSignedVAAByType returned an error.")
				t.Fail()
				return
			}

			vaaRes := res.GetVaaType()
			switch resType := vaaRes.(type) {
			case *spyv1.SubscribeSignedVAAByTypeResponse_SignedBatchVaa:
				vaaBytes := resType.SignedBatchVaa.BatchVaa

				// just unmarshal to smoke test
				_, err := vaa.UnmarshalBatch(vaaBytes)
				if err != nil {
					t.Log("failed unmarshaling BatchVAA from response")
					t.Fail()
					return
				}

				// check the response is the expected vaa (one that passes the supplied filter)
				if !bytes.Equal(resType.SignedBatchVaa.TxId, txID) {
					t.Log("Got a VAA from the server stream not matching the supplied filter.")
					t.Fail()
				}

				if resType.SignedBatchVaa.ChainId != uint32(chainID) {
					t.Fail()
				}

				// check that the VAA we got is exactly what we expect
				if !bytes.Equal(vaaBytes, gossipMsg.BatchVaa) {
					t.Log("vaaBytes of the response does not match what was sent")
					t.Fail()
				}

				return
			case *spyv1.SubscribeSignedVAAByTypeResponse_SignedVaa:
				t.Log("got SubscribeSignedVAAByTypeResponse_SignedVaa")
				return
			}
		}
	}()
	waitForClientSubscriptionInit(mockedSpyServer)

	// should not be sent to us by the server
	// everything passes the filter except for chainID
	msg1 := getBatchVAAQuorumMessage(vaa.ChainIDSolana, txID, nonce, emitter)
	err = mockedSpyServer.HandleGossipBatchVAA(msg1)
	if err != nil {
		t.Fatal("failed HandleGossipBatchVAA for msg1")
	}

	// should not be sent to us by the server
	// everything passes the filter except except for txID
	differentTx := []byte{1, 1, 1, 1} // anything that does not match txBytes
	msg2 := getBatchVAAQuorumMessage(chainID, differentTx, nonce, emitter)
	err = mockedSpyServer.HandleGossipBatchVAA(msg2)
	if err != nil {
		t.Fatal("failed HandleGossipBatchVAA for msg2")
	}

	// passes the filter - should be sent back to us by the server
	err = mockedSpyServer.HandleGossipBatchVAA(gossipMsg)
	if err != nil {
		t.Fatal("failed HandleGossipBatchVAA for gossipMsg")
	}

	<-doneCh
}

// Tests spySever's implementation of the spyv1.BatchFilter
func TestSpyHandleGossipBatchVAABatchFilter(t *testing.T) {
	ctx, conn, client := grpcClientSetup(t)
	defer conn.Close()

	// This test creates a message (gossipMsg) that will be published, and a filter that will allow the
	// message through (batchFilter). Then it subscribes to messages from the server (SubscribeSignedVAAByType),
	// and publishes the gossipMsg, along with some messages that do not pass the filter (msg1, msg2, etc).
	// This test passes if only the expected gossipMsg is recieved from the client stream (batchStream.Recv()).

	// gossipMsg will be published to the spyServer
	// the other values returned are used for setting up the filters we expect this message to pass
	gossipMsg, chainID, txID, nonce, emitter := batchMsg()

	// create a BatchTransactionFilter request
	batchFilter := &spyv1.BatchFilter{
		ChainId: publicrpcv1.ChainID(chainID),
		TxId:    txID,
		Nonce:   nonce,
	}
	filterEntryBatch := &spyv1.FilterEntry_BatchFilter{
		BatchFilter: batchFilter,
	}
	batchFilterEnvelope := &spyv1.FilterEntry{Filter: filterEntryBatch}
	batchReq := &spyv1.SubscribeSignedVAAByTypeRequest{Filters: []*spyv1.FilterEntry{batchFilterEnvelope}}

	batchStream, err := client.SubscribeSignedVAAByType(ctx, batchReq)
	if err != nil {
		t.Fatalf("SubscribeSignedVAAByType failed: %v", err)
	}

	doneCh := make(chan bool)
	go func() {
		defer close(doneCh)
		for {
			// recieve is a blocking call, it will keep recieving/looping until the pipe breaks.
			res, err := batchStream.Recv()
			if err == io.EOF {
				t.Log("the SignedVAA stream has closed, err == io.EOF. going to break.")
				t.Fail()
				return
			}
			if err != nil {
				t.Log("SubscribeSignedVAAByType returned an error.")
				t.Fail()
				return
			}

			vaaRes := res.GetVaaType()
			switch resType := vaaRes.(type) {
			case *spyv1.SubscribeSignedVAAByTypeResponse_SignedBatchVaa:
				vaaBytes := resType.SignedBatchVaa.BatchVaa

				// just unmarshal to smoke test
				_, err := vaa.UnmarshalBatch(vaaBytes)
				if err != nil {
					t.Log("failed unmarshaling BatchVAA from response")
					t.Fail()
					return
				}

				if resType.SignedBatchVaa.Nonce != uint32(nonce) {
					t.Fail()
				}

				// check the response is the expected vaa (one that passes the supplied filter)
				if !bytes.Equal(resType.SignedBatchVaa.TxId, txID) {
					t.Log("Got a VAA from the server stream not matching the supplied filter.")
					t.Fail()
				}

				if resType.SignedBatchVaa.ChainId != uint32(chainID) {
					t.Fail()
				}

				// check that the VAA we got is exactly what we expect
				if !bytes.Equal(vaaBytes, gossipMsg.BatchVaa) {
					t.Log("vaaBytes of the response does not match what was sent")
					t.Fail()
				}

				return
			case *spyv1.SubscribeSignedVAAByTypeResponse_SignedVaa:
				t.Log("got SubscribeSignedVAAByTypeResponse_SignedVaa")
				return
			}
		}
	}()
	waitForClientSubscriptionInit(mockedSpyServer)

	// should not be sent to us by the server
	// everything passes the filter except for chainID
	msg1 := getBatchVAAQuorumMessage(vaa.ChainIDSolana, txID, nonce, emitter)
	err = mockedSpyServer.HandleGossipBatchVAA(msg1)
	if err != nil {
		t.Fatal("failed HandleGossipBatchVAA for msg1")
	}

	// should not be sent to us by the server
	// everything passes the filter except except for txID
	differentTx := []byte{1, 1, 1, 1} // anything that does not match txBytes
	msg2 := getBatchVAAQuorumMessage(chainID, differentTx, nonce, emitter)
	err = mockedSpyServer.HandleGossipBatchVAA(msg2)
	if err != nil {
		t.Fatal("failed HandleGossipBatchVAA for msg2")
	}

	// should not be sent to us by the server
	// everything passes the filter except except for nonce
	differentNonce := uint32(8888) // anything that does not match txBytes
	msg3 := getBatchVAAQuorumMessage(chainID, txID, differentNonce, emitter)
	err = mockedSpyServer.HandleGossipBatchVAA(msg3)
	if err != nil {
		t.Fatal("failed HandleGossipBatchVAA for msg3")
	}

	// passes the filter - should be sent back to us by the server
	err = mockedSpyServer.HandleGossipBatchVAA(gossipMsg)
	if err != nil {
		t.Fatal("failed HandleGossipBatchVAA for gossipMsg")
	}

	<-doneCh
}
