package api

import (
	"context"
	"net"
	"os"
	"testing"

	"time"

	"code.fbi.h-da.de/danet/gosdn/api/go/gosdn/conflict"
	cpb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/core"
	ppb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/pnd"
	apb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/rbac"
	tpb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/transport"
	"code.fbi.h-da.de/danet/gosdn/controller/app"
	"code.fbi.h-da.de/danet/gosdn/controller/config"
	eventservice "code.fbi.h-da.de/danet/gosdn/controller/eventService"
	"code.fbi.h-da.de/danet/gosdn/controller/interfaces/networkdomain"
	"code.fbi.h-da.de/danet/gosdn/controller/interfaces/networkelement"
	"code.fbi.h-da.de/danet/gosdn/controller/interfaces/rbac"
	"code.fbi.h-da.de/danet/gosdn/controller/interfaces/southbound"
	"code.fbi.h-da.de/danet/gosdn/controller/mocks"
	nbi "code.fbi.h-da.de/danet/gosdn/controller/northbound/server"
	"code.fbi.h-da.de/danet/gosdn/controller/nucleus"
	"code.fbi.h-da.de/danet/gosdn/controller/nucleus/util/proto"
	rbacImpl "code.fbi.h-da.de/danet/gosdn/controller/rbac"
	"code.fbi.h-da.de/danet/gosdn/controller/topology"
	"code.fbi.h-da.de/danet/gosdn/controller/topology/links"
	"code.fbi.h-da.de/danet/gosdn/controller/topology/nodes"
	"code.fbi.h-da.de/danet/gosdn/controller/topology/ports"
	routingtables "code.fbi.h-da.de/danet/gosdn/controller/topology/routing-tables"
	"code.fbi.h-da.de/danet/gosdn/controller/topology/store"
	"code.fbi.h-da.de/danet/gosdn/models/generated/openconfig"
	"github.com/google/uuid"
	log "github.com/sirupsen/logrus"
	"github.com/stretchr/testify/mock"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/test/bufconn"

	gpb "github.com/openconfig/gnmi/proto/gnmi"
	pb "google.golang.org/protobuf/proto"
)

/*
Based on this StackOverflow answer: https://stackoverflow.com/a/52080545/4378176
*/

const bufSize = 1024 * 1024
const bufnet = "bufnet"
const pndID = "2043519e-46d1-4963-9a8e-d99007e104b8"
const changeID = "0992d600-f7d4-4906-9559-409b04d59a5f"
const sbiID = "f6fd4b35-f039-4111-9156-5e4501bb8a5a"
const mneID = "7e0ed8cc-ebf5-46fa-9794-741494914883"

var pndStore networkdomain.PndStore
var userService rbac.UserService
var roleService rbac.RoleService
var sbiStore southbound.Store
var lis *bufconn.Listener
var pndUUID uuid.UUID
var sbiUUID uuid.UUID

