package plugin

import (
	"context"
	"os/exec"
	"path/filepath"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"github.com/argoproj/argo-cd/v2/cmpserver/apiclient"
	"github.com/argoproj/argo-cd/v2/test"
)

func newService(configFilePath string) (*Service, error) {
	config, err := ReadPluginConfig(configFilePath)
	if err != nil {
		return nil, err
	}

	initConstants := CMPServerInitConstants{
		PluginConfig: *config,
	}

	service := &Service{
		initConstants: initConstants,
	}
	return service, nil
}

type pluginOpt func(*CMPServerInitConstants)

func withDiscover(d Discover) pluginOpt {
	return func(cic *CMPServerInitConstants) {
		cic.PluginConfig.Spec.Discover = d
	}
}

func buildPluginConfig(opts ...pluginOpt) *CMPServerInitConstants {
	cic := &CMPServerInitConstants{
		PluginConfig: PluginConfig{
			TypeMeta: metav1.TypeMeta{
				Kind:       "ConfigManagementPlugin",
				APIVersion: "argoproj.io/v1alpha1",
			},
			Metadata: metav1.ObjectMeta{
				Name: "some-plugin",
			},
			Spec: PluginConfigSpec{
				Version: "v1.0",
			},
		},
	}
	for _, opt := range opts {
		opt(cic)
	}
	return cic
}

func TestMatchRepository(t *testing.T) {
	type fixture struct {
		service *Service
		path    string
		env     []*apiclient.EnvEntry
	}
	setup := func(t *testing.T, opts ...pluginOpt) *fixture {
		t.Helper()
		cic := buildPluginConfig(opts...)
		path := filepath.Join(test.GetTestDir(t), "testdata", "kustomize")
		s := NewService(*cic)
		return &fixture{
			service: s,
			path:    path,
			env:     []*apiclient.EnvEntry{{Name: "ENV_VAR", Value: "1"}},
		}
	}
	t.Run("will match plugin by filename", func(t *testing.T) {
		// given
		d := Discover{
			FileName: "kustomization.yaml",
		}
		f := setup(t, withDiscover(d))

		// when
		match, err := f.service.matchRepository(context.Background(), f.path, f.env)

		// then
		assert.NoError(t, err)
		assert.True(t, match)
	})
	t.Run("will not match plugin by filename if file not found", func(t *testing.T) {
		// given
		d := Discover{
			FileName: "not_found.yaml",
		}
		f := setup(t, withDiscover(d))

		// when
		match, err := f.service.matchRepository(context.Background(), f.path, f.env)

		// then
		assert.NoError(t, err)
		assert.False(t, match)
	})
	t.Run("will match plugin by glob", func(t *testing.T) {
		// given
		d := Discover{
			Find: Find{
				Glob: "**/*/plugin.yaml",
			},
		}
		f := setup(t, withDiscover(d))

		// when
		match, err := f.service.matchRepository(context.Background(), f.path, f.env)

		// then
		assert.NoError(t, err)
		assert.True(t, match)
	})
	t.Run("will not match plugin by glob if not found", func(t *testing.T) {
		// given
		d := Discover{
			Find: Find{
				Glob: "**/*/not_found.yaml",
			},
		}
		f := setup(t, withDiscover(d))

		// when
		match, err := f.service.matchRepository(context.Background(), f.path, f.env)

		// then
		assert.NoError(t, err)
		assert.False(t, match)
	})
	t.Run("will match plugin by command when returns any output", func(t *testing.T) {
		// given
		d := Discover{
			Find: Find{
				Command: Command{
					Command: []string{"echo", "test"},
				},
			},
		}
		f := setup(t, withDiscover(d))

		// when
		match, err := f.service.matchRepository(context.Background(), f.path, f.env)

		// then
		assert.NoError(t, err)
		assert.True(t, match)
	})
	t.Run("will not match plugin by command when returns no output", func(t *testing.T) {
		// given
		d := Discover{
			Find: Find{
				Command: Command{
					Command: []string{"echo"},
				},
			},
		}
		f := setup(t, withDiscover(d))

		// when
		match, err := f.service.matchRepository(context.Background(), f.path, f.env)

		// then
		assert.NoError(t, err)
		assert.False(t, match)
	})
	t.Run("will match plugin because env var defined", func(t *testing.T) {
		// given
		d := Discover{
			Find: Find{
				Command: Command{
					Command: []string{"sh", "-c", "echo -n $ENV_VAR"},
				},
			},
		}
		f := setup(t, withDiscover(d))

		// when
		match, err := f.service.matchRepository(context.Background(), f.path, f.env)

		// then
		assert.NoError(t, err)
		assert.True(t, match)
	})
	t.Run("will not match plugin because no env var defined", func(t *testing.T) {
		// given
		d := Discover{
			Find: Find{
				Command: Command{
					// Use printf instead of echo since OSX prints the "-n" when there's no additional arg.
					Command: []string{"sh", "-c", `printf "%s" "$ENV_NO_VAR"`},
				},
			},
		}
		f := setup(t, withDiscover(d))

		// when
		match, err := f.service.matchRepository(context.Background(), f.path, f.env)

		// then
		assert.NoError(t, err)
		assert.False(t, match)
	})
	t.Run("will not match plugin by command when command fails", func(t *testing.T) {
		// given
		d := Discover{
			Find: Find{
				Command: Command{
					Command: []string{"cat", "nil"},
				},
			},
		}
		f := setup(t, withDiscover(d))

		// when
		match, err := f.service.matchRepository(context.Background(), f.path, f.env)

		// then
		assert.Error(t, err)
		assert.False(t, match)
	})
}

func Test_Negative_ConfigFile_DoesnotExist(t *testing.T) {
	configFilePath := "./testdata/kustomize-neg/config"
	service, err := newService(configFilePath)
	require.Error(t, err)
	require.Nil(t, service)
}

func TestGenerateManifest(t *testing.T) {
	configFilePath := "./testdata/kustomize/config"
	service, err := newService(configFilePath)
	require.NoError(t, err)

	res1, err := service.generateManifest(context.Background(), "", nil)
	require.NoError(t, err)
	require.NotNil(t, res1)

	expectedOutput := "{\"apiVersion\":\"v1\",\"data\":{\"foo\":\"bar\"},\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"
	if res1 != nil {
		require.Equal(t, expectedOutput, res1.Manifests[0])
	}
}

// TestRunCommandContextTimeout makes sure the command dies at timeout rather than sleeping past the timeout.
func TestRunCommandContextTimeout(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 990*time.Millisecond)
	defer cancel()
	// Use a subshell so there's a child command.
	command := Command{
		Command: []string{"sh", "-c"},
		Args:    []string{"sleep 5"},
	}
	before := time.Now()
	_, err := runCommand(ctx, command, "", []string{})
	after := time.Now()
	assert.Error(t, err) // The command should time out, causing an error.
	assert.Less(t, after.Sub(before), 1*time.Second)
}

func Test_getCommandArgsToLog(t *testing.T) {
	testCases := []struct {
		name     string
		args     []string
		expected string
	}{
		{
			name:     "no spaces",
			args:     []string{"sh", "-c", "cat"},
			expected: "sh -c cat",
		},
		{
			name:     "spaces",
			args:     []string{"sh", "-c", `echo "hello world"`},
			expected: `sh -c "echo \"hello world\""`,
		},
	}

	for _, tc := range testCases {
		tcc := tc
		t.Run(tcc.name, func(t *testing.T) {
			t.Parallel()
			assert.Equal(t, tcc.expected, getCommandArgsToLog(exec.Command(tcc.args[0], tcc.args[1:]...)))
		})
	}
}
