// Copyright (c) Huawei Technologies Co., Ltd. 2020. All rights reserved.
// isula-build licensed under the Mulan PSL v2.
// You can use this software according to the terms and conditions of the Mulan PSL v2.
// You may obtain a copy of Mulan PSL v2 at:
//     http://license.coscl.org.cn/MulanPSL2
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR
// PURPOSE.
// See the Mulan PSL v2 for more details.
// Author: iSula Team
// Create: 2020-03-20
// Description: Builder related functions tests

package dockerfile

import (
	"context"
	"crypto/sha256"
	"io/ioutil"
	"os"
	"path/filepath"
	"reflect"
	"regexp"
	"strings"
	"testing"

	"gotest.tools/assert"
	"gotest.tools/fs"

	constant "isula.org/isula-build"
	pb "isula.org/isula-build/api/services"
	"isula.org/isula-build/image"
	"isula.org/isula-build/pkg/logger"
	"isula.org/isula-build/pkg/parser"
	"isula.org/isula-build/store"
	testutil "isula.org/isula-build/tests/util"
	"isula.org/isula-build/util"
)

func TestParseFiles(t *testing.T) {
	dockerfile := `
ARG testArg
FROM alpine AS uuid
COPY uuid /src/data

FROM alpine AS date
COPY date /src/data

FROM alpine
COPY --from=uuid /src/data /uuid
COPY --from=date /src/data /date
`
	dockerignore := `
# comment
*/temp*
*/*/temp*
temp?`
	ctxDir := fs.NewDir(t, t.Name(), fs.WithFile(".dockerignore", dockerignore))
	defer ctxDir.Remove()

	b := &Builder{
		buildOpts: BuildOptions{
			ContextDir: ctxDir.Path(),
			File:       dockerfile,
		},
	}

	err := b.parseFiles()
	assert.NilError(t, err)

	expectedPlayBook := &parser.PlayBook{
		HeadingArgs: []string{"testArg"},
		Pages: []*parser.Page{
			{
				Name: "uuid",
				Lines: []*parser.Line{
					{
						Cells: []*parser.Cell{
							{Value: "alpine"},
							{Value: "AS"},
							{Value: "uuid"},
						},
						Begin:   3,
						End:     3,
						Command: "FROM",
						Raw:     "alpine AS uuid",
						Flags:   make(map[string]string),
					},
					{
						Cells: []*parser.Cell{
							{Value: "uuid"},
							{Value: "/src/data"},
						},
						Begin:   4,
						End:     4,
						Command: "COPY",
						Raw:     "uuid /src/data",
						Flags:   make(map[string]string),
					},
				},
				Begin: 3,
				End:   4,
			},
			{
				Name: "date",
				Lines: []*parser.Line{
					{
						Cells: []*parser.Cell{
							{Value: "alpine"},
							{Value: "AS"},
							{Value: "date"},
						},
						Begin:   6,
						End:     6,
						Command: "FROM",
						Raw:     "alpine AS date",
						Flags:   make(map[string]string),
					},
					{
						Cells: []*parser.Cell{
							{Value: "date"},
							{Value: "/src/data"},
						},
						Begin:   7,
						End:     7,
						Command: "COPY",
						Raw:     "date /src/data",
						Flags:   make(map[string]string),
					},
				},
				Begin: 6,
				End:   7,
			},
			{
				Name: "2",
				Lines: []*parser.Line{
					{
						Cells: []*parser.Cell{
							{Value: "alpine"},
						},
						Begin:   9,
						End:     9,
						Command: "FROM",
						Raw:     "alpine",
						Flags:   make(map[string]string),
					},
					{
						Cells: []*parser.Cell{
							{Value: "/src/data"},
							{Value: "/uuid"},
						},
						Begin:   10,
						End:     10,
						Command: "COPY",
						Raw:     "--from=uuid /src/data /uuid",
						Flags:   map[string]string{"from": "uuid"},
					},
					{
						Cells: []*parser.Cell{
							{Value: "/src/data"},
							{Value: "/date"},
						},
						Begin:   11,
						End:     11,
						Command: "COPY",
						Raw:     "--from=date /src/data /date",
						Flags:   map[string]string{"from": "date"},
					},
				},
				Begin: 9,
				End:   11,
			},
		},
		Warnings: nil,
	}
	assert.DeepEqual(t, b.playbook, expectedPlayBook)

	expectedIgnores := strings.Split(dockerignore, "\n")
	// expectedIgnores[2:0]: trim empty line and comment line
	assert.DeepEqual(t, b.ignores, expectedIgnores[2:])
}

func TestAnalysePlayBookWithStageName(t *testing.T) {
	dockerfile := `
FROM alpine AS noarg
RUN ls

FROM busybox AS hasarg
RUN ls

FROM alpine
RUN ls

FROM noarg
RUN ls
`
	b := &Builder{
		buildOpts: BuildOptions{
			File: dockerfile,
		},
	}
	err := b.parseFiles()
	assert.NilError(t, err)
	err = b.newStageBuilders()
	assert.NilError(t, err)

	// check the arg and env taken by the command: RUN ls
	assert.DeepEqual(t, b.stageBuilders[0].fromImage, "alpine")
	assert.DeepEqual(t, b.stageBuilders[0].name, "noarg")
	assert.DeepEqual(t, b.stageBuilders[1].fromImage, "busybox")
	assert.DeepEqual(t, b.stageBuilders[1].name, "hasarg")
	assert.DeepEqual(t, b.stageBuilders[2].fromImage, "alpine")
	assert.DeepEqual(t, b.stageBuilders[2].name, "2")
	assert.DeepEqual(t, b.stageBuilders[3].fromImage, "noarg")
	assert.DeepEqual(t, b.stageBuilders[3].name, "3")
}

