package tests

import (
	"bytes"
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"testing"
	"time"

	"code.gitea.io/sdk/gitea"
	"github.com/go-git/go-billy/v5/memfs"
	"github.com/go-git/go-git/v5"
	gitconfig "github.com/go-git/go-git/v5/config"
	"github.com/go-git/go-git/v5/plumbing/object"
	githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
	"github.com/go-git/go-git/v5/storage/memory"
	"github.com/sorintlab/errors"
	"gotest.tools/v3/assert"
	"gotest.tools/v3/assert/cmp"

	"agola.io/agola/internal/testutil"
	"agola.io/agola/internal/util"
	gwapitypes "agola.io/agola/services/gateway/api/types"
	gwclient "agola.io/agola/services/gateway/client"
	rstypes "agola.io/agola/services/runservice/types"
)

func TestPush(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name        string
		config      string
		num         int
		annotations map[string]string
		message     string
	}{
		{
			name: "push",
			config: `
			{
				runs: [
					{
						name: 'run01',
						tasks: [
							{
								name: 'task01',
								runtime: {
									containers: [
										{
											image: 'alpine/git',
										},
									],
								},
								steps: [
									{ type: 'clone' },
									{ type: 'run', command: 'env' },
								],
							},
						],
					},
				],
			}
			`,
			num: 1,
			annotations: map[string]string{
				"branch":   "master",
				"ref":      "refs/heads/master",
				"ref_type": "branch",
			},
			message: "commit",
		},
		{
			name: "push with unmatched branch",
			config: `
			{
				runs: [
					{
						name: 'run01',
						tasks: [
							{
								name: 'task01',
								runtime: {
									containers: [
										{
											image: 'alpine/git',
										},
									],
								},
								steps: [
									{ type: 'clone' },
									{ type: 'run', command: 'env' },
								],
							},
						],
						when: {
							branch: 'notmaster',
						},
					},
				],
			}
			`,
			num:     0,
			message: "commit",
		},
		{
			name: "push with [ci skip] in subject",
			config: `
			{
				runs: [
					{
						name: 'run01',
						tasks: [
							{
								name: 'task01',
								runtime: {
									containers: [
										{
											image: 'alpine/git',
										},
									],
								},
								steps: [
									{ type: 'clone' },
									{ type: 'run', command: 'env' },
								],
							},
						],
					},
				],
			}
			`,
			num:     0,
			message: "[ci skip] commit",
		},
		{
			name: "push with [ci skip] in body",
			config: `
			{
				runs: [
					{
						name: 'run01',
						tasks: [
							{
								name: 'task01',
								runtime: {
									containers: [
										{
											image: 'alpine/git',
										},
									],
								},
								steps: [
									{ type: 'clone' },
									{ type: 'run', command: 'env' },
								],
							},
						],
					},
				],
			}
			`,
			num:     0,
			message: "commit\n\n[ci skip] body",
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			dir := t.TempDir()

			ctx, cancel := context.WithCancel(context.Background())
			defer cancel()

			sc := setup(ctx, t, dir, withGitea(true))
			defer sc.stop()

			giteaAPIURL := fmt.Sprintf("http://%s:%s", sc.gitea.HTTPListenAddress, sc.gitea.HTTPPort)

			giteaToken, token := createLinkedAccount(ctx, t, sc.gitea, sc.config)

			giteaClient, err := gitea.NewClient(giteaAPIURL, gitea.SetToken(giteaToken))
			testutil.NilError(t, err)

			gwClient := gwclient.NewClient(sc.config.Gateway.APIExposedURL, token)

			giteaRepo, project := createProject(ctx, t, giteaClient, gwClient)

			push(t, tt.config, giteaRepo.CloneURL, giteaToken, tt.message, false)

			err = testutil.Wait(60*time.Second, func() (bool, error) {
				runs, _, err := gwClient.GetProjectRuns(ctx, project.ID, &gwclient.GetRunsOptions{ListOptions: &gwclient.ListOptions{SortDirection: gwapitypes.SortDirectionDesc}})
				if err != nil {
					return false, nil
				}

				if len(runs) != tt.num {
					return false, nil
				}
				for _, run := range runs {
					if run.Phase != rstypes.RunPhaseFinished {
						return false, nil
					}
				}

				return true, nil
			})
			testutil.NilError(t, err)

			runs, _, err := gwClient.GetProjectRuns(ctx, project.ID, &gwclient.GetRunsOptions{ListOptions: &gwclient.ListOptions{SortDirection: gwapitypes.SortDirectionDesc}})
			testutil.NilError(t, err)

			assert.Assert(t, cmp.Len(runs, tt.num))

			if len(runs) > 0 {
				run := runs[0]
				assert.Equal(t, run.Phase, rstypes.RunPhaseFinished)
				assert.Equal(t, run.Result, rstypes.RunResultSuccess)
				for k, v := range tt.annotations {
					assert.Equal(t, run.Annotations[k], v)
				}
			}
		})
	}
}

func TestDirectRun(t *testing.T) {
	testDirectRun(t, true)
}

func TestDirectRunWithoutInternalServicesAuth(t *testing.T) {
	testDirectRun(t, false)
}