func bootstrapUnitTest() {
	dialOptions = []grpc.DialOption{
		grpc.WithContextDialer(bufDialer),
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	}
	lis = bufconn.Listen(bufSize)
	s := grpc.NewServer()

	changeUUID, err := uuid.Parse(changeID)
	if err != nil {
		log.Fatal(err)
	}

	pndUUID, err = uuid.Parse(pndID)
	if err != nil {
		log.Fatal(err)
	}

	sbiUUID, err = uuid.Parse(sbiID)
	if err != nil {
		log.Fatal(err)
	}

	mneUUID, err := uuid.Parse(mneID)
	if err != nil {
		log.Fatal(err)
	}

	eventService := eventservice.NewMockEventService()

	pndStore = nucleus.NewMemoryPndStore()
	sbiStore = nucleus.NewMemorySbiStore()
	userService = rbacImpl.NewUserService(rbacImpl.NewMemoryUserStore(), eventService)
	roleService = rbacImpl.NewRoleService(rbacImpl.NewMemoryRoleStore(), eventService)
	if err := clearAndCreateAuthTestSetup(); err != nil {
		log.Fatal(err)
	}

	previousHostname := "previousHostname"
	intendedHostname := "intendedHostname"

	mockChange := &mocks.Change{}
	mockChange.On("Age").Return(time.Hour)
	mockChange.On("State").Return(ppb.ChangeState_CHANGE_STATE_INCONSISTENT)
	mockChange.On("PreviousState").Return(&openconfig.Device{
		System: &openconfig.OpenconfigSystem_System{
			Config: &openconfig.OpenconfigSystem_System_Config{
				Hostname: &previousHostname,
			},
		},
	})
	mockChange.On("IntendedState").Return(&openconfig.Device{
		System: &openconfig.OpenconfigSystem_System{
			Config: &openconfig.OpenconfigSystem_System_Config{
				Hostname: &intendedHostname,
			},
		},
	})

	sbi := &nucleus.OpenConfig{}
	schema, err := sbi.Schema()
	if err != nil {
		log.Fatal(err)
	}

	mockNetworkElement := &mocks.NetworkElement{}
	mockNetworkElement.On("SBI").Return(sbi)
	mockNetworkElement.On("ID").Return(mneUUID)
	mockNetworkElement.On("GetModel").Return(schema.Root)
	mockNetworkElement.On("Name").Return("openconfig")
	mockNetworkElement.On("TransportAddress").Return("127.0.0.1:6030")
	mockNetworkElement.On("GetMetadata").Return(conflict.Metadata{ResourceVersion: 0})

	mockPnd := mocks.NetworkDomain{}
	mockPnd.On("ID").Return(pndUUID)
	mockPnd.On("GetName").Return("test")
	mockPnd.On("GetDescription").Return("test")
	mockPnd.On("PendingChanges").Return([]uuid.UUID{changeUUID})
	mockPnd.On("CommittedChanges").Return([]uuid.UUID{changeUUID})
	mockPnd.On("GetChange", mock.Anything).Return(mockChange, nil)
	mockPnd.On("AddNetworkElement", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
	mockPnd.On("GetNetworkElement", mock.Anything).Return(mockNetworkElement, nil)
	mockPnd.On("Commit", mock.Anything).Return(nil)
	mockPnd.On("Confirm", mock.Anything).Return(nil)
	mockPnd.On("NetworkElements").Return([]networkelement.NetworkElement{
		&nucleus.CommonNetworkElement{
			UUID:  mneUUID,
			Model: &openconfig.Device{},
		},
	})
	mockPnd.On("GetSBIs").Return([]mocks.SouthboundInterface{})
	mockPnd.On("ChangeMNE", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(uuid.Nil, nil)

	if err := pndStore.Add(&mockPnd); err != nil {
		log.Fatal(err)
	}

	jwtManager := rbacImpl.NewJWTManager("", (10000 * time.Hour))
	appService := app.NewMockAppService()

	nodeStore := store.NewGenericStore[nodes.Node]()
	nodeService := nodes.NewNodeService(nodeStore, eventService)

	portStore := store.NewGenericStore[ports.Port]()
	portService := ports.NewPortService(portStore, eventService)

	topoloyStore := store.NewGenericStore[links.Link]()
	topologyService := topology.NewTopologyService(topoloyStore, nodeService, portService, eventService)

	routeStore := store.NewGenericStore[routingtables.RoutingTable]()
	routeService := routingtables.NewRoutingTableService(routeStore, nodeService, portService, eventService)

	northbound := nbi.NewNBI(
		pndStore,
		userService,
		roleService,
		*jwtManager,
		topologyService,
		nodeService,
		portService,
		routeService,
		appService,
		&mockPnd,
	)

	cpb.RegisterCoreServiceServer(s, northbound.Core)
	ppb.RegisterPndServiceServer(s, northbound.Pnd)
	apb.RegisterAuthServiceServer(s, northbound.Auth)
	apb.RegisterUserServiceServer(s, northbound.User)
	apb.RegisterRoleServiceServer(s, northbound.Role)

	go func() {
		if err := s.Serve(lis); err != nil {
			log.Fatalf("Server exited with error: %v", err)
		}
	}()
}

func bufDialer(context.Context, string) (net.Conn, error) {
	return lis.Dial()
}

const testPath = "/system/config/hostname"

var testAddress = "10.254.254.105:6030"
var testAPIEndpoint = "gosdn-latest.apps.ocp.fbi.h-da.de"
var testUsername = "admin"
var testPassword = "arista"
var opt *tpb.TransportOption
var gnmiMessages map[string]pb.Message

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

func bootstrapIntegrationTest() {
	log.SetLevel(config.LogLevel)

	addr := os.Getenv("CEOS_TEST_ENDPOINT")
	if addr != "" {
		testAddress = addr
		log.Infof("CEOS_TEST_ENDPOINT set to %v", testAddress)
	}
	api := os.Getenv("GOSDN_TEST_API_ENDPOINT")
	if api != "" {
		testAPIEndpoint = api
		log.Infof("GOSDN_TEST_API_ENDPOINT set to %v", testAPIEndpoint)
	}
	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{
		"../test/proto/cap-resp-arista-ceos":                  &gpb.CapabilityResponse{},
		"../test/proto/req-full-node":                         &gpb.GetRequest{},
		"../test/proto/req-full-node-arista-ceos":             &gpb.GetRequest{},
		"../test/proto/req-interfaces-arista-ceos":            &gpb.GetRequest{},
		"../test/proto/req-interfaces-interface-arista-ceos":  &gpb.GetRequest{},
		"../test/proto/req-interfaces-wildcard":               &gpb.GetRequest{},
		"../test/proto/resp-full-node":                        &gpb.GetResponse{},
		"../test/proto/resp-full-node-arista-ceos":            &gpb.GetResponse{},
		"../test/proto/resp-interfaces-arista-ceos":           &gpb.GetResponse{},
		"../test/proto/resp-interfaces-interface-arista-ceos": &gpb.GetResponse{},
		"../test/proto/resp-interfaces-wildcard":              &gpb.GetResponse{},
		"../test/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,
	}
}