// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements.  See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You 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 code_processing

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/google/go-cmp/cmp"
	"io/fs"
	"os"
	"os/exec"
	"path/filepath"
	"reflect"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/go-redis/redismock/v8"
	"github.com/google/uuid"
	"go.uber.org/goleak"

	pb "beam.apache.org/playground/backend/internal/api/v1"
	"beam.apache.org/playground/backend/internal/cache"
	"beam.apache.org/playground/backend/internal/cache/local"
	"beam.apache.org/playground/backend/internal/cache/redis"
	"beam.apache.org/playground/backend/internal/db/entity"
	"beam.apache.org/playground/backend/internal/environment"
	"beam.apache.org/playground/backend/internal/executors"
	"beam.apache.org/playground/backend/internal/fs_tool"
	"beam.apache.org/playground/backend/internal/utils"
	"beam.apache.org/playground/backend/internal/validators"
)

const (
	javaConfig      = "{\n  \"compile_cmd\": \"javac\",\n  \"run_cmd\": \"java\",\n  \"test_cmd\": \"java\",\n  \"compile_args\": [\n    \"-d\",\n    \"bin\",\n    \"-parameters\",\n    \"-classpath\"\n  ],\n  \"run_args\": [\n    \"-cp\",\n    \"bin:\"\n  ],\n  \"test_args\": [\n    \"-cp\",\n    \"bin:\",\n    \"JUnit\"\n  ]\n}"
	pythonConfig    = "{\n  \"compile_cmd\": \"\",\n  \"run_cmd\": \"python3\",\n  \"compile_args\": [],\n  \"run_args\": []\n}"
	goConfig        = "{\n  \"compile_cmd\": \"go\",\n  \"run_cmd\": \"\",\n  \"compile_args\": [\n    \"build\",\n    \"-o\",\n    \"bin\"\n  ],\n  \"run_args\": [\n  ]\n}"
	pipelinesFolder = "executable_files"
	configFolder    = "configs"
	resourcesFolder = "resources"
	helloWordPython = "if __name__ == \"__main__\":\n    print(\"Hello world!\")\n"
	helloWordGo     = "package main\nimport \"fmt\"\nfunc main() {\n    fmt.Println(\"hello world\")\n}\n"
	helloWordJava   = "class HelloWorld {\n    public static void main(String[] args) {\n        System.out.println(\"Hello world!\");\n    }\n}"
	graphFilePath   = "resources/graph.dot"
	jsonExtension   = ".json"
)

var opt goleak.Option
var cacheService cache.Cache

func TestMain(m *testing.M) {
	setup()
	opt = goleak.IgnoreCurrent()
	exitValue := m.Run()
	teardown()
	os.Exit(exitValue)
}

func setup() {
	// create configs for java
	err := os.MkdirAll(configFolder, fs.ModePerm)
	if err != nil {
		panic(err)
	}
	filePath := filepath.Join(configFolder, fmt.Sprintf("%s%s", pb.Sdk_SDK_JAVA.String(), jsonExtension))
	err = os.WriteFile(filePath, []byte(javaConfig), 0600)
	if err != nil {
		panic(err)
	}

	// create dir with graph file
	err = os.MkdirAll(resourcesFolder, fs.ModePerm)
	if err != nil {
		panic(err)
	}
	err = os.WriteFile(graphFilePath, []byte("graph"), 0600)
	if err != nil {
		panic(err)
	}

	path, err := os.Getwd()
	if err != nil {
		panic(err)
	}
	err = os.Setenv("BEAM_SDK", pb.Sdk_SDK_JAVA.String())
	if err != nil {
		panic(err)
	}
	err = os.Setenv("APP_WORK_DIR", path)
	if err != nil {
		panic(err)
	}

	cacheService = local.New(context.Background())
}

func teardown() {
	err := os.RemoveAll(configFolder)
	if err != nil {
		panic(fmt.Errorf("error during test teardown: %s", err.Error()))
	}
	err = os.RemoveAll(pipelinesFolder)
	if err != nil {
		panic(fmt.Errorf("error during test teardown: %s", err.Error()))
	}
	err = os.RemoveAll(resourcesFolder)
	if err != nil {
		panic(fmt.Errorf("error during test teardown: %s", err.Error()))
	}
	os.Clearenv()
}

