// Copyright (c) 2017 Arista Networks, Inc.
// Use of this source code is governed by the Apache License 2.0
// that can be found in the COPYING file.

package gnmi

import (
	"bytes"
	"encoding/json"
	"io/ioutil"
	"os"
	"testing"

	"github.com/aristanetworks/goarista/test"
	"github.com/golang/protobuf/proto"
	"github.com/golang/protobuf/ptypes/any"

	pb "github.com/openconfig/gnmi/proto/gnmi"
)

func TestNewSetRequest(t *testing.T) {
	pathFoo := &pb.Path{
		Element: []string{"foo"},
		Elem:    []*pb.PathElem{{Name: "foo"}},
	}
	pathCli := &pb.Path{
		Origin: "cli",
	}
	pathP4 := &pb.Path{
		Origin: "p4_config",
	}

	p4FileContent := "p4_config test"
	p4TestFile, err := ioutil.TempFile("", "p4TestFile")
	if err != nil {
		t.Errorf("cannot create test file for p4_config")
	}
	p4Filename := p4TestFile.Name()

	defer os.Remove(p4Filename)

	if _, err := p4TestFile.WriteString(p4FileContent); err != nil {
		t.Errorf("cannot write test file for p4_config")
	}
	p4TestFile.Close()

	testCases := map[string]struct {
		setOps []*Operation
		exp    pb.SetRequest
	}{
		"delete": {
			setOps: []*Operation{{Type: "delete", Path: []string{"foo"}}},
			exp:    pb.SetRequest{Delete: []*pb.Path{pathFoo}},
		},
		"update": {
			setOps: []*Operation{{Type: "update", Path: []string{"foo"}, Val: "true"}},
			exp: pb.SetRequest{
				Update: []*pb.Update{{
					Path: pathFoo,
					Val: &pb.TypedValue{
						Value: &pb.TypedValue_JsonIetfVal{JsonIetfVal: []byte("true")}},
				}},
			},
		},
		"replace": {
			setOps: []*Operation{{Type: "replace", Path: []string{"foo"}, Val: "true"}},
			exp: pb.SetRequest{
				Replace: []*pb.Update{{
					Path: pathFoo,
					Val: &pb.TypedValue{
						Value: &pb.TypedValue_JsonIetfVal{JsonIetfVal: []byte("true")}},
				}},
			},
		},
		"cli-replace": {
			setOps: []*Operation{{Type: "replace", Origin: "cli",
				Val: "hostname foo\nip routing"}},
			exp: pb.SetRequest{
				Replace: []*pb.Update{{
					Path: pathCli,
					Val: &pb.TypedValue{
						Value: &pb.TypedValue_AsciiVal{AsciiVal: "hostname foo\nip routing"}},
				}},
			},
		},
		"p4_config": {
			setOps: []*Operation{{Type: "replace", Origin: "p4_config",
				Val: p4Filename}},
			exp: pb.SetRequest{
				Replace: []*pb.Update{{
					Path: pathP4,
					Val: &pb.TypedValue{
						Value: &pb.TypedValue_ProtoBytes{ProtoBytes: []byte(p4FileContent)}},
				}},
			},
		},
		"target": {
			setOps: []*Operation{{Type: "replace", Target: "JPE1234567",
				Path: []string{"foo"}, Val: "true"}},
			exp: pb.SetRequest{
				Prefix: &pb.Path{Target: "JPE1234567"},
				Replace: []*pb.Update{{
					Path: pathFoo,
					Val: &pb.TypedValue{
						Value: &pb.TypedValue_JsonIetfVal{JsonIetfVal: []byte("true")}},
				}},
			},
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			got, err := newSetRequest(tc.setOps)
			if err != nil {
				t.Fatal(err)
			}
			if diff := test.Diff(tc.exp, *got); diff != "" {
				t.Errorf("unexpected diff: %s", diff)
			}
		})
	}
}