func TestAnalysePlayBookWithNoArgBeforeFrom(t *testing.T) {
	dockerfile := `
FROM alpine AS noArg
RUN ls

FROM alpine AS hasArg
ARG testArg
RUN ls

FROM alpine AS hasSameEnv
ARG testArg
ENV testArg 1.0
RUN ls
`
	b := &Builder{
		buildOpts: BuildOptions{
			File:      dockerfile,
			BuildArgs: map[string]string{"testArg": "0.1", "no_proxy": "10.0.0.0"},
		},
		ctx: context.Background(),
	}
	err := b.parseFiles()
	assert.NilError(t, err)
	err = b.newStageBuilders()
	assert.NilError(t, err)
	for _, sb := range b.stageBuilders {
		err = sb.analyzeStage(context.Background())
		assert.NilError(t, err)
	}

	// check the arg and env taken by the command: RUN ls
	assert.DeepEqual(t, b.stageBuilders[0].commands[0].args,
		map[string]string{"no_proxy": "10.0.0.0"})
	assert.DeepEqual(t, b.stageBuilders[1].commands[1].args,
		map[string]string{"testArg": "0.1", "no_proxy": "10.0.0.0"})
	assert.DeepEqual(t, b.stageBuilders[2].commands[2].args,
		map[string]string{"no_proxy": "10.0.0.0"})
	assert.DeepEqual(t, b.stageBuilders[2].commands[2].envs,
		map[string]string{"testArg": "1.0"})
}

func TestAnalysePlayBookWithArgBeforeFrom(t *testing.T) {
	dockerfile := `
ARG testArg=0.1
FROM alpine AS noArg
RUN ls

FROM alpine AS hasArg
ARG testArg
RUN ls

FROM alpine AS hasSameEnv
ARG testArg
ENV testArg 1.0
RUN ls
`
	b := &Builder{
		buildOpts: BuildOptions{
			File:      dockerfile,
			BuildArgs: map[string]string{"HTTPS_PROXY": "127.0.0.1"},
		},
		ctx: context.Background(),
	}

	err := b.parseFiles()
	assert.NilError(t, err)
	err = b.newStageBuilders()
	assert.NilError(t, err)
	for _, sb := range b.stageBuilders {
		err = sb.analyzeStage(context.Background())
		assert.NilError(t, err)
	}

	// check the arg and env taken by the command: RUN ls
	assert.DeepEqual(t, b.stageBuilders[0].commands[0].args,
		map[string]string{"HTTPS_PROXY": "127.0.0.1"})
	assert.DeepEqual(t, b.stageBuilders[1].commands[1].args,
		map[string]string{"testArg": "0.1", "HTTPS_PROXY": "127.0.0.1"})
	assert.DeepEqual(t, b.stageBuilders[2].commands[2].args,
		map[string]string{"HTTPS_PROXY": "127.0.0.1"})
	assert.DeepEqual(t, b.stageBuilders[2].commands[2].envs,
		map[string]string{"testArg": "1.0"})
}

// the tested Dockerfile changes from https://github.com/moby/moby/issues/18119#issuecomment-589704252
func TestAnalysePlayBookWithArgs(t *testing.T) {
	dockerfile := `
ARG GLOBALARG_ONE=globalarg_one_default_value
ARG GLOBALARG_TWO=globalarg_two_default_value

FROM myimage:${GLOBALARG_ONE} AS stage1
ARG STAGE1_ARG=foo
RUN ls
ARG GLOBALARG_ONE
RUN ls
ARG GLOBALARG_TWO=my-local-value
RUN ls

FROM ${GLOBALARG_TWO}:${GLOBALARG_ONE} AS stage1
RUN ls
ARG STAGE2_ARG=bar
RUN ls
`
	b := &Builder{
		buildOpts: BuildOptions{
			File:      dockerfile,
			BuildArgs: map[string]string{"GLOBALARG_TWO": "override"},
		},
		ctx: context.Background(),
	}

	err := b.parseFiles()
	assert.NilError(t, err)
	err = b.newStageBuilders()
	assert.NilError(t, err)
	for _, sb := range b.stageBuilders {
		err = sb.analyzeStage(context.Background())
		assert.NilError(t, err)
	}

	// check the arg and env taken by the command: RUN ls
	assert.DeepEqual(t, b.stageBuilders[0].fromImage, "myimage:globalarg_one_default_value")
	assert.DeepEqual(t, b.stageBuilders[0].commands[1].args, map[string]string{"STAGE1_ARG": "foo"})
	assert.DeepEqual(t, b.stageBuilders[0].commands[3].args,
		map[string]string{"STAGE1_ARG": "foo", "GLOBALARG_ONE": "globalarg_one_default_value"})
	assert.DeepEqual(t, b.stageBuilders[0].commands[5].args,
		map[string]string{"STAGE1_ARG": "foo", "GLOBALARG_ONE": "globalarg_one_default_value", "GLOBALARG_TWO": "override"})

	//assert.DeepEqual(t, b.stageBuilders[1].(*stageBuilder).fromImage, "override:globalarg_one_default_value")
	assert.DeepEqual(t, b.stageBuilders[1].commands[0].args,
		map[string]string{})
	assert.DeepEqual(t, b.stageBuilders[1].commands[2].args,
		map[string]string{"STAGE2_ARG": "bar"})
}