func Test_Process(t *testing.T) {
	defer goleak.VerifyNone(t, opt)
	appEnvs, err := environment.GetApplicationEnvsFromOsEnvs()
	if err != nil {
		panic(err)
	}
	sdkJavaEnv, err := environment.ConfigureBeamEnvs(appEnvs.WorkingDir())
	if err != nil {
		panic(err)
	}
	sdkGoEnv := *sdkJavaEnv
	sdkGoEnv.ApacheBeamSdk = pb.Sdk_SDK_GO
	incorrectGoHelloWord := "package main\nimport \"fmt\"\nfunc main() {\n    fmt.Println(\"hello world\").\n}\n"
	type args struct {
		ctx             context.Context
		appEnv          *environment.ApplicationEnvs
		sdkEnv          *environment.BeamEnvs
		pipelineId      uuid.UUID
		pipelineOptions string
	}
	tests := []struct {
		name                  string
		createExecFile        bool
		code                  string
		cancelFunc            bool
		expectedStatus        pb.Status
		expectedRunOutput     interface{}
		expectedRunError      interface{}
		expectedCompileOutput interface{}
		args                  args
	}{
		{
			// Test case with calling processCode method with small timeout.
			// As a result status into cache should be set as Status_STATUS_RUN_TIMEOUT.
			name:                  "Small pipeline execution timeout",
			createExecFile:        false,
			code:                  "",
			cancelFunc:            false,
			expectedStatus:        pb.Status_STATUS_RUN_TIMEOUT,
			expectedCompileOutput: nil,
			expectedRunOutput:     nil,
			expectedRunError:      nil,
			args: args{
				ctx:             context.Background(),
				appEnv:          &environment.ApplicationEnvs{},
				sdkEnv:          sdkJavaEnv,
				pipelineId:      uuid.New(),
				pipelineOptions: "",
			},
		},
		{
			// Test case with calling processCode method without preparing files with code.
			// As a result status into cache should be set as Status_STATUS_VALIDATION_ERROR.
			name:                  "Validation failed",
			createExecFile:        false,
			code:                  "",
			cancelFunc:            false,
			expectedStatus:        pb.Status_STATUS_VALIDATION_ERROR,
			expectedCompileOutput: nil,
			expectedRunOutput:     nil,
			expectedRunError:      nil,
			args: args{
				ctx:             context.Background(),
				appEnv:          appEnvs,
				sdkEnv:          sdkJavaEnv,
				pipelineId:      uuid.New(),
				pipelineOptions: "",
			},
		},
		{
			// Test case with calling processCode method with incorrect code.
			// As a result status into cache should be set as Status_STATUS_COMPILE_ERROR.
			name:                  "Compilation failed",
			createExecFile:        true,
			code:                  "MOCK_CODE",
			cancelFunc:            false,
			expectedStatus:        pb.Status_STATUS_COMPILE_ERROR,
			expectedCompileOutput: "error: exit status 1\noutput: %s:1: error: reached end of file while parsing\nMOCK_CODE\n^\n1 error\n",
			expectedRunOutput:     nil,
			expectedRunError:      nil,
			args: args{
				ctx:             context.Background(),
				appEnv:          appEnvs,
				sdkEnv:          sdkJavaEnv,
				pipelineId:      uuid.New(),
				pipelineOptions: "",
			},
		},
		{
			// Test case with calling processCode method with incorrect logic into code.
			// As a result status into cache should be set as Status_STATUS_RUN_ERROR.
			name:                  "Run failed",
			createExecFile:        true,
			code:                  "class HelloWorld {\n    public static void main(String[] args) {\n        System.out.println(1/0);\n    }\n}",
			cancelFunc:            false,
			expectedStatus:        pb.Status_STATUS_RUN_ERROR,
			expectedCompileOutput: "",
			expectedRunOutput:     "",
			expectedRunError:      "error: exit status 1\noutput: Exception in thread \"main\" java.lang.ArithmeticException: / by zero\n\tat HelloWorld.main(%s.java:3)\n",
			args: args{
				ctx:             context.Background(),
				appEnv:          appEnvs,
				sdkEnv:          sdkJavaEnv,
				pipelineId:      uuid.New(),
				pipelineOptions: "",
			},
		},
		{
			// Test case with calling processCode with canceling code processing.
			// As a result status into cache should be set as Status_STATUS_CANCELED.
			name:                  "Cancel",
			createExecFile:        true,
			code:                  "class HelloWorld {\n    public static void main(String[] args) {\n        while(true){}\n    }\n}",
			cancelFunc:            true,
			expectedStatus:        pb.Status_STATUS_CANCELED,
			expectedCompileOutput: "",
			expectedRunOutput:     "",
			expectedRunError:      "",
			args: args{
				ctx:             context.Background(),
				appEnv:          appEnvs,
				sdkEnv:          sdkJavaEnv,
				pipelineId:      uuid.New(),
				pipelineOptions: "",
			},
		},
		{
			// Test case with calling processCode without any error cases.
			// As a result status into cache should be set as Status_STATUS_FINISHED.
			name:                  "Processing complete successfully on java sdk",
			createExecFile:        true,
			cancelFunc:            false,
			code:                  helloWordJava,
			expectedStatus:        pb.Status_STATUS_FINISHED,
			expectedCompileOutput: "",
			expectedRunOutput:     "Hello world!\n",
			expectedRunError:      "",
			args: args{
				ctx:             context.Background(),
				appEnv:          appEnvs,
				sdkEnv:          sdkJavaEnv,
				pipelineId:      uuid.New(),
				pipelineOptions: "",
			},
		},
		{
			// Test case with calling processCode method with incorrect go code.
			// As a result status into cache should be set as Status_STATUS_PREPARATION_ERROR.
			name:                  "Prepare step failed",
			createExecFile:        true,
			code:                  incorrectGoHelloWord,
			cancelFunc:            false,
			expectedStatus:        pb.Status_STATUS_PREPARATION_ERROR,
			expectedCompileOutput: nil,
			expectedRunOutput:     nil,
			expectedRunError:      nil,
			args: args{
				ctx:             context.Background(),
				appEnv:          appEnvs,
				sdkEnv:          &sdkGoEnv,
				pipelineId:      uuid.New(),
				pipelineOptions: "",
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			lc, _ := fs_tool.NewLifeCycle(pb.Sdk_SDK_JAVA, tt.args.pipelineId, filepath.Join(os.Getenv("APP_WORK_DIR"), pipelinesFolder))
			err := lc.CreateFolders()
			if err != nil {
				t.Fatalf("error during prepare folders: %s", err.Error())
			}

			sources := []entity.FileEntity{{Name: "main.java", Content: tt.code, IsMain: true}}
			if tt.createExecFile {
				_ = lc.CreateSourceCodeFiles(sources)
			}
			if err = utils.SetToCache(cacheService, tt.args.pipelineId, cache.Canceled, false); err != nil {
				t.Fatal("error during set cancel flag to cache")
			}
			if tt.cancelFunc {
				go func(ctx context.Context, pipelineId uuid.UUID) {
					// to imitate behavior of cancellation
					time.Sleep(5 * time.Second)
					_ = cacheService.SetValue(ctx, pipelineId, cache.Canceled, true)
				}(tt.args.ctx, tt.args.pipelineId)
			}
			Process(tt.args.ctx, cacheService, lc, tt.args.pipelineId, tt.args.appEnv, tt.args.sdkEnv, tt.args.pipelineOptions)

			status, _ := cacheService.GetValue(tt.args.ctx, tt.args.pipelineId, cache.Status)
			if !reflect.DeepEqual(status, tt.expectedStatus) {
				t.Errorf("processCode() set status: %s, but expectes: %s", status, tt.expectedStatus)
			}

			compileOutput, _ := cacheService.GetValue(tt.args.ctx, tt.args.pipelineId, cache.CompileOutput)
			if tt.expectedCompileOutput != nil && strings.Contains(tt.expectedCompileOutput.(string), "%s") {
				tt.expectedCompileOutput = fmt.Sprintf(tt.expectedCompileOutput.(string), lc.Paths.AbsoluteSourceFilePath)
			}
			if !reflect.DeepEqual(compileOutput, tt.expectedCompileOutput) {
				t.Errorf("processCode() set compileOutput: %s, but expectes: %s", compileOutput, tt.expectedCompileOutput)
			}

			runOutput, _ := cacheService.GetValue(tt.args.ctx, tt.args.pipelineId, cache.RunOutput)
			if !reflect.DeepEqual(runOutput, tt.expectedRunOutput) {
				t.Errorf("processCode() set runOutput: %s, but expectes: %s", runOutput, tt.expectedRunOutput)
			}

			runError, _ := cacheService.GetValue(tt.args.ctx, tt.args.pipelineId, cache.RunError)
			if tt.expectedRunError != nil && strings.Contains(tt.expectedRunError.(string), "%s") {
				tt.expectedRunError = fmt.Sprintf(tt.expectedRunError.(string), tt.args.pipelineId)
			}
			if !reflect.DeepEqual(runError, tt.expectedRunError) {
				t.Errorf("processCode() set runError: %s, but expectes: %s", runError, tt.expectedRunError)
			}
		})
	}
}

