package analyzer_test

import (
	"context"
	"errors"
	"fmt"
	"os"
	"sync"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/sync/semaphore"
	"golang.org/x/xerrors"

	dio "github.com/aquasecurity/go-dep-parser/pkg/io"
	"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
	aos "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os"
	"github.com/aquasecurity/trivy/pkg/fanal/types"

	_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/command/apk"
	_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/ruby/bundler"
	_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os/alpine"
	_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os/ubuntu"
	_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/pkg/apk"
	_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/repo/apk"
	_ "github.com/aquasecurity/trivy/pkg/fanal/handler/all"
)

type mockConfigAnalyzer struct{}

func (mockConfigAnalyzer) Required(targetOS types.OS) bool {
	return targetOS.Family == "alpine"
}

func (mockConfigAnalyzer) Analyze(targetOS types.OS, configBlob []byte) ([]types.Package, error) {
	if string(configBlob) != `foo` {
		return nil, errors.New("error")
	}
	return []types.Package{
		{Name: "musl", Version: "1.1.24-r2"},
	}, nil
}

func (mockConfigAnalyzer) Type() analyzer.Type {
	return analyzer.Type("test")
}

func (mockConfigAnalyzer) Version() int {
	return 1
}

func TestMain(m *testing.M) {
	analyzer.RegisterConfigAnalyzer(mockConfigAnalyzer{})
	os.Exit(m.Run())
}