func TestUsedHeadingArgs(t *testing.T) {
	type fields struct {
		buildOpts  BuildOptions
		playbook   *parser.PlayBook
		unusedArgs map[string]string
	}
	type wants struct {
		headingArgs map[string]string
		reserved    map[string]string
	}
	tests := []struct {
		name   string
		fields fields
		want   wants
	}{
		{
			// no matching args between BuildArgs and HeadingArgs
			name: "no matching args between BuildArgs and HeadingArgs",
			fields: fields{
				buildOpts: BuildOptions{
					BuildArgs: map[string]string{"barg1": "arg1", "barg2": "arg2"},
				},
				playbook: &parser.PlayBook{
					HeadingArgs: []string{"harg1", "harg2"},
				},
				unusedArgs: map[string]string{"barg1": "arg1", "barg2": "arg2"},
			},
			want: wants{map[string]string{}, map[string]string{}},
		},
		{
			// no matching args between BuildArgs and HeadingArgs, and 1 HeadingArgs has default value
			name: "no matching args between BuildArgs and HeadingArgs, and 1 HeadingArgs has default value",
			fields: fields{
				buildOpts: BuildOptions{
					BuildArgs: map[string]string{"barg1": "arg1", "barg2": "arg2"},
				},
				playbook: &parser.PlayBook{
					HeadingArgs: []string{"harg1", "harg2=arg2"},
				},
				unusedArgs: map[string]string{"barg1": "arg1", "barg2": "arg2"},
			},
			want: wants{map[string]string{"harg2": "arg2"}, map[string]string{}},
		},
		{
			// has 2 matching args
			name: "has 2 matching args, and 1 HeadingArgs has default value",
			fields: fields{
				buildOpts: BuildOptions{
					BuildArgs: map[string]string{"barg1": "arg1", "barg2": "arg2", "barg3": "arg3"},
				},
				playbook: &parser.PlayBook{
					HeadingArgs: []string{"barg1", "barg2=arg2", "harg3=hhhharg3"},
				},
				unusedArgs: map[string]string{"barg3": "arg3"},
			},
			want: wants{map[string]string{"barg1": "arg1", "barg2": "arg2", "harg3": "hhhharg3"}, map[string]string{}},
		},
		{
			// all matched
			name: "all matched",
			fields: fields{
				buildOpts: BuildOptions{
					BuildArgs: map[string]string{"barg1": "arg1", "barg2": "arg2", "barg3": "arg3"},
				},
				playbook: &parser.PlayBook{
					HeadingArgs: []string{"barg1", "barg2", "barg3=arg3"},
				},
				unusedArgs: make(map[string]string),
			},
			want: wants{map[string]string{"barg1": "arg1", "barg2": "arg2", "barg3": "arg3"}, map[string]string{}},
		},
		{
			// preserved args
			name: "preserved args",
			fields: fields{
				buildOpts: BuildOptions{
					BuildArgs: map[string]string{"barg1": "arg1", "HTTP_PROXY": "http://www.test.com/", "no_proxy": "10.0.0.1"},
				},
				playbook: &parser.PlayBook{
					HeadingArgs: []string{"barg1=arg1", "barg2=arg2"},
				},
				unusedArgs: map[string]string{},
			},
			want: wants{map[string]string{"barg1": "arg1", "barg2": "arg2"},
				map[string]string{"HTTP_PROXY": "http://www.test.com/", "no_proxy": "10.0.0.1"}},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			b := &Builder{
				buildOpts:  tt.fields.buildOpts,
				playbook:   tt.fields.playbook,
				unusedArgs: tt.fields.unusedArgs,
			}
			err := b.usedHeadingArgs()
			assert.NilError(t, err)
			if !reflect.DeepEqual(b.headingArgs, tt.want.headingArgs) {
				t.Errorf("usedHeadingArgs() headingArgs = %v, want %v", b.headingArgs, tt.want.headingArgs)
			}
			if !reflect.DeepEqual(b.reservedArgs, tt.want.reserved) {
				t.Errorf("usedHeadingArgs() reserved = %v, want %v", b.reservedArgs, tt.want.reserved)
			}
		})
	}
}

