// Copyright (C) 2016-2021  Nexedi SA and Contributors.
//                          Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.

package proto
// NEO. protocol encoding tests

import (
	"encoding/binary"
	hexpkg "encoding/hex"
	"fmt"
	"reflect"
	"runtime"
	"strings"
	"testing"
	"unsafe"

	"lab.nexedi.com/kirr/neo/go/zodb"
)

// decode string as hex; panic on error
func hex(s string) string {
	b, err := hexpkg.DecodeString(s)
	if err != nil {
		panic(err)
	}
	return string(b)
}

// uint8 -> string as encoded on the wire
func u8(v uint8) string {
	return string(v)
}

// uint16 -> string as encoded on the wire
func u16(v uint16) string {
	var b [2]byte
	binary.BigEndian.PutUint16(b[:], v)
	return string(b[:])
}

// uint32 -> string as encoded on the wire
func u32(v uint32) string {
	var b [4]byte
	binary.BigEndian.PutUint32(b[:], v)
	return string(b[:])
}

// uint64 -> string as encoded on the wire
func u64(v uint64) string {
	var b [8]byte
	binary.BigEndian.PutUint64(b[:], v)
	return string(b[:])
}

func TestPktHeaderN(t *testing.T) {
	// make sure PktHeaderN is really packed and its size matches PktHeaderLenN
	if unsafe.Sizeof(PktHeaderN{}) != 10 {
		t.Fatalf("sizeof(PktHeaderN) = %v  ; want 10", unsafe.Sizeof(PktHeaderN{}))
	}
	if unsafe.Sizeof(PktHeaderN{}) != PktHeaderLenN {
		t.Fatalf("sizeof(PktHeaderN) = %v  ; want %v", unsafe.Sizeof(PktHeaderN{}), PktHeaderLenN)
	}
}

// test marshalling for one message type
func testMsgMarshal(t *testing.T, enc Encoding, msg Msg, encoded string) {
	typ := reflect.TypeOf(msg).Elem() // type of *msg
	msg2 := reflect.New(typ).Interface().(Msg)
	defer func() {
		if e := recover(); e != nil {
			t.Errorf("%c/%v: panic ↓↓↓:", enc, typ)
			panic(e) // to show traceback
		}
	}()

	// msg.encode() == expected
	msgCode := msg.neoMsgCode()
	n := enc.MsgEncodedLen(msg)
	msgType := MsgType(msgCode)
	if msgType != typ {
		t.Errorf("%c/%v: msgCode = %v  which corresponds to %v", enc, typ, msgCode, msgType)
	}
	if n != len(encoded) {
		t.Errorf("%c/%v: encodedLen = %v  ; want %v", enc, typ, n, len(encoded))
	}

	buf := make([]byte, n)
	enc.MsgEncode(msg, buf)
	if string(buf) != encoded {
		t.Errorf("%c/%v: encode result unexpected:", enc, typ)
		t.Errorf("\thave: %s", hexpkg.EncodeToString(buf))
		t.Errorf("\twant: %s", hexpkg.EncodeToString([]byte(encoded)))
	}

	// encode must panic if passed a smaller buffer
	for l := len(buf) - 1; l >= 0; l-- {
		func() {
			defer func() {
				subj := fmt.Sprintf("%c/%v: encode(buf[:encodedLen-%v])", enc, typ, len(encoded)-l)
				e := recover()
				if e == nil {
					t.Errorf("%s did not panic", subj)
					return
				}

				err, ok := e.(runtime.Error)
				if !ok {
					t.Errorf("%s panic(%#v)  ; want runtime.Error", subj, e)
				}

				estr := err.Error()
				if !(strings.Contains(estr, "slice bounds out of range") ||
				     strings.Contains(estr, "index out of range")) {
					t.Errorf("%s unexpected runtime panic: %v", subj, estr)
				}
			}()

			enc.MsgEncode(msg, buf[:l])
		}()
	}

	// msg.decode() == expected
	data := []byte(encoded + "noise")
	n, err := enc.MsgDecode(msg2, data)
	if err != nil {
		t.Errorf("%c/%v: decode error %v", enc, typ, err)
	}
	if n != len(encoded) {
		t.Errorf("%c/%v: nread = %v  ; want %v", enc, typ, n, len(encoded))
	}

	if !reflect.DeepEqual(msg2, msg) {
		t.Errorf("%c/%v: decode result unexpected: %v  ; want %v", enc, typ, msg2, msg)
	}

	// decode must detect buffer overflow
	for l := len(encoded) - 1; l >= 0; l-- {
		n, err = enc.MsgDecode(msg2, data[:l])
		if !(n == 0 && err == ErrDecodeOverflow) {
			t.Errorf("%c/%v: decode overflow not detected on [:%v]", enc, typ, l)
		}

	}
}