func testDirectRun(t *testing.T, internalServicesAuth bool) {
	t.Parallel()

	config := `
	{
		runs: [
			{
				name: 'run01',
				tasks: [
					{
						name: 'task01',
						runtime: {
							containers: [
								{
									image: 'alpine/git',
								},
							],
						},
						steps: [
							{ type: 'clone' },
							{ type: 'run', command: 'env' },
						],
					},
				],
			},
		],
	}
	`

	tests := []struct {
		name        string
		args        []string
		annotations map[string]string
	}{
		{
			name: "direct run",
			annotations: map[string]string{
				"branch":   "master",
				"ref":      "refs/heads/master",
				"ref_type": "branch",
			},
		},
		{
			name: "direct run with destination branch",
			args: []string{"--branch", "develop"},
			annotations: map[string]string{
				"branch":   "develop",
				"ref":      "refs/heads/develop",
				"ref_type": "branch",
			},
		},
		{
			name: "direct run with destination tag",
			args: []string{"--tag", "v0.1.0"},
			annotations: map[string]string{
				"tag":      "v0.1.0",
				"ref":      "refs/tags/v0.1.0",
				"ref_type": "tag",
			},
		},
		{
			name: "direct run with destination ref as a pr",
			args: []string{"--ref", "refs/pull/1/head"},
			annotations: map[string]string{
				"pull_request_id": "1",
				"ref":             "refs/pull/1/head",
				"ref_type":        "pull_request",
			},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			dir := t.TempDir()

			ctx, cancel := context.WithCancel(context.Background())
			defer cancel()

			sc := setup(ctx, t, dir, withGitea(true), withInternalServicesAuth(internalServicesAuth))
			defer sc.stop()

			gwClient := gwclient.NewClient(sc.config.Gateway.APIExposedURL, "admintoken")
			user, _, err := gwClient.CreateUser(ctx, &gwapitypes.CreateUserRequest{UserName: agolaUser01})
			testutil.NilError(t, err)

			t.Logf("created agola user: %s", user.UserName)

			token := createAgolaUserToken(ctx, t, sc.config)

			// From now use the user token
			gwClient = gwclient.NewClient(sc.config.Gateway.APIExposedURL, token)

			directRun(t, dir, config, ConfigFormatJsonnet, sc.config.Gateway.APIExposedURL, token, tt.args...)

			err = testutil.Wait(60*time.Second, func() (bool, error) {
				runs, _, err := gwClient.GetUserRuns(ctx, user.ID, &gwclient.GetRunsOptions{ListOptions: &gwclient.ListOptions{SortDirection: gwapitypes.SortDirectionDesc}})
				if err != nil {
					return false, nil
				}

				if len(runs) != 1 {
					return false, nil
				}

				if runs[0].Phase != rstypes.RunPhaseFinished {
					return false, nil
				}

				return true, nil
			})
			testutil.NilError(t, err)

			runs, _, err := gwClient.GetUserRuns(ctx, user.ID, &gwclient.GetRunsOptions{ListOptions: &gwclient.ListOptions{SortDirection: gwapitypes.SortDirectionDesc}})
			testutil.NilError(t, err)

			assert.Assert(t, cmp.Len(runs, 1))

			run := runs[0]
			assert.Equal(t, run.Phase, rstypes.RunPhaseFinished)
			assert.Equal(t, run.Result, rstypes.RunResultSuccess)

			for k, v := range tt.annotations {
				assert.Equal(t, run.Annotations[k], v)
			}
		})
	}
}

func TestDirectRunVariables(t *testing.T) {
	t.Parallel()

	config := `
	{
		runs: [
			{
				name: 'run01',
				tasks: [
					{
						name: 'task01',
						runtime: {
							containers: [
								{
									image: 'alpine/git',
								},
							],
						},
						environment: {
							ENV01: { from_variable: 'variable01' },
							ENV02: { from_variable: 'variable02' },
						},
						steps: [
							{ type: 'clone' },
							{ type: 'run', command: 'env' },
						],
					},
				],
			},
		],
	}
	`

	varfile01 := `
variable01: "variable value 01"
variable02: variable value 02
`

	tests := []struct {
		name string
		args []string
		env  map[string]string
	}{
		{
			name: "direct run without variables",
			args: []string{},
			env: map[string]string{
				"ENV01": "",
				"ENV02": "",
			},
		},
		{
			name: "direct run with two variables",
			args: []string{"--var", "variable01=VARIABLEVALUE01", "--var", "variable02=VARIABLEVALUE02"},
			env: map[string]string{
				"ENV01": "VARIABLEVALUE01",
				"ENV02": "VARIABLEVALUE02",
			},
		},
		{
			name: "direct run with a var file",
			args: []string{"--var-file", "../varfile01.yml"},
			env: map[string]string{
				"ENV01": "variable value 01",
				"ENV02": "variable value 02",
			},
		},
		{
			name: "direct run with a var file and a var that overrides",
			args: []string{"--var-file", "../varfile01.yml", "--var", "variable02=VARIABLEVALUE02"},
			env: map[string]string{
				"ENV01": "variable value 01",
				"ENV02": "VARIABLEVALUE02",
			},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			dir := t.TempDir()
			err := os.WriteFile(filepath.Join(dir, "varfile01.yml"), []byte(varfile01), 0644)
			testutil.NilError(t, err)

			ctx, cancel := context.WithCancel(context.Background())
			defer cancel()

			sc := setup(ctx, t, dir, withGitea(true))
			defer sc.stop()

			gwClient := gwclient.NewClient(sc.config.Gateway.APIExposedURL, "admintoken")
			user, _, err := gwClient.CreateUser(ctx, &gwapitypes.CreateUserRequest{UserName: agolaUser01})
			testutil.NilError(t, err)

			t.Logf("created agola user: %s", user.UserName)

			token := createAgolaUserToken(ctx, t, sc.config)

			// From now use the user token
			gwClient = gwclient.NewClient(sc.config.Gateway.APIExposedURL, token)

			directRun(t, dir, config, ConfigFormatJsonnet, sc.config.Gateway.APIExposedURL, token, tt.args...)

			// TODO(sgotti) add an util to wait for a run phase
			err = testutil.Wait(60*time.Second, func() (bool, error) {
				runs, _, err := gwClient.GetUserRuns(ctx, user.ID, &gwclient.GetRunsOptions{ListOptions: &gwclient.ListOptions{SortDirection: gwapitypes.SortDirectionDesc}})
				if err != nil {
					return false, nil
				}

				if len(runs) != 1 {
					return false, nil
				}

				if runs[0].Phase != rstypes.RunPhaseFinished {
					return false, nil
				}

				return true, nil
			})
			testutil.NilError(t, err)

			runs, _, err := gwClient.GetUserRuns(ctx, user.ID, &gwclient.GetRunsOptions{ListOptions: &gwclient.ListOptions{SortDirection: gwapitypes.SortDirectionDesc}})
			testutil.NilError(t, err)

			assert.Assert(t, cmp.Len(runs, 1))

			run, _, err := gwClient.GetUserRun(ctx, user.ID, runs[0].Number)
			testutil.NilError(t, err)

			assert.Equal(t, run.Phase, rstypes.RunPhaseFinished)
			assert.Equal(t, run.Result, rstypes.RunResultSuccess)

			var task *gwapitypes.RunResponseTask
			for _, t := range run.Tasks {
				if t.Name == "task01" {
					task = t
					break
				}
			}

			resp, err := gwClient.GetUserLogs(ctx, user.ID, run.Number, task.ID, false, 1, false)
			testutil.NilError(t, err)

			defer resp.Body.Close()

			logs, err := io.ReadAll(resp.Body)
			testutil.NilError(t, err)

			curEnv, err := testutil.ParseEnvs(bytes.NewReader(logs))
			testutil.NilError(t, err)

			for n, e := range tt.env {
				ce, ok := curEnv[n]
				assert.Assert(t, ok, "missing env var %s", n)
				assert.Equal(t, ce, e, "different env var %s value, want: %q, got %q", n, e, ce)
			}
		})
	}
}