func TestAnalysePlayBookWithUnusedArgs(t *testing.T) {
	var testcases = []struct {
		name       string
		dockerfile string
		buildArgs  map[string]string
		funcCheck  func(t *testing.T, b *Builder)
	}{
		{
			name: "test 1",
			dockerfile: `ARG testArg
FROM alpine AS stage1
ARG testArg2=foo

FROM stage1
ARG testArg3`,
			buildArgs: map[string]string{"unusedArg1": "arg1", "testArg2": "arg2"},
			funcCheck: func(t *testing.T, b *Builder) {
				assert.DeepEqual(t, b.headingArgs, map[string]string{})
				assert.DeepEqual(t, b.reservedArgs, map[string]string{})
				assert.DeepEqual(t, b.unusedArgs, map[string]string{"unusedArg1": "arg1"})
			},
		}, {
			name: "test 2 - no matching args between BuildArgs and HeadingArgs",
			dockerfile: `ARG harg1
ARG harg2
FROM alpine
WORKDIR /home`,
			buildArgs: map[string]string{"barg1": "arg1", "barg2": "arg2"},
			funcCheck: func(t *testing.T, b *Builder) {
				assert.DeepEqual(t, b.headingArgs, map[string]string{})
				assert.DeepEqual(t, b.reservedArgs, map[string]string{})
				assert.DeepEqual(t, b.unusedArgs, map[string]string{"barg1": "arg1", "barg2": "arg2"})
			},
		}, {
			name: "test 3 - no matching args between BuildArgs and HeadingArgs, and 1 HeadingArgs has default value",
			dockerfile: `ARG harg1
ARG harg2=arg2
FROM alpine
WORKDIR /home`,
			buildArgs: map[string]string{"barg1": "arg1", "barg2": "arg2"},
			funcCheck: func(t *testing.T, b *Builder) {
				assert.DeepEqual(t, b.headingArgs, map[string]string{"harg2": "arg2"})
				assert.DeepEqual(t, b.reservedArgs, map[string]string{})
				assert.DeepEqual(t, b.unusedArgs, map[string]string{"barg1": "arg1", "barg2": "arg2"})
			},
		}, {
			name: "test 4 - has 2 matching args, and 1 HeadingArgs has default value",
			dockerfile: `ARG barg1
ARG barg2=arg2
ARG harg3=hhhharg3
FROM alpine
WORKDIR /home`,
			buildArgs: map[string]string{"barg1": "arg1", "barg2": "arg2", "barg3": "arg3"},
			funcCheck: func(t *testing.T, b *Builder) {
				assert.DeepEqual(t, b.headingArgs, map[string]string{"barg1": "arg1", "barg2": "arg2", "harg3": "hhhharg3"})
				assert.DeepEqual(t, b.reservedArgs, map[string]string{})
				assert.DeepEqual(t, b.unusedArgs, map[string]string{"barg3": "arg3"})
			},
		}, {
			name: "test 5 - all matched",
			dockerfile: `ARG barg1
ARG barg2
ARG barg3
FROM alpine
WORKDIR /home`,
			buildArgs: map[string]string{"barg1": "arg1", "barg2": "arg2", "barg3": "arg3"},
			funcCheck: func(t *testing.T, b *Builder) {
				assert.DeepEqual(t, b.headingArgs, map[string]string{"barg1": "arg1", "barg2": "arg2", "barg3": "arg3"})
				assert.DeepEqual(t, b.reservedArgs, map[string]string{})
				assert.DeepEqual(t, b.unusedArgs, map[string]string{})
			},
		}, {
			name: "test 6 - preserved args",
			dockerfile: `ARG barg1=arg1
ARG barg2=arg2
ARG barg3
FROM alpine
WORKDIR /home`,
			buildArgs: map[string]string{"barg1": "arg1", "HTTP_PROXY": "http://www.test.com/", "no_proxy": "10.0.0.1"},
			funcCheck: func(t *testing.T, b *Builder) {
				assert.DeepEqual(t, b.headingArgs, map[string]string{"barg1": "arg1", "barg2": "arg2"})
				assert.DeepEqual(t, b.reservedArgs, map[string]string{"HTTP_PROXY": "http://www.test.com/", "no_proxy": "10.0.0.1"})
				assert.DeepEqual(t, b.unusedArgs, map[string]string{})
			},
		}, {
			name: "test 7 - FROM args",
			dockerfile: `ARG barg1
FROM ${barg1:+busybox}
WORKDIR /home`,
			buildArgs: map[string]string{"barg1": "arg1"},
			funcCheck: func(t *testing.T, b *Builder) {
				assert.DeepEqual(t, b.headingArgs, map[string]string{"barg1": "arg1"})
				assert.DeepEqual(t, b.reservedArgs, map[string]string{})
				assert.DeepEqual(t, b.unusedArgs, map[string]string{})
				assert.DeepEqual(t, b.stageBuilders[0].fromImage, "busybox")
			},
		},
	}

	for _, tt := range testcases {
		t.Run(tt.name, func(t *testing.T) {
			b := &Builder{
				buildOpts: BuildOptions{
					File:      tt.dockerfile,
					BuildArgs: tt.buildArgs,
				},
				ctx: context.Background(),
			}

			err := b.parseFiles()
			assert.NilError(t, err)
			err = b.newStageBuilders()
			assert.NilError(t, err)
			for _, sb := range b.stageBuilders {
				err = sb.analyzeStage(context.Background())
				assert.NilError(t, err)
			}
			tt.funcCheck(t, b)
		})
	}
}

func TestCleanupBuilder(t *testing.T) {
	dockerfile := `
ARG testArg
FROM alpine AS stage1
ARG testArg2=foo

FROM stage1
ARG testArg3
`
	ctx := context.Background()
	b := &Builder{
		buildOpts: BuildOptions{
			File:      dockerfile,
			BuildArgs: map[string]string{"unusedArg1": "arg1", "testArg2": "arg2", "unnamedArg": "arg3"},
		},
		cliLog: logger.NewCliLogger(constant.CliLogBufferLen),
		ctx:    context.WithValue(ctx, util.BuildDirKey(util.BuildDir), "/tmp/isula-build-test"),
	}

	err := b.parseFiles()
	assert.NilError(t, err)
	err = b.newStageBuilders()
	assert.NilError(t, err)
	for _, sb := range b.stageBuilders {
		err = sb.analyzeStage(context.Background())
		assert.NilError(t, err)
	}

	b.cleanup()
	logMsg, ok := <-b.cliLog.GetContent()
	assert.Equal(t, ok, true)
	assert.Equal(t, logMsg, "[Warning] One or more build-args [unnamedArg unusedArg1] were not consumed\n")
}

func TestGetFlagsAndArgs(t *testing.T) {
	type testcase struct {
		line        *parser.Line
		expectArgs  []string
		expectFlags map[string]string
	}

	var testcases = []testcase{
		// 1. all
		{
			line: &parser.Line{
				Command: "HEALTHCHECK",
				Cells: []*parser.Cell{
					{
						Value: "CMD",
					},
					{
						Value: "curl -fs http://localhost/ || exit 1",
					},
				},
				Flags: map[string]string{"start-period": "5s", "interval": "5s", "timeout": "3s", "retries": "3"},
			},
			expectArgs:  []string{"CMD", "curl -fs http://localhost/ || exit 1"},
			expectFlags: map[string]string{"start-period": "5s", "interval": "5s", "timeout": "3s", "retries": "3"},
		},
		// 2. no retries
		{
			line: &parser.Line{
				Command: "HEALTHCHECK",
				Cells: []*parser.Cell{
					{
						Value: "CMD",
					},
					{
						Value: "curl -fs http://localhost/ || exit 1",
					},
				},
				Flags: map[string]string{"start-period": "5s", "interval": "5s", "timeout": "3s"},
			},
			expectArgs:  []string{"CMD", "curl -fs http://localhost/ || exit 1"},
			expectFlags: map[string]string{"start-period": "5s", "interval": "5s", "timeout": "3s"},
		},
		// 3. no timeout
		{
			line: &parser.Line{
				Command: "HEALTHCHECK",
				Cells: []*parser.Cell{
					{
						Value: "CMD",
					},
					{
						Value: "curl -fs http://localhost/ || exit 1",
					},
				},
				Flags: map[string]string{"start-period": "5s", "interval": "5s"},
			},
			expectArgs:  []string{"CMD", "curl -fs http://localhost/ || exit 1"},
			expectFlags: map[string]string{"start-period": "5s", "interval": "5s"},
		},
		// 4. no interval
		{
			line: &parser.Line{
				Command: "HEALTHCHECK",
				Cells: []*parser.Cell{
					{
						Value: "CMD",
					},
					{
						Value: "curl -fs http://localhost/ || exit 1",
					},
				},
				Flags: map[string]string{"start-period": "5s"},
			},
			expectArgs:  []string{"CMD", "curl -fs http://localhost/ || exit 1"},
			expectFlags: map[string]string{"start-period": "5s"},
		},
		// 5. all default
		{
			line: &parser.Line{
				Command: "HEALTHCHECK",
				Cells: []*parser.Cell{
					{
						Value: "CMD",
					},
					{
						Value: "curl -fs http://localhost/ || exit 1",
					},
				},
				Flags: map[string]string{},
			},
			expectArgs:  []string{"CMD", "curl -fs http://localhost/ || exit 1"},
			expectFlags: map[string]string{},
		},
	}

	for _, tc := range testcases {
		allowFlags := map[string]bool{"start-period": true, "interval": true, "timeout": true, "retries": true}
		flags, args := getFlagsAndArgs(tc.line, allowFlags)
		assert.DeepEqual(t, flags, tc.expectFlags)
		assert.DeepEqual(t, args, tc.expectArgs)
	}
}

