//go:build unix

package plugin_test

import (
	"archive/zip"
	"bytes"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/go-git/go-git/v5"
	v1 "github.com/google/go-containerregistry/pkg/v1"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/aquasecurity/trivy/internal/gittest"
	"github.com/aquasecurity/trivy/pkg/clock"
	ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
	"github.com/aquasecurity/trivy/pkg/log"
	"github.com/aquasecurity/trivy/pkg/plugin"
	"github.com/aquasecurity/trivy/pkg/utils/fsutils"
)

func setupGitRepository(t *testing.T, repo, dir string) *httptest.Server {
	gs := gittest.NewServer(t, repo, dir, gittest.Options{})

	worktree := t.TempDir()
	r := gittest.Clone(t, gs, repo, worktree, gittest.Options{})

	// git tag
	gittest.SetTag(t, r, "v0.2.0")

	// git commit
	modifyManifest(t, worktree, "0.3.0")
	gittest.CommitAll(t, r, "bump up to 0.3.0")

	err := r.Push(&git.PushOptions{})
	require.NoError(t, err)

	// git tag
	gittest.SetTag(t, r, "v0.3.0")

	// git push --tags
	gittest.PushTags(t, r)

	return gs
}

func modifyManifest(t *testing.T, worktree, version string) {
	manifestPath := filepath.Join(worktree, "plugin.yaml")
	b, err := os.ReadFile(manifestPath)
	require.NoError(t, err)

	b = bytes.ReplaceAll(b, []byte("0.2.0"), []byte(version))
	err = os.WriteFile(manifestPath, b, 0o644)
	require.NoError(t, err)
}

