package server

import (
	"context"
	"log"
	"net"
	"testing"
	"time"

	apb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/rbac"
	spb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/southbound"
	eventservice "code.fbi.h-da.de/danet/gosdn/controller/eventService"
	"code.fbi.h-da.de/danet/gosdn/controller/nucleus"
	"code.fbi.h-da.de/danet/gosdn/controller/rbac"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/test/bufconn"
)

func getTestAuthInterceptorServer(t *testing.T) (*AuthInterceptor, *UserServer, *RoleServer, *SbiServer) {
	initUUIDs(t)
	jwtManager := rbac.NewJWTManager("test", time.Minute)
	eventService := eventservice.NewMockEventService()

	userStore := rbac.NewMemoryUserStore()
	userService := rbac.NewUserService(userStore, eventService)

	roleStore := rbac.NewMemoryRoleStore()
	roleService := rbac.NewRoleService(roleStore, eventService)

	mockPnd := getMockPnd(t)

	pndStore := nucleus.NewMemoryPndStore()
	if err := pndStore.Add(mockPnd); err != nil {
		t.Fatal(err)
	}

	s := NewAuthInterceptor(jwtManager, userService, roleService)
	u := NewUserServer(jwtManager, userService)
	r := NewRoleServer(jwtManager, roleService)
	sbiServer := NewSbiServer(pndStore)

	clearAndCreateAuthTestSetup(userService, roleService)

	return s, u, r, sbiServer
}

func dialer(interceptorServer *AuthInterceptor, userServer *UserServer, roleServer *RoleServer, sbiServer *SbiServer) func(context.Context, string) (net.Conn, error) {
	listener := bufconn.Listen(1024 * 1024)

	interceptor := interceptorServer
	server := grpc.NewServer(grpc.UnaryInterceptor(interceptor.Unary()), grpc.StreamInterceptor(interceptor.Stream()))

	apb.RegisterUserServiceServer(server, userServer)
	spb.RegisterSbiServiceServer(server, sbiServer)

	go func() {
		if err := server.Serve(listener); err != nil {
			log.Fatal(err)
		}
	}()

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

func TestAuthInterceptor_Unary(t *testing.T) {
	authServer, userServer, roleServer, sbiServer := getTestAuthInterceptorServer(t)
	validToken, err := createTestUserToken("testAdmin", true, authServer.userService, authServer.jwtManager)
	if err != nil {
		t.Fatal(err)
	}

	wrongUserToken, err := createTestUserToken("foo", false, authServer.userService, authServer.jwtManager)
	if err != nil {
		t.Fatal(err)
	}

	ctx := context.Background()
	conn, err := grpc.DialContext(
		ctx,
		"",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithContextDialer(dialer(authServer, userServer, roleServer, sbiServer)),
	)
	if err != nil {
		t.Fatal(err)
	}
	defer conn.Close()

	client := apb.NewUserServiceClient(conn)

	type args struct {
		ctx     context.Context
		request *apb.GetUsersRequest
	}
	tests := []struct {
		name    string
		args    args
		want    *apb.GetUsersResponse
		wantErr bool
	}{
		{
			name: "default unary interceptor",
			args: args{
				ctx:     metadata.NewOutgoingContext(context.Background(), metadata.Pairs("authorize", validToken)),
				request: &apb.GetUsersRequest{},
			},
			want: &apb.GetUsersResponse{
				Status: apb.Status_STATUS_OK,
			},
			wantErr: false,
		},
		{
			name: "error unary invalid user token",
			args: args{
				ctx:     metadata.NewOutgoingContext(context.Background(), metadata.Pairs("authorize", wrongUserToken)),
				request: &apb.GetUsersRequest{},
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "error unary invalid token string",
			args: args{
				ctx:     metadata.NewOutgoingContext(context.Background(), metadata.Pairs("authorize", "foo")),
				request: &apb.GetUsersRequest{},
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "error unary no token in metadata",
			args: args{
				ctx:     metadata.NewOutgoingContext(context.Background(), metadata.Pairs("foo", "foo")),
				request: &apb.GetUsersRequest{},
			},
			want:    nil,
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := client.GetUsers(tt.args.ctx, tt.args.request)
			if (err != nil) != tt.wantErr {
				t.Errorf("AuthInterceptor.Unary() = %v, wantErr %v", err, tt.wantErr)
				return
			}

			if got != nil && got.Status != tt.want.Status {
				t.Errorf("AuthInterceptor.Unary() = %v, wantErr %v", err, tt.wantErr)
				return
			}
		})
	}
}

func TestAuthInterceptor_Stream(t *testing.T) {
	authServer, userServer, roleServer, sbiServer := getTestAuthInterceptorServer(t)
	validToken, err := createTestUserToken("testAdmin", true, authServer.userService, authServer.jwtManager)
	if err != nil {
		t.Fatal(err)
	}

	ctx := context.Background()
	conn, err := grpc.DialContext(
		ctx,
		"",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithContextDialer(dialer(authServer, userServer, roleServer, sbiServer)),
	)
	if err != nil {
		t.Fatal(err)
	}
	defer conn.Close()

	client := spb.NewSbiServiceClient(conn)

	type args struct {
		ctx     context.Context
		request *spb.GetSchemaRequest
	}
	tests := []struct {
		name string
		args args
		want bool
	}{
		{
			name: "default stream interceptor",
			args: args{
				ctx: metadata.NewOutgoingContext(context.Background(), metadata.Pairs("authorize", validToken)),
				request: &spb.GetSchemaRequest{
					Pid: pndID,
					Sid: sbiID,
				},
			},
			want: true,
		},
		{
			name: "error stream interceptor",
			args: args{
				ctx: metadata.NewOutgoingContext(context.Background(), metadata.Pairs("authorize", "foo")),
				request: &spb.GetSchemaRequest{
					Pid: pndID,
					Sid: sbiID,
				},
			},
			want: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := client.GetSchema(tt.args.ctx, tt.args.request)
			if err != nil {
				t.Errorf("AuthInterceptor.Stream() = %v", err)
				return
			}

			payload, _ := got.Recv()
			if (payload != nil) != tt.want {
				t.Errorf("AuthInterceptor.Stream() = %v", tt.want)
				return
			}
		})
	}
}

func TestAuthInterceptor_authorize(t *testing.T) {
	authServer, _, _, _ := getTestAuthInterceptorServer(t)
	validToken, err := createTestUserToken("testAdmin", true, authServer.userService, authServer.jwtManager)
	if err != nil {
		t.Fatal(err)
	}

	wrongUserToken, err := createTestUserToken("foo", false, authServer.userService, authServer.jwtManager)
	if err != nil {
		t.Fatal(err)
	}

	type args struct {
		ctx    context.Context
		method string
	}
	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		{
			name: "default authorize",
			args: args{
				ctx:    metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorize", validToken)),
				method: "/gosdn.rbac.UserService/GetUsers",
			},
			wantErr: false,
		},
		{
			name: "error invalid token",
			args: args{
				ctx:    metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorize", wrongUserToken)),
				method: "/gosdn.rbac.UserService/GetUsers",
			},
			wantErr: true,
		},
		{
			name: "error no permission for request",
			args: args{
				ctx:    metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorize", validToken)),
				method: "/gosdn.pnd.PndService/DeleteOnd",
			},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if err := authServer.authorize(tt.args.ctx, tt.args.method); (err != nil) != tt.wantErr {
				t.Errorf("AuthInterceptor.authorize() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}