func TestDirectRunLogs(t *testing.T) {
	t.Parallel()

	config := `
	{
		runs: [
			{
				name: 'run01',
				tasks: [
					{
						name: 'task01',
						runtime: {
							containers: [
								{
									image: 'alpine/git',
								},
							],
						},
						steps: [
							{ type: 'clone' },
							{ type: 'run', command: 'echo STEPLOG' },
						],
					},
				],
			},
		],
	}
	`

	tests := []struct {
		name   string
		setup  bool
		step   int
		delete bool
		err    string
	}{
		{
			name: "get log step 1",
			step: 1,
		},
		{
			name:  "get log setup",
			setup: true,
		},
		{
			name: "get log with unexisting step",
			step: 99,
			err:  "remote error notexist",
		},
		{
			name:   "delete log step 1",
			step:   1,
			delete: true,
		},
		{
			name:   "delete log setup",
			setup:  true,
			delete: true,
		},
		{
			name:   "delete log with unexisting step",
			step:   99,
			delete: true,
			err:    "remote error notexist",
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			dir := t.TempDir()

			ctx, cancel := context.WithCancel(context.Background())
			defer cancel()

			sc := setup(ctx, t, dir, withGitea(true))
			defer sc.stop()

			gwClient := gwclient.NewClient(sc.config.Gateway.APIExposedURL, "admintoken")
			user, _, err := gwClient.CreateUser(ctx, &gwapitypes.CreateUserRequest{UserName: agolaUser01})
			testutil.NilError(t, err)

			t.Logf("created agola user: %s", user.UserName)

			token := createAgolaUserToken(ctx, t, sc.config)

			// From now use the user token
			gwClient = gwclient.NewClient(sc.config.Gateway.APIExposedURL, token)

			directRun(t, dir, config, ConfigFormatJsonnet, sc.config.Gateway.APIExposedURL, token)

			err = testutil.Wait(60*time.Second, func() (bool, error) {
				runs, _, err := gwClient.GetUserRuns(ctx, user.ID, &gwclient.GetRunsOptions{ListOptions: &gwclient.ListOptions{SortDirection: gwapitypes.SortDirectionDesc}})
				if err != nil {
					return false, nil
				}

				if len(runs) != 1 {
					return false, nil
				}

				if runs[0].Phase != rstypes.RunPhaseFinished {
					return false, nil
				}

				return true, nil
			})
			testutil.NilError(t, err)

			runs, _, err := gwClient.GetUserRuns(ctx, user.ID, &gwclient.GetRunsOptions{ListOptions: &gwclient.ListOptions{SortDirection: gwapitypes.SortDirectionDesc}})
			testutil.NilError(t, err)

			assert.Assert(t, cmp.Len(runs, 1))

			run, _, err := gwClient.GetUserRun(ctx, user.ID, runs[0].Number)
			testutil.NilError(t, err)

			assert.Equal(t, run.Phase, rstypes.RunPhaseFinished)
			assert.Equal(t, run.Result, rstypes.RunResultSuccess)

			var task *gwapitypes.RunResponseTask
			for _, t := range run.Tasks {
				if t.Name == "task01" {
					task = t
					break
				}
			}

			err = testutil.Wait(30*time.Second, func() (bool, error) {
				t, _, err := gwClient.GetUserRunTask(ctx, user.ID, runs[0].Number, task.ID)
				if err != nil {
					return false, nil
				}
				if tt.step >= len(t.Steps) {
					return true, nil
				}
				if !t.Steps[tt.step].LogArchived {
					return false, nil
				}
				return true, nil
			})
			testutil.NilError(t, err)

			if tt.delete {
				_, err = gwClient.DeleteUserLogs(ctx, user.ID, run.Number, task.ID, tt.setup, tt.step)
			} else {
				_, err = gwClient.GetUserLogs(ctx, user.ID, run.Number, task.ID, tt.setup, tt.step, false)
			}

			if tt.err != "" {
				assert.Error(t, err, tt.err)
			} else {
				testutil.NilError(t, err)
			}
		})
	}
}

