/*
 * Copyright 2018-present Open Networking Foundation

 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at

 * http://www.apache.org/licenses/LICENSE-2.0

 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package model

import (
	"context"
	"encoding/hex"
	"math/rand"
	"reflect"
	"strconv"
	"sync"
	"testing"

	"github.com/google/uuid"
	"github.com/opencord/voltha-lib-go/v3/pkg/log"
	"github.com/opencord/voltha-protos/v3/go/common"
	"github.com/opencord/voltha-protos/v3/go/openflow_13"
	"github.com/opencord/voltha-protos/v3/go/voltha"
	"github.com/stretchr/testify/assert"
)

var (
	BenchmarkProxyRoot        Root
	BenchmarkProxyDeviceProxy *Proxy
	BenchmarkProxyPLT         *proxyLoadTest
	BenchmarkProxyLogger      log.Logger
)

type proxyLoadChanges struct {
	ID     string
	Before interface{}
	After  interface{}
}
type proxyLoadTest struct {
	mutex sync.RWMutex

	addMutex     sync.RWMutex
	addedDevices []string

	firmwareMutex    sync.RWMutex
	updatedFirmwares []proxyLoadChanges
	flowMutex        sync.RWMutex
	updatedFlows     []proxyLoadChanges

	preAddExecuted     bool
	postAddExecuted    bool
	preUpdateExecuted  bool
	postUpdateExecuted bool
}

func (plt *proxyLoadTest) SetPreAddExecuted(status bool) {
	plt.mutex.Lock()
	defer plt.mutex.Unlock()
	plt.preAddExecuted = status
}
func (plt *proxyLoadTest) SetPostAddExecuted(status bool) {
	plt.mutex.Lock()
	defer plt.mutex.Unlock()
	plt.postAddExecuted = status
}
func (plt *proxyLoadTest) SetPreUpdateExecuted(status bool) {
	plt.mutex.Lock()
	defer plt.mutex.Unlock()
	plt.preUpdateExecuted = status
}
func (plt *proxyLoadTest) SetPostUpdateExecuted(status bool) {
	plt.mutex.Lock()
	defer plt.mutex.Unlock()
	plt.postUpdateExecuted = status
}

func init() {
	var err error
	BenchmarkProxyRoot = NewRoot(&voltha.Voltha{}, nil)

	BenchmarkProxyLogger, _ = log.AddPackage(log.JSON, log.DebugLevel, log.Fields{"instanceId": "PLT"})
	//log.UpdateAllLoggers(log.Fields{"instanceId": "PROXY_LOAD_TEST"})
	//Setup default logger - applies for packages that do not have specific logger set
	if _, err := log.SetDefaultLogger(log.JSON, log.DebugLevel, log.Fields{"instanceId": "PLT"}); err != nil {
		log.With(log.Fields{"error": err}).Fatal("Cannot setup logging")
	}

	// Update all loggers (provisioned via init) with a common field
	if err := log.UpdateAllLoggers(log.Fields{"instanceId": "PLT"}); err != nil {
		log.With(log.Fields{"error": err}).Fatal("Cannot setup logging")
	}
	log.SetPackageLogLevel("github.com/opencord/voltha-go/db/model", log.DebugLevel)

	if BenchmarkProxyDeviceProxy, err = BenchmarkProxyRoot.CreateProxy(context.Background(), "/", false); err != nil {
		log.With(log.Fields{"error": err}).Fatal("Cannot create benchmark proxy")
	}
	// Register ADD instructions callbacks
	BenchmarkProxyPLT = &proxyLoadTest{}

	BenchmarkProxyDeviceProxy.RegisterCallback(PreAdd, commonCallbackFunc, "PreAdd", BenchmarkProxyPLT.SetPreAddExecuted)
	BenchmarkProxyDeviceProxy.RegisterCallback(PostAdd, commonCallbackFunc, "PostAdd", BenchmarkProxyPLT.SetPostAddExecuted)

	//// Register UPDATE instructions callbacks
	BenchmarkProxyDeviceProxy.RegisterCallback(PreUpdate, commonCallbackFunc, "PreUpdate", BenchmarkProxyPLT.SetPreUpdateExecuted)
	BenchmarkProxyDeviceProxy.RegisterCallback(PostUpdate, commonCallbackFunc, "PostUpdate", BenchmarkProxyPLT.SetPostUpdateExecuted)

}

func BenchmarkProxy_AddDevice(b *testing.B) {
	var err error
	defer GetProfiling().Report()
	b.RunParallel(func(pb *testing.PB) {
		b.Log("Started adding devices")
		for pb.Next() {
			ltPorts := []*voltha.Port{
				{
					PortNo:     123,
					Label:      "lt-port-0",
					Type:       voltha.Port_PON_OLT,
					AdminState: common.AdminState_ENABLED,
					OperStatus: common.OperStatus_ACTIVE,
					DeviceId:   "lt-port-0-device-id",
					Peers:      []*voltha.Port_PeerPort{},
				},
			}

			ltStats := &openflow_13.OfpFlowStats{
				Id: 1000,
			}
			ltFlows := &openflow_13.Flows{
				Items: []*openflow_13.OfpFlowStats{ltStats},
			}
			ltDevice := &voltha.Device{
				Id:         "",
				Type:       "simulated_olt",
				Address:    &voltha.Device_HostAndPort{HostAndPort: "1.2.3.4:5555"},
				AdminState: voltha.AdminState_PREPROVISIONED,
				Flows:      ltFlows,
				Ports:      ltPorts,
			}

			ltDevIDBin, _ := uuid.New().MarshalBinary()
			ltDevID := "0001" + hex.EncodeToString(ltDevIDBin)[:12]
			ltDevice.Id = ltDevID

			BenchmarkProxyPLT.SetPreAddExecuted(false)
			BenchmarkProxyPLT.SetPostAddExecuted(false)

			var added interface{}
			// Add the device
			if added, err = BenchmarkProxyDeviceProxy.AddWithID(context.Background(), "/devices", ltDevID, ltDevice, ""); err != nil {
				log.With(log.Fields{"error": err}).Fatal("Cannot create proxy")
			}
			if added == nil {
				BenchmarkProxyLogger.Errorf("Failed to add device: %+v", ltDevice)
				continue
			} else {
				BenchmarkProxyLogger.Infof("Device was added 1: %+v", added)
			}

			BenchmarkProxyPLT.addMutex.Lock()
			BenchmarkProxyPLT.addedDevices = append(BenchmarkProxyPLT.addedDevices, added.(*voltha.Device).Id)
			BenchmarkProxyPLT.addMutex.Unlock()
		}
	})

	BenchmarkProxyLogger.Infof("Number of added devices : %d", len(BenchmarkProxyPLT.addedDevices))
}

func BenchmarkProxy_UpdateFirmware(b *testing.B) {
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			//for i:=0; i < b.N; i++ {

			if len(BenchmarkProxyPLT.addedDevices) > 0 {
				var target interface{}
				randomID := BenchmarkProxyPLT.addedDevices[rand.Intn(len(BenchmarkProxyPLT.addedDevices))]
				firmProxy, err := BenchmarkProxyRoot.CreateProxy(context.Background(), "/", false)
				if err != nil {
					log.With(log.Fields{"error": err}).Fatal("Cannot create firmware proxy")
				}
				target, err = firmProxy.Get(context.Background(), "/devices/"+randomID, 0, false,
					"")
				if err != nil {
					BenchmarkProxyLogger.Errorf("Failed to create target due to error %v", err)
					assert.NotNil(b, err)
				}
				if !reflect.ValueOf(target).IsValid() {
					BenchmarkProxyLogger.Errorf("Failed to find device: %s %+v", randomID, target)
					continue
				}

				BenchmarkProxyPLT.SetPreUpdateExecuted(false)
				BenchmarkProxyPLT.SetPostUpdateExecuted(false)
				firmProxy.RegisterCallback(PreUpdate, commonCallbackFunc, "PreUpdate", BenchmarkProxyPLT.SetPreUpdateExecuted)
				firmProxy.RegisterCallback(PostUpdate, commonCallbackFunc, "PostUpdate", BenchmarkProxyPLT.SetPostUpdateExecuted)

				var fwVersion int

				before := target.(*voltha.Device).FirmwareVersion
				if target.(*voltha.Device).FirmwareVersion == "n/a" {
					fwVersion = 0
				} else {
					fwVersion, _ = strconv.Atoi(target.(*voltha.Device).FirmwareVersion)
					fwVersion++
				}

				target.(*voltha.Device).FirmwareVersion = strconv.Itoa(fwVersion)
				after := target.(*voltha.Device).FirmwareVersion

				var updated interface{}
				if updated, err = firmProxy.Update(context.Background(), "/devices/"+randomID, target.(*voltha.Device), false, ""); err != nil {
					BenchmarkProxyLogger.Errorf("Failed to update firmware proxy due to error %v", err)
					assert.NotNil(b, err)
				}
				if updated == nil {
					BenchmarkProxyLogger.Errorf("Failed to update device: %+v", target)
					continue
				} else {
					BenchmarkProxyLogger.Infof("Device was updated : %+v", updated)

				}

				d, err := firmProxy.Get(context.Background(), "/devices/"+randomID, 0, false, "")
				if err != nil {
					BenchmarkProxyLogger.Errorf("Failed to get device info from firmware proxy due to error %v", err)
					assert.NotNil(b, err)
				}
				if !reflect.ValueOf(d).IsValid() {
					BenchmarkProxyLogger.Errorf("Failed to get device: %s", randomID)
					continue
				} else if d.(*voltha.Device).FirmwareVersion == after {
					BenchmarkProxyLogger.Infof("Imm Device was updated with new value: %s %+v", randomID, d)
				} else if d.(*voltha.Device).FirmwareVersion == before {
					BenchmarkProxyLogger.Errorf("Imm Device kept old value: %s %+v %+v", randomID, d, target)
				} else {
					BenchmarkProxyLogger.Errorf("Imm Device has unknown value: %s %+v %+v", randomID, d, target)
				}

				BenchmarkProxyPLT.firmwareMutex.Lock()

				BenchmarkProxyPLT.updatedFirmwares = append(
					BenchmarkProxyPLT.updatedFirmwares,
					proxyLoadChanges{ID: randomID, Before: before, After: after},
				)
				BenchmarkProxyPLT.firmwareMutex.Unlock()
			}
		}
	})
}

func BenchmarkProxy_UpdateFlows(b *testing.B) {
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			if len(BenchmarkProxyPLT.addedDevices) > 0 {
				randomID := BenchmarkProxyPLT.addedDevices[rand.Intn(len(BenchmarkProxyPLT.addedDevices))]

				flowsProxy, err := BenchmarkProxyRoot.CreateProxy(context.Background(), "/devices/"+randomID+"/flows", false)
				if err != nil {
					log.With(log.Fields{"error": err}).Fatal("Cannot create flows proxy")
				}
				flows, err := flowsProxy.Get(context.Background(), "/", 0, false, "")
				if err != nil {
					BenchmarkProxyLogger.Errorf("Failed to get flows from flows proxy due to error: %v", err)
					assert.NotNil(b, err)
				}

				before := flows.(*openflow_13.Flows).Items[0].TableId
				flows.(*openflow_13.Flows).Items[0].TableId = uint32(rand.Intn(3000))
				after := flows.(*openflow_13.Flows).Items[0].TableId

				flowsProxy.RegisterCallback(
					PreUpdate,
					commonCallback2,
				)
				flowsProxy.RegisterCallback(
					PostUpdate,
					commonCallback2,
				)

				var updated interface{}
				if updated, err = flowsProxy.Update(context.Background(), "/", flows.(*openflow_13.Flows), false, ""); err != nil {
					BenchmarkProxyLogger.Errorf("Cannot update flows proxy due to error: %v", err)
					assert.NotNil(b, err)
				}
				if updated == nil {
					b.Errorf("Failed to update flows for device: %+v", flows)
				} else {
					BenchmarkProxyLogger.Infof("Flows were updated : %+v", updated)
				}
				BenchmarkProxyPLT.flowMutex.Lock()
				BenchmarkProxyPLT.updatedFlows = append(
					BenchmarkProxyPLT.updatedFlows,
					proxyLoadChanges{ID: randomID, Before: before, After: after},
				)
				BenchmarkProxyPLT.flowMutex.Unlock()
			}
		}
	})
}

func BenchmarkProxy_GetDevices(b *testing.B) {
	//traverseBranches(BenchmarkProxy_DeviceProxy.Root.node.Branches[NONE].GetLatest(), 0)

	for i := 0; i < len(BenchmarkProxyPLT.addedDevices); i++ {
		devToGet := BenchmarkProxyPLT.addedDevices[i]
		// Verify that the added device can now be retrieved
		d, err := BenchmarkProxyDeviceProxy.Get(context.Background(), "/devices/"+devToGet, 0, false, "")
		if err != nil {
			BenchmarkProxyLogger.Errorf("Failed to get device info from device proxy due to error: %v", err)
			assert.NotNil(b, err)
		}
		if !reflect.ValueOf(d).IsValid() {
			BenchmarkProxyLogger.Errorf("Failed to get device: %s", devToGet)
			continue
		} else {
			BenchmarkProxyLogger.Infof("Got device: %s %+v", devToGet, d)
		}
	}
}

func BenchmarkProxy_GetUpdatedFirmware(b *testing.B) {
	for i := 0; i < len(BenchmarkProxyPLT.updatedFirmwares); i++ {
		devToGet := BenchmarkProxyPLT.updatedFirmwares[i].ID
		// Verify that the updated device can be retrieved and that the updates were actually applied
		d, err := BenchmarkProxyDeviceProxy.Get(context.Background(), "/devices/"+devToGet, 0, false, "")
		if err != nil {
			BenchmarkProxyLogger.Errorf("Failed to get device info from device proxy due to error: %v", err)
			assert.NotNil(b, err)
		}
		if !reflect.ValueOf(d).IsValid() {
			BenchmarkProxyLogger.Errorf("Failed to get device: %s", devToGet)
			continue
		} else if d.(*voltha.Device).FirmwareVersion == BenchmarkProxyPLT.updatedFirmwares[i].After.(string) {
			BenchmarkProxyLogger.Infof("Device was updated with new value: %s %+v", devToGet, d)
		} else if d.(*voltha.Device).FirmwareVersion == BenchmarkProxyPLT.updatedFirmwares[i].Before.(string) {
			BenchmarkProxyLogger.Errorf("Device kept old value: %s %+v %+v", devToGet, d, BenchmarkProxyPLT.updatedFirmwares[i])
		} else {
			BenchmarkProxyLogger.Errorf("Device has unknown value: %s %+v %+v", devToGet, d, BenchmarkProxyPLT.updatedFirmwares[i])
		}
	}
}
