package nucleus

import (
	"context"
	"errors"
	"reflect"
	"testing"

	"code.fbi.h-da.de/danet/gosdn/controller/interfaces/change"
	"code.fbi.h-da.de/danet/gosdn/controller/nucleus/types"

	mnepb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/networkelement"
	tpb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/transport"

	"code.fbi.h-da.de/danet/gosdn/controller/mocks"
	"code.fbi.h-da.de/danet/gosdn/forks/goarista/gnmi"
	openconfig "code.fbi.h-da.de/danet/gosdn/models/generated/openconfig"
	pluginSDK "code.fbi.h-da.de/danet/gosdn/plugins/sdk"
	gpb "github.com/openconfig/gnmi/proto/gnmi"
	"github.com/openconfig/ygot/ygot"
	"github.com/stretchr/testify/mock"
)

// testSetupGnmi bootstraps tests for gnmi transport.
func testSetupGnmi() {
	// TODO: Set sane defaults
	gnmiConfig = &gnmi.Config{
		Username: "test",
		Password: "test",
		Addr:     "localhost:13371",
		Encoding: gpb.Encoding_PROTO,
	}

	startGnmiTarget = make(chan string)
	stopGnmiTarget = make(chan bool)
	go targetRunner()
}

func TestGnmi_Capabilities(t *testing.T) {
	transport := mockTransport(t)
	capabilityResponse := &gpb.CapabilityResponse{
		SupportedModels:    nil,
		SupportedEncodings: []gpb.Encoding{gpb.Encoding_PROTO, gpb.Encoding_JSON_IETF, gpb.Encoding_JSON},
		GNMIVersion:        "0.6.0",
		Extension:          nil,
	}

	capabilityRequest := &gpb.CapabilityRequest{}

	transport.client.(*mocks.GNMIClient).
		On("NewContext", mockContext, mock.Anything).
		Return(mockContext)
	transport.client.(*mocks.GNMIClient).
		On("Capabilities", mockContext, capabilityRequest).
		Return(capabilityResponse, nil)

	type fields struct {
		transport *Gnmi
	}
	type args struct {
		endpoint    string
		runEndpoint bool
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		want    *gpb.CapabilityResponse
		wantErr bool
	}{
		{
			name:   "mock",
			fields: fields{transport: &transport},
			args: args{
				endpoint:    gnmiConfig.Addr,
				runEndpoint: false,
			},
			want:    capabilityResponse,
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if tt.args.runEndpoint {
				startGnmiTarget <- tt.args.endpoint
			}

			got, err := tt.fields.transport.Capabilities(context.Background())
			if (err != nil) != tt.wantErr {
				t.Errorf("Capabilities() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("Get() got = %v, want %v", got, tt.want)
			}
			if tt.args.runEndpoint {
				stopGnmiTarget <- true
			}
		})
	}
}