// test encoding/decoding of messages
func TestMsgMarshal(t *testing.T) {
	var testv = []struct {
		msg      Msg
		encodedN string // []byte
		encodedM string // []byte
	}{
		// empty
		{&Ping{},
			"",
			"\x90",
		},

		// uint32(N)/enum(M), string
		{&Error{Code: 0x00000045, Message: "hello"},
			"\x00\x00\x00\x45\x00\x00\x00\x05hello",
			hex("92") + hex("d40045") + "\xc4\x05hello",
		},

		// Oid, Tid, bool, Checksum, []byte
		{&StoreObject{
			Oid:         0x0102030405060708,
			Serial:      0x0a0b0c0d0e0f0102,
			Compression: false,
			Checksum:    Checksum{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20}, // XXX simpler?
			Data:        []byte("hello world"),
			DataSerial:  0x0a0b0c0d0e0f0103,
			Tid:         0x0a0b0c0d0e0f0104,
			},

			hex("01020304050607080a0b0c0d0e0f010200") +
			hex("0102030405060708090a0b0c0d0e0f1011121314") +
			hex("0000000b") + "hello world" +
			hex("0a0b0c0d0e0f01030a0b0c0d0e0f0104"),

			// M
			hex("97") +
			hex("c408") + hex("0102030405060708") +
			hex("c408") + hex("0a0b0c0d0e0f0102") +
			hex("c2") +
			hex("c414") + hex("0102030405060708090a0b0c0d0e0f1011121314") +
			hex("c40b") + "hello world" +
			hex("c408") + hex("0a0b0c0d0e0f0103") +
			hex("c408") + hex("0a0b0c0d0e0f0104"),
		},

		// PTid, [] (of [] of {NodeID, CellState})
		{&AnswerPartitionTable{
			PTid:        0x0102030405060708,
			NumReplicas: 34,
			RowList: []RowInfo{
				{[]CellInfo{{11, UP_TO_DATE}, {17, OUT_OF_DATE}}},
				{[]CellInfo{{11, FEEDING}}},
				{[]CellInfo{{11, CORRUPTED}, {15, DISCARDED}, {23, UP_TO_DATE}}},
			},
			},

			// N
			hex("0102030405060708") +
			hex("00000022") +
			hex("00000003") +
				hex("000000020000000b010000001100") +
				hex("000000010000000b02") +
				hex("000000030000000b030000000f040000001701"),

			// M
			hex("93") +
			hex("cf0102030405060708") +
			hex("22") +
			hex("93") +
				hex("91"+"92"+"920bd40401"+"9211d40400") +
				hex("91"+"91"+"920bd40402") +
				hex("91"+"93"+"920bd40403"+"920fd40404"+"9217d40401"),
		},

		// map[Oid]struct {Tid,Tid,bool}
		{&AnswerObjectUndoSerial{
			ObjectTIDDict: map[zodb.Oid]struct {
				CurrentSerial zodb.Tid
				UndoSerial    zodb.Tid
				IsCurrent     bool
			}{
				1: {1, 0, false},
				2: {7, 1, true},
				8: {7, 1, false},
				5: {4, 3, true},
			}},

			// N
			u32(4) +
				u64(1) + u64(1) + u64(0) + hex("00") +
				u64(2) + u64(7) + u64(1) + hex("01") +
				u64(5) + u64(4) + u64(3) + hex("01") +
				u64(8) + u64(7) + u64(1) + hex("00"),

			// M
			hex("91") +
			hex("84") +
				hex("c408")+u64(1) + hex("93") + hex("c408")+u64(1) + hex("c408")+u64(0) + hex("c2") +
				hex("c408")+u64(2) + hex("93") + hex("c408")+u64(7) + hex("c408")+u64(1) + hex("c3") +
				hex("c408")+u64(5) + hex("93") + hex("c408")+u64(4) + hex("c408")+u64(3) + hex("c3") +
				hex("c408")+u64(8) + hex("93") + hex("c408")+u64(7) + hex("c408")+u64(1) + hex("c2"),
		},

		// map[uint32]NodeID + trailing ...
		{&CheckReplicas{
			PartitionDict: map[uint32]NodeID{
				1: 7,
				2: 9,
				7: 3,
				4: 17,
			},
			MinTID: 23,
			MaxTID: 128,
			},

			// N
			u32(4) +
				u32(1) + u32(7) +
				u32(2) + u32(9) +
				u32(4) + u32(17) +
				u32(7) + u32(3) +
			u64(23) + u64(128),

			// M
			hex("93") +
			hex("84") +
				hex("01" + "07") +
				hex("02" + "09") +
				hex("04" + "11") +
				hex("07" + "03") +
			hex("c408") + u64(23) +
			hex("c408") + u64(128),
		},

		// uint32, []uint32
		{&PartitionCorrupted{7, []NodeID{1, 3, 9, 4}},
			// N
			u32(7) + u32(4) + u32(1) + u32(3) + u32(9) + u32(4),

			// M
			hex("92") +
			hex("07") +
			hex("94") +
				hex("01030904"),
		},

		// uint32, Address, string, IdTime
		{&RequestIdentification{CLIENT, 17, Address{"localhost", 7777}, "myname", 0.12345678, []string{"room1", "rack234"}, []uint32{3,4,5} },
			// N
			u8(2) + u32(17) + u32(9) +
			"localhost" + u16(7777) +
			u32(6) + "myname" +
			hex("3fbf9add1091c895") +
			u32(2) + u32(5)+"room1" + u32(7)+"rack234" +
			u32(3) + u32(3)+u32(4)+u32(5),

			// M
			hex("97") +
			hex("d40202") +
			hex("11") +
			hex("92") + hex("c409")+"localhost" + hex("cd")+u16(7777) +
			hex("c406")+"myname" +
			hex("cb" + "3fbf9add1091c895") +
			hex("92") + hex("c405")+"room1" + hex("c407")+"rack234" +
			hex("93") + hex("030405"),
		},

		// IdTime, empty Address, int32
		{&NotifyNodeInformation{1504466245.926185, []NodeInfo{
			{CLIENT, Address{}, NID(CLIENT, 1), RUNNING, 1504466245.925599}}},
			// N
			hex("41d66b15517b469d") + u32(1) +
				u8(2) + u32(0) /* <- ø Address */ + hex("e0000001") + u8(2) +
				hex("41d66b15517b3d04"),
			// M
			hex("92") +
			hex("cb" + "41d66b15517b469d") +
			hex("91") +
				hex("95") +
					hex("d40202") +
					hex("92" + "c400"+"" + "00") +
					hex("d2" + "e0000001") +
					hex("d40302") +
					hex("cb" + "41d66b15517b3d04"),
		},

		// empty IdTime
		{&NotifyNodeInformation{IdTimeNone, []NodeInfo{}},
			// N
			hex("ffffffffffffffff") + hex("00000000"),
			// M
			hex("92") +
			hex("cb" + "fff0000000000000") + // XXX nan/-inf not handled yet
			hex("90"),
		},

		// TODO we need tests for:
		// []varsize + trailing
		// map[]varsize + trailing
	}

	for _, tt := range testv {
		testMsgMarshal(t, 'N', tt.msg, tt.encodedN)
		testMsgMarshal(t, 'M', tt.msg, tt.encodedM)
	}
}