// FROM alpine:latest
// FROM docker.io/alpine:2.0
// FROM alpine@digest
// FROM alpine:latest@digest <- fail
// FROM alpine:latest:latest <- fail
// FROM alpine@digest@digest <- fail
func TestResolveImageName(t *testing.T) {
	type args struct {
		s   string
		reg *regexp.Regexp
	}
	tests := []struct {
		name    string
		args    args
		ret     string
		wantErr bool
	}{
		{
			name:    "test 1",
			args:    args{s: "alpine:latest"},
			ret:     "alpine:latest",
			wantErr: false,
		},
		{
			name:    "test 2",
			args:    args{s: "alpine:3.2"},
			ret:     "alpine:3.2",
			wantErr: false,
		},
		{
			name:    "test 3",
			args:    args{s: "alpine@sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72"},
			ret:     "alpine@sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72",
			wantErr: false,
		},
		{
			name:    "test 4 - with tag and digest",
			args:    args{s: "alpine:3.2@sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72"},
			ret:     "alpine:3.2@sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72",
			wantErr: false,
		},
		{
			name:    "test 6 - incorrect format with 2 * digests",
			args:    args{s: "alpine@sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72@sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72"},
			ret:     "",
			wantErr: true,
		},
		{
			name:    "test 7",
			args:    args{s: "docker.io/busybox:monica"},
			ret:     "docker.io/busybox:monica",
			wantErr: false,
		},
		{
			name:    "test 8",
			args:    args{s: "localhost:8080/busybox:monica"},
			ret:     "localhost:8080/busybox:monica",
			wantErr: false,
		},
		{
			name:    "test 9",
			args:    args{s: "127.0.0.1:8080/busybox:monica"},
			ret:     "127.0.0.1:8080/busybox:monica",
			wantErr: false,
		},
		{
			name:    "test 10",
			args:    args{s: "127.0.0.1:8080/alpine@sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72"},
			ret:     "127.0.0.1:8080/alpine@sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72",
			wantErr: false,
		},
		{
			name:    "test 11",
			args:    args{s: "non-existent-registry.com/alpine"},
			ret:     "non-existent-registry.com/alpine",
			wantErr: false,
		},
		{
			name:    "test 12",
			args:    args{s: "golang:1.11.1-alpine3.8"},
			ret:     "golang:1.11.1-alpine3.8",
			wantErr: false,
		},
		{
			name:    "test 13",
			args:    args{s: "registry.centos.org/centos/centos:centos7"},
			ret:     "registry.centos.org/centos/centos:centos7",
			wantErr: false,
		},
		{
			name:    "test 14",
			args:    args{s: "testuser/busybox:git-base@sha256:b39333801e01efb8cb3e346aa866879107dffacdb83e555f877d6e7215db14da"},
			ret:     "testuser/busybox:git-base@sha256:b39333801e01efb8cb3e346aa866879107dffacdb83e555f877d6e7215db14da",
			wantErr: false,
		},
		{
			name:    "test 15",
			args:    args{s: "registry:2"},
			ret:     "registry:2",
			wantErr: false,
		},
		{
			name:    "test 21",
			args:    args{s: "https://www.dropbox.com/s/kpbrx26bwhoa1rp/moment.js?raw=1"},
			ret:     "",
			wantErr: true,
		},
		{
			name:    "test 22",
			args:    args{s: "https://hub.docker.com/_/busybox"},
			ret:     "",
			wantErr: true,
		},
		{
			name:    "test 23",
			args:    args{s: "hub.docker.com/_/busybox"},
			ret:     "",
			wantErr: true,
		},
		{
			name:    "test 24",
			args:    args{s: "https://"},
			ret:     "",
			wantErr: true,
		},
		{
			name:    "test 25",
			args:    args{s: "Busybox"},
			ret:     "",
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			b := &Builder{
				buildOpts: BuildOptions{
					BuildArgs: map[string]string{},
				},
				cliLog:      logger.NewCliLogger(constant.CliLogBufferLen),
				headingArgs: map[string]string{},
				unusedArgs:  map[string]string{},
			}
			ret, err := image.ResolveImageName(tt.args.s, b.searchArg)
			if (err != nil) != tt.wantErr {
				t.Errorf("resolveImageName() error = %v, wantErr %v", err, tt.wantErr)
				t.FailNow()
			}
			if tt.ret != ret {
				t.Errorf("Failed: expected ret: %q, got: %q", tt.ret, ret)
				t.FailNow()
			}
		})
	}
}