func TestStrUpdateVal(t *testing.T) {
	anyBytes, err := proto.Marshal(&pb.ModelData{Name: "foobar"})
	if err != nil {
		t.Fatal(err)
	}
	anyMessage := &any.Any{TypeUrl: "gnmi/ModelData", Value: anyBytes}
	anyString := proto.CompactTextString(anyMessage)

	for name, tc := range map[string]struct {
		update *pb.Update
		exp    string
	}{
		"JSON Value": {
			update: &pb.Update{
				Value: &pb.Value{
					Value: []byte(`{"foo":"bar"}`),
					Type:  pb.Encoding_JSON}},
			exp: `{"foo":"bar"}`,
		},
		"JSON_IETF Value": {
			update: &pb.Update{
				Value: &pb.Value{
					Value: []byte(`{"foo":"bar"}`),
					Type:  pb.Encoding_JSON_IETF}},
			exp: `{"foo":"bar"}`,
		},
		"BYTES Value": {
			update: &pb.Update{
				Value: &pb.Value{
					Value: []byte{0xde, 0xad},
					Type:  pb.Encoding_BYTES}},
			exp: "3q0=",
		},
		"PROTO Value": {
			update: &pb.Update{
				Value: &pb.Value{
					Value: []byte{0xde, 0xad},
					Type:  pb.Encoding_PROTO}},
			exp: "3q0=",
		},
		"ASCII Value": {
			update: &pb.Update{
				Value: &pb.Value{
					Value: []byte("foobar"),
					Type:  pb.Encoding_ASCII}},
			exp: "foobar",
		},
		"INVALID Value": {
			update: &pb.Update{
				Value: &pb.Value{
					Value: []byte("foobar"),
					Type:  pb.Encoding(42)}},
			exp: "foobar",
		},
		"StringVal": {
			update: &pb.Update{Val: &pb.TypedValue{
				Value: &pb.TypedValue_StringVal{StringVal: "foobar"}}},
			exp: "foobar",
		},
		"IntVal": {
			update: &pb.Update{Val: &pb.TypedValue{
				Value: &pb.TypedValue_IntVal{IntVal: -42}}},
			exp: "-42",
		},
		"UintVal": {
			update: &pb.Update{Val: &pb.TypedValue{
				Value: &pb.TypedValue_UintVal{UintVal: 42}}},
			exp: "42",
		},
		"BoolVal": {
			update: &pb.Update{Val: &pb.TypedValue{
				Value: &pb.TypedValue_BoolVal{BoolVal: true}}},
			exp: "true",
		},
		"BytesVal": {
			update: &pb.Update{Val: &pb.TypedValue{
				Value: &pb.TypedValue_BytesVal{BytesVal: []byte{0xde, 0xad}}}},
			exp: "3q0=",
		},
		"FloatVal": {
			update: &pb.Update{Val: &pb.TypedValue{
				Value: &pb.TypedValue_FloatVal{FloatVal: 3.14}}},
			exp: "3.14",
		},
		"DecimalVal": {
			update: &pb.Update{Val: &pb.TypedValue{
				Value: &pb.TypedValue_DecimalVal{
					DecimalVal: &pb.Decimal64{Digits: 314, Precision: 2},
				}}},
			exp: "3.14",
		},
		"LeafListVal": {
			update: &pb.Update{Val: &pb.TypedValue{
				Value: &pb.TypedValue_LeaflistVal{
					LeaflistVal: &pb.ScalarArray{Element: []*pb.TypedValue{
						{Value: &pb.TypedValue_BoolVal{BoolVal: true}},
						{Value: &pb.TypedValue_AsciiVal{AsciiVal: "foobar"}},
					}},
				}}},
			exp: "[true, foobar]",
		},
		"AnyVal": {
			update: &pb.Update{Val: &pb.TypedValue{
				Value: &pb.TypedValue_AnyVal{AnyVal: anyMessage}}},
			exp: anyString,
		},
		"JsonVal": {
			update: &pb.Update{Val: &pb.TypedValue{
				Value: &pb.TypedValue_JsonVal{JsonVal: []byte(`{"foo":"bar"}`)}}},
			exp: `{"foo":"bar"}`,
		},
		"JsonVal_complex": {
			update: &pb.Update{Val: &pb.TypedValue{
				Value: &pb.TypedValue_JsonVal{JsonVal: []byte(`{"foo":"bar","baz":"qux"}`)}}},
			exp: `{
  "foo": "bar",
  "baz": "qux"
}`,
		},
		"JsonIetfVal": {
			update: &pb.Update{Val: &pb.TypedValue{
				Value: &pb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(`{"foo":"bar"}`)}}},
			exp: `{"foo":"bar"}`,
		},
		"AsciiVal": {
			update: &pb.Update{Val: &pb.TypedValue{
				Value: &pb.TypedValue_AsciiVal{AsciiVal: "foobar"}}},
			exp: "foobar",
		},
		"ProtoBytes": {
			update: &pb.Update{Val: &pb.TypedValue{
				Value: &pb.TypedValue_ProtoBytes{ProtoBytes: anyBytes}}},
			exp: "CgZmb29iYXI=",
		},
	} {
		t.Run(name, func(t *testing.T) {
			got := StrUpdateVal(tc.update)
			if got != tc.exp {
				t.Errorf("Expected: %q Got: %q", tc.exp, got)
			}
		})
	}
}

