package dynamic

import (
	"fmt"
	"github.com/golang/protobuf/protoc-gen-go/descriptor"
	"math"
	"reflect"
	"testing"

	"github.com/golang/protobuf/proto"

	"github.com/jhump/protoreflect/desc"
	"github.com/jhump/protoreflect/internal/testprotos"
	"github.com/jhump/protoreflect/internal/testutil"
)

// Shared stuff for marshalling and unmarshalling tests. This is used for the binary format, the text
// format, and the JSON format.

var unaryFieldsPosMsg = &testprotos.UnaryFields{
	I: proto.Int32(1),
	J: proto.Int64(2),
	K: proto.Int32(3),
	L: proto.Int64(4),
	M: proto.Uint32(5),
	N: proto.Uint64(6),
	O: proto.Uint32(7),
	P: proto.Uint64(8),
	Q: proto.Int32(9),
	R: proto.Int64(10),
	S: proto.Float32(11),
	T: proto.Float64(12),
	U: []byte{0, 1, 2, 3, 4, 5, 6, 7},
	V: proto.String("foobar"),
	W: proto.Bool(true),
	X: &testprotos.RepeatedFields{
		I: []int32{3},
		V: []string{"baz"},
	},
	Groupy: &testprotos.UnaryFields_GroupY{
		Ya: proto.String("bedazzle"),
		Yb: proto.Int32(42),
	},
	Z: testprotos.TestEnum_SECOND.Enum(),
}

var unaryFieldsNegMsg = &testprotos.UnaryFields{
	I: proto.Int32(-1),
	J: proto.Int64(-2),
	K: proto.Int32(-3),
	L: proto.Int64(-4),
	M: proto.Uint32(5),
	N: proto.Uint64(6),
	O: proto.Uint32(7),
	P: proto.Uint64(8),
	Q: proto.Int32(-9),
	R: proto.Int64(-10),
	S: proto.Float32(-11),
	T: proto.Float64(-12),
	U: []byte{0, 1, 2, 3, 4, 5, 6, 7},
	V: proto.String("foobar"),
	W: proto.Bool(true),
	X: &testprotos.RepeatedFields{
		I: []int32{-3},
		V: []string{"baz"},
	},
	Groupy: &testprotos.UnaryFields_GroupY{
		Ya: proto.String("bedazzle"),
		Yb: proto.Int32(-42),
	},
	Z: testprotos.TestEnum_SECOND.Enum(),
}

var unaryFieldsPosInfMsg = &testprotos.UnaryFields{
	S: proto.Float32(float32(math.Inf(1))),
	T: proto.Float64(math.Inf(1)),
}

var unaryFieldsNegInfMsg = &testprotos.UnaryFields{
	S: proto.Float32(float32(math.Inf(-1))),
	T: proto.Float64(math.Inf(-1)),
}

var unaryFieldsNanMsg = &testprotos.UnaryFields{
	S: proto.Float32(float32(math.NaN())),
	T: proto.Float64(math.NaN()),
}

var repeatedFieldsMsg = &testprotos.RepeatedFields{
	I: []int32{1, -2, 3},
	J: []int64{-4, 5, -6},
	K: []int32{7, -8, 9},
	L: []int64{-10, 11, -12},
	M: []uint32{13, 14, 15},
	N: []uint64{16, 17, 18},
	O: []uint32{19, 20, 21},
	P: []uint64{22, 23, 24},
	Q: []int32{25, 26, 27},
	R: []int64{28, 29, 30},
	S: []float32{31, 32, 33},
	T: []float64{34, 35, 36},
	U: [][]byte{{0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11}},
	V: []string{"foo", "bar", "baz"},
	W: []bool{true, false, true},
	X: []*testprotos.UnaryFields{
		{I: proto.Int32(-32), V: proto.String("baz")},
		{I: proto.Int32(-64), V: proto.String("bozo")},
	},
	Groupy: []*testprotos.RepeatedFields_GroupY{
		{Ya: proto.String("bedazzle"), Yb: proto.Int32(42)},
		{Ya: proto.String("buzzard"), Yb: proto.Int32(-421)},
	},
	Z: []testprotos.TestEnum{testprotos.TestEnum_SECOND, testprotos.TestEnum_THIRD, testprotos.TestEnum_FIRST},
}

var repeatedFieldsInfNanMsg = &testprotos.RepeatedFields{
	S: []float32{float32(math.Inf(1)), float32(math.Inf(-1)), float32(math.NaN())},
	T: []float64{math.Inf(1), math.Inf(-1), math.NaN()},
}

