package integration

import (
	"context"
	"os"
	"reflect"
	"sort"
	"testing"
	"time"

	"github.com/google/uuid"

	ppb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/pnd"
	spb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/southbound"
	tpb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/transport"
	"code.fbi.h-da.de/danet/gosdn/controller/interfaces/change"

	"code.fbi.h-da.de/danet/forks/goarista/gnmi"
	"code.fbi.h-da.de/danet/gosdn/controller/nucleus"
	"code.fbi.h-da.de/danet/gosdn/controller/nucleus/errors"
	"code.fbi.h-da.de/danet/gosdn/controller/nucleus/types"
	"code.fbi.h-da.de/danet/gosdn/controller/nucleus/util/proto"
	gpb "github.com/openconfig/gnmi/proto/gnmi"
	log "github.com/sirupsen/logrus"
	pb "google.golang.org/protobuf/proto"
)

const unreachable = "203.0.113.10:6030"
const testPath = "/system/config/hostname"

var modifiedHostname = "ceos3000"
var testAddress = "10.254.254.105:6030"
var testUsername = "admin"
var testPassword = "arista"
var opt *tpb.TransportOption
var gnmiMessages map[string]pb.Message

func TestMain(m *testing.M) {
	testSetupIntegration()
	os.Exit(m.Run())
}

func testSetupIntegration() {
	if os.Getenv("GOSDN_LOG") == "nolog" {
		log.SetLevel(log.PanicLevel)
	}

	addr := os.Getenv("CEOS_TEST_ENDPOINT")
	if addr != "" {
		testAddress = addr
		log.Infof("CEOS_TEST_ENDPOINT set to %v", testAddress)
	}
	u := os.Getenv("GOSDN_TEST_USER")
	if u != "" {
		testUsername = u
		log.Infof("GOSDN_TEST_USER set to %v", testUsername)
	}
	p := os.Getenv("GOSDN_TEST_PASSWORD")
	if p != "" {
		testPassword = p
		log.Infof("GOSDN_TEST_PASSWORD set to %v", testPassword)
	}

	gnmiMessages = map[string]pb.Message{
		"../proto/cap-resp-arista-ceos":                  &gpb.CapabilityResponse{},
		"../proto/req-full-node":                         &gpb.GetRequest{},
		"../proto/req-full-node-arista-ceos":             &gpb.GetRequest{},
		"../proto/req-interfaces-arista-ceos":            &gpb.GetRequest{},
		"../proto/req-interfaces-interface-arista-ceos":  &gpb.GetRequest{},
		"../proto/req-interfaces-wildcard":               &gpb.GetRequest{},
		"../proto/resp-full-node":                        &gpb.GetResponse{},
		"../proto/resp-full-node-arista-ceos":            &gpb.GetResponse{},
		"../proto/resp-interfaces-arista-ceos":           &gpb.GetResponse{},
		"../proto/resp-interfaces-interface-arista-ceos": &gpb.GetResponse{},
		"../proto/resp-interfaces-wildcard":              &gpb.GetResponse{},
		"../proto/resp-set-system-config-hostname":       &gpb.SetResponse{},
	}
	for k, v := range gnmiMessages {
		if err := proto.Read(k, v); err != nil {
			log.Fatalf("error parsing %v: %v", k, err)
		}
	}

	opt = &tpb.TransportOption{
		Address:  testAddress,
		Username: testUsername,
		Password: testPassword,
		TransportOption: &tpb.TransportOption_GnmiTransportOption{
			GnmiTransportOption: &tpb.GnmiTransportOption{},
		},
	}
}