func TestPullRequest(t *testing.T) {
	t.Parallel()

	config := `
	{
		runs: [
			{
				name: 'run01',
				tasks: [
					{
						name: 'task01',
						runtime: {
							containers: [
								{
									image: 'alpine/git',
								},
							],
						},
						environment: {
							MYPASSWORD: { from_variable: 'mypassword' },
						},
						steps: [
							{ type: 'clone' },
							{ type: 'run', command: 'echo -n $MYPASSWORD' },
						],
					},
				],
				when: {
					ref: '#refs/pull/\\d+/head#',
				},
			},
		],
	}
	`

	tests := []struct {
		name               string
		passVarsToForkedPR bool
		prFromSameRepo     bool
		expected           string
	}{
		{
			name:               "PR from same repo with PassVarsToForkedPR set to false",
			passVarsToForkedPR: false,
			prFromSameRepo:     true,
			expected:           "mysupersecretpassword",
		},
		{
			name:               "PR from same repo with PassVarsToForkedPR set to true",
			passVarsToForkedPR: true,
			prFromSameRepo:     true,
			expected:           "mysupersecretpassword",
		},
		{
			name:               "PR from forked repo with PassVarsToForkedPR set to false",
			passVarsToForkedPR: false,
			prFromSameRepo:     false,
			expected:           "",
		},
		{
			name:               "PR from forked repo with PassVarsToForkedPR set to true",
			passVarsToForkedPR: true,
			prFromSameRepo:     false,
			expected:           "mysupersecretpassword",
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			dir := t.TempDir()

			ctx, cancel := context.WithCancel(context.Background())
			defer cancel()

			sc := setup(ctx, t, dir, withGitea(true))
			defer sc.stop()

			giteaAPIURL := fmt.Sprintf("http://%s:%s", sc.gitea.HTTPListenAddress, sc.gitea.HTTPPort)

			giteaToken, token := createLinkedAccount(ctx, t, sc.gitea, sc.config)

			giteaClient, err := gitea.NewClient(giteaAPIURL, gitea.SetToken(giteaToken))
			testutil.NilError(t, err)

			gwClient := gwclient.NewClient(sc.config.Gateway.APIExposedURL, token)

			giteaRepo, project := createProject(ctx, t, giteaClient, gwClient)

			project, _, err = gwClient.UpdateProject(ctx, project.ID, &gwapitypes.UpdateProjectRequest{PassVarsToForkedPR: util.Ptr(tt.passVarsToForkedPR)})
			testutil.NilError(t, err, "failed to update project")

			//create project secret
			secretData := map[string]string{"mypassword": "mysupersecretpassword"}
			sreq := &gwapitypes.CreateSecretRequest{
				Name: "mysecret",
				Type: gwapitypes.SecretTypeInternal,
				Data: secretData,
			}

			secret, _, err := gwClient.CreateProjectSecret(context.TODO(), project.ID, sreq)
			testutil.NilError(t, err, "failed to create project secret")

			// create project variable
			rvalues := []gwapitypes.VariableValueRequest{}
			rvalues = append(rvalues, gwapitypes.VariableValueRequest{
				SecretName: secret.Name,
				SecretVar:  "mypassword",
			})

			vreq := &gwapitypes.CreateVariableRequest{
				Name:   "mypassword",
				Values: rvalues,
			}

			_, _, err = gwClient.CreateProjectVariable(context.TODO(), project.ID, vreq)
			testutil.NilError(t, err, "failed to create project variable")

			if tt.prFromSameRepo {
				// create PR from branch on same repo
				push(t, config, giteaRepo.CloneURL, giteaToken, "commit", true)

				// Looks like there're some async handlings in gitea when pushing and then instantly creating a pull request that could fail with a 404 Not found
				// We tried with a wait, before pr creation, to check that the branch exists but this always returns true also if the pr creation then fails.
				// So retry the creation for some time on 404

				prOpts := gitea.CreatePullRequestOption{
					Head:  "new-branch",
					Base:  "master",
					Title: "add file1 from new-branch on same repo",
				}

				err := testutil.Wait(10*time.Second, func() (bool, error) {
					_, resp, err := giteaClient.CreatePullRequest(giteaUser01, "repo01", prOpts)
					if err != nil {
						if resp.StatusCode == http.StatusNotFound {
							return false, nil
						}
						return false, errors.WithStack(err)
					}

					return true, nil
				})
				testutil.NilError(t, err, "failed to create pull request")
			} else {
				// create PR from forked repo
				push(t, config, giteaRepo.CloneURL, giteaToken, "commit", false)

				userOpts := gitea.CreateUserOption{
					Username:           giteaUser02,
					Password:           giteaUser02Password,
					Email:              "user02@example.com",
					MustChangePassword: util.Ptr(false),
				}
				_, _, err := giteaClient.AdminCreateUser(userOpts)
				testutil.NilError(t, err, "failed to create user02")

				giteaClient.SetBasicAuth(giteaUser02, giteaUser02Password)
				giteaUser02Token, _, err := giteaClient.CreateAccessToken(gitea.CreateAccessTokenOption{Name: "token01"})
				testutil.NilError(t, err, "failed to create token for user02")

				giteaUser02Client, err := gitea.NewClient(giteaAPIURL, gitea.SetToken(giteaUser02Token.Token))
				testutil.NilError(t, err)

				giteaForkedRepo, _, err := giteaUser02Client.CreateFork(giteaUser01, "repo01", gitea.CreateForkOption{})
				testutil.NilError(t, err, "failed to fork repo01")

				gitfs := memfs.New()
				r, err := git.Clone(memory.NewStorage(), gitfs, &git.CloneOptions{
					Auth: &githttp.BasicAuth{
						Username: giteaUser02,
						Password: giteaUser02Token.Token,
					},
					URL: giteaForkedRepo.CloneURL,
				})
				testutil.NilError(t, err, "failed to clone forked repo")

				wt, err := r.Worktree()
				testutil.NilError(t, err)

				f, err := gitfs.Create("file2")
				testutil.NilError(t, err)
				_, err = f.Write([]byte("file2 content"))
				testutil.NilError(t, err)

				_, err = wt.Add("file2")
				testutil.NilError(t, err)

				_, err = wt.Commit("commit from user02", &git.CommitOptions{
					Author: &object.Signature{
						Name:  giteaUser02,
						Email: "user02@example.com",
						When:  time.Now(),
					},
				})
				testutil.NilError(t, err)

				err = r.Push(&git.PushOptions{
					RemoteName: "origin",
					RefSpecs: []gitconfig.RefSpec{
						gitconfig.RefSpec("refs/heads/master:refs/heads/master"),
					},
					Auth: &githttp.BasicAuth{
						Username: giteaUser02,
						Password: giteaUser02Token.Token,
					},
				})
				testutil.NilError(t, err)

				prOpts := gitea.CreatePullRequestOption{
					Head:  "user02:master",
					Base:  "master",
					Title: "add file1 from master on forked repo",
				}
				_, _, err = giteaUser02Client.CreatePullRequest(giteaUser01, "repo01", prOpts)
				testutil.NilError(t, err, "failed to create pull request")
			}
			err = testutil.Wait(60*time.Second, func() (bool, error) {
				runs, _, err := gwClient.GetProjectRuns(ctx, project.ID, &gwclient.GetRunsOptions{ListOptions: &gwclient.ListOptions{SortDirection: gwapitypes.SortDirectionDesc}})
				if err != nil {
					return false, nil
				}

				if len(runs) == 0 {
					return false, nil
				}
				if runs[0].Phase != rstypes.RunPhaseFinished {
					return false, nil
				}

				return true, nil
			})
			testutil.NilError(t, err)

			runs, _, err := gwClient.GetProjectRuns(ctx, project.ID, &gwclient.GetRunsOptions{ListOptions: &gwclient.ListOptions{SortDirection: gwapitypes.SortDirectionDesc}})
			testutil.NilError(t, err)

			run, _, err := gwClient.GetProjectRun(ctx, project.ID, runs[0].Number)
			testutil.NilError(t, err)

			var task *gwapitypes.RunResponseTask
			for _, t := range run.Tasks {
				if t.Name == "task01" {
					task = t
					break
				}
			}

			if len(runs) > 0 {
				run := runs[0]
				assert.Equal(t, run.Phase, rstypes.RunPhaseFinished)
				assert.Equal(t, run.Result, rstypes.RunResultSuccess)

				resp, err := gwClient.GetProjectLogs(ctx, project.ID, run.Number, task.ID, false, 1, false)
				testutil.NilError(t, err, "failed to get log")
				defer resp.Body.Close()

				mypassword, err := io.ReadAll(resp.Body)
				testutil.NilError(t, err, "failed to read log: %v")
				assert.Equal(t, tt.expected, string(mypassword))
			}
		})
	}
}