func TestAnalysisResult_Merge(t *testing.T) {
	type fields struct {
		m            sync.Mutex
		OS           *types.OS
		PackageInfos []types.PackageInfo
		Applications []types.Application
	}
	type args struct {
		new *analyzer.AnalysisResult
	}
	tests := []struct {
		name   string
		fields fields
		args   args
		want   analyzer.AnalysisResult
	}{
		{
			name: "happy path",
			fields: fields{
				OS: &types.OS{
					Family: aos.Debian,
					Name:   "9.8",
				},
				PackageInfos: []types.PackageInfo{
					{
						FilePath: "var/lib/dpkg/status.d/libc",
						Packages: []types.Package{
							{Name: "libc", Version: "1.2.3"},
						},
					},
				},
				Applications: []types.Application{
					{
						Type:     "bundler",
						FilePath: "app/Gemfile.lock",
						Libraries: []types.Package{
							{
								Name:    "rails",
								Version: "5.0.0",
							},
						},
					},
				},
			},
			args: args{
				new: &analyzer.AnalysisResult{
					PackageInfos: []types.PackageInfo{
						{
							FilePath: "var/lib/dpkg/status.d/openssl",
							Packages: []types.Package{
								{Name: "openssl", Version: "1.1.1"},
							},
						},
					},
					Applications: []types.Application{
						{
							Type:     "bundler",
							FilePath: "app2/Gemfile.lock",
							Libraries: []types.Package{
								{
									Name:    "nokogiri",
									Version: "1.0.0",
								},
							},
						},
					},
				},
			},
			want: analyzer.AnalysisResult{
				OS: &types.OS{
					Family: aos.Debian,
					Name:   "9.8",
				},
				PackageInfos: []types.PackageInfo{
					{
						FilePath: "var/lib/dpkg/status.d/libc",
						Packages: []types.Package{
							{Name: "libc", Version: "1.2.3"},
						},
					},
					{
						FilePath: "var/lib/dpkg/status.d/openssl",
						Packages: []types.Package{
							{Name: "openssl", Version: "1.1.1"},
						},
					},
				},
				Applications: []types.Application{
					{
						Type:     "bundler",
						FilePath: "app/Gemfile.lock",
						Libraries: []types.Package{
							{
								Name:    "rails",
								Version: "5.0.0",
							},
						},
					},
					{
						Type:     "bundler",
						FilePath: "app2/Gemfile.lock",
						Libraries: []types.Package{
							{
								Name:    "nokogiri",
								Version: "1.0.0",
							},
						},
					},
				},
			},
		},
		{
			name: "redhat must be replaced with oracle",
			fields: fields{
				OS: &types.OS{
					Family: aos.RedHat, // this must be overwritten
					Name:   "8.0",
				},
			},
			args: args{
				new: &analyzer.AnalysisResult{
					OS: &types.OS{
						Family: aos.Oracle,
						Name:   "8.0",
					},
				},
			},
			want: analyzer.AnalysisResult{
				OS: &types.OS{
					Family: aos.Oracle,
					Name:   "8.0",
				},
			},
		},
		{
			name: "debian must be replaced with ubuntu",
			fields: fields{
				OS: &types.OS{
					Family: aos.Debian, // this must be overwritten
					Name:   "9.0",
				},
			},
			args: args{
				new: &analyzer.AnalysisResult{
					OS: &types.OS{
						Family: aos.Ubuntu,
						Name:   "18.04",
					},
				},
			},
			want: analyzer.AnalysisResult{
				OS: &types.OS{
					Family: aos.Ubuntu,
					Name:   "18.04",
				},
			},
		},
		{
			name: "alpine OS needs to be extended with apk repositories",
			fields: fields{
				OS: &types.OS{
					Family: aos.Alpine,
					Name:   "3.15.3",
				},
			},
			args: args{
				new: &analyzer.AnalysisResult{
					Repository: &types.Repository{
						Family:  aos.Alpine,
						Release: "edge",
					},
				},
			},
			want: analyzer.AnalysisResult{
				OS: &types.OS{
					Family: aos.Alpine,
					Name:   "3.15.3",
				},
				Repository: &types.Repository{
					Family:  aos.Alpine,
					Release: "edge",
				},
			},
		},
		{
			name: "alpine must not be replaced with oracle",
			fields: fields{
				OS: &types.OS{
					Family: aos.Alpine, // this must not be overwritten
					Name:   "3.11",
				},
			},
			args: args{
				new: &analyzer.AnalysisResult{
					OS: &types.OS{
						Family: aos.Oracle,
						Name:   "8.0",
					},
				},
			},
			want: analyzer.AnalysisResult{
				OS: &types.OS{
					Family: aos.Alpine, // this must not be overwritten
					Name:   "3.11",
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			r := analyzer.AnalysisResult{
				OS:           tt.fields.OS,
				PackageInfos: tt.fields.PackageInfos,
				Applications: tt.fields.Applications,
			}
			r.Merge(tt.args.new)
			assert.Equal(t, tt.want, r)
		})
	}
}

func TestAnalyzeFile(t *testing.T) {
	type args struct {
		filePath          string
		testFilePath      string
		disabledAnalyzers []analyzer.Type
	}
	tests := []struct {
		name    string
		args    args
		want    *analyzer.AnalysisResult
		wantErr string
	}{
		{
			name: "happy path with os analyzer",
			args: args{
				filePath:     "/etc/alpine-release",
				testFilePath: "testdata/etc/alpine-release",
			},
			want: &analyzer.AnalysisResult{
				OS: &types.OS{
					Family: "alpine",
					Name:   "3.11.6",
				},
			},
		},
		{
			name: "happy path with disabled os analyzer",
			args: args{
				filePath:          "/etc/alpine-release",
				testFilePath:      "testdata/etc/alpine-release",
				disabledAnalyzers: []analyzer.Type{analyzer.TypeAlpine},
			},
			want: &analyzer.AnalysisResult{},
		},
		{
			name: "happy path with package analyzer",
			args: args{
				filePath:     "/lib/apk/db/installed",
				testFilePath: "testdata/lib/apk/db/installed",
			},
			want: &analyzer.AnalysisResult{
				PackageInfos: []types.PackageInfo{
					{
						FilePath: "/lib/apk/db/installed",
						Packages: []types.Package{
							{Name: "musl", Version: "1.1.24-r2", SrcName: "musl", SrcVersion: "1.1.24-r2", License: "MIT"},
						},
					},
				},
				SystemInstalledFiles: []string{
					"lib/libc.musl-x86_64.so.1",
					"lib/ld-musl-x86_64.so.1",
				},
			},
		},
		{
			name: "happy path with disabled package analyzer",
			args: args{
				filePath:          "/lib/apk/db/installed",
				testFilePath:      "testdata/lib/apk/db/installed",
				disabledAnalyzers: []analyzer.Type{analyzer.TypeApk},
			},
			want: &analyzer.AnalysisResult{},
		},
		{
			name: "happy path with library analyzer",
			args: args{
				filePath:     "/app/Gemfile.lock",
				testFilePath: "testdata/app/Gemfile.lock",
			},
			want: &analyzer.AnalysisResult{
				Applications: []types.Application{
					{
						Type:     "bundler",
						FilePath: "/app/Gemfile.lock",
						Libraries: []types.Package{
							{
								Name:    "actioncable",
								Version: "5.2.3",
							},
						},
					},
				},
			},
		},
		{
			name: "happy path with invalid os information",
			args: args{
				filePath:     "/etc/lsb-release",
				testFilePath: "testdata/etc/hostname",
			},
			want: &analyzer.AnalysisResult{},
		},
		{
			name: "happy path with a directory",
			args: args{
				filePath:     "/etc/lsb-release",
				testFilePath: "testdata/etc",
			},
			want: &analyzer.AnalysisResult{},
		},
		{
			name: "ignore permission error",
			args: args{
				filePath:     "/etc/alpine-release",
				testFilePath: "testdata/no-permission",
			},
			want: &analyzer.AnalysisResult{},
		},
		{
			name: "sad path with opener error",
			args: args{
				filePath:     "/lib/apk/db/installed",
				testFilePath: "testdata/error",
			},
			wantErr: "unable to open /lib/apk/db/installed",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var wg sync.WaitGroup
			limit := semaphore.NewWeighted(3)

			got := new(analyzer.AnalysisResult)
			a := analyzer.NewAnalyzerGroup(analyzer.GroupBuiltin, tt.args.disabledAnalyzers)

			info, err := os.Stat(tt.args.testFilePath)
			require.NoError(t, err)

			ctx := context.Background()
			err = a.AnalyzeFile(ctx, &wg, limit, got, "", tt.args.filePath, info,
				func() (dio.ReadSeekCloserAt, error) {
					if tt.args.testFilePath == "testdata/error" {
						return nil, xerrors.New("error")
					} else if tt.args.testFilePath == "testdata/no-permission" {
						os.Chmod(tt.args.testFilePath, 0000)
						t.Cleanup(func() {
							os.Chmod(tt.args.testFilePath, 0644)
						})
					}
					return os.Open(tt.args.testFilePath)
				},
				nil, analyzer.AnalysisOptions{},
			)

			wg.Wait()
			if tt.wantErr != "" {
				require.NotNil(t, err)
				assert.Contains(t, err.Error(), tt.wantErr)
				return
			}

			require.NoError(t, err)
			assert.Equal(t, tt.want, got)
		})
	}
}