func TestTypedValue(t *testing.T) {
	for tname, tcase := range map[string]struct {
		in  interface{}
		exp *pb.TypedValue
	}{
		"string": {
			in:  "foo",
			exp: &pb.TypedValue{Value: &pb.TypedValue_StringVal{StringVal: "foo"}},
		},
		"int": {
			in:  42,
			exp: &pb.TypedValue{Value: &pb.TypedValue_IntVal{IntVal: 42}},
		},
		"int64": {
			in:  int64(42),
			exp: &pb.TypedValue{Value: &pb.TypedValue_IntVal{IntVal: 42}},
		},
		"uint": {
			in:  uint(42),
			exp: &pb.TypedValue{Value: &pb.TypedValue_UintVal{UintVal: 42}},
		},
		"bool": {
			in:  true,
			exp: &pb.TypedValue{Value: &pb.TypedValue_BoolVal{BoolVal: true}},
		},
		"slice": {
			in: []interface{}{"foo", 1, uint(2), true},
			exp: &pb.TypedValue{Value: &pb.TypedValue_LeaflistVal{LeaflistVal: &pb.ScalarArray{
				Element: []*pb.TypedValue{
					{Value: &pb.TypedValue_StringVal{StringVal: "foo"}},
					{Value: &pb.TypedValue_IntVal{IntVal: 1}},
					{Value: &pb.TypedValue_UintVal{UintVal: 2}},
					{Value: &pb.TypedValue_BoolVal{BoolVal: true}},
				}}}},
		},
	} {
		t.Run(tname, func(t *testing.T) {
			if got := TypedValue(tcase.in); !test.DeepEqual(got, tcase.exp) {
				t.Errorf("Expected: %q Got: %q", tcase.exp, got)
			}
		})
	}
}

func TestExtractJSON(t *testing.T) {
	jsonFile, err := ioutil.TempFile("", "extractJSON")
	if err != nil {
		t.Fatal(err)
	}
	defer os.Remove(jsonFile.Name())
	if _, err := jsonFile.Write([]byte(`"jsonFile"`)); err != nil {
		jsonFile.Close()
		t.Fatal(err)
	}
	if err := jsonFile.Close(); err != nil {
		t.Fatal(err)
	}

	for val, exp := range map[string][]byte{
		jsonFile.Name(): []byte(`"jsonFile"`),
		"foobar":        []byte(`"foobar"`),
		`"foobar"`:      []byte(`"foobar"`),
		"Val: true":     []byte(`"Val: true"`),
		"host42":        []byte(`"host42"`),
		"42":            []byte("42"),
		"-123.43":       []byte("-123.43"),
		"0xFFFF":        []byte("0xFFFF"),
		// Int larger than can fit in 32 bits should be quoted
		"0x8000000000":  []byte(`"0x8000000000"`),
		"-0x8000000000": []byte(`"-0x8000000000"`),
		"true":          []byte("true"),
		"false":         []byte("false"),
		"null":          []byte("null"),
		"{true: 42}":    []byte("{true: 42}"),
		"[]":            []byte("[]"),
	} {
		t.Run(val, func(t *testing.T) {
			got := extractJSON(val)
			if !bytes.Equal(exp, got) {
				t.Errorf("Unexpected diff. Expected: %q Got: %q", exp, got)
			}
		})
	}
}