func TestTaskTimeout(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name                 string
		config               string
		tasksResultExpected  map[string]rstypes.RunTaskStatus
		taskTimedoutExpected map[string]bool
	}{
		{
			name:                 "timeout string value",
			tasksResultExpected:  map[string]rstypes.RunTaskStatus{"task01": rstypes.RunTaskStatusFailed},
			taskTimedoutExpected: map[string]bool{"task01": true},
			config: `
			{
				runs: [
					{
						name: 'run01',
						tasks: [
							{
								name: 'task01',
								runtime: {
									containers: [
										{
											image: 'alpine/git',
										},
									],
								},
								task_timeout_interval: "15s",
								steps: [
									{ type: 'run', command: 'sleep 30' },
								],
							},
						],
					},
				],
			}
		`,
		},
		{
			name:                 "timeout int value",
			tasksResultExpected:  map[string]rstypes.RunTaskStatus{"task01": rstypes.RunTaskStatusFailed},
			taskTimedoutExpected: map[string]bool{"task01": true},
			config: `
			{
				runs: [
					{
						name: 'run01',
						tasks: [
							{
								name: 'task01',
								runtime: {
									containers: [
										{
											image: 'alpine/git',
										},
									],
								},
								task_timeout_interval: 15000000000,
								steps: [
									{ type: 'run', command: 'sleep 30' },
								],
							},
						],
					},
				],
			}
		`,
		},
		{
			name:                 "timeout child timeout",
			tasksResultExpected:  map[string]rstypes.RunTaskStatus{"task01": rstypes.RunTaskStatusSuccess, "task02": rstypes.RunTaskStatusFailed},
			taskTimedoutExpected: map[string]bool{"task01": false, "task02": true},
			config: `
			{
				runs: [
					{
						name: 'run01',
						tasks: [
							{
								name: 'task01',
								runtime: {
									containers: [
										{
											image: 'alpine/git',
										},
									],
								},
								steps: [
									{ type: 'run', command: 'sleep 30' },
								],
							},
							{
								name: 'task02',
								depends: ['task01'],
								runtime: {
									containers: [
										{
											image: 'alpine/git',
										},
									],
								},
								task_timeout_interval: "15s",
								steps: [
									{ type: 'run', command: 'sleep 30' },
								],
							},
						],
					},
				],
			}
		`,
		},
		{
			name:                 "timeout parent timeout",
			tasksResultExpected:  map[string]rstypes.RunTaskStatus{"task01": rstypes.RunTaskStatusFailed, "task02": rstypes.RunTaskStatusSkipped},
			taskTimedoutExpected: map[string]bool{"task01": true, "task02": false},
			config: `
			{
				runs: [
					{
						name: 'run01',
						tasks: [
							{
								name: 'task01',
								runtime: {
									containers: [
										{
											image: 'alpine/git',
										},
									],
								},
								task_timeout_interval: "15s",
								steps: [
									{ type: 'run', command: 'sleep 30' },
								],
							},
							{
								name: 'task02',
								depends: ['task01'],
								runtime: {
									containers: [
										{
											image: 'alpine/git',
										},
									],
								},
								steps: [
									{ type: 'run', command: 'sleep 30' },
								],
							},
						],
					},
				],
			}
		`,
		},
		{
			name:                 "timeout parent and child timeout",
			tasksResultExpected:  map[string]rstypes.RunTaskStatus{"task01": rstypes.RunTaskStatusFailed, "task02": rstypes.RunTaskStatusSkipped},
			taskTimedoutExpected: map[string]bool{"task01": true, "task02": false},
			config: `
			{
				runs: [
					{
						name: 'run01',
						tasks: [
							{
								name: 'task01',
								runtime: {
									containers: [
										{
											image: 'alpine/git',
										},
									],
								},
								task_timeout_interval: "15s",
								steps: [
									{ type: 'run', command: 'sleep 30' },
								],
							},
							{
								name: 'task02',
								depends: ['task01'],
								runtime: {
									containers: [
										{
											image: 'alpine/git',
										},
									],
								},
								task_timeout_interval: "15s",
								steps: [
									{ type: 'run', command: 'sleep 30' },
								],
							},
						],
					},
				],
			}
		`,
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			dir := t.TempDir()

			ctx, cancel := context.WithCancel(context.Background())
			defer cancel()

			sc := setup(ctx, t, dir, withGitea(true))
			defer sc.stop()

			gwClient := gwclient.NewClient(sc.config.Gateway.APIExposedURL, "admintoken")
			user, _, err := gwClient.CreateUser(ctx, &gwapitypes.CreateUserRequest{UserName: agolaUser01})
			testutil.NilError(t, err)

			t.Logf("created agola user: %s", user.UserName)

			token := createAgolaUserToken(ctx, t, sc.config)

			// From now use the user token
			gwClient = gwclient.NewClient(sc.config.Gateway.APIExposedURL, token)

			directRun(t, dir, tt.config, ConfigFormatJsonnet, sc.config.Gateway.APIExposedURL, token)

			err = testutil.Wait(120*time.Second, func() (bool, error) {
				run, _, err := gwClient.GetUserRun(ctx, user.ID, 1)
				if err != nil {
					return false, nil
				}

				if run == nil {
					return false, nil
				}

				if run.Phase != rstypes.RunPhaseFinished {
					return false, nil
				}

				return true, nil
			})
			testutil.NilError(t, err)

			run, _, err := gwClient.GetUserRun(ctx, user.ID, 1)
			testutil.NilError(t, err)

			assert.Assert(t, run != nil)
			assert.Equal(t, run.Phase, rstypes.RunPhaseFinished)
			assert.Equal(t, run.Result, rstypes.RunResultFailed)
			assert.Assert(t, cmp.Len(run.Tasks, len(tt.tasksResultExpected)))
			for _, task := range run.Tasks {
				assert.Equal(t, task.Status, tt.tasksResultExpected[task.Name])
				assert.Equal(t, task.Timedout, tt.taskTimedoutExpected[task.Name])
			}
		})
	}
}

