package client_test

import (
	"context"
	"errors"
	"math"
	"testing"
	"time"

	"github.com/libp2p/go-libp2p"
	"github.com/libp2p/go-libp2p/core/crypto"
	"github.com/libp2p/go-libp2p/core/network"
	"github.com/libp2p/go-libp2p/core/peer"
	"github.com/libp2p/go-libp2p/core/record"
	"github.com/libp2p/go-libp2p/core/test"
	"github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/client"
	pbv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/pb"
	"github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/proto"
	"github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/util"

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

func TestReservationFailures(t *testing.T) {
	type testcase struct {
		name          string
		streamHandler network.StreamHandler
		err           string
		status        pbv2.Status
	}
	testcases := []testcase{
		{
			name:          "unsupported protocol",
			streamHandler: nil,
			err:           "protocols not supported",
		},
		{
			name: "wrong message type",
			streamHandler: func(s network.Stream) {
				util.NewDelimitedWriter(s).WriteMsg(&pbv2.HopMessage{
					Type: pbv2.HopMessage_RESERVE.Enum(),
				})
			},
			err:    "unexpected relay response: not a status message",
			status: pbv2.Status_MALFORMED_MESSAGE,
		},
		{
			name: "unknown status",
			streamHandler: func(s network.Stream) {
				status := pbv2.Status(1337)
				util.NewDelimitedWriter(s).WriteMsg(&pbv2.HopMessage{
					Type:   pbv2.HopMessage_STATUS.Enum(),
					Status: &status,
				})
			},
			err:    "reservation failed",
			status: pbv2.Status(1337),
		},
		{
			name: "invalid time",
			streamHandler: func(s network.Stream) {
				status := pbv2.Status_OK
				expire := uint64(math.MaxUint64)
				util.NewDelimitedWriter(s).WriteMsg(&pbv2.HopMessage{
					Type:        pbv2.HopMessage_STATUS.Enum(),
					Status:      &status,
					Reservation: &pbv2.Reservation{Expire: &expire},
				})
			},
			err:    "received reservation with expiration date in the past",
			status: pbv2.Status_MALFORMED_MESSAGE,
		},
		{
			name: "invalid voucher",
			streamHandler: func(s network.Stream) {
				status := pbv2.Status_OK
				expire := uint64(time.Now().Add(time.Hour).UnixNano())
				util.NewDelimitedWriter(s).WriteMsg(&pbv2.HopMessage{
					Type:   pbv2.HopMessage_STATUS.Enum(),
					Status: &status,
					Reservation: &pbv2.Reservation{
						Expire:  &expire,
						Voucher: []byte("foobar"),
					},
				})
			},
			err:    "error consuming voucher envelope: failed when unmarshalling the envelope",
			status: pbv2.Status_MALFORMED_MESSAGE,
		},
		{
			name: "invalid voucher 2",
			streamHandler: func(s network.Stream) {
				status := pbv2.Status_OK
				expire := uint64(time.Now().Add(time.Hour).UnixNano())
				priv, _, err := test.RandTestKeyPair(crypto.Ed25519, 256)
				if err != nil {
					s.Reset()
					return
				}
				relay, _ := test.RandPeerID()
				peer, _ := test.RandPeerID()
				voucher := &proto.ReservationVoucher{
					Relay:      relay,
					Peer:       peer,
					Expiration: time.Now().Add(time.Hour),
				}
				signedVoucher, err := record.Seal(voucher, priv)
				if err != nil {
					s.Reset()
					return
				}
				env, err := signedVoucher.Marshal()
				if err != nil {
					s.Reset()
					return
				}
				util.NewDelimitedWriter(s).WriteMsg(&pbv2.HopMessage{
					Type:   pbv2.HopMessage_STATUS.Enum(),
					Status: &status,
					Reservation: &pbv2.Reservation{
						Expire:  &expire,
						Voucher: env,
					},
				})
			},
			err:    "invalid voucher relay id",
			status: pbv2.Status_MALFORMED_MESSAGE,
		},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			host, err := libp2p.New(libp2p.ResourceManager(&network.NullResourceManager{}))
			require.NoError(t, err)
			defer host.Close()
			if tc.streamHandler != nil {
				host.SetStreamHandler(proto.ProtoIDv2Hop, tc.streamHandler)
			}

			cl, err := libp2p.New(libp2p.ResourceManager(&network.NullResourceManager{}))
			require.NoError(t, err)
			defer cl.Close()
			_, err = client.Reserve(context.Background(), cl, peer.AddrInfo{ID: host.ID(), Addrs: host.Addrs()})
			if tc.err == "" {
				require.NoError(t, err)
			} else {
				require.Error(t, err)
				require.Contains(t, err.Error(), tc.err)
				if tc.status != 0 {
					var re client.ReservationError
					if !errors.As(err, &re) {
						t.Errorf("expected error to be of type %T", re)
					}
					if re.Status != tc.status {
						t.Errorf("expected status %d got %d", tc.status, re.Status)
					}
				}
			}
		})
	}
}