func TestAnalyzeConfig(t *testing.T) {

	type args struct {
		targetOS          types.OS
		configBlob        []byte
		disabledAnalyzers []analyzer.Type
	}
	tests := []struct {
		name string
		args args
		want []types.Package
	}{
		{
			name: "happy path",
			args: args{
				targetOS: types.OS{
					Family: "alpine",
					Name:   "3.11.6",
				},
				configBlob: []byte("foo"),
			},
			want: []types.Package{
				{Name: "musl", Version: "1.1.24-r2"},
			},
		},
		{
			name: "non-target OS",
			args: args{
				targetOS: types.OS{
					Family: "debian",
					Name:   "9.2",
				},
				configBlob: []byte("foo"),
			},
		},
		{
			name: "Analyze returns an error",
			args: args{
				targetOS: types.OS{
					Family: "alpine",
					Name:   "3.11.6",
				},
				configBlob: []byte("bar"),
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			a := analyzer.NewAnalyzerGroup(analyzer.GroupBuiltin, tt.args.disabledAnalyzers)
			got := a.AnalyzeImageConfig(tt.args.targetOS, tt.args.configBlob)
			assert.Equal(t, tt.want, got)
		})
	}
}

func TestAnalyzer_AnalyzerVersions(t *testing.T) {
	tests := []struct {
		name     string
		disabled []analyzer.Type
		want     map[string]int
	}{
		{
			name:     "happy path",
			disabled: []analyzer.Type{},
			want: map[string]int{
				"alpine":   1,
				"apk-repo": 1,
				"apk":      1,
				"bundler":  1,
				"ubuntu":   1,
			},
		},
		{
			name:     "disable analyzers",
			disabled: []analyzer.Type{analyzer.TypeAlpine, analyzer.TypeApkRepo, analyzer.TypeUbuntu},
			want: map[string]int{
				"apk":     1,
				"bundler": 1,
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			a := analyzer.NewAnalyzerGroup(analyzer.GroupBuiltin, tt.disabled)
			got := a.AnalyzerVersions()
			fmt.Printf("%v\n", got)
			assert.Equal(t, tt.want, got)
		})
	}
}

func TestAnalyzer_ImageConfigAnalyzerVersions(t *testing.T) {
	tests := []struct {
		name     string
		disabled []analyzer.Type
		want     map[string]int
	}{
		{
			name:     "happy path",
			disabled: []analyzer.Type{},
			want: map[string]int{
				"apk-command": 1,
				"test":        1,
			},
		},
		{
			name:     "disable analyzers",
			disabled: []analyzer.Type{analyzer.TypeAlpine, analyzer.TypeApkCommand},
			want: map[string]int{
				"test": 1,
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			a := analyzer.NewAnalyzerGroup(analyzer.GroupBuiltin, tt.disabled)
			got := a.ImageConfigAnalyzerVersions()
			assert.Equal(t, tt.want, got)
		})
	}
}