// FROM $imageName
// FROM ${imageName}
// FROM {imageName}  <- fail
// FROM ${imageName}:$tag
// FROM $imageName@${digest}
// FROM ${imageName}${namePart2}$namePart3$namePart4
// FROM $imageName@${digest}$ <- fail
// FROM $imageName@${digest}$${digest2} <- fail
func TestResolveImageNameWithArgs(t *testing.T) {
	type args struct {
		s   string
		reg *regexp.Regexp
	}
	tests := []struct {
		name        string
		args        args
		ret         string
		wantErr     bool
		headingArgs map[string]string
		unusedArgs  map[string]string
		funcCheck   func(t *testing.T, b *Builder)
	}{
		{
			name:        "test 1",
			args:        args{s: "${imageName}"},
			ret:         "alpine",
			wantErr:     false,
			headingArgs: map[string]string{"imageName": "alpine"},
			unusedArgs:  map[string]string{"imageName": "alpine", "imageName2": "busybox"},
			funcCheck: func(t *testing.T, b *Builder) {
				assert.DeepEqual(t, b.unusedArgs, map[string]string{"imageName2": "busybox"})
			},
		},
		{
			name:        "test 2",
			args:        args{s: "$imageName"},
			ret:         "alpine",
			wantErr:     false,
			headingArgs: map[string]string{"imageName": "alpine", "imageName2": "busybox"},
			unusedArgs:  map[string]string{"imageName": "alpine", "imageName2": "busybox"},
			funcCheck: func(t *testing.T, b *Builder) {
				assert.DeepEqual(t, b.unusedArgs, map[string]string{"imageName2": "busybox"})
			},
		},
		{
			name:        "test 3",
			args:        args{s: "$imageName:$tag"},
			ret:         "euleros:2.0",
			wantErr:     false,
			headingArgs: map[string]string{"imageName": "euleros", "tag": "2.0"},
			unusedArgs:  map[string]string{"imageName2": "busybox"},
			funcCheck: func(t *testing.T, b *Builder) {
				assert.DeepEqual(t, b.unusedArgs, map[string]string{"imageName2": "busybox"})
			},
		},
		{
			name:        "test 4",
			args:        args{s: "$imageName@${digest}"},
			ret:         "openeuler@sha256:bc3e77282fe1d51402141daad38fed0226c15f6de18243faf64de4f5d67347e1",
			wantErr:     false,
			headingArgs: map[string]string{"imageName": "openeuler", "tag": "20.03", "digest": "sha256:bc3e77282fe1d51402141daad38fed0226c15f6de18243faf64de4f5d67347e1"},
			unusedArgs: map[string]string{"imageName": "openeuler", "tag": "20.03",
				"digest": "bc3e77282fe1d51402141daad38fed0226c15f6de18243faf64de4f5d67347e1"},
			funcCheck: func(t *testing.T, b *Builder) {
				assert.DeepEqual(t, b.unusedArgs, map[string]string{"tag": "20.03"})
			},
		},
		{
			name:        "test 5",
			args:        args{s: "${imageName}${namePart2}$namePart3$namePart4"},
			ret:         "n1n2p3p4",
			wantErr:     false,
			headingArgs: map[string]string{"imageName": "n1", "namePart2": "n2", "namePart3": "p3", "namePart4": "p4"},
			unusedArgs:  map[string]string{},
			funcCheck:   func(t *testing.T, b *Builder) {},
		},
		{
			name:        "test 6",
			args:        args{s: "${imageName}-${namePart2}_$namePart3/$namePart4"},
			ret:         "n1-n2_p3/p4",
			wantErr:     false,
			headingArgs: map[string]string{"imageName": "n1", "namePart2": "n2", "namePart3": "p3", "namePart4": "p4"},
			unusedArgs:  map[string]string{},
			funcCheck:   func(t *testing.T, b *Builder) {},
		},
		{
			name:        "test 7",
			args:        args{s: "$imageName@${digest}$"},
			ret:         "",
			wantErr:     true,
			headingArgs: map[string]string{},
			unusedArgs:  map[string]string{},
			funcCheck:   func(t *testing.T, b *Builder) {},
		},
		{
			name:        "test 8",
			args:        args{s: "$imageName@${digest}$${digest2}"},
			ret:         "",
			wantErr:     true,
			headingArgs: map[string]string{},
			unusedArgs:  map[string]string{},
			funcCheck:   func(t *testing.T, b *Builder) {},
		},
		{
			name:        "test 9",
			args:        args{s: "${imageName"},
			ret:         "",
			wantErr:     true,
			headingArgs: map[string]string{},
			unusedArgs:  map[string]string{},
			funcCheck:   func(t *testing.T, b *Builder) {},
		},
		{
			name:        "test 10",
			args:        args{s: "${imageName$namePart2"},
			ret:         "",
			wantErr:     true,
			headingArgs: map[string]string{},
			unusedArgs:  map[string]string{},
			funcCheck:   func(t *testing.T, b *Builder) {},
		},
		{
			// FROM doesn't accept "\$"
			name:        "test 11",
			args:        args{s: "\\${imageName}"},
			ret:         "",
			wantErr:     true,
			headingArgs: map[string]string{"imageName": "alpine"},
			unusedArgs:  map[string]string{},
			funcCheck:   func(t *testing.T, b *Builder) {},
		},
		{
			name:        "test 12 - normal with :-",
			args:        args{s: "${imageName:-busybox}"},
			ret:         "busybox",
			wantErr:     false,
			headingArgs: map[string]string{},
			unusedArgs:  map[string]string{},
			funcCheck:   func(t *testing.T, b *Builder) {},
		},
		{
			name:        "test 13 - normal with :-",
			args:        args{s: "${imageName:-busybox}"},
			ret:         "alpine",
			wantErr:     false,
			headingArgs: map[string]string{"imageName": "alpine"},
			unusedArgs:  map[string]string{},
			funcCheck:   func(t *testing.T, b *Builder) {},
		},
		{
			name:        "test 14 - normal with :+",
			args:        args{s: "${imageName:+busybox}"},
			ret:         "",
			wantErr:     true, // args will be resolved to "", which is not allowed
			headingArgs: map[string]string{},
			unusedArgs:  map[string]string{},
			funcCheck:   func(t *testing.T, b *Builder) {},
		},
		{
			name:        "test 15 - normal with :+",
			args:        args{s: "${imageName:+busybox}"},
			ret:         "busybox",
			wantErr:     false,
			headingArgs: map[string]string{"imageName": "alpine"},
			unusedArgs:  map[string]string{},
			funcCheck:   func(t *testing.T, b *Builder) {},
		},
		{
			name:        "test 16 - abnormal with :=",
			args:        args{s: "${imageName:=busybox}"},
			ret:         "",
			wantErr:     true,
			headingArgs: map[string]string{"imageName": "alpine"},
			unusedArgs:  map[string]string{},
			funcCheck:   func(t *testing.T, b *Builder) {},
		},
		{
			name:        "test 17 - abnormal with :-",
			args:        args{s: "$imageName:-busybox"},
			ret:         "",
			wantErr:     true,
			headingArgs: map[string]string{"imageName": "alpine"},
			unusedArgs:  map[string]string{},
			funcCheck:   func(t *testing.T, b *Builder) {},
		},
		{
			name:        "test 18 - abnormal with :+",
			args:        args{s: "$imageName:+busybox"},
			ret:         "",
			wantErr:     true,
			headingArgs: map[string]string{},
			unusedArgs:  map[string]string{},
			funcCheck:   func(t *testing.T, b *Builder) {},
		},
		{
			name:        "test 19 - abnormal with incorrect arg",
			args:        args{s: "$1imageName"},
			ret:         "",
			wantErr:     true,
			headingArgs: map[string]string{},
			unusedArgs:  map[string]string{},
			funcCheck:   func(t *testing.T, b *Builder) {},
		},
		{
			name:        "test 20 - abnormal with incorrect arg",
			args:        args{s: "$-imageName"},
			ret:         "",
			wantErr:     true,
			headingArgs: map[string]string{},
			unusedArgs:  map[string]string{},
			funcCheck:   func(t *testing.T, b *Builder) {},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			b := &Builder{
				cliLog:      logger.NewCliLogger(constant.CliLogBufferLen),
				headingArgs: tt.headingArgs,
				unusedArgs:  tt.unusedArgs,
			}
			ret, err := image.ResolveImageName(tt.args.s, b.searchArg)
			if (err != nil) != tt.wantErr {
				t.Errorf("ResolveImageName() error = %v, wantErr %v", err, tt.wantErr)
				t.FailNow()
			}
			if tt.ret != ret {
				t.Errorf("Failed: expected ret: %q, got: %q", tt.ret, ret)
				t.FailNow()
			}
			tt.funcCheck(t, b)
		})
	}
}