// For all message types: same as testMsgMarshal but zero-values only.
// this way we additionally lightly check encode / decode overflow behaviour for all types.
func TestMsgMarshalAllOverflowLightly(t *testing.T) {
	for _, typ := range msgTypeRegistry {
		for _, enc := range []Encoding{'N', 'M'} {
			// zero-value for a type
			msg := reflect.New(typ).Interface().(Msg)
			l := enc.MsgEncodedLen(msg)
			zerol := make([]byte, l)
			if enc != 'N' { // M-encoding of zero-value is not all zeros
				enc.MsgEncode(msg, zerol)
			}
			// decoding will turn nil slice & map into empty allocated ones.
			// we need it so that reflect.DeepEqual works for msg encode/decode comparison
			n, err := enc.MsgDecode(msg, zerol)
			if !(n == l && err == nil) {
				t.Errorf("%c/%v: zero-decode unexpected: %v, %v  ; want %v, nil", enc, typ, n, err, l)
			}

			testMsgMarshal(t, enc, msg, string(zerol))
		}
	}
}

// Verify overflow handling on decodeN len checks
// TODO + M-variants with big len too?
func TestMsgDecodeLenOverflowN(t *testing.T) {
	enc := Encoding('N')

	var testv = []struct {
		msg  Msg    // of type to decode into
		data string // []byte - tricky data to exercise decoder u32 len checks overflow
	}{
		// [] with sizeof(item) = 8	-> len*sizeof(item) = 0 if u32
		{&AnswerTIDs{},               u32(0x20000000)},

		// {} with sizeof(key) = 8, sizeof(value) = 8	-> len*sizeof(key+value) = 0 if u32
		{&AnswerLockedTransactions{}, u32(0x10000000)},
	}

	for _, tt := range testv {
		data := []byte(tt.data)
		func() {
			defer func() {
				if e := recover(); e != nil {
					t.Errorf("%T: decode: panic on %x", tt.msg, data)
				}
			}()

			n, err := enc.MsgDecode(tt.msg, data)
			if !(n == 0 && err == ErrDecodeOverflow) {
				t.Errorf("%T: decode %x\nhave: %d, %v\nwant: %d, %v", tt.msg, data,
					n, err, 0, ErrDecodeOverflow)
			}
		}()
	}
}