func TestManager_Install(t *testing.T) {
	gs := setupGitRepository(t, "test_plugin", "testdata/test_plugin/test_plugin")
	t.Cleanup(gs.Close)

	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		zr := zip.NewWriter(w)
		switch r.URL.Path {
		case "/test_plugin.zip":
			assert.NoError(t, zr.AddFS(os.DirFS("testdata/test_plugin/test_plugin")))
		case "/test_nested.zip":
			assert.NoError(t, zr.AddFS(os.DirFS("testdata/test_plugin")))
		}
		assert.NoError(t, zr.Close())
	}))
	t.Cleanup(ts.Close)

	wantPlugin := plugin.Plugin{
		Name:        "test_plugin",
		Repository:  "testdata/test_plugin",
		Version:     "0.2.0",
		Summary:     "test",
		Description: "test",
		Platforms: []plugin.Platform{
			{
				Selector: &plugin.Selector{
					OS:   "linux",
					Arch: "amd64",
				},
				URI: "./test.sh",
				Bin: "./test.sh",
			},
		},
		Installed: plugin.Installed{
			Platform: plugin.Selector{
				OS:   "linux",
				Arch: "amd64",
			},
		},
	}
	wantPluginWithGit := wantPlugin
	wantPluginWithGit.Version = "0.3.0"

	wantLogs := `2021-08-25T12:20:30Z	INFO	Installing the plugin...	src="%s"
2021-08-25T12:20:30Z	INFO	Plugin successfully installed	name="test_plugin" version="%s"
`

	tests := []struct {
		name       string
		pluginName string
		installed  *plugin.Plugin
		want       plugin.Plugin
		wantFile   string
		wantLogs   string
		wantErr    string
	}{
		{
			name:       "http",
			pluginName: ts.URL + "/test_plugin.zip",
			want:       wantPlugin,
			wantFile:   ".trivy/plugins/test_plugin/test.sh",
			wantLogs:   fmt.Sprintf(wantLogs, ts.URL+"/test_plugin.zip", "0.2.0"),
		},
		{
			name:       "nested archive",
			pluginName: ts.URL + "/test_nested.zip",
			want:       wantPlugin,
			wantFile:   ".trivy/plugins/test_plugin/test.sh",
			wantLogs:   fmt.Sprintf(wantLogs, ts.URL+"/test_nested.zip", "0.2.0"),
		},
		{
			name:       "local path",
			pluginName: "testdata/test_plugin",
			want:       wantPlugin,
			wantFile:   ".trivy/plugins/test_plugin/test.sh",
			wantLogs:   fmt.Sprintf(wantLogs, "testdata/test_plugin", "0.2.0"),
		},
		{
			name:       "git",
			pluginName: "git::" + gs.URL + "/test_plugin.git",
			want:       wantPluginWithGit,
			wantFile:   ".trivy/plugins/test_plugin/test.sh",
			wantLogs:   fmt.Sprintf(wantLogs, "git::"+gs.URL+"/test_plugin.git", "0.3.0"),
		},
		{
			name:       "with version",
			pluginName: "git::" + gs.URL + "/test_plugin.git@v0.2.0",
			want:       wantPlugin,
			wantFile:   ".trivy/plugins/test_plugin/test.sh",
			wantLogs:   fmt.Sprintf(wantLogs, "git::"+gs.URL+"/test_plugin.git", "0.2.0"),
		},
		{
			name:       "via index",
			pluginName: "test_plugin",
			want:       wantPlugin,
			wantFile:   ".trivy/plugins/test_plugin/test.sh",
			wantLogs:   fmt.Sprintf(wantLogs, "testdata/test_plugin", "0.2.0"),
		},
		{
			name:       "installed",
			pluginName: "test_plugin",
			installed: &plugin.Plugin{
				Name:       "test_plugin",
				Repository: "testdata/test_plugin",
				Version:    "0.2.0",
			},
			want:     wantPlugin,
			wantLogs: "2021-08-25T12:20:30Z	INFO	The plugin is already installed	name=\"test_plugin\"\n",
		},
		{
			name:       "different version installed",
			pluginName: "test_plugin@v0.2.0",
			installed: &plugin.Plugin{
				Name:       "test_plugin",
				Repository: "testdata/test_plugin",
				Version:    "0.1.0",
			},
			want:     wantPlugin,
			wantLogs: fmt.Sprintf(wantLogs, "testdata/test_plugin", "0.2.0"),
		},
		{
			name:       "plugin not found",
			pluginName: "testdata/not_found",
			wantErr:    "no such file or directory",
		},
		{
			name:       "no plugin.yaml",
			pluginName: "testdata/no_yaml",
			wantErr:    "file open error",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// The test plugin will be installed here
			dst := t.TempDir()
			t.Setenv("XDG_DATA_HOME", dst)

			// For plugin index
			pluginDir := filepath.Join(dst, ".trivy", "plugins")
			err := os.MkdirAll(pluginDir, 0o755)
			require.NoError(t, err)
			_, err = fsutils.CopyFile("testdata/.trivy/plugins/index.yaml", filepath.Join(pluginDir, "index.yaml"))
			require.NoError(t, err)

			if tt.installed != nil {
				setupInstalledPlugin(t, dst, *tt.installed)
			}

			var gotLogs bytes.Buffer
			logger := log.New(log.NewHandler(&gotLogs, nil))

			ctx := clock.With(t.Context(), time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC))

			got, err := plugin.NewManager(plugin.WithLogger(logger)).Install(ctx, tt.pluginName, plugin.Options{
				Platform: ftypes.Platform{
					Platform: &v1.Platform{
						Architecture: "amd64",
						OS:           "linux",
					},
				},
			})
			if tt.wantErr != "" {
				require.ErrorContains(t, err, tt.wantErr)
				return
			}
			require.NoError(t, err)

			assert.EqualExportedValues(t, tt.want, got)
			if tt.wantFile != "" {
				assert.FileExists(t, filepath.Join(dst, tt.wantFile))
			}
			assert.Equal(t, tt.wantLogs, gotLogs.String())
		})
	}
}