func TestWriteImageId(t *testing.T) {
	tmpDir := fs.NewDir(t, t.Name())
	defer tmpDir.Remove()

	imageIDFilePath := tmpDir.Join("iidfile")
	b := &Builder{
		buildOpts: BuildOptions{
			Iidfile: imageIDFilePath,
		},
		cliLog: logger.NewCliLogger(constant.CliLogBufferLen),
	}

	imageID := "38b993607bcabe01df1dffdf01b329005c6a10a36d557f9d073fc25943840c66"

	err := b.writeImageID(imageID)
	assert.NilError(t, err)
	bytes, err := ioutil.ReadFile(imageIDFilePath)
	assert.NilError(t, err)
	assert.Equal(t, imageID, string(bytes))
}

func TestSearchArg(t *testing.T) {
	tests := []struct {
		name     string
		arg      string
		headArgs map[string]string
		ret      string
	}{
		{
			name:     "test 1",
			arg:      "alpine:latest",
			headArgs: map[string]string{},
			ret:      "",
		},
		{
			name:     "test 2",
			arg:      "testArg:-v1.0",
			headArgs: map[string]string{"testArg": "vvv"},
			ret:      "vvv",
		},
		{
			name:     "test 3",
			arg:      "testArg:-v1.0",
			headArgs: map[string]string{},
			ret:      "v1.0",
		},
		{
			name:     "test 4",
			arg:      "testArg:-",
			headArgs: map[string]string{},
			ret:      "",
		},
		{
			name:     "test 10",
			arg:      "testArg:+v1.0",
			headArgs: map[string]string{"testArg": "vvv"},
			ret:      "v1.0",
		},
		{
			name:     "test 11",
			arg:      "testArg:+v1.0",
			headArgs: map[string]string{},
			ret:      "",
		},
		{
			name:     "test 12",
			arg:      "testArg:+",
			headArgs: map[string]string{},
			ret:      "",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			b := &Builder{
				headingArgs: tt.headArgs,
			}
			assert.Equal(t, tt.ret, b.searchArg(tt.arg))
		})
	}
}

func TestParseRequestBuildArgs(t *testing.T) {
	var tests = []struct {
		name      string
		buildArgs []string
		decrypt   bool
		wantErr   bool
	}{
		{
			name:      "case 1 - no build-args",
			buildArgs: []string{},
			decrypt:   false,
			wantErr:   false,
		},
		{
			name:      "case 2 - normal build-args",
			buildArgs: []string{"foo=bar"},
			decrypt:   false,
			wantErr:   false,
		},
		{
			name:      "case 3 - build-args needs decrypt",
			buildArgs: []string{"foo=bar", "http_proxy=test"},
			decrypt:   true,
			wantErr:   false,
		},
	}

	b := getBuilder()
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var (
				argsParsed map[string]string
				err        error
			)
			if tt.decrypt {
				oriKey, _ := util.GenerateCryptoKey(util.CryptoKeyLen)
				key, _ := util.PBKDF2(oriKey, util.CryptoKeyLen, sha256.New)
				var args = make([]string, 0, 10)
				for _, v := range tt.buildArgs {
					encryptedArg, encErr := util.EncryptAES(v, key)
					assert.NilError(t, encErr)
					args = append(args, encryptedArg)
				}
				argsParsed, err = b.parseBuildArgs(args, key)
				b.buildOpts.BuildArgs = argsParsed

			} else {
				argsParsed, err = b.parseBuildArgs(tt.buildArgs, "")
				b.buildOpts.BuildArgs = argsParsed
			}

			if (err == nil) != (!tt.wantErr) {
				t.FailNow()
			}

			var argsMap = make(map[string]string, len(tt.buildArgs))
			for _, arg := range tt.buildArgs {
				av := strings.SplitN(arg, "=", 2)
				argsMap[av[0]] = av[1]
			}
			assert.DeepEqual(t, argsMap, b.buildOpts.BuildArgs)

			b.buildOpts.BuildArgs = nil
		})
	}
}