func TestGnmi_SetInvalidIntegration(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test")
	}
	type fields struct {
		opt *tpb.TransportOption
	}
	type args struct {
		ctx     context.Context
		payload change.Payload
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		wantErr bool
	}{
		{
			name: "destination unreachable",
			fields: fields{
				opt: &tpb.TransportOption{
					Address: unreachable,
					TransportOption: &tpb.TransportOption_GnmiTransportOption{
						GnmiTransportOption: &tpb.GnmiTransportOption{}},
				},
			},
			args: args{
				ctx:     context.Background(),
				payload: change.Payload{},
			},
			wantErr: true,
		},
		{
			name:   "invalid update",
			fields: fields{opt: opt},
			args: args{
				ctx:     context.Background(),
				payload: change.Payload{},
			},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			sbi, err := nucleus.NewSBI(spb.Type_TYPE_OPENCONFIG)
			if err != nil {
				t.Errorf("SetInvalidIntegration() error = %v", err)
				return
			}
			g, err := nucleus.NewTransport(tt.fields.opt, sbi)
			if (err != nil) != tt.wantErr {
				t.Errorf("SetInvalidIntegration() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			err = g.Set(tt.args.ctx, tt.args.payload)
			if (err != nil) != tt.wantErr {
				t.Errorf("SetInvalidIntegration() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
		})
	}
}

func TestGnmi_SetValidIntegration(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test")
	}

	sbi, err := nucleus.NewSBI(spb.Type_TYPE_OPENCONFIG)
	if err != nil {
		t.Errorf("SetValidIntegration() err = %v", err)
		return
	}
	opt := &tpb.TransportOption{
		Address:  testAddress,
		Username: testUsername,
		Password: testPassword,
		TransportOption: &tpb.TransportOption_GnmiTransportOption{
			GnmiTransportOption: &tpb.GnmiTransportOption{},
		},
	}
	pnd, err := nucleus.NewPND("test", "test", uuid.New(), sbi, nil, nil)
	if err != nil {
		t.Error(err)
		return
	}
	if err := pnd.AddDevice("test", opt, sbi.ID()); err != nil {
		t.Error(err)
		return
	}
	device, err := pnd.GetDevice("test")
	if err != nil {
		t.Error(err)
		return
	}

	tests := []struct {
		name  string
		apiOp ppb.ApiOperation
		path  string
		value string
		want  string
	}{
		{
			name:  "update",
			apiOp: ppb.ApiOperation_API_OPERATION_UPDATE,
			path:  testPath,
			value: modifiedHostname,
			want:  modifiedHostname,
		},
		{
			name:  "replace",
			apiOp: ppb.ApiOperation_API_OPERATION_REPLACE,
			path:  "/system/config/domain-name",
			value: modifiedHostname,
			want:  modifiedHostname,
		},
		{
			name:  "delete",
			apiOp: ppb.ApiOperation_API_OPERATION_DELETE,
			path:  testPath,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cuid, err := pnd.ChangeOND(device.ID(), tt.apiOp, tt.path, tt.value)
			if err != nil {
				t.Error(err)
				return
			}
			if err := pnd.Commit(cuid); err != nil {
				t.Error(err)
				return
			}
			if err := pnd.Confirm(cuid); err != nil {
				t.Error(err)
				return
			}
			if tt.name != "delete" {
				resp, err := pnd.Request(device.ID(), tt.path)
				if err != nil {
					t.Error(err)
					return
				}
				r, ok := resp.(*gpb.GetResponse)
				if !ok {
					t.Error(&errors.ErrInvalidTypeAssertion{
						Value: resp,
						Type:  &gpb.GetResponse{},
					})
					return
				}
				got := r.Notification[0].Update[0].Val.GetStringVal()
				if !reflect.DeepEqual(got, tt.want) {
					t.Errorf("GetDevice() got = %v, want %v", got, tt.want)
				}
			}
		})
	}
}

func TestGnmi_GetIntegration(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test")
	}

	paths := []string{
		"/interfaces/interface",
		"system/config/hostname",
	}
	type fields struct {
		opt *tpb.TransportOption
	}
	type args struct {
		ctx    context.Context
		params []string
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		want    interface{}
		wantErr bool
	}{
		{
			name:   "default",
			fields: fields{opt: opt},
			args: args{
				ctx:    context.Background(),
				params: paths[:1],
			},
			want:    gnmiMessages["../proto/resp-interfaces-arista-ceos"],
			wantErr: false,
		},
		{
			name: "destination unreachable",
			fields: fields{
				opt: &tpb.TransportOption{
					Address: unreachable,
					TransportOption: &tpb.TransportOption_GnmiTransportOption{
						GnmiTransportOption: &tpb.GnmiTransportOption{}},
				},
			},
			args: args{
				ctx:    context.Background(),
				params: paths,
			},
			want:    nil,
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			sbi, err := nucleus.NewSBI(spb.Type_TYPE_OPENCONFIG)
			if err != nil {
				t.Errorf("Get() error = %v", err)
				return
			}
			g, err := nucleus.NewTransport(tt.fields.opt, sbi)
			if err != nil {
				t.Error(err)
				return
			}
			got, err := g.Get(tt.args.ctx, tt.args.params...)
			if (err != nil) != tt.wantErr {
				t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if reflect.TypeOf(got) != reflect.TypeOf(tt.want) {
				t.Errorf("Get() got = %v, want %v", got, tt.want)
			}
		})
	}
}
func TestGnmi_SubscribeIntegration(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test")
	}

	type fields struct {
		opt *tpb.TransportOption
	}
	type args struct {
		ctx  context.Context
		opts *gnmi.SubscribeOptions
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		wantErr bool
	}{
		{
			name: "default",
			fields: fields{
				opt: &tpb.TransportOption{
					Address:  testAddress,
					Username: testUsername,
					Password: testPassword,
					Tls:      false,
					TransportOption: &tpb.TransportOption_GnmiTransportOption{
						GnmiTransportOption: &tpb.GnmiTransportOption{
							Compression:     "",
							GrpcDialOptions: nil,
							Token:           "",
							Encoding:        0,
						},
					},
				},
			},
			args: args{
				ctx: context.Background(),
				opts: &gnmi.SubscribeOptions{
					Mode:              "stream",
					StreamMode:        "sample",
					SampleInterval:    uint64(1 * time.Second),
					HeartbeatInterval: uint64(100 * time.Millisecond),
					Paths: gnmi.SplitPaths([]string{
						"/interfaces/interface/name",
						"/system/config/hostname",
					}),
					Target: testAddress,
				},
			},
			wantErr: false,
		},
		{
			name: "wrong path",
			fields: fields{
				opt: &tpb.TransportOption{
					TransportOption: &tpb.TransportOption_GnmiTransportOption{
						GnmiTransportOption: &tpb.GnmiTransportOption{}},
				},
			},
			args: args{
				opts: &gnmi.SubscribeOptions{
					Mode:              "stream",
					StreamMode:        "sample",
					SampleInterval:    uint64(1 * time.Second),
					HeartbeatInterval: uint64(100 * time.Millisecond),
					Paths: gnmi.SplitPaths([]string{
						"interfaces/interface/name",
						"ystem/config/hostname",
					}),
					Target: testAddress,
				},
			},
			wantErr: true,
		},
		{
			name: "destination unreachable",
			fields: fields{
				opt: &tpb.TransportOption{
					Address: "203.0.113.10:6030",
					TransportOption: &tpb.TransportOption_GnmiTransportOption{
						GnmiTransportOption: &tpb.GnmiTransportOption{}},
				},
			},
			args: args{
				opts: &gnmi.SubscribeOptions{},
			},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var wantErr = tt.wantErr
			sbi, err := nucleus.NewSBI(spb.Type_TYPE_OPENCONFIG)
			if err != nil {
				t.Errorf("Subscribe() error = %v", err)
				return
			}
			g, err := nucleus.NewTransport(tt.fields.opt, sbi)
			if err != nil {
				t.Error(err)
				return
			}
			ctx := context.WithValue(context.Background(), types.CtxKeyOpts, tt.args.opts) //nolint
			ctx, cancel := context.WithCancel(ctx)
			go func() {
				subErr := g.Subscribe(ctx)
				if (subErr != nil) != wantErr {
					if !wantErr && subErr != nil {
						if subErr.Error() != "rpc error: code = Canceled desc = context canceled" {
							t.Errorf("Subscribe() error = %v, wantErr %v", subErr, tt.wantErr)
						}
					}
				}
			}()
			time.Sleep(time.Second * 3)
			cancel()
			time.Sleep(time.Second * 1)
		})
	}
}