func TestRunRequiredEnvVariables(t *testing.T) {
	t.Parallel()

	config := `
	{
		runs: [
			{
				name: 'run01',
				tasks: [
					{
						name: 'task01',
						runtime: {
							containers: [
								{
									image: 'alpine/git',
								},
							],
						},
						steps: [
							{ type: 'run', command: 'env -u AGOLA_SSHPRIVKEY' },
						],
					},
				],
			},
		],
	}
	`

	tests := []struct {
		name string
		env  map[string]string
	}{
		{
			name: "push with run count 1",
			env: map[string]string{
				"AGOLA_GIT_REF_TYPE": "branch",
				"AGOLA_GIT_REF":      "refs/heads/master",
				"AGOLA_GIT_BRANCH":   "master",
				"AGOLA_GIT_TAG":      "",
			},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			dir := t.TempDir()

			ctx, cancel := context.WithCancel(context.Background())
			defer cancel()

			sc := setup(ctx, t, dir, withGitea(true))
			defer sc.stop()

			giteaToken, tokenUser01 := createLinkedAccount(ctx, t, sc.gitea, sc.config)
			gwClient := gwclient.NewClient(sc.config.Gateway.APIExposedURL, tokenUser01)

			giteaAPIURL := fmt.Sprintf("http://%s:%s", sc.gitea.HTTPListenAddress, sc.gitea.HTTPPort)

			giteaClient, err := gitea.NewClient(giteaAPIURL, gitea.SetToken(giteaToken))
			testutil.NilError(t, err)

			giteaRepo, project := createProject(ctx, t, giteaClient, gwClient, withVisibility(gwapitypes.VisibilityPrivate))

			push(t, config, giteaRepo.CloneURL, giteaToken, "commit", false)

			// TODO(sgotti) add an util to wait for a run phase
			err = testutil.Wait(60*time.Second, func() (bool, error) {
				runs, _, err := gwClient.GetProjectRuns(ctx, project.ID, &gwclient.GetRunsOptions{ListOptions: &gwclient.ListOptions{SortDirection: gwapitypes.SortDirectionDesc}})
				if err != nil {
					return false, nil
				}

				if len(runs) != 1 {
					return false, nil
				}

				if runs[0].Phase != rstypes.RunPhaseFinished {
					return false, nil
				}

				return true, nil
			})
			testutil.NilError(t, err)

			runs, _, err := gwClient.GetProjectRuns(ctx, project.ID, &gwclient.GetRunsOptions{ListOptions: &gwclient.ListOptions{SortDirection: gwapitypes.SortDirectionDesc}})
			testutil.NilError(t, err)

			assert.Assert(t, cmp.Len(runs, 1))

			run, _, err := gwClient.GetProjectRun(ctx, project.ID, runs[0].Number)
			testutil.NilError(t, err)

			assert.Equal(t, run.Phase, rstypes.RunPhaseFinished)
			assert.Equal(t, run.Result, rstypes.RunResultSuccess)

			// update commit sha from annotations since it will change at every test
			tt.env["AGOLA_GIT_COMMITSHA"] = run.Annotations["commit_sha"]

			tt.env["AGOLA_RUN_COUNTER"] = strconv.FormatUint(run.Number, 10)

			var task *gwapitypes.RunResponseTask
			for _, t := range run.Tasks {
				if t.Name == "task01" {
					task = t
					break
				}
			}

			resp, err := gwClient.GetProjectLogs(ctx, project.ID, run.Number, task.ID, false, 0, false)
			testutil.NilError(t, err)

			defer resp.Body.Close()

			logs, err := io.ReadAll(resp.Body)
			testutil.NilError(t, err)

			curEnv, err := testutil.ParseEnvs(bytes.NewReader(logs))
			testutil.NilError(t, err)

			for n, e := range tt.env {
				ce, ok := curEnv[n]
				assert.Assert(t, ok, "missing env var %s", n)
				assert.Equal(t, ce, e, "different env var %s value, want: %q, got %q", n, e, ce)
			}
		})
	}
}

