//go:build unix

package plugin_test

import (
	"bytes"
	"context"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"runtime"
	"testing"
	"time"

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

	"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"
)

func setupInstalledPlugin(t *testing.T, homeDir string, p plugin.Plugin) {
	pluginDir := filepath.Join(homeDir, ".trivy", "plugins", p.Name)

	// Create the test plugin directory
	err := os.MkdirAll(pluginDir, os.ModePerm)
	require.NoError(t, err)

	// write the plugin name
	pluginMetadata := fmt.Sprintf(`name: "%s"
repository: "%s"
version: "%s"
usage: test
description: test
platforms:
  - selector:
      os: linux
      arch: amd64
    uri: ./test.sh
    bin: ./test.sh  
installed:
  platform:
    os: linux
    arch: amd64`, p.Name, p.Repository, p.Version)

	err = os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), []byte(pluginMetadata), os.ModePerm)
	require.NoError(t, err)
}

func TestManager_Run(t *testing.T) {
	if runtime.GOOS == "windows" {
		// the test.sh script can't be run on windows so skipping
		t.Skip("Test satisfied adequately by Linux tests")
	}

	type fields struct {
		Name        string
		Repository  string
		Version     string
		Summary     string
		Description string
		Platforms   []plugin.Platform
		GOOS        string
		GOARCH      string
	}
	tests := []struct {
		name    string
		fields  fields
		opts    plugin.Options
		wantErr string
	}{
		{
			name: "happy path",
			fields: fields{
				Name:        "test_plugin",
				Repository:  "github.com/aquasecurity/trivy-plugin-test",
				Version:     "0.1.0",
				Summary:     "test",
				Description: "test",
				Platforms: []plugin.Platform{
					{
						Selector: &plugin.Selector{
							OS:   "linux",
							Arch: "amd64",
						},
						URI: "github.com/aquasecurity/trivy-plugin-test",
						Bin: "test.sh",
					},
				},
				GOOS:   "linux",
				GOARCH: "amd64",
			},
		},
		{
			name: "no selector",
			fields: fields{
				Name:        "test_plugin",
				Repository:  "github.com/aquasecurity/trivy-plugin-test",
				Version:     "0.1.0",
				Summary:     "test",
				Description: "test",
				Platforms: []plugin.Platform{
					{
						URI: "github.com/aquasecurity/trivy-plugin-test",
						Bin: "test.sh",
					},
				},
			},
		},
		{
			name: "no matched platform",
			fields: fields{
				Name:        "test_plugin",
				Repository:  "github.com/aquasecurity/trivy-plugin-test",
				Version:     "0.1.0",
				Summary:     "test",
				Description: "test",
				Platforms: []plugin.Platform{
					{
						Selector: &plugin.Selector{
							OS:   "darwin",
							Arch: "amd64",
						},
						URI: "github.com/aquasecurity/trivy-plugin-test",
						Bin: "test.sh",
					},
				},
				GOOS:   "linux",
				GOARCH: "amd64",
			},
			wantErr: "platform not found",
		},
		{
			name: "no execution file",
			fields: fields{
				Name:        "test_plugin",
				Repository:  "github.com/aquasecurity/trivy-plugin-test",
				Version:     "0.1.0",
				Summary:     "test",
				Description: "test",
				Platforms: []plugin.Platform{
					{
						Selector: &plugin.Selector{
							OS:   "linux",
							Arch: "amd64",
						},
						URI: "github.com/aquasecurity/trivy-plugin-test",
						Bin: "nonexistence.sh",
					},
				},
				GOOS:   "linux",
				GOARCH: "amd64",
			},
			wantErr: "no such file or directory",
		},
		{
			name: "plugin exec error",
			fields: fields{
				Name:        "error_plugin",
				Repository:  "github.com/aquasecurity/trivy-plugin-error",
				Version:     "0.1.0",
				Summary:     "test",
				Description: "test",
				Platforms: []plugin.Platform{
					{
						Selector: &plugin.Selector{
							OS:   "linux",
							Arch: "amd64",
						},
						URI: "github.com/aquasecurity/trivy-plugin-test",
						Bin: "test.sh",
					},
				},
				GOOS:   "linux",
				GOARCH: "amd64",
			},
			wantErr: "exit status 1",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Setenv("XDG_DATA_HOME", "testdata")

			p := plugin.Plugin{
				Name:        tt.fields.Name,
				Repository:  tt.fields.Repository,
				Version:     tt.fields.Version,
				Summary:     tt.fields.Summary,
				Description: tt.fields.Description,
				Platforms:   tt.fields.Platforms,
			}

			err := p.Run(t.Context(), plugin.Options{
				Platform: ftypes.Platform{
					Platform: &v1.Platform{
						OS:           "linux",
						Architecture: "amd64",
					},
				},
			})
			if tt.wantErr != "" {
				require.ErrorContains(t, err, tt.wantErr)
				return
			}
			require.NoError(t, err)
		})
	}
}