func TestNID(t *testing.T) {
	var testv = []struct{typ NodeType; num int32; nid uint32; str string}{
		{STORAGE, 1, 0x00000001, "S1"},
		{MASTER,  2, 0xf0000002, "M2"},
		{CLIENT,  3, 0xe0000003, "C3"},
		{ADMIN,   4, 0xd0000004, "A4"},
	}

	for _, tt := range testv {
		nid := NID(tt.typ, tt.num)
		if uint32(nid) != tt.nid {
			t.Errorf("%v: nid=%08x  ; want %08x", tt, nid, tt.nid)
		}
		if nids := nid.String(); nids != tt.str {
			t.Errorf("%v: str(nid): %q  ; want %q", tt, nids, tt.str)
		}
	}
}

func TestNIDDecode(t *testing.T) {
	var testv = []struct{nid uint32; str string}{
//		{0,          "?(0)0"},
		{0,          "S?"},	// XXX X0 used as X? until neo.NewNode uses temporary bit
		{0x00000001, "S1"},
		{0xf0000002, "M2"},
		{0xe0000003, "C3"},
		{0xd0000004, "A4"},
		{0xc0000005, "?(4)5"},
		{0xb0000006, "?(5)6"},
		{0xa0000007, "?(6)7"},
		{0x90000008, "?(7)8"},
		{0x80000009, "?(8)9"},
		{0x7000000a, "?(9)10"},
		{0x6000000b, "?(10)11"},
		{0x5000000c, "?(11)12"},
		{0x4000000d, "?(12)13"},
		{0x3000000e, "?(13)14"},
		{0x2000000f, "?(14)15"},
		{0x10000010, "?(15)16"},
		{0x00000011, "S17"},
	}

	for _, tt := range testv {
		str := NodeID(tt.nid).String()
		if str != tt.str {
			t.Errorf("%08x -> %q  ; want %q", tt.nid, str, tt.str)
		}
	}
}