func TestConfigContext(t *testing.T) {
	t.Parallel()

	jsonnetConfig := `
function(ctx) {
	runs: [
		{
			name: 'run01',
			tasks: [
				{
					name: 'task01',
					runtime: {
						containers: [
							{
								image: 'alpine/git',
							},
						],
					},
					environment: {
						REF_TYPE: ctx.ref_type,
						REF: ctx.ref,
						BRANCH: ctx.branch,
						TAG: ctx.tag,
						PULL_REQUEST_ID: ctx.pull_request_id,
						COMMIT_SHA: ctx.commit_sha,
					},
					steps: [
						{ type: 'clone' },
						{ type: 'run', command: 'env' },
					],
				},
			],
		},
	],
}
`

	starlarkConfig := `
def main(ctx):
	return {
		"runs": [
			{
			"name": 'run01',
			"tasks": [
				{
					"name": 'task01',
					"runtime": {
						"containers": [
							{
								"image": 'alpine/git',
							}
						]
					},
					"environment": {
						"REF_TYPE": ctx["ref_type"],
						"REF": ctx["ref"],
						"BRANCH": ctx["branch"],
						"TAG": ctx["tag"],
						"PULL_REQUEST_ID": ctx["pull_request_id"],
						"COMMIT_SHA": ctx["commit_sha"]
					},
					"steps": [
						{ "type": 'clone' },
						{ "type": 'run', "command": 'env' }
					],
				},
			],
		},
	]
}
`

	tests := []struct {
		name string
		args []string
		env  map[string]string
	}{
		{
			name: "direct run branch",
			env: map[string]string{
				"REF_TYPE":        "branch",
				"REF":             "refs/heads/master",
				"BRANCH":          "master",
				"TAG":             "",
				"PULL_REQUEST_ID": "",
				"COMMIT_SHA":      "",
			},
		},
		{
			name: "direct run tag",
			args: []string{"--tag", "v0.1.0"},
			env: map[string]string{
				"REF_TYPE":        "tag",
				"REF":             "refs/tags/v0.1.0",
				"BRANCH":          "",
				"TAG":             "v0.1.0",
				"PULL_REQUEST_ID": "",
				"COMMIT_SHA":      "",
			},
		},
		{
			name: "direct run with pr",
			args: []string{"--ref", "refs/pull/1/head"},
			env: map[string]string{
				"REF_TYPE":        "pull_request",
				"REF":             "refs/pull/1/head",
				"BRANCH":          "",
				"TAG":             "",
				"PULL_REQUEST_ID": "1",
				"COMMIT_SHA":      "",
			},
		},
	}

	for _, configFormat := range []ConfigFormat{ConfigFormatJsonnet, ConfigFormatStarlark} {
		configFormat := configFormat
		for _, tt := range tests {
			tt := tt
			t.Run(fmt.Sprintf("%s with %s config", tt.name, configFormat), func(t *testing.T) {
				t.Parallel()

				var config string
				switch configFormat {
				case ConfigFormatJsonnet:
					config = jsonnetConfig
				case ConfigFormatStarlark:
					config = starlarkConfig
				}

				dir := t.TempDir()

				ctx, cancel := context.WithCancel(context.Background())
				defer cancel()

				sc := setup(ctx, t, dir, withGitea(true))
				defer sc.stop()

				gwClient := gwclient.NewClient(sc.config.Gateway.APIExposedURL, "admintoken")
				user, _, err := gwClient.CreateUser(ctx, &gwapitypes.CreateUserRequest{UserName: agolaUser01})
				testutil.NilError(t, err)

				t.Logf("created agola user: %s", user.UserName)

				token := createAgolaUserToken(ctx, t, sc.config)

				// From now use the user token
				gwClient = gwclient.NewClient(sc.config.Gateway.APIExposedURL, token)

				directRun(t, dir, config, configFormat, sc.config.Gateway.APIExposedURL, token, tt.args...)

				// TODO(sgotti) add an util to wait for a run phase
				err = testutil.Wait(60*time.Second, func() (bool, error) {
					runs, _, err := gwClient.GetUserRuns(ctx, user.ID, &gwclient.GetRunsOptions{ListOptions: &gwclient.ListOptions{SortDirection: gwapitypes.SortDirectionDesc}})
					if err != nil {
						return false, nil
					}

					if len(runs) != 1 {
						return false, nil
					}

					if runs[0].Phase != rstypes.RunPhaseFinished {
						return false, nil
					}

					return true, nil
				})
				testutil.NilError(t, err)

				runs, _, err := gwClient.GetUserRuns(ctx, user.ID, &gwclient.GetRunsOptions{ListOptions: &gwclient.ListOptions{SortDirection: gwapitypes.SortDirectionDesc}})
				testutil.NilError(t, err)

				assert.Assert(t, cmp.Len(runs, 1))

				run, _, err := gwClient.GetUserRun(ctx, user.ID, runs[0].Number)
				testutil.NilError(t, err)

				assert.Equal(t, run.Phase, rstypes.RunPhaseFinished)
				assert.Equal(t, run.Result, rstypes.RunResultSuccess)

				var task *gwapitypes.RunResponseTask
				for _, t := range run.Tasks {
					if t.Name == "task01" {
						task = t
						break
					}
				}

				resp, err := gwClient.GetUserLogs(ctx, user.ID, run.Number, task.ID, false, 1, false)
				testutil.NilError(t, err)

				defer resp.Body.Close()

				logs, err := io.ReadAll(resp.Body)
				testutil.NilError(t, err)

				curEnv, err := testutil.ParseEnvs(bytes.NewReader(logs))
				testutil.NilError(t, err)

				// update commit sha from annotations since it will change at every test
				tt.env["COMMIT_SHA"] = run.Annotations["commit_sha"]

				for n, e := range tt.env {
					ce, ok := curEnv[n]
					assert.Assert(t, ok, "missing env var %s", n)
					assert.Equal(t, ce, e, "different env var %s value, want: %q, got %q", n, e, ce)
				}
			})
		}
	}
}

