/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package plugin

import (
	"strconv"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestValidatePluginData(t *testing.T) {
	// A mock plugin with no commands
	mockNoCommand := mockSubprocessCLIPlugin(t, "foo")
	mockNoCommand.metadata.RuntimeConfig = &RuntimeConfigSubprocess{
		PlatformCommand: []PlatformCommand{},
		PlatformHooks:   map[string][]PlatformCommand{},
	}

	// A mock plugin with legacy commands
	mockLegacyCommand := mockSubprocessCLIPlugin(t, "foo")
	mockLegacyCommand.metadata.RuntimeConfig = &RuntimeConfigSubprocess{
		PlatformCommand: []PlatformCommand{
			{
				Command: "echo \"mock plugin\"",
			},
		},
		PlatformHooks: map[string][]PlatformCommand{
			Install: {
				PlatformCommand{
					Command: "echo installing...",
				},
			},
		},
	}

	for i, item := range []struct {
		pass      bool
		plug      Plugin
		errString string
	}{
		{true, mockSubprocessCLIPlugin(t, "abcdefghijklmnopqrstuvwxyz0123456789_-ABC"), ""},
		{true, mockSubprocessCLIPlugin(t, "foo-bar-FOO-BAR_1234"), ""},
		{false, mockSubprocessCLIPlugin(t, "foo -bar"), "invalid plugin name"},
		{false, mockSubprocessCLIPlugin(t, "$foo -bar"), "invalid plugin name"}, // Test leading chars
		{false, mockSubprocessCLIPlugin(t, "foo -bar "), "invalid plugin name"}, // Test trailing chars
		{false, mockSubprocessCLIPlugin(t, "foo\nbar"), "invalid plugin name"},  // Test newline
		{true, mockNoCommand, ""},     // Test no command metadata works
		{true, mockLegacyCommand, ""}, // Test legacy command metadata works
	} {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			err := item.plug.Metadata().Validate()
			if item.pass {
				require.NoError(t, err, "failed to validate case %d", i)
			} else {
				require.Error(t, err, "expected case %d to fail", i)
				assert.ErrorContains(t, err, item.errString, "expected case %d error to contain %q", i, item.errString)
			}
		})
	}
}

func TestMetadataValidateVersion(t *testing.T) {
	testValid := map[string]struct {
		version string
	}{
		"valid semver":                 {version: "1.0.0"},
		"valid semver with prerelease": {version: "1.2.3-alpha.1+build.123"},
		"empty version":                {version: ""},
	}

	testInvalid := map[string]struct {
		version string
	}{
		"valid semver with v prefix": {version: "v1.0.0"},
		"path traversal":             {version: "../../../../tmp/evil"},
		"path traversal in version":  {version: "1.0.0/../../etc"},
		"not a version":              {version: "not-a-version"},
	}

	for name, tc := range testValid {
		t.Run(name, func(t *testing.T) {
			m := mockSubprocessCLIPlugin(t, "testplugin")
			m.metadata.Version = tc.version
			assert.NoError(t, m.Metadata().Validate())
		})
	}

	for name, tc := range testInvalid {
		t.Run(name, func(t *testing.T) {
			m := mockSubprocessCLIPlugin(t, "testplugin")
			m.metadata.Version = tc.version
			assert.ErrorContains(t, m.Metadata().Validate(), "invalid plugin version")
		})
	}
}

func TestMetadataValidateMultipleErrors(t *testing.T) {
	// Create metadata with multiple validation issues
	metadata := Metadata{
		Name:          "invalid name with spaces", // Invalid name
		APIVersion:    "",                         // Empty API version
		Type:          "",                         // Empty type
		Runtime:       "",                         // Empty runtime
		Config:        nil,                        // Missing config
		RuntimeConfig: nil,                        // Missing runtime config
	}

	err := metadata.Validate()
	require.Error(t, err, "expected validation to fail with multiple errors")

	errStr := err.Error()

	// Check that all expected errors are present in the joined error
	expectedErrors := []string{
		"invalid plugin name",
		"empty APIVersion",
		"empty type field",
		"empty runtime field",
		"missing config field",
		"missing runtimeConfig field",
	}

	for _, expectedErr := range expectedErrors {
		require.ErrorContains(t, err, expectedErr)
	}

	// Verify that the error contains the correct number of error messages
	errorCount := 0
	for _, expectedErr := range expectedErrors {
		if strings.Contains(errStr, expectedErr) {
			errorCount++
		}
	}

	assert.GreaterOrEqual(t, errorCount, len(expectedErrors), "expected %d errors, but only found %d in: %v", len(expectedErrors), errorCount, errStr)
}