func TestManager_Uninstall(t *testing.T) {
	ctx := clock.With(t.Context(), time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC))
	pluginName := "test_plugin"

	tempDir := t.TempDir()
	t.Setenv("XDG_DATA_HOME", tempDir)
	pluginDir := filepath.Join(tempDir, ".trivy", "plugins", pluginName)

	t.Run("plugin found", func(t *testing.T) {
		// Create the test plugin directory
		err := os.MkdirAll(pluginDir, os.ModePerm)
		require.NoError(t, err)

		// Create the test file
		err = os.WriteFile(filepath.Join(pluginDir, "test.sh"), []byte(`foo`), os.ModePerm)
		require.NoError(t, err)

		// Uninstall the plugin
		err = plugin.NewManager().Uninstall(ctx, pluginName)
		require.NoError(t, err)
		assert.NoDirExists(t, pluginDir)
	})

	t.Run("plugin not found", func(t *testing.T) {
		t.Setenv("NO_COLOR", tempDir)
		buf := bytes.NewBuffer(nil)
		slog.SetDefault(slog.New(log.NewHandler(buf, &log.Options{Level: log.LevelInfo})))

		err := plugin.NewManager().Uninstall(ctx, pluginName)
		require.NoError(t, err)
		assert.Equal(t, "2021-08-25T12:20:30Z\tERROR\t[plugin] No such plugin\n", buf.String())
	})
}

func TestManager_Information(t *testing.T) {
	pluginName := "test_plugin"

	tempDir := t.TempDir()
	pluginDir := filepath.Join(tempDir, ".trivy", "plugins", pluginName)

	t.Setenv("XDG_DATA_HOME", tempDir)

	// Create the test plugin directory
	err := os.MkdirAll(pluginDir, os.ModePerm)
	require.NoError(t, err)

	// write the plugin name
	pluginMetadata := `name: "test_plugin"
repository: github.com/aquasecurity/trivy-plugin-test
version: "0.1.0"
usage: test
description: A simple test plugin`

	err = os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), []byte(pluginMetadata), os.ModePerm)
	require.NoError(t, err)

	var got bytes.Buffer
	manager := plugin.NewManager(plugin.WithWriter(&got))

	// Get Information for the plugin
	err = manager.Information(pluginName)
	require.NoError(t, err)
	assert.Equal(t, `
Plugin: test_plugin
  Version:     0.1.0
  Summary:     test
  Description: A simple test plugin
`, got.String())
	got.Reset()

	// Get Information for unknown plugin
	err = manager.Information("unknown")
	require.Error(t, err)
	assert.ErrorContains(t, err, "could not find a plugin called 'unknown', did you install it?")
}

func TestManager_LoadAll(t *testing.T) {
	tests := []struct {
		name    string
		dir     string
		want    []plugin.Plugin
		wantErr string
	}{
		{
			name: "happy path",
			dir:  "testdata",
			want: []plugin.Plugin{
				{
					Name:        "test_plugin",
					Repository:  "github.com/aquasecurity/trivy-plugin-test",
					Version:     "0.1.0",
					Summary:     "test",
					Description: "test",
					Platforms: []plugin.Platform{
						{
							Selector: &plugin.Selector{
								OS:   "linux",
								Arch: "amd64",
							},
							URI: "./test.sh",
							Bin: "./test.sh",
						},
					},
				},
			},
		},
		{
			name:    "sad path",
			dir:     "sad",
			wantErr: "failed to read",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Setenv("XDG_DATA_HOME", tt.dir)

			got, err := plugin.NewManager().LoadAll(t.Context())
			if tt.wantErr != "" {
				require.ErrorContains(t, err, tt.wantErr)
				return
			}
			require.NoError(t, err)
			require.Len(t, got, len(tt.want))
			for i := range tt.want {
				assert.EqualExportedValues(t, tt.want[i], got[i])
			}
		})
	}
}

func TestManager_Upgrade(t *testing.T) {
	if runtime.GOOS == "windows" {
		// the test.sh script can't be run on windows so skipping
		t.Skip("Test satisfied adequately by Linux tests")
	}
	pluginName := "test_plugin"
	pluginVersion := "0.0.5"

	tempDir := t.TempDir()
	t.Setenv("XDG_DATA_HOME", tempDir)
	setupInstalledPlugin(t, tempDir, plugin.Plugin{
		Name:       pluginName,
		Version:    pluginVersion,
		Repository: "testdata/test_plugin",
	})

	ctx := t.Context()
	m := plugin.NewManager()

	// verify initial version
	verifyVersion(t, ctx, m, pluginName, pluginVersion)

	// Upgrade the existing plugin
	err := m.Upgrade(ctx, nil)
	require.NoError(t, err)

	// verify plugin updated
	verifyVersion(t, ctx, m, pluginName, "0.2.0")
}

func verifyVersion(t *testing.T, ctx context.Context, m *plugin.Manager, pluginName, expectedVersion string) {
	plugins, err := m.LoadAll(ctx)
	require.NoError(t, err)
	for _, p := range plugins {
		if p.Name == pluginName {
			assert.Equal(t, expectedVersion, p.Version)
		}
	}
}