func TestGnmi_CapabilitiesIntegration(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test")
	}
	type fields struct {
		opt *tpb.TransportOption
	}
	type args struct {
		ctx context.Context
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		want    interface{}
		wantErr bool
	}{
		{
			name:    "supported models",
			fields:  fields{opt: opt},
			args:    args{ctx: context.Background()},
			want:    gnmiMessages["../proto/cap-resp-arista-ceos"].(*gpb.CapabilityResponse).SupportedModels,
			wantErr: false,
		},
		{
			name:    "supported encodings",
			fields:  fields{opt: opt},
			args:    args{ctx: context.Background()},
			want:    gnmiMessages["../proto/cap-resp-arista-ceos"].(*gpb.CapabilityResponse).SupportedEncodings,
			wantErr: false,
		},
		{
			name:    "gnmi version",
			fields:  fields{opt: opt},
			args:    args{ctx: context.Background()},
			want:    gnmiMessages["../proto/cap-resp-arista-ceos"].(*gpb.CapabilityResponse).GNMIVersion,
			wantErr: false,
		},
		{
			name: "destination unreachable",
			fields: fields{opt: &tpb.TransportOption{
				Address: "203.0.113.10:6030",
				TransportOption: &tpb.TransportOption_GnmiTransportOption{
					GnmiTransportOption: &tpb.GnmiTransportOption{}},
			},
			},
			args:    args{ctx: context.Background()},
			want:    nil,
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			sbi, err := nucleus.NewSBI(spb.Type_TYPE_OPENCONFIG)
			if err != nil {
				t.Errorf("Capabilities() error = %v", err)
				return
			}
			tr, err := nucleus.NewTransport(tt.fields.opt, sbi)
			if err != nil {
				t.Error(err)
				return
			}
			g, ok := tr.(*nucleus.Gnmi)
			if !ok {
				t.Error(&errors.ErrInvalidTypeAssertion{
					Value: tr,
					Type:  &nucleus.Gnmi{},
				})
			}
			resp, err := g.Capabilities(tt.args.ctx)
			if (err != nil) != tt.wantErr {
				t.Errorf("Capabilities() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			var got interface{}
			switch tt.name {
			case "supported encodings":
				got = resp.(*gpb.CapabilityResponse).SupportedEncodings
			case "supported models":
				t.Skip("test causes false negative")
				got = resp.(*gpb.CapabilityResponse).SupportedModels
				sort.Slice(got.([]*gpb.ModelData), func(i, j int) bool {
					return got.([]*gpb.ModelData)[i].Name < got.([]*gpb.ModelData)[j].Name
				})
				sort.Slice(tt.want.([]*gpb.ModelData), func(i, j int) bool {
					return tt.want.([]*gpb.ModelData)[i].Name < tt.want.([]*gpb.ModelData)[j].Name
				})
			case "gnmi version":
				got = resp.(*gpb.CapabilityResponse).GNMIVersion
			default:
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("Type() = %v, want %v", got, tt.want)
			}
		})
	}
}