func TestGnmi_Close(t *testing.T) {
	type fields struct {
		SetNode  func(path *gpb.Path, value *gpb.TypedValue) error
		RespChan chan *gpb.SubscribeResponse
	}
	tests := []struct {
		name    string
		fields  fields
		wantErr bool
	}{
		{name: "dummy", fields: fields{}, wantErr: false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			g := &Gnmi{
				SetNode:  tt.fields.SetNode,
				RespChan: tt.fields.RespChan,
			}
			if err := g.Close(); (err != nil) != tt.wantErr {
				t.Errorf("Close() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func TestGnmi_Get(t *testing.T) {
	transport := mockTransport(t)
	getRequest := &gpb.GetRequest{
		Prefix:    nil,
		Path:      nil,
		Type:      0,
		Encoding:  0,
		UseModels: nil,
		Extension: nil,
	}

	getResponse := &gpb.GetResponse{
		Notification: nil,
		Error:        nil,
		Extension:    nil,
	}

	transport.client.(*mocks.GNMIClient).
		On("NewContext", mockContext, mock.Anything).
		Return(mockContext)
	transport.client.(*mocks.GNMIClient).
		On("NewGetRequest", mockContext, mock.Anything, mock.Anything).
		Return(getRequest, nil)
	transport.client.(*mocks.GNMIClient).
		On("Get", mockContext, mock.Anything).
		Return(getResponse, nil)

	type fields struct {
		transport *Gnmi
	}
	type args struct {
		params      []string
		runEndpoint bool
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		want    interface{}
		wantErr bool
	}{
		{
			name:   "uninitialised",
			fields: fields{&Gnmi{}},
			args: args{
				params: nil,
			},
			want:    nil,
			wantErr: true,
		},
		{
			name:    "mock",
			fields:  fields{transport: &transport},
			args:    args{},
			want:    getResponse,
			wantErr: false,
		},
		{
			name:   "endpoint",
			fields: fields{transport: &transport},
			args: args{
				runEndpoint: true,
			},
			want:    getResponse,
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if tt.args.runEndpoint {
				startGnmiTarget <- tt.fields.transport.config.Addr
			}
			got, err := tt.fields.transport.Get(context.Background(), tt.args.params...)
			if (err != nil) != tt.wantErr {
				t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("Get() got = %v, want %v", got, tt.want)
			}
			if tt.args.runEndpoint {
				stopGnmiTarget <- true
			}
		})
	}
}

func TestGnmi_ProcessResponse(t *testing.T) {
	type args struct {
		path string
	}
	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		{
			name: "Interfaces Interface",
			args: args{
				path: "../test/proto/resp-interfaces-interface-arista-ceos",
			},
			wantErr: false,
		},
		{
			name: "Interfaces Wildcard",
			args: args{
				path: "../test/proto/resp-interfaces-wildcard",
			},
			wantErr: false,
		},
		{
			name: "Root",
			args: args{
				path: "../test/proto/resp-full-node-arista-ceos",
			},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			networkElementModel, err := pluginSDK.NewDeviceModel(openconfig.Schema, openconfig.Unmarshal, openconfig.SchemaTreeGzip)
			if err != nil {
				t.Errorf("ProcessResponse() error = %v", err)
			}

			g := &Gnmi{
				SetNode:   networkElementModel.SetNode,
				Unmarshal: networkElementModel.Unmarshal,
			}

			resp := gnmiMessages[tt.args.path]
			if err := g.ProcessResponse(resp); (err != nil) != tt.wantErr {
				t.Errorf("ProcessResponse() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func TestGnmi_Set(t *testing.T) {
	hostnamePath, err := ygot.StringToStructuredPath("system/config/hostname")
	if err != nil {
		t.Fatal(err)
	}
	setResponse := &gpb.SetResponse{}

	type fields struct {
		transport           Gnmi
		mockArgumentMatcher any
	}
	type args struct {
		payload change.Payload
		path    string
		ctx     context.Context
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		wantErr bool
	}{
		{
			name: "updateValue",
			fields: fields{
				transport: mockTransport(t),
				mockArgumentMatcher: mock.MatchedBy(func(input *gpb.SetRequest) bool {
					if len(input.Update) == 0 {
						return false
					}
					path, err := ygot.PathToString(input.Update[0].Path)
					if err != nil {
						return false
					}
					value := input.Update[0].Val.GetStringVal()

					valCheck := value == "newName"
					pathCheck := path == "/system/config/hostname"

					return valCheck && pathCheck
				}),
			},
			args: args{
				payload: change.Payload{
					Original: []byte("update"),
					Modified: []byte("update2"),
					Diff: &gpb.Notification{
						Update: []*gpb.Update{
							{
								Path: hostnamePath,
								Val: &gpb.TypedValue{
									Value: &gpb.TypedValue_StringVal{
										StringVal: "newName",
									},
								},
							},
						},
					},
				},
				path: "/system/config/hostname",
				ctx:  context.WithValue(context.Background(), types.CtxKeyOperation, mnepb.ApiOperation_API_OPERATION_UPDATE), // nolint
			},
			wantErr: false,
		},
		{
			name: "removeValue",
			fields: fields{
				transport: mockTransport(t),
				mockArgumentMatcher: mock.MatchedBy(func(input *gpb.SetRequest) bool {
					if len(input.Delete) == 0 {
						return false
					}
					test, _ := ygot.PathToString(input.Delete[0])
					return test == "/system/config/hostname"
				}),
			},
			args: args{
				payload: change.Payload{
					Original: []byte("delete"),
					Modified: []byte("delete2"),
					Diff: &gpb.Notification{
						Delete: []*gpb.Path{
							hostnamePath,
						},
					},
				},
				path: "/system/config/hostname",
				ctx:  context.WithValue(context.Background(), types.CtxKeyOperation, mnepb.ApiOperation_API_OPERATION_DELETE), // nolint
			},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.fields.transport.client.(*mocks.GNMIClient).
				On("Set", mockContext, tt.fields.mockArgumentMatcher).Return(setResponse, nil)
			err := tt.fields.transport.Set(tt.args.ctx, tt.args.payload)
			if (err != nil) != tt.wantErr {
				t.Errorf("Set() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func TestGnmi_Subscribe(t *testing.T) {
	type args struct {
		ctx    context.Context
		params []string
	}
	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		{name: "nil client", args: args{}, wantErr: true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			g := &Gnmi{}
			if err := g.Subscribe(tt.args.ctx, tt.args.params...); (err != nil) != tt.wantErr {
				t.Errorf("Subscribe() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func TestGnmi_Type(t *testing.T) {
	tests := []struct {
		name string
		want string
	}{
		{name: "dummy", want: "gnmi"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			g := &Gnmi{}
			if got := g.Type(); got != tt.want {
				t.Errorf("Type() = %v, want %v", got, tt.want)
			}
		})
	}
}

//nolint:errcheck
func TestGnmi_getWithRequest(t *testing.T) {
	transport := mockTransport(t)
	reqFullNode := gnmiMessages["../test/proto/req-full-node"].(*gpb.GetRequest)
	reqInterfacesWildcard := gnmiMessages["../test/proto/req-interfaces-wildcard"].(*gpb.GetRequest)
	respFullNode := gnmiMessages["../test/proto/resp-full-node"].(*gpb.GetResponse)
	respInterfacesWildcard := gnmiMessages["../test/proto/resp-interfaces-wildcard"].(*gpb.GetResponse)

	transport.client.(*mocks.GNMIClient).
		On("Get", mockContext, reqFullNode).
		Return(respFullNode, nil)

	transport.client.(*mocks.GNMIClient).
		On("Get", mockContext, reqInterfacesWildcard).
		Return(respInterfacesWildcard, nil)

	transport.client.(*mocks.GNMIClient).
		On("Get", mockContext, mock.Anything).
		Return(nil, errors.New("expected mock gnmi error"))

	type fields struct {
		transport *Gnmi
	}
	type args struct {
		request     *gpb.GetRequest
		runEndpoint bool
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		want    interface{}
		wantErr bool
	}{
		{
			name:   "getFullNode",
			fields: fields{transport: &transport},
			args: args{
				request: reqFullNode,
			},
			want:    respFullNode,
			wantErr: false,
		},
		{
			name:   "getInterfacesWildcard",
			fields: fields{transport: &transport},
			args: args{
				request: reqInterfacesWildcard,
			},
			want:    respInterfacesWildcard,
			wantErr: false,
		},
		{
			name:   "invalid request",
			fields: fields{transport: &transport},
			args: args{
				request:     nil,
				runEndpoint: false,
			},
			want:    nil,
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := tt.fields.transport.getWithRequest(context.Background(), tt.args.request)
			if (err != nil) != tt.wantErr {
				t.Errorf("getWithRequest() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("getWithRequest() got = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestNewGnmiTransport(t *testing.T) {
	type args struct {
		opts *tpb.TransportOption
	}
	tests := []struct {
		name    string
		args    args
		want    *Gnmi
		wantErr bool
	}{
		{
			name: "unsupported compression",
			args: args{
				opts: &tpb.TransportOption{
					TransportOption: &tpb.TransportOption_GnmiTransportOption{
						GnmiTransportOption: &tpb.GnmiTransportOption{
							Compression: "brotli",
						},
					}}},
			want:    nil,
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if tt.name == "default" {
				startGnmiTarget <- gnmiConfig.Addr
			}

			mockPlugin := mockPlugin(t)

			got, err := newGnmiTransport(tt.args.opts, mockPlugin)
			if (err != nil) != tt.wantErr {
				t.Errorf("NewGnmiTransport() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if tt.name == "default" && got != nil {
				tt.want.client = got.client
				tt.want.config = got.config
				tt.want.SetNode = got.SetNode
				tt.want.RespChan = got.RespChan
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("NewGnmiTransport() got = %#v, want %#v", got, tt.want)
			}
			if tt.name == "default" {
				stopGnmiTarget <- true
			}
		})
	}
}