var repeatedPackedFieldsMsg = &testprotos.RepeatedPackedFields{
	I: []int32{1, -2, 3},
	J: []int64{-4, 5, -6},
	K: []int32{7, -8, 9},
	L: []int64{-10, 11, -12},
	M: []uint32{13, 14, 15},
	N: []uint64{16, 17, 18},
	O: []uint32{19, 20, 21},
	P: []uint64{22, 23, 24},
	Q: []int32{25, 26, 27},
	R: []int64{28, 29, 30},
	S: []float32{31, 32, 33},
	T: []float64{34, 35, 36},
	U: []bool{true, false, true},
	Groupy: []*testprotos.RepeatedPackedFields_GroupY{
		{Yb: []int32{42, 84, 126, 168, 210}},
		{Yb: []int32{-210, -168, -126, -84, -42}},
	},
	V: []testprotos.TestEnum{testprotos.TestEnum_SECOND, testprotos.TestEnum_THIRD, testprotos.TestEnum_FIRST},
}

var repeatedPackedFieldsInfNanMsg = &testprotos.RepeatedPackedFields{
	S: []float32{float32(math.Inf(1)), float32(math.Inf(-1)), float32(math.NaN())},
	T: []float64{math.Inf(1), math.Inf(-1), math.NaN()},
}

var mapKeyFieldsMsg = &testprotos.MapKeyFields{
	I: map[int32]string{1: "foo", -2: "bar", 3: "baz"},
	J: map[int64]string{-4: "foo", 5: "bar", -6: "baz"},
	K: map[int32]string{7: "foo", -8: "bar", 9: "baz"},
	L: map[int64]string{-10: "foo", 11: "bar", -12: "baz"},
	M: map[uint32]string{13: "foo", 14: "bar", 15: "baz"},
	N: map[uint64]string{16: "foo", 17: "bar", 18: "baz"},
	O: map[uint32]string{19: "foo", 20: "bar", 21: "baz"},
	P: map[uint64]string{22: "foo", 23: "bar", 24: "baz"},
	Q: map[int32]string{25: "foo", 26: "bar", 27: "baz"},
	R: map[int64]string{28: "foo", 29: "bar", 30: "baz"},
	S: map[string]string{"a": "foo", "b": "bar", "c": "baz"},
	T: map[bool]string{true: "foo", false: "bar"},
}

var mapValueFieldsMsg = &testprotos.MapValFields{
	I: map[string]int32{"a": 1, "b": -2, "c": 3},
	J: map[string]int64{"a": -4, "b": 5, "c": -6},
	K: map[string]int32{"a": 7, "b": -8, "c": 9},
	L: map[string]int64{"a": -10, "b": 11, "c": -12},
	M: map[string]uint32{"a": 13, "b": 14, "c": 15},
	N: map[string]uint64{"a": 16, "b": 17, "c": 18},
	O: map[string]uint32{"a": 19, "b": 20, "c": 21},
	P: map[string]uint64{"a": 22, "b": 23, "c": 24},
	Q: map[string]int32{"a": 25, "b": 26, "c": 27},
	R: map[string]int64{"a": 28, "b": 29, "c": 30},
	S: map[string]float32{"a": 31, "b": 32, "c": 33},
	T: map[string]float64{"a": 34, "b": 35, "c": 36},
	U: map[string][]byte{"a": {0, 1, 2, 3}, "b": {4, 5, 6, 7}, "c": {8, 9, 10, 11}},
	V: map[string]string{"a": "foo", "b": "bar", "c": "baz"},
	W: map[string]bool{"a": true, "b": false, "c": true},
	X: map[string]*testprotos.UnaryFields{
		"a": {I: proto.Int32(-32), V: proto.String("baz")},
		"b": {I: proto.Int32(-64), V: proto.String("bozo")},
	},
	Y: map[string]testprotos.TestEnum{"a": testprotos.TestEnum_SECOND, "b": testprotos.TestEnum_THIRD, "c": testprotos.TestEnum_FIRST},
}

var mapValueFieldsInfNanMsg = &testprotos.MapValFields{
	S: map[string]float32{"a": float32(math.Inf(1)), "b": float32(math.Inf(-1)), "c": float32(math.NaN())},
	T: map[string]float64{"a": math.Inf(1), "b": math.Inf(-1), "c": math.NaN()},
}

var mapValueFieldsNilMsg = &testprotos.TestRequest{
	Others: map[string]*testprotos.TestMessage{"a": nil, "b": nil},
}

var mapValueFieldsNilUnknownMsg proto.Message
var mdForUnknownMsg *desc.MessageDescriptor