func TestGetProcessingOutput(t *testing.T) {
	defer goleak.VerifyNone(t, opt)
	pipelineId := uuid.New()
	incorrectConvertPipelineId := uuid.New()
	err := cacheService.SetValue(context.Background(), pipelineId, cache.RunOutput, "MOCK_RUN_OUTPUT")
	if err != nil {
		panic(err)
	}
	err = cacheService.SetValue(context.Background(), incorrectConvertPipelineId, cache.RunOutput, cache.RunOutput)
	if err != nil {
		panic(err)
	}

	type args struct {
		ctx          context.Context
		cacheService cache.Cache
		key          uuid.UUID
		subKey       cache.SubKey
		errorTitle   string
	}
	tests := []struct {
		name    string
		args    args
		want    string
		wantErr bool
	}{
		{
			// Test case with calling GetProcessingOutput with pipelineId which doesn't contain run output.
			// As a result, want to receive an error.
			name: "Get run output with incorrect pipelineId",
			args: args{
				ctx:          context.Background(),
				cacheService: cacheService,
				key:          uuid.New(),
				subKey:       cache.RunOutput,
				errorTitle:   "",
			},
			want:    "",
			wantErr: true,
		},
		{
			// Test case with calling GetProcessingOutput with pipelineId which contains incorrect run output.
			// As a result, want to receive an error.
			name: "Get run output with incorrect run output",
			args: args{
				ctx:          context.Background(),
				cacheService: cacheService,
				key:          incorrectConvertPipelineId,
				subKey:       cache.RunOutput,
				errorTitle:   "",
			},
			want:    "",
			wantErr: true,
		},
		{
			// Test case with calling GetProcessingOutput with pipelineId which contains run output.
			// As a result, want to receive an expected string.
			name: "Get run output with correct pipelineId",
			args: args{
				ctx:          context.Background(),
				cacheService: cacheService,
				key:          pipelineId,
				subKey:       cache.RunOutput,
				errorTitle:   "",
			},
			want:    "MOCK_RUN_OUTPUT",
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := GetProcessingOutput(tt.args.ctx, tt.args.cacheService, tt.args.key, tt.args.subKey, tt.args.errorTitle)
			if (err != nil) != tt.wantErr {
				t.Errorf("GetProcessingOutput() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != tt.want {
				t.Errorf("GetProcessingOutput() got = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestGetProcessingStatus(t *testing.T) {
	defer goleak.VerifyNone(t, opt)
	pipelineId := uuid.New()
	incorrectConvertPipelineId := uuid.New()
	err := cacheService.SetValue(context.Background(), pipelineId, cache.Status, pb.Status_STATUS_FINISHED)
	if err != nil {
		panic(err)
	}
	err = cacheService.SetValue(context.Background(), incorrectConvertPipelineId, cache.Status, "MOCK_STATUS")
	if err != nil {
		panic(err)
	}

	type args struct {
		ctx          context.Context
		cacheService cache.Cache
		key          uuid.UUID
		errorTitle   string
	}
	tests := []struct {
		name    string
		args    args
		want    pb.Status
		wantErr bool
	}{
		{
			// Test case with calling GetProcessingStatus with pipelineId which doesn't contain status.
			// As a result, want to receive an error.
			name: "Get status with incorrect pipelineId",
			args: args{
				ctx:          context.Background(),
				cacheService: cacheService,
				key:          uuid.New(),
				errorTitle:   "",
			},
			want:    pb.Status_STATUS_UNSPECIFIED,
			wantErr: true,
		},
		{
			// Test case with calling GetProcessingStatus with pipelineId which contains incorrect status value in cache.
			// As a result, want to receive an error.
			name: "Get status with incorrect cache value",
			args: args{
				ctx:          context.Background(),
				cacheService: cacheService,
				key:          incorrectConvertPipelineId,
				errorTitle:   "",
			},
			want:    pb.Status_STATUS_UNSPECIFIED,
			wantErr: true,
		},
		{
			// Test case with calling GetProcessingStatus with pipelineId which contains status.
			// As a result, want to receive an expected status.
			name: "Get status with correct pipelineId",
			args: args{
				ctx:          context.Background(),
				cacheService: cacheService,
				key:          pipelineId,
				errorTitle:   "",
			},
			want:    pb.Status_STATUS_FINISHED,
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := GetProcessingStatus(tt.args.ctx, tt.args.cacheService, tt.args.key, tt.args.errorTitle)
			if (err != nil) != tt.wantErr {
				t.Errorf("GetProcessingStatus() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != tt.want {
				t.Errorf("GetProcessingStatus() got = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestGetLastIndex(t *testing.T) {
	defer goleak.VerifyNone(t, opt)
	pipelineId := uuid.New()
	incorrectConvertPipelineId := uuid.New()
	err := cacheService.SetValue(context.Background(), pipelineId, cache.RunOutputIndex, 2)
	if err != nil {
		panic(err)
	}

	type args struct {
		ctx          context.Context
		cacheService cache.Cache
		key          uuid.UUID
		subKey       cache.SubKey
		errorTitle   string
	}
	tests := []struct {
		name    string
		args    args
		want    int
		wantErr bool
	}{
		{
			// Test case with calling GetLastIndex with pipelineId which doesn't contain last index.
			// As a result, want to receive an error.
			name: "Get last index with incorrect pipelineId",
			args: args{
				ctx:          context.Background(),
				cacheService: cacheService,
				key:          uuid.New(),
				subKey:       cache.RunOutputIndex,
				errorTitle:   "",
			},
			want:    0,
			wantErr: true,
		},
		{
			// Test case with calling GetLastIndex with pipelineId which contains incorrect status value in cache.
			// As a result, want to receive an error.
			name: "Get last index with incorrect cache value",
			args: args{
				ctx:          context.Background(),
				cacheService: cacheService,
				key:          incorrectConvertPipelineId,
				subKey:       cache.RunOutputIndex,
				errorTitle:   "",
			},
			want:    0,
			wantErr: true,
		},
		{
			// Test case with calling GetLastIndex with pipelineId which contains last index.
			// As a result, want to receive an expected last index.
			name: "Get last index with correct pipelineId",
			args: args{
				ctx:          context.Background(),
				cacheService: cacheService,
				key:          pipelineId,
				subKey:       cache.RunOutputIndex,
				errorTitle:   "",
			},
			want:    2,
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := GetLastIndex(tt.args.ctx, tt.args.cacheService, tt.args.key, tt.args.subKey, tt.args.errorTitle)
			if (err != nil) != tt.wantErr {
				t.Errorf("GetLastIndex() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != tt.want {
				t.Errorf("GetLastIndex() got = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_getRunOrTestCmd(t *testing.T) {
	runEx := executors.NewExecutorBuilder().
		WithRunner().
		WithCommand("runCommand").
		WithArgs([]string{"arg1"}).
		WithPipelineOptions([]string{""}).
		Build()

	testEx := executors.NewExecutorBuilder().
		WithTestRunner().
		WithCommand("testCommand").
		WithArgs([]string{"arg1"}).
		Build()

	wantRunExec := exec.CommandContext(context.Background(), "runCommand", "arg1")
	wantTestExec := exec.CommandContext(context.Background(), "testCommand", "arg1")

	type args struct {
		isUnitTest     bool
		executor       *executors.Executor
		ctxWithTimeout context.Context
	}

	tests := []struct {
		name string
		args args
		want *exec.Cmd
	}{
		{
			//Get cmd objects with set run executor
			name: "Get run cmd",
			args: args{
				isUnitTest:     false,
				executor:       &runEx,
				ctxWithTimeout: context.Background(),
			},
			want: wantRunExec,
		},
		{
			//Get cmd objects with set test executor
			name: "Get test cmd",
			args: args{
				isUnitTest:     true,
				executor:       &testEx,
				ctxWithTimeout: context.Background(),
			},
			want: wantTestExec,
		},
	}

	execComparer := cmp.Comparer(func(a exec.Cmd, b exec.Cmd) bool {
		return a.Path == b.Path &&
			cmp.Equal(a.Args, b.Args) &&
			cmp.Equal(a.Env, b.Env) &&
			a.Dir == b.Dir
	})

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := getExecuteCmd(tt.args.isUnitTest, tt.args.executor, tt.args.ctxWithTimeout); !cmp.Equal(got, tt.want, execComparer) {
				t.Errorf("getExecuteCmd() = '%v', want '%v', diff = %v", got, tt.want, cmp.Diff(got, tt.want, execComparer))
			}
		})
	}
}

func getSdkEnv(sdk pb.Sdk) (*environment.BeamEnvs, error) {
	setupSDK(sdk)
	appEnvs, err := environment.GetApplicationEnvsFromOsEnvs()
	if err != nil {
		return nil, err
	}
	sdkEnv, err := environment.ConfigureBeamEnvs(appEnvs.WorkingDir())
	if err != nil {
		return nil, err
	}
	return sdkEnv, nil
}

func setupSDK(sdk pb.Sdk) {
	err := os.MkdirAll(configFolder, fs.ModePerm)
	if err != nil {
		panic(err)
	}
	filePath := filepath.Join(configFolder, fmt.Sprintf("%s%s", sdk.String(), jsonExtension))
	switch sdk {
	case pb.Sdk_SDK_JAVA:
		err = os.WriteFile(filePath, []byte(javaConfig), 0600)
	case pb.Sdk_SDK_PYTHON:
		err = os.WriteFile(filePath, []byte(pythonConfig), 0600)
	case pb.Sdk_SDK_GO:
		err = os.WriteFile(filePath, []byte(goConfig), 0600)
	}
	if err != nil {
		panic(err)
	}

	err = os.Setenv("BEAM_SDK", sdk.String())
	if err != nil {
		panic(err)
	}
	err = os.Setenv("APP_WORK_DIR", "")
	if err != nil {
		panic(err)
	}
	err = os.Setenv("PREPARED_MOD_DIR", "")
	if err != nil {
		panic(err)
	}

	cacheService = local.New(context.Background())
}

func teardownBenchmarks() {
	err := os.RemoveAll(configFolder)
	if err != nil {
		panic(fmt.Errorf("error during test teardown: %s", err.Error()))
	}
	err = os.RemoveAll(pipelinesFolder)
	if err != nil {
		panic(fmt.Errorf("error during test teardown: %s", err.Error()))
	}
}

func prepareFiles(b *testing.B, pipelineId uuid.UUID, code string, sdk pb.Sdk) *fs_tool.LifeCycle {
	lc, err := fs_tool.NewLifeCycle(sdk, pipelineId, pipelinesFolder)
	if err != nil {
		b.Fatalf("error during initializse lc: %s", err.Error())
	}
	err = lc.CreateFolders()
	if err != nil {
		b.Fatalf("error during prepare folders: %s", err.Error())
	}
	sources := []entity.FileEntity{{Name: "main.java", Content: code, IsMain: true}}
	err = lc.CreateSourceCodeFiles(sources)
	if err != nil {
		b.Fatalf("error during prepare source code file: %s", err.Error())
	}
	return lc
}

func Benchmark_ProcessJava(b *testing.B) {
	setupSDK(pb.Sdk_SDK_JAVA)
	defer teardownBenchmarks()

	appEnv, err := environment.GetApplicationEnvsFromOsEnvs()
	if err != nil {
		b.Fatalf("error during preparing appEnv: %s", err)
	}
	sdkEnv, err := environment.ConfigureBeamEnvs(appEnv.WorkingDir())
	if err != nil {
		b.Fatalf("error during preparing sdkEnv: %s", err)
	}

	ctx := context.Background()
	code := "class HelloWorld {\n    public static void main(String[] args) {\n        System.out.println(\"Hello world!\");\n    }\n}"

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		b.StopTimer()
		pipelineId := uuid.New()
		lc := prepareFiles(b, pipelineId, code, pb.Sdk_SDK_JAVA)
		if err = utils.SetToCache(cacheService, pipelineId, cache.Canceled, false); err != nil {
			b.Fatal("error during set cancel flag to cache")
		}
		b.StartTimer()

		Process(ctx, cacheService, lc, pipelineId, appEnv, sdkEnv, "")
	}
}

func Benchmark_ProcessPython(b *testing.B) {
	setupSDK(pb.Sdk_SDK_PYTHON)
	defer teardownBenchmarks()

	appEnv, err := environment.GetApplicationEnvsFromOsEnvs()
	if err != nil {
		b.Fatalf("error during preparing appEnv: %s", err)
	}
	sdkEnv, err := environment.ConfigureBeamEnvs(appEnv.WorkingDir())
	if err != nil {
		b.Fatalf("error during preparing sdkEnv: %s", err)
	}

	ctx := context.Background()
	wordCountCode := "import argparse\nimport logging\nimport re\n\nimport apache_beam as beam\nfrom apache_beam.io import ReadFromText\nfrom apache_beam.io import WriteToText\nfrom apache_beam.options.pipeline_options import PipelineOptions\nfrom apache_beam.options.pipeline_options import SetupOptions\n\n\nclass WordExtractingDoFn(beam.DoFn):\n  \"\"\"Parse each line of input text into words.\"\"\"\n  def process(self, element):\n    \"\"\"Returns an iterator over the words of this element.\n\n    The element is a line of text.  If the line is blank, note that, too.\n\n    Args:\n      element: the element being processed\n\n    Returns:\n      The processed element.\n    \"\"\"\n    return re.findall(r'[\\w\\']+', element, re.UNICODE)\n\n\ndef run(argv=None, save_main_session=True):\n  \"\"\"Main entry point; defines and runs the wordcount pipeline.\"\"\"\n  parser = argparse.ArgumentParser()\n  parser.add_argument(\n      '--input',\n      dest='input',\n      default='gs://dataflow-samples/shakespeare/kinglear.txt',\n      help='Input file to process.')\n  parser.add_argument(\n      '--output',\n      dest='output',\n      required=True,\n      help='Output file to write results to.')\n  known_args, pipeline_args = parser.parse_known_args(argv)\n\n  # We use the save_main_session option because one or more DoFn's in this\n  # workflow rely on global context (e.g., a module imported at module level).\n  pipeline_options = PipelineOptions(pipeline_args)\n  pipeline_options.view_as(SetupOptions).save_main_session = save_main_session\n\n  # The pipeline will be run on exiting the with block.\n  with beam.Pipeline(options=pipeline_options) as p:\n\n    # Read the text file[pattern] into a PCollection.\n    lines = p | 'Read' >> ReadFromText(known_args.input)\n\n    counts = (\n        lines\n        | 'Split' >> (beam.ParDo(WordExtractingDoFn()).with_output_types(str))\n        | 'PairWithOne' >> beam.Map(lambda x: (x, 1))\n        | 'GroupAndSum' >> beam.CombinePerKey(sum))\n\n    # Format the counts into a PCollection of strings.\n    def format_result(word, count):\n      return '%s: %d' % (word, count)\n\n    output = counts | 'Format' >> beam.MapTuple(format_result)\n\n    # Write the output using a \"Write\" transform that has side effects.\n    # pylint: disable=expression-not-assigned\n    output | 'Write' >> WriteToText(known_args.output)\n\n\nif __name__ == '__main__':\n  logging.getLogger().setLevel(logging.INFO)\n  run()"
	pipelineOptions := "--output t.txt"
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		b.StopTimer()
		pipelineId := uuid.New()
		lc := prepareFiles(b, pipelineId, wordCountCode, pb.Sdk_SDK_PYTHON)
		if err = utils.SetToCache(cacheService, pipelineId, cache.Canceled, false); err != nil {
			b.Fatal("error during set cancel flag to cache")
		}
		b.StartTimer()

		Process(ctx, cacheService, lc, pipelineId, appEnv, sdkEnv, pipelineOptions)
	}
}

func Benchmark_ProcessGo(b *testing.B) {
	setupSDK(pb.Sdk_SDK_GO)
	defer teardownBenchmarks()

	appEnv, err := environment.GetApplicationEnvsFromOsEnvs()
	if err != nil {
		b.Fatalf("error during preparing appEnv: %s", err)
	}
	sdkEnv, err := environment.ConfigureBeamEnvs(appEnv.WorkingDir())
	if err != nil {
		b.Fatalf("error during preparing sdkEnv: %s", err)
	}

	ctx := context.Background()
	code := "package main\n\nimport \"fmt\"\n\nfunc main() {\n    fmt.Println(\"Hello world!\")\n}"

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		b.StopTimer()
		pipelineId := uuid.New()
		lc := prepareFiles(b, pipelineId, code, pb.Sdk_SDK_GO)
		if err = utils.SetToCache(cacheService, pipelineId, cache.Canceled, false); err != nil {
			b.Fatal("error during set cancel flag to cache")
		}
		b.StartTimer()

		Process(ctx, cacheService, lc, pipelineId, appEnv, sdkEnv, "")
	}
}

func Benchmark_GetProcessingOutput(b *testing.B) {
	pipelineId := uuid.New()
	subKey := cache.RunOutput
	ctx := context.Background()

	err := cacheService.SetValue(ctx, pipelineId, subKey, "MOCK_RUN_OUTPUT")
	if err != nil {
		b.Fatalf("error during prepare cache value: %s", err.Error())
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, _ = GetProcessingOutput(ctx, cacheService, pipelineId, subKey, "")
	}
}

func Benchmark_GetProcessingStatus(b *testing.B) {
	pipelineId := uuid.New()
	subKey := cache.Status
	ctx := context.Background()

	err := cacheService.SetValue(ctx, pipelineId, subKey, pb.Status_STATUS_FINISHED)
	if err != nil {
		b.Fatalf("error during prepare cache value: %s", err.Error())
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, _ = GetProcessingStatus(ctx, cacheService, pipelineId, "")
	}
}

func Benchmark_GetLastIndex(b *testing.B) {
	pipelineId := uuid.New()
	subKey := cache.RunOutputIndex
	ctx := context.Background()

	err := cacheService.SetValue(ctx, pipelineId, subKey, 5)
	if err != nil {
		b.Fatalf("error during prepare cache value: %s", err.Error())
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, _ = GetLastIndex(ctx, cacheService, pipelineId, subKey, "")
	}
}

func Test_validateStep(t *testing.T) {
	javaSdkEnv, err := getSdkEnv(pb.Sdk_SDK_JAVA)
	if err != nil {
		panic(err)
	}
	incorrectSdkEnv := &environment.BeamEnvs{
		ApacheBeamSdk:  pb.Sdk_SDK_UNSPECIFIED,
		ExecutorConfig: nil,
	}
	type args struct {
		ctx                  context.Context
		cacheService         cache.Cache
		pipelineId           uuid.UUID
		sdkEnv               *environment.BeamEnvs
		pipelineLifeCycleCtx context.Context
		validationResults    *sync.Map
	}
	tests := []struct {
		name string
		args args
		want int
		code string
	}{
		{
			name: "Test validation step by checking number of validators",
			args: args{
				ctx:                  context.Background(),
				cacheService:         cacheService,
				pipelineId:           uuid.New(),
				sdkEnv:               javaSdkEnv,
				pipelineLifeCycleCtx: context.Background(),
				validationResults:    &sync.Map{},
			},
			want: 3,
			code: helloWordJava,
		},
		{
			name: "Test validation step with incorrect sdkEnv",
			args: args{
				ctx:                  context.Background(),
				cacheService:         cacheService,
				pipelineId:           uuid.New(),
				sdkEnv:               incorrectSdkEnv,
				pipelineLifeCycleCtx: context.Background(),
				validationResults:    &sync.Map{},
			},
			want: 0,
			code: helloWordJava,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			lc, _ := fs_tool.NewLifeCycle(pb.Sdk_SDK_JAVA, tt.args.pipelineId, filepath.Join(os.Getenv("APP_WORK_DIR"), pipelinesFolder))
			err := lc.CreateFolders()
			if err != nil {
				t.Fatalf("error during prepare folders: %s", err.Error())
			}
			sources := []entity.FileEntity{{Name: "main.java", Content: tt.code, IsMain: true}}
			_ = lc.CreateSourceCodeFiles(sources)
			err = validateStep(tt.args.pipelineLifeCycleCtx, tt.args.cacheService, &lc.Paths, tt.args.pipelineId, tt.args.sdkEnv, tt.args.validationResults)
			got := syncMapLen(tt.args.validationResults)
			if err != nil && !reflect.DeepEqual(got, tt.want) {
				t.Errorf("validateStep() = %d, want %d", got, tt.want)
			}
		})
	}
}

func Test_prepareStep(t *testing.T) {
	javaSdkEnv, err := getSdkEnv(pb.Sdk_SDK_JAVA)
	if err != nil {
		panic(err)
	}
	incorrectSdkEnv := &environment.BeamEnvs{
		ApacheBeamSdk:  pb.Sdk_SDK_UNSPECIFIED,
		ExecutorConfig: nil,
	}
	validationResults := sync.Map{}
	validationResults.Store(validators.UnitTestValidatorName, false)
	validationResults.Store(validators.KatasValidatorName, false)
	pipelineLifeCycleCtx, cancel := context.WithTimeout(context.Background(), 1)
	defer cancel()
	type args struct {
		ctx                  context.Context
		cacheService         cache.Cache
		pipelineId           uuid.UUID
		sdkEnv               *environment.BeamEnvs
		pipelineLifeCycleCtx context.Context
		validationResults    *sync.Map
	}
	tests := []struct {
		name           string
		args           args
		code           string
		expectedStatus pb.Status
	}{
		{
			name: "Test preparer step working without an error",
			args: args{
				ctx:                  context.Background(),
				cacheService:         cacheService,
				pipelineId:           uuid.New(),
				sdkEnv:               javaSdkEnv,
				pipelineLifeCycleCtx: context.Background(),
				validationResults:    &validationResults,
			},
			code:           helloWordJava,
			expectedStatus: pb.Status_STATUS_COMPILING,
		},
		{
			name: "Test preparer step working with incorrect sdkEnv",
			args: args{
				ctx:                  context.Background(),
				cacheService:         cacheService,
				pipelineId:           uuid.New(),
				sdkEnv:               incorrectSdkEnv,
				pipelineLifeCycleCtx: context.Background(),
				validationResults:    &validationResults,
			},
			code:           "",
			expectedStatus: pb.Status_STATUS_ERROR,
		},
		{
			name: "Error during expired context of the example",
			args: args{
				ctx:                  context.Background(),
				cacheService:         cacheService,
				pipelineId:           uuid.New(),
				sdkEnv:               javaSdkEnv,
				pipelineLifeCycleCtx: pipelineLifeCycleCtx,
				validationResults:    &validationResults,
			},
			code:           "",
			expectedStatus: pb.Status_STATUS_RUN_TIMEOUT,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			lc, _ := fs_tool.NewLifeCycle(pb.Sdk_SDK_JAVA, tt.args.pipelineId, filepath.Join(os.Getenv("APP_WORK_DIR"), pipelinesFolder))
			err := lc.CreateFolders()
			if err != nil {
				t.Fatalf("error during prepare folders: %s", err.Error())
			}
			sources := []entity.FileEntity{{Name: "main.java", Content: tt.code, IsMain: true}}
			_ = lc.CreateSourceCodeFiles(sources)
			_ = prepareStep(tt.args.pipelineLifeCycleCtx, tt.args.cacheService, &lc.Paths, tt.args.pipelineId, tt.args.sdkEnv, tt.args.validationResults, nil)
			status, _ := cacheService.GetValue(tt.args.ctx, tt.args.pipelineId, cache.Status)
			if status != tt.expectedStatus {
				t.Errorf("prepareStep: got status = %v, want %v", status, tt.expectedStatus)
			}
		})
	}
}

func Test_compileStep(t *testing.T) {
	sdkJavaEnv, err := getSdkEnv(pb.Sdk_SDK_JAVA)
	if err != nil {
		panic(err)
	}
	sdkPythonEnv, err := getSdkEnv(pb.Sdk_SDK_PYTHON)
	if err != nil {
		panic(err)
	}
	pipelineLifeCycleCtx, cancel := context.WithTimeout(context.Background(), 1)
	defer cancel()
	type args struct {
		ctx                  context.Context
		cacheService         cache.Cache
		pipelineId           uuid.UUID
		sdkEnv               *environment.BeamEnvs
		isUnitTest           bool
		pipelineLifeCycleCtx context.Context
	}
	tests := []struct {
		name           string
		args           args
		code           string
		expectedStatus pb.Status
	}{
		{
			name: "Test compilation step finishes successfully on java sdk",
			args: args{
				ctx:                  context.Background(),
				cacheService:         cacheService,
				pipelineId:           uuid.New(),
				sdkEnv:               sdkJavaEnv,
				isUnitTest:           false,
				pipelineLifeCycleCtx: context.Background(),
			},
			code:           helloWordJava,
			expectedStatus: pb.Status_STATUS_EXECUTING,
		},
		{
			name: "Test compilation step finishes successfully on python sdk",
			args: args{
				ctx:                  context.Background(),
				cacheService:         cacheService,
				pipelineId:           uuid.New(),
				sdkEnv:               sdkPythonEnv,
				isUnitTest:           false,
				pipelineLifeCycleCtx: context.Background(),
			},
			code:           helloWordPython,
			expectedStatus: pb.Status_STATUS_EXECUTING,
		},
		{
			name: "Error during expired context of the example",
			args: args{
				ctx:                  context.Background(),
				cacheService:         cacheService,
				pipelineId:           uuid.New(),
				sdkEnv:               sdkJavaEnv,
				isUnitTest:           false,
				pipelineLifeCycleCtx: pipelineLifeCycleCtx,
			},
			code:           helloWordJava,
			expectedStatus: pb.Status_STATUS_RUN_TIMEOUT,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			lc, _ := fs_tool.NewLifeCycle(tt.args.sdkEnv.ApacheBeamSdk, tt.args.pipelineId, filepath.Join(os.Getenv("APP_WORK_DIR"), pipelinesFolder))
			err := lc.CreateFolders()
			if err != nil {
				t.Fatalf("error during prepare folders: %s", err.Error())
			}
			sources := []entity.FileEntity{{Name: "main.java", Content: tt.code, IsMain: true}}
			_ = lc.CreateSourceCodeFiles(sources)
			_ = compileStep(tt.args.pipelineLifeCycleCtx, tt.args.cacheService, &lc.Paths, tt.args.pipelineId, tt.args.sdkEnv, tt.args.isUnitTest)
			status, _ := cacheService.GetValue(tt.args.ctx, tt.args.pipelineId, cache.Status)
			if status != tt.expectedStatus {
				t.Errorf("compileStep: got status = %v, want %v", status, tt.expectedStatus)
			}
		})
	}
}

func Test_runStep(t *testing.T) {
	sdkJavaEnv, err := getSdkEnv(pb.Sdk_SDK_JAVA)
	if err != nil {
		panic(err)
	}
	sdkPythonEnv, err := getSdkEnv(pb.Sdk_SDK_PYTHON)
	if err != nil {
		panic(err)
	}
	sdkGoEnv, err := getSdkEnv(pb.Sdk_SDK_GO)
	if err != nil {
		panic(err)
	}
	type args struct {
		ctx                  context.Context
		cacheService         cache.Cache
		pipelineId           uuid.UUID
		isUnitTest           bool
		sdkEnv               *environment.BeamEnvs
		pipelineOptions      string
		pipelineLifeCycleCtx context.Context
		createExecFile       bool
	}
	tests := []struct {
		name           string
		args           args
		code           string
		expectedStatus pb.Status
	}{
		{
			// Test case with calling runStep method on python sdk.
			// cmd.Run return error during saving output.
			// As a result, the pipeline status should be Status_STATUS_RUN_ERROR.
			name: "Test run step working on python sdk",
			args: args{
				ctx:                  context.Background(),
				cacheService:         cacheService,
				pipelineId:           uuid.New(),
				isUnitTest:           false,
				sdkEnv:               sdkPythonEnv,
				pipelineOptions:      "",
				pipelineLifeCycleCtx: context.Background(),
				createExecFile:       true,
			},
			code:           helloWordPython,
			expectedStatus: pb.Status_STATUS_RUN_ERROR,
		},
		{
			// Test case with calling runStep method on go sdk.
			// cmd.Run return error due to missing executable file.
			// As a result, the pipeline status should be Status_STATUS_RUN_ERROR.
			name: "Test run step working on go sdk",
			args: args{
				ctx:                  context.Background(),
				cacheService:         cacheService,
				pipelineId:           uuid.New(),
				isUnitTest:           true,
				sdkEnv:               sdkGoEnv,
				pipelineOptions:      "",
				pipelineLifeCycleCtx: context.Background(),
				createExecFile:       true,
			},
			code:           helloWordGo,
			expectedStatus: pb.Status_STATUS_RUN_ERROR,
		},
		{
			// Test case with calling runStep method without preparing files with code.
			// As a result, the pipeline status should be Status_STATUS_ERROR.
			name: "Test run step without preparing files with code",
			args: args{
				ctx:                  context.Background(),
				cacheService:         cacheService,
				pipelineId:           uuid.UUID{},
				isUnitTest:           true,
				sdkEnv:               sdkJavaEnv,
				pipelineOptions:      "",
				pipelineLifeCycleCtx: context.Background(),
				createExecFile:       false,
			},
			code:           helloWordJava,
			expectedStatus: pb.Status_STATUS_ERROR,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			lc, _ := fs_tool.NewLifeCycle(tt.args.sdkEnv.ApacheBeamSdk, tt.args.pipelineId, filepath.Join(os.Getenv("APP_WORK_DIR"), pipelinesFolder))
			if tt.args.createExecFile {
				err := lc.CreateFolders()
				if err != nil {
					t.Fatalf("error during prepare folders: %s", err.Error())
				}
				sources := []entity.FileEntity{{Name: "main.java", Content: tt.code, IsMain: true}}
				_ = lc.CreateSourceCodeFiles(sources)
			}
			_ = runStep(tt.args.pipelineLifeCycleCtx, tt.args.cacheService, &lc.Paths, tt.args.pipelineId, tt.args.isUnitTest, tt.args.sdkEnv, tt.args.pipelineOptions)
			status, _ := cacheService.GetValue(tt.args.ctx, tt.args.pipelineId, cache.Status)
			if status != tt.expectedStatus {
				t.Errorf("runStep() got status = %v, want %v", status, tt.expectedStatus)
			}
		})
	}
}

func syncMapLen(syncMap *sync.Map) int {
	length := 0
	syncMap.Range(func(_, _ interface{}) bool {
		length++
		return true
	})
	return length
}

func TestGetGraph(t *testing.T) {
	ctx := context.Background()
	pipelineId1 := uuid.New()
	graph := "GRAPH"
	err := cacheService.SetValue(ctx, pipelineId1, cache.Graph, graph)
	if err != nil {
		return
	}
	pipelineId2 := uuid.New()
	err = cacheService.SetValue(ctx, pipelineId2, cache.Graph, 1)
	if err != nil {
		return
	}
	type args struct {
		ctx          context.Context
		cacheService cache.Cache
		key          uuid.UUID
		errorTitle   string
	}
	tests := []struct {
		name    string
		args    args
		want    string
		wantErr bool
	}{
		{
			name: "Get graph when key exist in cache",
			args: args{
				ctx:          context.Background(),
				cacheService: cacheService,
				key:          pipelineId1,
				errorTitle:   "error",
			},
			want:    graph,
			wantErr: false,
		},
		{
			name: "Get graph when key doesn't exist in cache",
			args: args{
				ctx:          context.Background(),
				cacheService: cacheService,
				key:          uuid.New(),
				errorTitle:   "error",
			},
			want:    "",
			wantErr: true,
		},
		{
			name: "Get graph when value from cache by key couldn't be converted to a string",
			args: args{
				ctx:          context.Background(),
				cacheService: cacheService,
				key:          pipelineId2,
				errorTitle:   "error",
			},
			want:    "",
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := GetGraph(tt.args.ctx, tt.args.cacheService, tt.args.key, tt.args.errorTitle)
			if (err != nil) != tt.wantErr {
				t.Errorf("GetGraph error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != tt.want {
				t.Errorf("GetGraph got = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_processSetupError(t *testing.T) {
	client, mock := redismock.NewClientMock()
	pipelineId := uuid.New()
	errorMessage := "MOCK_ERROR"
	type args struct {
		err            error
		pipelineId     uuid.UUID
		cacheService   cache.Cache
		ctxWithTimeout context.Context
	}
	tests := []struct {
		name    string
		mocks   func()
		args    args
		wantErr bool
	}{
		{
			name: "Error during HSet operation",
			mocks: func() {
				mock.ExpectHSet(pipelineId.String(), "MOCK_VALUE").SetErr(fmt.Errorf(errorMessage))
			},
			args: args{
				err:        fmt.Errorf(errorMessage),
				pipelineId: pipelineId,
				cacheService: &redis.Cache{
					Client: client,
				},
				ctxWithTimeout: nil,
			},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.mocks()
			if err := processSetupError(tt.args.err, tt.args.pipelineId, tt.args.cacheService); (err != nil) != tt.wantErr {
				t.Errorf("processSetupError() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func Test_processErrorWithSavingOutput(t *testing.T) {
	client, mock := redismock.NewClientMock()
	pipelineId := uuid.New()
	errorMessage := "MOCK_ERROR"
	subKey := cache.RunOutput
	type args struct {
		ctx          context.Context
		err          error
		errorOutput  []byte
		pipelineId   uuid.UUID
		subKey       cache.SubKey
		cacheService cache.Cache
		errorTitle   string
		newStatus    pb.Status
	}
	tests := []struct {
		name    string
		mocks   func()
		args    args
		wantErr bool
	}{
		{
			name: "Error during HSet operation",
			mocks: func() {
				mock.ExpectHSet(pipelineId.String(), subKey).SetErr(fmt.Errorf(errorMessage))
			},
			args: args{
				ctx:          context.Background(),
				err:          fmt.Errorf(errorMessage),
				errorOutput:  nil,
				pipelineId:   pipelineId,
				subKey:       subKey,
				cacheService: &redis.Cache{Client: client},
				errorTitle:   "",
				newStatus:    0,
			},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.mocks()
			if err := processErrorWithSavingOutput(tt.args.err, tt.args.errorOutput, tt.args.pipelineId, tt.args.subKey, tt.args.cacheService, tt.args.errorTitle, tt.args.newStatus); (err != nil) != tt.wantErr {
				t.Errorf("processErrorWithSavingOutput() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func Test_processRunError(t *testing.T) {
	client, mock := redismock.NewClientMock()
	pipelineId := uuid.New()
	errorMessage := "MOCK_ERROR"
	subKey := cache.RunError
	errorChannel := make(chan error, 1)
	errorChannel <- fmt.Errorf(errorMessage)
	type args struct {
		ctx                   context.Context
		errorChannel          chan error
		errorOutput           []byte
		pipelineId            uuid.UUID
		cacheService          cache.Cache
		stopReadLogsChannel   chan bool
		finishReadLogsChannel chan bool
	}
	tests := []struct {
		name    string
		mocks   func()
		args    args
		wantErr bool
	}{
		{
			name: "Error during HSet operation",
			mocks: func() {
				mock.ExpectHSet(pipelineId.String(), subKey).SetErr(fmt.Errorf(errorMessage))
			},
			args: args{
				ctx:                   context.Background(),
				errorChannel:          errorChannel,
				errorOutput:           nil,
				pipelineId:            pipelineId,
				cacheService:          &redis.Cache{Client: client},
				stopReadLogsChannel:   nil,
				finishReadLogsChannel: nil,
			},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.mocks()
			if err := processRunError(tt.args.errorChannel, tt.args.errorOutput, tt.args.pipelineId, tt.args.cacheService, tt.args.stopReadLogsChannel, tt.args.finishReadLogsChannel); (err != nil) != tt.wantErr {
				t.Errorf("processRunError() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func Test_processCompileSuccess(t *testing.T) {
	client, mock := redismock.NewClientMock()
	pipelineId := uuid.New()
	output := "output"
	cacheMock := &redis.Cache{Client: client}
	marshalLogs, _ := json.Marshal(cache.Logs)
	marshalCompileOutput, _ := json.Marshal(cache.CompileOutput)
	marshalRunOutput, _ := json.Marshal(cache.RunOutput)
	marshalRunError, _ := json.Marshal(cache.RunError)
	outputMarshal, _ := json.Marshal(output)
	marshalEmptyString, _ := json.Marshal("")
	type args struct {
		ctx          context.Context
		output       []byte
		pipelineId   uuid.UUID
		cacheService cache.Cache
	}
	tests := []struct {
		name    string
		mocks   func()
		args    args
		wantErr bool
	}{
		{
			name: "Error during set value to CompileOutput subKey",
			mocks: func() {
			},
			args: args{
				ctx:          context.Background(),
				output:       []byte(output),
				pipelineId:   pipelineId,
				cacheService: cacheMock,
			},
			wantErr: true,
		},
		{
			name: "Error during set value to RunOutput subKey",
			mocks: func() {
				mock.ExpectHSet(pipelineId.String(), marshalCompileOutput, outputMarshal).SetVal(1)
			},
			args: args{
				ctx:          context.Background(),
				output:       []byte(output),
				pipelineId:   pipelineId,
				cacheService: cacheMock,
			},
			wantErr: true,
		},
		{
			name: "Error during set value to RunError subKey",
			mocks: func() {
				mock.ExpectHSet(pipelineId.String(), marshalCompileOutput, outputMarshal).SetVal(1)
				mock.ExpectHSet(pipelineId.String(), marshalRunOutput, marshalEmptyString).SetVal(1)
			},
			args: args{
				ctx:          context.Background(),
				output:       []byte(output),
				pipelineId:   pipelineId,
				cacheService: cacheMock,
			},
			wantErr: true,
		},
		{
			name: "Error during set value to Logs subKey",
			mocks: func() {
				mock.ExpectHSet(pipelineId.String(), marshalCompileOutput, outputMarshal).SetVal(1)
				mock.ExpectHSet(pipelineId.String(), marshalRunOutput, marshalEmptyString).SetVal(1)
				mock.ExpectHSet(pipelineId.String(), marshalRunError, marshalEmptyString).SetVal(1)
			},
			args: args{
				ctx:          context.Background(),
				output:       []byte(output),
				pipelineId:   pipelineId,
				cacheService: cacheMock,
			},
			wantErr: true,
		},
		{
			name: "Error during set value to Graph subKey",
			mocks: func() {
				mock.ExpectHSet(pipelineId.String(), marshalCompileOutput, outputMarshal).SetVal(1)
				mock.ExpectHSet(pipelineId.String(), marshalRunOutput, marshalEmptyString).SetVal(1)
				mock.ExpectHSet(pipelineId.String(), marshalRunError, marshalEmptyString).SetVal(1)
				mock.ExpectHSet(pipelineId.String(), marshalLogs, marshalEmptyString).SetVal(1)
			},
			args: args{
				ctx:          context.Background(),
				output:       []byte(output),
				pipelineId:   pipelineId,
				cacheService: cacheMock,
			},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.mocks()
			if err := processCompileSuccess(tt.args.output, tt.args.pipelineId, tt.args.cacheService); (err != nil) != tt.wantErr {
				t.Errorf("processCompileSuccess() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func Test_readGraphFile(t *testing.T) {
	pipelineLifeCycleCtx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()
	type args struct {
		pipelineLifeCycleCtx context.Context
		backgroundCtx        context.Context
		cacheService         cache.Cache
		graphFilePath        string
		pipelineId           uuid.UUID
	}
	tests := []struct {
		name string
		args args
	}{
		{
			name: "Successfully saving the prepared graph to the cache",
			args: args{
				pipelineLifeCycleCtx: pipelineLifeCycleCtx,
				backgroundCtx:        context.Background(),
				cacheService:         cacheService,
				graphFilePath:        graphFilePath,
				pipelineId:           uuid.New(),
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			readGraphFile(tt.args.pipelineLifeCycleCtx, tt.args.cacheService, tt.args.graphFilePath, tt.args.pipelineId)
			if v, _ := cacheService.GetValue(tt.args.backgroundCtx, tt.args.pipelineId, cache.Graph); v == nil {
				t.Errorf("readGraphFile() error: the graph was not cached")
			}
		})
	}
}

func Test_cancelCheck(t *testing.T) {
	type args struct {
		timeout        time.Duration
		cancelWaitTime time.Duration
		setCancel      bool
	}
	tests := []struct {
		name string
		args args
	}{
		{
			name: "Successfully canceling the pipeline immediately",
			args: args{
				timeout:        5 * time.Minute,
				cancelWaitTime: 0,
				setCancel:      true,
			},
		},
		{
			name: "Successfully canceling the pipeline after timeout",
			args: args{
				timeout:        2 * pauseDuration,
				cancelWaitTime: 3 * pauseDuration,
				setCancel:      true,
			},
		},
		{
			name: "Successfully canceling the pipeline after timeout (immediate timeout)",
			args: args{
				timeout:        0,
				cancelWaitTime: 3 * pauseDuration,
				setCancel:      true,
			},
		},
		{
			name: "Successfully timing out the pipeline without cancel",
			args: args{
				timeout:        3 * pauseDuration,
				cancelWaitTime: 0,
				setCancel:      false,
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx, cancel := context.WithTimeout(context.Background(), tt.args.timeout)
			defer cancel()

			id := uuid.New()
			localCache := local.New(ctx)

			go cancelCheck(ctx, id, cancel, localCache)

			if tt.args.setCancel {
				_ = localCache.SetValue(ctx, id, cache.Canceled, true)
			}

			// Wait some time for the cancelCheck to be executed
			time.Sleep(5 * pauseDuration)

			if err := ctx.Err(); err == nil {
				t.Errorf("cancelCheck() error expected, err = %v", err)
			}
		})
	}
}