func TestRunEventsNotification(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name                   string
		config                 string
		expectedRunResult      rstypes.RunResult
		expectedRunPhase       rstypes.RunPhase
		expectedRunPhaseEvents []rstypes.RunPhase
		expectedRunTaskStatus  []rstypes.RunTaskStatus
	}{
		{
			name:                   "run result success",
			config:                 EnvRunConfig,
			expectedRunResult:      rstypes.RunResultSuccess,
			expectedRunPhase:       rstypes.RunPhaseFinished,
			expectedRunPhaseEvents: []rstypes.RunPhase{rstypes.RunPhaseQueued, rstypes.RunPhaseRunning, rstypes.RunPhaseRunning, rstypes.RunPhaseFinished},
			expectedRunTaskStatus:  []rstypes.RunTaskStatus{rstypes.RunTaskStatusNotStarted, rstypes.RunTaskStatusNotStarted, rstypes.RunTaskStatusSuccess, rstypes.RunTaskStatusSuccess},
		},
		{
			name:                   "run result failed",
			config:                 FailingRunConfig,
			expectedRunResult:      rstypes.RunResultFailed,
			expectedRunPhase:       rstypes.RunPhaseFinished,
			expectedRunPhaseEvents: []rstypes.RunPhase{rstypes.RunPhaseQueued, rstypes.RunPhaseRunning, rstypes.RunPhaseRunning, rstypes.RunPhaseFinished},
			expectedRunTaskStatus:  []rstypes.RunTaskStatus{rstypes.RunTaskStatusNotStarted, rstypes.RunTaskStatusNotStarted, rstypes.RunTaskStatusFailed, rstypes.RunTaskStatusFailed},
		},
		{
			name: "run setup config error",
			config: `
			{
				runserror:
			}
			`,
			expectedRunResult:      rstypes.RunResultUnknown,
			expectedRunPhase:       rstypes.RunPhaseSetupError,
			expectedRunPhaseEvents: []rstypes.RunPhase{rstypes.RunPhaseSetupError},
			expectedRunTaskStatus:  []rstypes.RunTaskStatus{rstypes.RunTaskStatusNotStarted},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			dir := t.TempDir()
			wrDir := t.TempDir()

			ctx, cancel := context.WithCancel(context.Background())
			defer cancel()

			wr := setupWebhooksReceiver(ctx, t, wrDir)
			defer wr.stop()

			sc := setup(ctx, t, dir, withGitea(true), withWebhooks(fmt.Sprintf("%s/%s", wr.exposedURL, "webhooks"), webhookSecret))
			defer sc.stop()

			giteaToken, tokenUser01 := createLinkedAccount(ctx, t, sc.gitea, sc.config)

			giteaAPIURL := fmt.Sprintf("http://%s:%s", sc.gitea.HTTPListenAddress, sc.gitea.HTTPPort)

			giteaClient, err := gitea.NewClient(giteaAPIURL, gitea.SetToken(giteaToken))
			testutil.NilError(t, err)

			gwClient := gwclient.NewClient(sc.config.Gateway.APIExposedURL, tokenUser01)

			giteaRepo, project := createProject(ctx, t, giteaClient, gwClient)

			push(t, tt.config, giteaRepo.CloneURL, giteaToken, "commit", false)

			err = testutil.Wait(60*time.Second, func() (bool, error) {
				runs, _, err := gwClient.GetProjectRuns(ctx, project.ID, &gwclient.GetRunsOptions{ListOptions: &gwclient.ListOptions{SortDirection: gwapitypes.SortDirectionDesc}})
				if err != nil {
					return false, nil
				}

				if len(runs) == 0 {
					return false, nil
				}
				if runs[0].Phase != tt.expectedRunPhase {
					return false, nil
				}

				return true, nil
			})
			testutil.NilError(t, err)

			runs, _, err := gwClient.GetProjectRuns(ctx, project.ID, &gwclient.GetRunsOptions{ListOptions: &gwclient.ListOptions{SortDirection: gwapitypes.SortDirectionDesc}})
			testutil.NilError(t, err)

			assert.Assert(t, cmp.Len(runs, 1))

			assert.Equal(t, runs[0].Phase, tt.expectedRunPhase)
			assert.Equal(t, runs[0].Result, tt.expectedRunResult)

			run, _, err := gwClient.GetProjectRun(ctx, project.ID, runs[0].Number)
			testutil.NilError(t, err)

			err = testutil.Wait(30*time.Second, func() (bool, error) {
				webhooks, err := wr.webhooks.getWebhooks()
				testutil.NilError(t, err)

				if len(webhooks) < len(tt.expectedRunPhaseEvents) {
					return false, nil
				}

				return true, nil
			})
			testutil.NilError(t, err)

			webhooks, err := wr.webhooks.getWebhooks()
			testutil.NilError(t, err)

			for i, w := range webhooks {
				data, err := json.Marshal(w.webhookData)
				testutil.NilError(t, err)

				sig256 := hmac.New(sha256.New, []byte(sc.config.Notification.WebhookSecret))
				_, err = io.MultiWriter(sig256).Write(data)
				testutil.NilError(t, err)

				signatureSHA256 := hex.EncodeToString(sig256.Sum(nil))
				assert.Equal(t, signatureSHA256, w.signature)

				assert.Equal(t, w.webhookData.Run.Counter, run.Number)
				assert.Equal(t, w.webhookData.Run.Name, run.Name)
				assert.Equal(t, w.webhookData.Run.Phase, string(tt.expectedRunPhaseEvents[i]))
				assert.Assert(t, cmp.Len(w.webhookData.Run.Tasks, len(run.Tasks)))

				if len(run.Tasks) > 0 {
					var taskID string
					for id := range w.webhookData.Run.Tasks {
						taskID = id
					}
					whTask := w.webhookData.Run.Tasks[taskID]
					task := run.Tasks[taskID]

					assert.Equal(t, whTask.ID, task.ID)
					assert.Equal(t, whTask.Name, task.Name)
					assert.Equal(t, whTask.Status, string(tt.expectedRunTaskStatus[i]))
					assert.Assert(t, !whTask.Approved)
					assert.Assert(t, !whTask.Skip)
					assert.Assert(t, !whTask.Timedout)
					assert.Assert(t, !whTask.WaitingApproval)
					assert.Assert(t, cmp.Len(whTask.Steps, 1))
					assert.Equal(t, whTask.Level, 0)
				}
			}
		})
	}
}