func init() {
	// NB: can't use desc/builder package because that would cause dependency cycle :(
	fdp := &descriptor.FileDescriptorProto{
		Name:    proto.String("foo.proto"),
		Syntax:  proto.String("proto3"),
		Package: proto.String("example"),
		MessageType: []*descriptor.DescriptorProto{
			{
				Name: proto.String("Message"),
				Field: []*descriptor.FieldDescriptorProto{
					{
						Name:     proto.String("vals"),
						Type:     descriptor.FieldDescriptorProto_TYPE_MESSAGE.Enum(),
						Number:   proto.Int32(1),
						Label:    descriptor.FieldDescriptorProto_LABEL_REPEATED.Enum(),
						TypeName: proto.String(".example.Message.ValsEntry"),
					},
				},
				NestedType: []*descriptor.DescriptorProto{
					{
						Name: proto.String("ValsEntry"),
						Options: &descriptor.MessageOptions{
							MapEntry: proto.Bool(true),
						},
						Field: []*descriptor.FieldDescriptorProto{
							{
								Name:   proto.String("key"),
								Number: proto.Int32(1),
								Type:   descriptor.FieldDescriptorProto_TYPE_STRING.Enum(),
								Label:  descriptor.FieldDescriptorProto_LABEL_OPTIONAL.Enum(),
							},
							{
								Name:     proto.String("value"),
								Number:   proto.Int32(2),
								Type:     descriptor.FieldDescriptorProto_TYPE_MESSAGE.Enum(),
								Label:    descriptor.FieldDescriptorProto_LABEL_OPTIONAL.Enum(),
								TypeName: proto.String(".example.Message"),
							},
						},
					},
				},
			},
		},
	}
	fd, err := desc.CreateFileDescriptor(fdp)
	if err != nil {
		panic(err)
	}
	mdForUnknownMsg = fd.GetMessageTypes()[0]
	mapValueFieldsNilUnknownMsg = &unknownMsg{
		Vals: map[string]*unknownMsg{"a": nil, "b": nil},
	}
}

// This message looks and acts like a proto but is NOT in the registry, so proto.MessageType
// returns nil, which forces us to fallback to a nil *dynamic.Message when representing a
// nil map value in a dynamic message, allowing tests to check that strange edge case.
type unknownMsg struct {
	Vals map[string]*unknownMsg `protobuf:"bytes,1,rep,name=vals,proto3" json:"vals,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}

func (m *unknownMsg) XXX_MessageName() string {
	return "example.Message"
}
func (m *unknownMsg) Reset() {
	m.Vals = nil
}
func (m *unknownMsg) String() string {
	return fmt.Sprintf("%#v", m.Vals)
}
func (m *unknownMsg) ProtoMessage() {
}
func (m *unknownMsg) GetMessageDescriptor() *desc.MessageDescriptor {
	return mdForUnknownMsg
}

func doTranslationParty(t *testing.T, msg proto.Message,
	marshalPm func(proto.Message) ([]byte, error), unmarshalPm func([]byte, proto.Message) error,
	marshalDm func(*Message) ([]byte, error), unmarshalDm func(*Message, []byte) error,
	includesNaN, compareBytes, outputIsString bool) {

	md, err := desc.LoadMessageDescriptorForMessage(msg)
	testutil.Ok(t, err)
	dm := NewMessage(md)

	b, err := marshalPm(msg)
	testutil.Ok(t, err)
	err = unmarshalDm(dm, b)
	testutil.Ok(t, err, "failed to unmarshal from: %s", b)

	// both techniques to marshal do the same thing
	b2a, err := marshalPm(dm)
	testutil.Ok(t, err)
	b2b, err := marshalDm(dm)
	testutil.Ok(t, err)
	testutil.Eq(t, b2a, b2b)

	// round trip back to proto.Message
	msg2 := reflect.New(reflect.TypeOf(msg).Elem()).Interface().(proto.Message)
	err = unmarshalPm(b2a, msg2)
	testutil.Ok(t, err, "failed to unmarshal from: %s", b2a)

	if !includesNaN {
		// NaN fields are never equal so this would always be false
		testutil.Ceq(t, msg, msg2, eqpm)
	}
	if compareBytes {
		if outputIsString {
			testutil.Eq(t, string(b), string(b2a))
		} else {
			testutil.Eq(t, b, b2a)
		}
	}

	// and back again
	b3, err := marshalPm(msg2)
	testutil.Ok(t, err)
	dm2 := NewMessage(md)
	err = unmarshalDm(dm2, b3)
	testutil.Ok(t, err, "failed to unmarshal from: %s", b3)

	if !includesNaN {
		testutil.Ceq(t, dm, dm2, eqdm)
	}

	// dynamic message -> (bytes) -> dynamic message
	// both techniques to unmarshal are equivalent
	dm3 := NewMessage(md)
	err = unmarshalPm(b2a, dm3)
	testutil.Ok(t, err, "failed to unmarshal from: %s", b2a)
	dm4 := NewMessage(md)
	err = unmarshalDm(dm4, b2a)
	testutil.Ok(t, err, "failed to unmarshal from: %s", b2a)

	if !includesNaN {
		testutil.Ceq(t, dm, dm3, eqdm)
		testutil.Ceq(t, dm, dm4, eqdm)
	}
}