func TestParseTag(t *testing.T) {
	type testcase struct {
		name   string
		output string
		tag    string
	}
	testcases := []testcase{
		{
			name:   "docker-daemon output",
			output: "docker-daemon:isula/test:latest",
			tag:    "isula/test:latest",
		},
		{
			name:   "isulad output",
			output: "isulad:isula/test:latest",
			tag:    "isula/test:latest",
		},
		{
			name:   "docker-archive output",
			output: "docker-archive:./isula.tar:isula:latest",
			tag:    "isula:latest",
		},
		{
			name:   "docker-archive output without tag",
			output: "docker-archive:./isula.tar",
			tag:    "",
		},
		{
			name:   "docker-archive output with long tag",
			output: "docker-archive:./isula.tar:aaa:bbb:ccc",
			tag:    "aaa:bbb:ccc",
		},
		{
			name:   "docker-archive output only with name",
			output: "docker-archive:./isula.tar:isula",
			tag:    "isula",
		},
		{
			name:   "docker output",
			output: "docker://localhost:5000/isula/test:latest",
			tag:    "isula/test:latest",
		},
		{
			name:   "docker output",
			output: "docker://localhost:5000/isula/test",
			tag:    "isula/test",
		},
		{
			name:   "invalid docker output",
			output: "docker:localhost:5000/isula/test:latest",
			tag:    "",
		},
	}
	for _, tc := range testcases {
		tag := parseOutputTag(tc.output)
		assert.Equal(t, tag, tc.tag, tc.name)
	}
}

func TestNewBuilder(t *testing.T) {
	// tmpfs doesn't not support chattr +i to immutable
	tmpDir, err := ioutil.TempDir("/var/tmp", t.Name())
	if err != nil {
		t.Log(err)
		t.FailNow()
	}
	defer os.RemoveAll(tmpDir)
	immutablePath := filepath.Join(tmpDir, "run")
	os.Mkdir(immutablePath, 0644)
	if err = testutil.Immutable(immutablePath, true); err != nil {
		t.Log(err)
		t.FailNow()
	}
	defer testutil.Immutable(immutablePath, false)

	type args struct {
		ctx         context.Context
		store       store.Store
		req         *pb.BuildRequest
		runtimePath string
		buildDir    string
		runDir      string
	}
	tests := []struct {
		name    string
		args    args
		want    *Builder
		wantErr bool
	}{
		{
			name: "NewBuilder - decrypts fail - wrong key",
			args: args{
				ctx:   context.Background(),
				store: store.Store{},
				req: &pb.BuildRequest{
					BuildArgs:  []string{"foo=bar", "http_proxy=test"},
					EncryptKey: "1",
				},
				buildDir: tmpDir,
				runDir:   tmpDir,
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "NewBuilder - wrong rundir",
			args: args{
				ctx:      context.Background(),
				store:    store.Store{},
				req:      &pb.BuildRequest{},
				buildDir: tmpDir,
				runDir:   "",
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "NewBuilder - parseOutput fail",
			args: args{
				ctx:   context.Background(),
				store: store.Store{},
				req: &pb.BuildRequest{
					Output: "docker-archive:/home/test/aa.tar",
				},
				buildDir: tmpDir,
				runDir:   immutablePath,
			},
			want:    nil,
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := NewBuilder(tt.args.ctx, tt.args.store, tt.args.req, tt.args.runtimePath, tt.args.buildDir, tt.args.runDir)
			if (err != nil) != tt.wantErr {
				t.Errorf("NewBuilder() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("NewBuilder() got = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestCheckAndExpandTag(t *testing.T) {
	type testcase struct {
		name    string
		tag     string
		output  string
		wantErr bool
	}
	testcases := []testcase{
		{
			name:    "test 1",
			tag:     "isula/test",
			output:  "isula/test:latest",
			wantErr: false,
		},
		{
			name:    "test 2",
			tag:     "localhost:5000/test",
			output:  "localhost:5000/test:latest",
			wantErr: false,
		},
		{
			name:    "test 3",
			tag:     "isula/test:latest",
			output:  "isula/test:latest",
			wantErr: false,
		},
		{
			name:    "test 4",
			tag:     "localhost:5000/test:latest",
			output:  "localhost:5000/test:latest",
			wantErr: false,
		},
		{
			name:    "test 5",
			tag:     "localhost:5000:aaa/test:latest",
			output:  "",
			wantErr: true,
		},
		{
			name:    "test 6",
			tag:     "localhost:5000:aaa/test",
			output:  "",
			wantErr: true,
		},
		{
			name:    "test 7",
			tag:     "localhost:5000/test:latest:latest",
			output:  "",
			wantErr: true,
		},
		{
			name:    "test 8",
			tag:     "test:latest:latest",
			output:  "",
			wantErr: true,
		},
		{
			name:    "test 9",
			tag:     "",
			output:  "<none>:<none>",
			wantErr: false,
		},
		{
			name:    "test 10",
			tag:     "abc efg:latest",
			output:  "",
			wantErr: true,
		},
		{
			name:    "test 10",
			tag:     "abc!@#:latest",
			output:  "",
			wantErr: true,
		},
	}
	for _, tc := range testcases {
		tag, err := CheckAndExpandTag(tc.tag)
		assert.Equal(t, tag, tc.output, tc.name)
		if (err != nil) != tc.wantErr {
			t.Errorf("getCheckAndExpandTag() error = %v, wantErr %v", err, tc.wantErr)
		}
	}
}