func TestExtractValue(t *testing.T) {
	cases := []struct {
		in  *pb.Update
		exp interface{}
	}{{
		in: &pb.Update{Val: &pb.TypedValue{
			Value: &pb.TypedValue_StringVal{StringVal: "foo"}}},
		exp: "foo",
	}, {
		in: &pb.Update{Val: &pb.TypedValue{
			Value: &pb.TypedValue_IntVal{IntVal: 123}}},
		exp: int64(123),
	}, {
		in: &pb.Update{Val: &pb.TypedValue{
			Value: &pb.TypedValue_UintVal{UintVal: 123}}},
		exp: uint64(123),
	}, {
		in: &pb.Update{Val: &pb.TypedValue{
			Value: &pb.TypedValue_BoolVal{BoolVal: true}}},
		exp: true,
	}, {
		in: &pb.Update{Val: &pb.TypedValue{
			Value: &pb.TypedValue_BytesVal{BytesVal: []byte{0xde, 0xad}}}},
		exp: []byte{0xde, 0xad},
	}, {
		in: &pb.Update{Val: &pb.TypedValue{
			Value: &pb.TypedValue_FloatVal{FloatVal: -12.34}}},
		exp: float32(-12.34),
	}, {
		in: &pb.Update{Val: &pb.TypedValue{
			Value: &pb.TypedValue_DecimalVal{DecimalVal: &pb.Decimal64{
				Digits: -1234, Precision: 2}}}},
		exp: &pb.Decimal64{Digits: -1234, Precision: 2},
	}, {
		in: &pb.Update{Val: &pb.TypedValue{
			Value: &pb.TypedValue_LeaflistVal{LeaflistVal: &pb.ScalarArray{
				Element: []*pb.TypedValue{
					{Value: &pb.TypedValue_StringVal{StringVal: "foo"}},
					{Value: &pb.TypedValue_IntVal{IntVal: 123}}}}}}},
		exp: []interface{}{"foo", int64(123)},
	}, {
		in: &pb.Update{Val: &pb.TypedValue{
			Value: &pb.TypedValue_JsonVal{JsonVal: []byte(`12.34`)}}},
		exp: json.Number("12.34"),
	}, {
		in: &pb.Update{Val: &pb.TypedValue{
			Value: &pb.TypedValue_JsonVal{JsonVal: []byte(`[12.34, 123, "foo"]`)}}},
		exp: []interface{}{json.Number("12.34"), json.Number("123"), "foo"},
	}, {
		in: &pb.Update{Val: &pb.TypedValue{
			Value: &pb.TypedValue_JsonVal{JsonVal: []byte(`{"foo":"bar"}`)}}},
		exp: map[string]interface{}{"foo": "bar"},
	}, {
		in: &pb.Update{Val: &pb.TypedValue{
			Value: &pb.TypedValue_JsonVal{JsonVal: []byte(`{"foo":45.67}`)}}},
		exp: map[string]interface{}{"foo": json.Number("45.67")},
	}, {
		in: &pb.Update{Val: &pb.TypedValue{
			Value: &pb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(`{"foo":"bar"}`)}}},
		exp: map[string]interface{}{"foo": "bar"},
	}}
	for _, tc := range cases {
		out, err := ExtractValue(tc.in)
		if err != nil {
			t.Errorf(err.Error())
		}
		if !test.DeepEqual(tc.exp, out) {
			t.Errorf("Extracted value is incorrect. Expected %+v, got %+v", tc.exp, out)
		}
	}
}
