package helmexec

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"reflect"
	"regexp"
	"strings"
	"testing"
	"time"

	"github.com/Masterminds/semver/v3"
	"github.com/google/go-cmp/cmp"
	"github.com/stretchr/testify/require"
	"github.com/tj/assert"
	"go.uber.org/zap"
)

// pluginCmd is the "plugin" helm subcommand used repeatedly across these tests.
// Extracted as constants so the repeated string literals do not trip goconst
// (min-occurrences: 8) once additional plugin tests are added.
const (
	pluginCmd  = "plugin"
	installCmd = "install"
)

// Mocking the command-line runner

type mockRunner struct {
	output        []byte
	versionOutput []byte // if set, returned for "helm version --short" probe; overrides default Helm 4 fallback
	err           error
	// failCount is the number of times Execute/ExecuteStdIn fail (with
	// errTransientNet) before succeeding. Used to exercise retry logic.
	failCount int
	calls     int
	// stdins records the stdin content passed to each ExecuteStdIn call,
	// so tests can assert that retries still pass the full payload (e.g. the
	// password is not consumed by a prior attempt).
	stdins []string
	// execArgs records the args passed to each Execute/ExecuteStdIn call, so
	// tests can assert that retries don't accumulate duplicated flags.
	execArgs [][]string
}

var errTransientNet = fmt.Errorf("transient network error")

func (mock *mockRunner) ExecuteStdIn(cmd string, args []string, env map[string]string, stdin io.Reader) ([]byte, error) {
	mock.calls++
	mock.execArgs = append(mock.execArgs, append([]string{}, args...))
	if stdin != nil {
		b, _ := io.ReadAll(stdin)
		mock.stdins = append(mock.stdins, string(b))
	}
	if mock.failCount > 0 && mock.calls <= mock.failCount {
		return nil, errTransientNet
	}
	return mock.output, mock.err
}

func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
	if strings.Join(args, " ") == "version --short" {
		if mock.versionOutput != nil {
			return mock.versionOutput, nil
		}
		if len(mock.output) == 0 {
			return []byte("v4.0.1+g12500dd"), nil
		}
	}
	mock.calls++
	mock.execArgs = append(mock.execArgs, append([]string{}, args...))
	if mock.failCount > 0 && mock.calls <= mock.failCount {
		return nil, errTransientNet
	}
	return mock.output, mock.err
}

func MockExecer(logger *zap.SugaredLogger, kubeconfig, kubeContext string) (*execer, error) {
	execer, err := New("helm", HelmExecOptions{}, logger, kubeconfig, kubeContext, &mockRunner{})
	if err != nil {
		return nil, err
	}
	return execer, nil
}

// Test methods

func TestNewHelmExec(t *testing.T) {
	buffer := bytes.NewBufferString("something")
	helm, err := MockExecer(NewLogger(buffer, "debug"), "config", "dev")
	if err != nil {
		t.Error(err)
	}
	if helm.kubeContext != "dev" {
		t.Error("helmexec.New() - kubeContext")
	}
	if buffer.String() != "something" {
		t.Error("helmexec.New() - changed buffer")
	}
	if len(helm.extra) != 0 {
		t.Error("helmexec.New() - extra args not empty")
	}
}

func Test_SetExtraArgs(t *testing.T) {
	helm, err := MockExecer(NewLogger(os.Stdout, "info"), "config", "dev")
	if err != nil {
		t.Error(err)
	}
	helm.SetExtraArgs()
	if len(helm.extra) != 0 {
		t.Error("helmexec.SetExtraArgs() - passing no arguments should not change extra field")
	}
	helm.SetExtraArgs("foo")
	if !reflect.DeepEqual(helm.extra, []string{"foo"}) {
		t.Error("helmexec.SetExtraArgs() - one extra argument missing")
	}
	helm.SetExtraArgs("alpha", "beta")
	if !reflect.DeepEqual(helm.extra, []string{"alpha", "beta"}) {
		t.Error("helmexec.SetExtraArgs() - two extra arguments missing (overwriting the previous value)")
	}
}

func Test_SetHelmBinary(t *testing.T) {
	helm, err := MockExecer(NewLogger(os.Stdout, "info"), "config", "dev")
	if err != nil {
		t.Error(err)
	}
	if helm.helmBinary != "helm" {
		t.Error("helmexec.command - default command is not helm")
	}
	helm.SetHelmBinary("foo")
	if helm.helmBinary != "foo" {
		t.Errorf("helmexec.SetHelmBinary() - actual = %s expect = foo", helm.helmBinary)
	}
}

func Test_SetEnableLiveOutput(t *testing.T) {
	helm, err := MockExecer(NewLogger(os.Stdout, "info"), "config", "dev")
	if err != nil {
		t.Error(err)
	}
	if helm.options.EnableLiveOutput {
		t.Error("helmexec.options.EnableLiveOutput should not be enabled by default")
	}
	helm.SetEnableLiveOutput(true)
	if !helm.options.EnableLiveOutput {
		t.Errorf("helmexec.SetEnableLiveOutput() - actual = %t expect = true", helm.options.EnableLiveOutput)
	}
}

func Test_AddRepo_Helm_Version(t *testing.T) {
	tests := []struct {
		name               string
		version            string
		disableForceUpdate bool
		expected           string
	}{
		{
			name:    "Helm 3.2.0 (before force-update)",
			version: "3.2.0",
			expected: `Adding repo myRepo https://repo.example.com/
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-file key.pem
`,
		},
		{
			name:    "Helm 3.3.2 (force-update added)",
			version: "3.3.2",
			expected: `Adding repo myRepo https://repo.example.com/
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --force-update --cert-file cert.pem --key-file key.pem
`,
		},
		{
			name:    "Helm 3.19.2 (with force-update)",
			version: "3.19.2",
			expected: `Adding repo myRepo https://repo.example.com/
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --force-update --cert-file cert.pem --key-file key.pem
`,
		},
		{
			name:               "Helm 3.19.2 (force-update disabled)",
			version:            "3.19.2",
			disableForceUpdate: true,
			expected: `Adding repo myRepo https://repo.example.com/
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-file key.pem
`,
		},
		{
			name:    "Helm 4.0.1 (force-update needed)",
			version: "4.0.1",
			expected: `Adding repo myRepo https://repo.example.com/
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --force-update --cert-file cert.pem --key-file key.pem
`,
		},
		{
			name:               "Helm 4.0.1 (force-update disabled)",
			version:            "4.0.1",
			disableForceUpdate: true,
			expected: `Adding repo myRepo https://repo.example.com/
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-file key.pem
`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var buffer bytes.Buffer
			logger := NewLogger(&buffer, "debug")
			helm := &execer{
				helmBinary:  "helm",
				version:     semver.MustParse(tt.version),
				logger:      logger,
				kubeconfig:  "config",
				kubeContext: "dev",
				runner:      &mockRunner{},
				options:     HelmExecOptions{DisableForceUpdate: tt.disableForceUpdate},
			}
			err := helm.AddRepo("myRepo", "https://repo.example.com/", "", "cert.pem", "key.pem", "", "", "", false, false)

			assert.NoError(t, err)
			assert.Equal(t, tt.expected, buffer.String())
		})
	}
}

func Test_AddRepo(t *testing.T) {
	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm, err := MockExecer(logger, "config", "dev")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}

	// Test case with certfile and keyfile
	buffer.Reset()
	err = helm.AddRepo("myRepo", "https://repo.example.com/", "", "cert.pem", "key.pem", "", "", "", false, false)
	expected := `Adding repo myRepo https://repo.example.com/
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --force-update --cert-file cert.pem --key-file key.pem
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}

	if buffer.String() != expected {
		t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	// Test case with cafile
	buffer.Reset()
	err = helm.AddRepo("myRepo", "https://repo.example.com/", "ca.crt", "", "", "", "", "", false, false)
	expected = `Adding repo myRepo https://repo.example.com/
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --force-update --ca-file ca.crt
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}

	if buffer.String() != expected {
		t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	// Test case with no certfile or cafile
	buffer.Reset()
	err = helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "", "", "", false, false)
	expected = `Adding repo myRepo https://repo.example.com/
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --force-update
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}

	if buffer.String() != expected {
		t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	// Test case with managed "acr"
	buffer.Reset()
	err = helm.AddRepo("acrRepo", "", "", "", "", "", "", "acr", false, false)
	expected = `Adding repo acrRepo (acr)
exec: az acr helm repo add --name acrRepo
exec: az acr helm repo add --name acrRepo:
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}

	if buffer.String() != expected {
		t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	// Test case with unknown managed type
	buffer.Reset()
	err = helm.AddRepo("otherRepo", "", "", "", "", "", "", "unknown", false, false)
	expected = `ERROR: unknown type 'unknown' for repository otherRepo
`
	if err == nil {
		t.Errorf("expected error for unknown managed type, got nil")
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	// Test case with username and password (using password-stdin)
	buffer.Reset()
	err = helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "example_user", "example_password", "", false, false)
	expected = `Adding repo myRepo https://repo.example.com/
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --force-update --username example_user --password-stdin
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}

	if buffer.String() != expected {
		t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	// Test case with username, password, and pass-credentials
	buffer.Reset()
	err = helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "example_user", "example_password", "", true, false)
	expected = `Adding repo myRepo https://repo.example.com/
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --force-update --pass-credentials --username example_user --password-stdin
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}

	actual := buffer.String()
	if actual != expected {
		t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", actual, expected)
	}

	// Test case with skipTLSVerify
	buffer.Reset()
	err = helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "", "", "", false, true)
	expected = `Adding repo myRepo https://repo.example.com/
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --force-update --insecure-skip-tls-verify
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}

	normalize := func(s string) string {
		return strings.Join(strings.Fields(s), " ")
	}

	actual = normalize(buffer.String())
	expected = normalize(expected)

	if actual != expected {
		t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", actual, expected)
	}

	// Test case with empty name
	buffer.Reset()
	err = helm.AddRepo("", "https://repo.example.com/", "", "", "", "", "", "", false, false)
	expected = `empty field name`

	if err != nil && err.Error() != "empty field name" {
		t.Errorf("unexpected error: %v", err)
	}
	actual = strings.TrimSpace(buffer.String())
	if actual != expected {
		t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", actual, expected)
	}
}

func Test_UpdateRepo(t *testing.T) {
	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm, err := MockExecer(logger, "config", "dev")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	err = helm.UpdateRepo()
	expected := `Updating repo
exec: helm --kubeconfig config --kube-context dev repo update
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.UpdateRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}
}

func Test_RegistryLogin(t *testing.T) {
	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm := &execer{
		helmBinary:  "helm",
		version:     semver.MustParse("v3.12.0"),
		logger:      logger,
		kubeconfig:  "config",
		kubeContext: "dev",
		runner:      &mockRunner{},
	}
	err := helm.RegistryLogin("repo.example.com", "example_user", "example_password", "example_ca", "example_cert", "example_key", true)
	expected := `Logging in to registry
exec: helm --kubeconfig config --kube-context dev registry login repo.example.com --cert-file example_cert --key-file example_key --ca-file example_ca --insecure --username example_user --password-stdin
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.RegistryLogin()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	// Test helm version prior to v3.12.0, without support for TLS
	buffer.Reset()
	helm.version = semver.MustParse("v3.11.0")

	err = helm.RegistryLogin("repo.example.com", "example_user", "example_password", "example_ca", "example_cert", "example_key", true)
	expected = `Logging in to registry
exec: helm --kubeconfig config --kube-context dev registry login repo.example.com --insecure --username example_user --password-stdin
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.RegistryLogin()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}
}

// newRetryExecer builds an execer with the given RepoRetry option and a
// mockRunner that fails failCount times before succeeding.
func newRetryExecer(t *testing.T, repoRetry, failCount int) (*execer, *mockRunner) {
	t.Helper()
	logger := NewLogger(os.Stdout, "debug")
	runner := &mockRunner{failCount: failCount}
	helm, err := New("helm", HelmExecOptions{RepoRetry: repoRetry}, logger, "config", "dev", runner)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	return helm, runner
}

// withTestBackoff shrinks repoRetryBaseBackoff for the test's duration so retry
// tests don't sleep for real. The restore is via t.Cleanup so it always runs.
func withTestBackoff(t *testing.T) {
	t.Helper()
	original := repoRetryBaseBackoff
	repoRetryBaseBackoff = time.Millisecond
	t.Cleanup(func() { repoRetryBaseBackoff = original })
}

// retryTestCase is the shared shape for the AddRepo/UpdateRepo/RegistryLogin
// retry table tests.
type retryTestCase struct {
	name      string
	repoRetry int
	failCount int
	wantErr   bool
	wantCalls int // expected number of exec (non-version) calls
	managed   string
}

func runRetryCases(t *testing.T, run func(*testing.T, *execer, *mockRunner, retryTestCase), cases []retryTestCase) {
	t.Helper()
	withTestBackoff(t)
	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			helm, runner := newRetryExecer(t, tt.repoRetry, tt.failCount)
			run(t, helm, runner, tt)
			if runner.calls != tt.wantCalls {
				t.Errorf("runner.calls = %d, want %d", runner.calls, tt.wantCalls)
			}
		})
	}
}

func Test_AddRepo_Retry(t *testing.T) {
	runRetryCases(t, func(t *testing.T, helm *execer, runner *mockRunner, tt retryTestCase) {
		err := helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "user", "pass", tt.managed, false, false)
		if (err != nil) != tt.wantErr {
			t.Errorf("AddRepo() error = %v, wantErr %v", err, tt.wantErr)
		}
		// For the normal (non-acr) path, each attempt's args must contain
		// --username exactly once: guards against the username/password flags
		// accumulating across retries.
		if tt.managed == "" {
			for i, a := range runner.execArgs {
				if c := strings.Count(strings.Join(a, " "), "--username"); c != 1 {
					t.Errorf("AddRepo() attempt %d: --username count = %d, want 1 (args=%v)", i+1, c, a)
				}
			}
		}
	}, []retryTestCase{
		{name: "succeeds after one retry", repoRetry: 2, failCount: 1, wantErr: false, wantCalls: 2},
		{name: "retries exhausted", repoRetry: 1, failCount: 2, wantErr: true, wantCalls: 2},
		{name: "retry disabled runs once", repoRetry: 0, failCount: 1, wantErr: true, wantCalls: 1},
		{name: "acr managed retries", repoRetry: 3, failCount: 2, wantErr: false, wantCalls: 3, managed: "acr"},
	})
}

func Test_UpdateRepo_Retry(t *testing.T) {
	runRetryCases(t, func(t *testing.T, helm *execer, _ *mockRunner, tt retryTestCase) {
		err := helm.UpdateRepo()
		if (err != nil) != tt.wantErr {
			t.Errorf("UpdateRepo() error = %v, wantErr %v", err, tt.wantErr)
		}
	}, []retryTestCase{
		{name: "succeeds after one retry", repoRetry: 2, failCount: 1, wantErr: false, wantCalls: 2},
		{name: "retries exhausted", repoRetry: 1, failCount: 2, wantErr: true, wantCalls: 2},
		{name: "retry disabled runs once", repoRetry: 0, failCount: 1, wantErr: true, wantCalls: 1},
	})
}

func Test_RegistryLogin_Retry(t *testing.T) {
	runRetryCases(t, func(t *testing.T, helm *execer, runner *mockRunner, tt retryTestCase) {
		err := helm.RegistryLogin("repo.example.com", "user", "pass", "", "", "", false)
		if (err != nil) != tt.wantErr {
			t.Errorf("RegistryLogin() error = %v, wantErr %v", err, tt.wantErr)
		}
		// Every attempt must carry the full password (regression guard:
		// the password buffer must not be consumed by a prior attempt).
		for i, s := range runner.stdins {
			if s != "pass\n" {
				t.Errorf("RegistryLogin() attempt %d stdin = %q, want %q", i+1, s, "pass\n")
			}
		}
	}, []retryTestCase{
		{name: "succeeds after one retry", repoRetry: 2, failCount: 1, wantErr: false, wantCalls: 2},
		{name: "retries exhausted", repoRetry: 1, failCount: 2, wantErr: true, wantCalls: 2},
		{name: "retry disabled runs once", repoRetry: 0, failCount: 1, wantErr: true, wantCalls: 1},
	})
}

// Test_Retry_LargeCount_NoOverflow verifies that a large --repo-retries value
// doesn't overflow the backoff duration (the shift exponent must be capped).
func Test_Retry_LargeCount_NoOverflow(t *testing.T) {
	withTestBackoff(t)
	// failCount > repoRetry so every attempt fails; attempt indices exceed the
	// shift cap of 5, exercising the overflow guard without panicking.
	const retries = 10
	helm, runner := newRetryExecer(t, retries, retries+1)
	err := helm.UpdateRepo()
	if err == nil {
		t.Fatal("UpdateRepo() expected error, got nil")
	}
	if runner.calls != retries+1 { // 1 initial + retries
		t.Errorf("runner.calls = %d, want %d", runner.calls, retries+1)
	}
}

// Test_SleepCtx_Canceled verifies sleepCtx returns promptly (and false) when
// the runner's context is already canceled, rather than blocking for the full
// duration.
func Test_SleepCtx_Canceled(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	cancel() // cancel immediately
	helm := &execer{
		runner: &ShellRunner{Ctx: ctx},
		logger: NewLogger(io.Discard, "debug"),
	}
	start := time.Now()
	completed := helm.sleepCtx(30 * time.Second)
	elapsed := time.Since(start)
	if completed {
		t.Error("sleepCtx() = true with a canceled context; want false")
	}
	if elapsed > time.Second {
		t.Errorf("sleepCtx blocked for %v with a canceled context; expected prompt return", elapsed)
	}
}

// Test_SleepCtx_NoContext verifies sleepCtx falls back to time.Sleep (returning
// true) when the runner has no context (e.g. mockRunner).
func Test_SleepCtx_NoContext(t *testing.T) {
	helm := &execer{
		runner: &mockRunner{},
		logger: NewLogger(io.Discard, "debug"),
	}
	start := time.Now()
	completed := helm.sleepCtx(5 * time.Millisecond)
	elapsed := time.Since(start)
	if !completed {
		t.Error("sleepCtx() = false without a context; want true")
	}
	if elapsed < 4*time.Millisecond {
		t.Errorf("sleepCtx returned in %v without a context; expected to sleep ~5ms", elapsed)
	}
}

// Test_Retry_AbortsOnCanceledContext verifies that retryRepoOp stops retrying
// once the runner's context is canceled, rather than spinning into a tight
// loop of rapid helm invocations.
func Test_Retry_AbortsOnCanceledContext(t *testing.T) {
	withTestBackoff(t)
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	runner := &mockRunner{failCount: 100} // always fails
	helm := &execer{
		options: HelmExecOptions{RepoRetry: 100},
		version: semver.MustParse("v4.0.1"),
		runner:  &ShellRunner{Ctx: ctx},
		logger:  NewLogger(io.Discard, "debug"),
	}
	// Cancel deterministically inside the op after the first attempt runs, so
	// the retry loop must bail out at sleepCtx — no timing-based flakiness.
	calls := 0
	_, err := helm.retryRepoOp("test", func() ([]byte, error) {
		calls++
		out, e := runner.Execute("helm", []string{"repo", "update"}, nil, false)
		if calls == 1 {
			cancel()
		}
		return out, e
	})
	if err == nil {
		t.Fatal("expected error, got nil")
	}
	// The context is canceled during the first attempt; sleepCtx must observe
	// it and abort, so only the first attempt should run.
	if calls > 2 {
		t.Errorf("calls = %d after cancellation; expected <= 2 (no tight loop)", calls)
	}
}

func Test_SyncRelease(t *testing.T) {
	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm, err := MockExecer(logger, "config", "dev")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	err = helm.SyncRelease(HelmContext{}, "release", "chart", "default", "--timeout 10", "--wait", "--wait-for-jobs")
	expected := `Upgrading release=release, chart=chart, namespace=default
exec: helm --kubeconfig config --kube-context dev upgrade --install release chart --timeout 10 --wait --wait-for-jobs --history-max 0
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	buffer.Reset()
	err = helm.SyncRelease(HelmContext{}, "release", "chart", "default")
	expected = `Upgrading release=release, chart=chart, namespace=default
exec: helm --kubeconfig config --kube-context dev upgrade --install release chart --history-max 0
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	buffer.Reset()
	err = helm.SyncRelease(HelmContext{}, "release", "https://example_user:example_password@repo.example.com/chart.tgz", "default")
	expected = `Upgrading release=release, chart=https://example_user:xxxxx@repo.example.com/chart.tgz, namespace=default
exec: helm --kubeconfig config --kube-context dev upgrade --install release https://example_user:example_password@repo.example.com/chart.tgz --history-max 0
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}
}

func Test_UpdateDeps(t *testing.T) {
	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm, err := MockExecer(logger, "config", "dev")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	err = helm.UpdateDeps("./chart/foo")
	expected := `Updating dependency ./chart/foo
exec: helm --kubeconfig config --kube-context dev dependency update ./chart/foo
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.UpdateDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	buffer.Reset()
	helm.SetExtraArgs("--verify")
	err = helm.UpdateDeps("./chart/foo")
	// --verify is a dependency-specific flag and should be preserved
	expected = `Updating dependency ./chart/foo
exec: helm --kubeconfig config --kube-context dev dependency update ./chart/foo --verify
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.UpdateDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}
}

func Test_BuildDeps(t *testing.T) {
	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm3Runner := mockRunner{output: []byte("v3.2.4+ge29ce2a")}
	helm, err := New("helm", HelmExecOptions{}, logger, "config", "dev", &helm3Runner)
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	err = helm.BuildDeps("foo", "./chart/foo", []string{"--skip-refresh"}...)
	expected := `Building dependency release=foo, chart=./chart/foo
exec: helm --kubeconfig config --kube-context dev dependency build ./chart/foo --skip-refresh
v3.2.4+ge29ce2a
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	buffer.Reset()
	err = helm.BuildDeps("foo", "./chart/foo")
	expected = `Building dependency release=foo, chart=./chart/foo
exec: helm --kubeconfig config --kube-context dev dependency build ./chart/foo
v3.2.4+ge29ce2a
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	buffer.Reset()
	helm.SetExtraArgs("--verify")
	err = helm.BuildDeps("foo", "./chart/foo", []string{"--skip-refresh"}...)
	// --verify is a dependency-specific flag and should be preserved
	expected = `Building dependency release=foo, chart=./chart/foo
exec: helm --kubeconfig config --kube-context dev dependency build ./chart/foo --skip-refresh --verify
v3.2.4+ge29ce2a
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	buffer.Reset()
	helm2Runner := mockRunner{output: []byte("Client: v2.16.1+ge13bc94")}
	helm, err = New("helm", HelmExecOptions{}, logger, "config", "dev", &helm2Runner)
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	err = helm.BuildDeps("foo", "./chart/foo")
	expected = `Building dependency release=foo, chart=./chart/foo
exec: helm --kubeconfig config --kube-context dev dependency build ./chart/foo
Client: v2.16.1+ge13bc94
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	// Test that --dry-run flag is filtered out (not supported by helm dependency build)
	buffer.Reset()
	helm3Runner = mockRunner{output: []byte("v3.2.4+ge29ce2a")}
	helm, err = New("helm", HelmExecOptions{}, logger, "config", "dev", &helm3Runner)
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	helm.SetExtraArgs("--dry-run=server")
	err = helm.BuildDeps("foo", "./chart/foo")
	expected = `Building dependency release=foo, chart=./chart/foo
exec: helm --kubeconfig config --kube-context dev dependency build ./chart/foo
v3.2.4+ge29ce2a
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.BuildDeps() with --dry-run should filter it out\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	// Test that global flags (--debug) and dependency flags (--verify) are preserved,
	// while template-specific flags (--dry-run) are filtered out
	buffer.Reset()
	helm3Runner = mockRunner{output: []byte("v3.2.4+ge29ce2a")}
	helm, err = New("helm", HelmExecOptions{}, logger, "config", "dev", &helm3Runner)
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	helm.SetExtraArgs("--debug", "--dry-run=server", "--verify", "--wait")
	err = helm.BuildDeps("foo", "./chart/foo")
	expected = `Building dependency release=foo, chart=./chart/foo
exec: helm --kubeconfig config --kube-context dev dependency build ./chart/foo --debug --verify
v3.2.4+ge29ce2a
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.BuildDeps() should preserve global and dependency flags\nactual = %v\nexpect = %v", buffer.String(), expected)
	}
}

func Test_DecryptSecret(t *testing.T) {
	// Set secrets plugin version to 4.7.4
	if err := os.Setenv("HELM_PLUGINS", "../../test/plugins/secrets/4.7.4"); err != nil {
		t.Errorf("failed to set environment HELM_PLUGINS error: %s", err)
	}
	defer func() {
		if err := os.Unsetenv("HELM_PLUGINS"); err != nil {
			t.Errorf("failed to unset environment HELM_PLUGINS error: %s", err)
		}
	}()
	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm, err := MockExecer(logger, "config", "dev")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}

	tmpFilePath := "path/to/temp/file"
	helm.writeTempFile = func(content []byte) (string, error) {
		return tmpFilePath, nil
	}

	_, err = helm.DecryptSecret(HelmContext{}, "secretName")
	if err != nil {
		t.Errorf("Error: %v", err)
	}
	cwd, err := filepath.Abs(".")
	if err != nil {
		t.Errorf("Error: %v", err)
	}
	// Run again for caching
	_, err = helm.DecryptSecret(HelmContext{}, "secretName")

	expected := fmt.Sprintf(`Preparing to decrypt secret %v/secretName
Decrypting secret %s/secretName
exec: helm --kubeconfig config --kube-context dev secrets decrypt %s/secretName
Decrypted %s/secretName into %s
Preparing to decrypt secret %s/secretName
Found secret in cache %s/secretName
Decrypted %s/secretName into %s
`, cwd, cwd, cwd, cwd, tmpFilePath, cwd, cwd, cwd, tmpFilePath)
	if err != nil {
		if _, ok := err.(*os.PathError); ok {
		} else {
			t.Errorf("Error: %v", err)
		}
	}
	if d := cmp.Diff(expected, buffer.String()); d != "" {
		t.Errorf("helmexec.DecryptSecret(): want (-), got (+):\n%s", d)
	}
}

func Test_DecryptSecretWithGotmpl(t *testing.T) {
	// Set secrets plugin version to 4.7.4
	if err := os.Setenv("HELM_PLUGINS", "../../test/plugins/secrets/4.7.4"); err != nil {
		t.Errorf("failed to set environment HELM_PLUGINS error: %s", err)
	}
	defer func() {
		if err := os.Unsetenv("HELM_PLUGINS"); err != nil {
			t.Errorf("failed to unset environment HELM_PLUGINS error: %s", err)
		}
	}()
	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm, err := MockExecer(logger, "config", "dev")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}

	tmpFilePath := "path/to/temp/file"
	helm.writeTempFile = func(content []byte) (string, error) {
		return tmpFilePath, nil
	}

	secretName := "secretName.yaml.gotmpl"
	_, err = helm.DecryptSecret(HelmContext{}, secretName)
	if err != nil {
		t.Errorf("Error: %v", err)
	}
	cwd, err := filepath.Abs(".")
	if err != nil {
		t.Errorf("Error: %v", err)
	}

	expected := fmt.Sprintf(`Preparing to decrypt secret %v/secretName.yaml.gotmpl
Decrypting secret %s/secretName.yaml.gotmpl
exec: helm --kubeconfig config --kube-context dev secrets decrypt %s/secretName.yaml.gotmpl
Decrypted %s/secretName.yaml.gotmpl into %s
`, cwd, cwd, cwd, cwd, tmpFilePath)
	if err != nil {
		t.Errorf("Error: %v", err)
	}
	if d := cmp.Diff(expected, buffer.String()); d != "" {
		t.Errorf("helmexec.DecryptSecret(): want (-), got (+):\n%s", d)
	}
}

func Test_DiffRelease(t *testing.T) {
	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm, err := MockExecer(logger, "config", "dev")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	err = helm.DiffRelease(HelmContext{}, "release", "chart", "default", false, "--timeout 10", "--wait", "--wait-for-jobs")
	expected := `Comparing release=release, chart=chart, namespace=default

exec: helm --kubeconfig config --kube-context dev diff upgrade --allow-unreleased release chart --timeout 10 --wait --wait-for-jobs
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	buffer.Reset()
	err = helm.DiffRelease(HelmContext{}, "release", "chart", "default", false)
	expected = `Comparing release=release, chart=chart, namespace=default

exec: helm --kubeconfig config --kube-context dev diff upgrade --allow-unreleased release chart
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	buffer.Reset()
	err = helm.DiffRelease(HelmContext{}, "release", "https://example_user:example_password@repo.example.com/chart.tgz", "default", false)
	expected = `Comparing release=release, chart=https://example_user:xxxxx@repo.example.com/chart.tgz, namespace=default

exec: helm --kubeconfig config --kube-context dev diff upgrade --allow-unreleased release https://example_user:example_password@repo.example.com/chart.tgz
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}
}

func Test_DiffRelease_ColorFlagHelm4(t *testing.T) {
	// Test that --color and --no-color flags are removed and HELM_DIFF_COLOR env var is set for Helm 4
	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")

	// MockExecer creates a Helm 4 execer by default (returns v4.0.1)
	helm, err := MockExecer(logger, "config", "dev")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}

	// Verify it's Helm 4
	if !helm.IsHelm4() {
		t.Errorf("expected Helm 4, got version: %v", helm.GetVersion())
	}

	// Test with --color flag
	buffer.Reset()
	err = helm.DiffRelease(HelmContext{}, "release", "chart", "default", false, "--color", "--context", "3")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}

	// The --color flag should be removed and --context should remain
	expected := `Comparing release=release, chart=chart, namespace=default

exec: helm --kubeconfig config --kube-context dev diff upgrade --allow-unreleased release chart --context 3
`
	actual := buffer.String()
	if actual != expected {
		t.Errorf("helmexec.DiffRelease() with --color\nactual = %v\nexpect = %v", actual, expected)
	}

	// Verify --color flag was removed
	if strings.Contains(actual, "--color") {
		t.Errorf("--color flag should have been removed, but got: %v", actual)
	}

	// Test with --no-color flag
	buffer.Reset()
	err = helm.DiffRelease(HelmContext{}, "release", "chart", "default", false, "--no-color", "--context", "3")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}

	// The --no-color flag should be removed and --context should remain
	expected = `Comparing release=release, chart=chart, namespace=default

exec: helm --kubeconfig config --kube-context dev diff upgrade --allow-unreleased release chart --context 3
`
	actual = buffer.String()
	if actual != expected {
		t.Errorf("helmexec.DiffRelease() with --no-color\nactual = %v\nexpect = %v", actual, expected)
	}

	// Verify --no-color flag was removed
	if strings.Contains(actual, "--no-color") {
		t.Errorf("--no-color flag should have been removed, but got: %v", actual)
	}
}

func Test_FilterColorFlagsForHelm4(t *testing.T) {
	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm, err := MockExecer(logger, "config", "dev")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	tests := []struct {
		name           string
		inputFlags     []string
		expectedFlags  []string
		expectedEnvKey string
		expectedEnvVal string
	}{
		{
			name:           "color flag",
			inputFlags:     []string{"--color", "--context", "3"},
			expectedFlags:  []string{"--context", "3"},
			expectedEnvKey: "HELM_DIFF_COLOR",
			expectedEnvVal: "true",
		},
		{
			name:           "no-color flag",
			inputFlags:     []string{"--no-color", "--context", "3"},
			expectedFlags:  []string{"--context", "3"},
			expectedEnvKey: "HELM_DIFF_COLOR",
			expectedEnvVal: "false",
		},
		{
			name:           "no color flags",
			inputFlags:     []string{"--context", "3", "--detailed-exitcode"},
			expectedFlags:  []string{"--context", "3", "--detailed-exitcode"},
			expectedEnvKey: "",
			expectedEnvVal: "",
		},
		{
			name:           "color flag with other flags",
			inputFlags:     []string{"--detailed-exitcode", "--color", "--suppress", "secret"},
			expectedFlags:  []string{"--detailed-exitcode", "--suppress", "secret"},
			expectedEnvKey: "HELM_DIFF_COLOR",
			expectedEnvVal: "true",
		},
		{
			name:           "both color and no-color flags (first wins with defensive check)",
			inputFlags:     []string{"--color", "--no-color", "--context", "3"},
			expectedFlags:  []string{"--context", "3"},
			expectedEnvKey: "HELM_DIFF_COLOR",
			expectedEnvVal: "true", // Changed: first flag wins due to defensive check
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			env := make(map[string]string)
			actualFlags := helm.filterColorFlagsForHelm4(tt.inputFlags, env)

			if !reflect.DeepEqual(actualFlags, tt.expectedFlags) {
				t.Errorf("filterColorFlagsForHelm4() flags\nactual = %v\nexpect = %v", actualFlags, tt.expectedFlags)
			}

			if tt.expectedEnvKey != "" {
				if env[tt.expectedEnvKey] != tt.expectedEnvVal {
					t.Errorf("filterColorFlagsForHelm4() env[%v]\nactual = %v\nexpect = %v",
						tt.expectedEnvKey, env[tt.expectedEnvKey], tt.expectedEnvVal)
				}
			} else if len(env) > 0 {
				t.Errorf("filterColorFlagsForHelm4() expected no env vars, but got: %v", env)
			}
		})
	}
}

func Test_DeleteRelease(t *testing.T) {
	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm, err := MockExecer(logger, "config", "dev")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	err = helm.DeleteRelease(HelmContext{}, "release")
	expected := `Deleting release
exec: helm --kubeconfig config --kube-context dev delete release
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.DeleteRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}
}
func Test_DeleteRelease_Flags(t *testing.T) {
	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm, err := MockExecer(logger, "config", "dev")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	err = helm.DeleteRelease(HelmContext{}, "release", "--purge")
	expected := `Deleting release
exec: helm --kubeconfig config --kube-context dev delete release --purge
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.DeleteRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}
}

func Test_TestRelease(t *testing.T) {
	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm, err := MockExecer(logger, "config", "dev")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	err = helm.TestRelease(HelmContext{}, "release")
	expected := `Testing release
exec: helm --kubeconfig config --kube-context dev test release
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.TestRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}
}
func Test_TestRelease_Flags(t *testing.T) {
	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm, err := MockExecer(logger, "config", "dev")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	err = helm.TestRelease(HelmContext{}, "release", "--cleanup", "--timeout", "60")
	expected := `Testing release
exec: helm --kubeconfig config --kube-context dev test release --cleanup --timeout 60
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.TestRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}
}

func Test_ReleaseStatus(t *testing.T) {
	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm, err := MockExecer(logger, "config", "dev")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	err = helm.ReleaseStatus(HelmContext{}, "myRelease")
	expected := `Getting status myRelease
exec: helm --kubeconfig config --kube-context dev status myRelease
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.ReleaseStatus()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}
}

func Test_exec(t *testing.T) {
	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm, err := MockExecer(logger, "", "")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	env := map[string]string{}
	_, err = helm.exec([]string{"version"}, env, nil)
	expected := `exec: helm version
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	helm, err = MockExecer(logger, "config", "dev")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	ret, _ := helm.exec([]string{"diff"}, env, nil)
	if len(ret) != 0 {
		t.Error("helmexec.exec() - expected empty return value")
	}

	buffer.Reset()
	helm, err = MockExecer(logger, "config", "dev")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	_, err = helm.exec([]string{"diff", "release", "chart", "--timeout 10", "--wait", "--wait-for-jobs"}, env, nil)
	expected = `exec: helm --kubeconfig config --kube-context dev diff release chart --timeout 10 --wait --wait-for-jobs
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	buffer.Reset()
	_, err = helm.exec([]string{"version"}, env, nil)
	expected = `exec: helm --kubeconfig config --kube-context dev version
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	buffer.Reset()
	helm.SetExtraArgs("foo")
	_, err = helm.exec([]string{"version"}, env, nil)
	expected = `exec: helm --kubeconfig config --kube-context dev version foo
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	buffer.Reset()
	helm, err = MockExecer(logger, "", "")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	helm.SetHelmBinary("overwritten")
	_, err = helm.exec([]string{"version"}, env, nil)
	expected = `exec: overwritten version
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}
}

func Test_Lint(t *testing.T) {
	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm, err := MockExecer(logger, "config", "dev")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	err = helm.Lint("release", "path/to/chart", "--values", "file.yml")
	expected := `Linting release=release, chart=path/to/chart
exec: helm --kubeconfig config --kube-context dev lint path/to/chart --values file.yml
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.Lint()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}
}

func Test_Fetch(t *testing.T) {
	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm, err := MockExecer(logger, "config", "dev")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	err = helm.Fetch("chart", "--version", "1.2.3", "--untar", "--untardir", "/tmp/dir")
	expected := `Fetching chart
exec: helm --kubeconfig config --kube-context dev fetch chart --version 1.2.3 --untar --untardir /tmp/dir
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.Fetch()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	buffer.Reset()
	err = helm.Fetch("https://example_user:example_password@repo.example.com/chart.tgz", "--version", "1.2.3", "--untar", "--untardir", "/tmp/dir")
	expected = `Fetching https://example_user:xxxxx@repo.example.com/chart.tgz
exec: helm --kubeconfig config --kube-context dev fetch https://example_user:example_password@repo.example.com/chart.tgz --version 1.2.3 --untar --untardir /tmp/dir
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.Fetch()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}
}

func Test_ChartPull(t *testing.T) {
	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	tests := []struct {
		name        string
		helmBin     string
		helmVersion string
		chartName   string
		chartPath   string
		chartFlags  []string
		listResult  string
	}{
		{
			name:        "less then v3.7.0",
			helmBin:     "helm",
			helmVersion: "v3.6.0",
			chartName:   "chart",
			chartPath:   "path1",
			chartFlags:  []string{"--untar", "--untardir", "/tmp/dir"},
			listResult: `Pulling chart
exec: helm --kubeconfig config --kube-context dev chart pull chart
`,
		},
		{
			name:        "more then v3.7.0",
			helmBin:     "helm",
			helmVersion: "v3.10.0",
			chartName:   "repo/helm-charts:0.14.0",
			chartPath:   "path1",
			chartFlags:  []string{"--untardir", "/tmp/dir", "--version", "0.14.0"},
			listResult: `Pulling repo/helm-charts:0.14.0
exec: helm --kubeconfig config --kube-context dev pull oci://repo/helm-charts --destination path1 --untar --untardir /tmp/dir --version 0.14.0
`,
		},
		{
			name:        "more then v3.7.0 with rc",
			helmBin:     "helm",
			helmVersion: "v3.14.0-rc.1+g69dcc92",
			chartName:   "repo/helm-charts",
			chartPath:   "path1",
			chartFlags:  []string{"--untardir", "/tmp/dir", "--devel"},
			listResult: `Pulling repo/helm-charts
exec: helm --kubeconfig config --kube-context dev pull oci://repo/helm-charts --destination path1 --untar --untardir /tmp/dir --devel
`,
		},
	}
	for i := range tests {
		tt := tests[i]
		t.Run(tt.name, func(t *testing.T) {
			buffer.Reset()
			helm, err := New(tt.helmBin, HelmExecOptions{}, logger, "config", "dev", &mockRunner{output: []byte(tt.helmVersion)})
			if err != nil {
				t.Errorf("unexpected error: %v", err)
			}
			err = helm.ChartPull(tt.chartName, tt.chartPath, tt.chartFlags...)
			if err != nil {
				t.Errorf("unexpected error: %v", err)
			}
			isMatch, _ := regexp.MatchString(tt.listResult, buffer.String())
			if !isMatch {
				t.Errorf("helmexec.ChartPull()\nactual = %v\nexpect = %v", buffer.String(), tt.listResult)
			}
		})
	}
}

func Test_ChartExport(t *testing.T) {
	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	tests := []struct {
		name          string
		helmBin       string
		helmVersion   string
		chartName     string
		chartPath     string
		listResult    string
		expectedError string
	}{
		{
			name:        "",
			helmBin:     "helm",
			helmVersion: "v3.6.0",
			chartName:   "chart",
			chartPath:   "path1",
			listResult: `Exporting chart
exec: helm --kubeconfig config --kube-context dev chart export chart --destination path1
`,
			expectedError: "",
		},
	}
	for i := range tests {
		tt := tests[i]
		t.Run(tt.name, func(t *testing.T) {
			buffer.Reset()
			helm := &execer{
				helmBinary:  tt.helmBin,
				version:     semver.MustParse(tt.helmVersion),
				logger:      logger,
				kubeconfig:  "config",
				kubeContext: "dev",
				runner:      &mockRunner{},
			}
			err := helm.ChartExport(tt.chartName, tt.chartPath)
			if err != nil {
				t.Errorf("unexpected error: %v", err)
			}
			if buffer.String() != tt.listResult {
				t.Errorf("helmexec.ChartExport()\nactual = %v\nexpect = %v", buffer.String(), tt.listResult)
			}
		})
	}
}

var logLevelTests = map[string]string{
	"debug": `Adding repo myRepo https://repo.example.com/
exec: helm repo add myRepo https://repo.example.com/ --force-update --username example_user --password-stdin
`,
	"info": `Adding repo myRepo https://repo.example.com/
`,
	"warn": ``,
}

func Test_LogLevels(t *testing.T) {
	var buffer bytes.Buffer
	for logLevel, expected := range logLevelTests {
		buffer.Reset()
		logger := NewLogger(&buffer, logLevel)
		helm, err := MockExecer(logger, "", "")
		if err != nil {
			t.Errorf("unexpected error: %v", err)
		}
		err = helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "example_user", "example_password", "", false, false)
		if err != nil {
			t.Errorf("unexpected error: %v", err)
		}

		actual := buffer.String()

		if strings.Contains(actual, "--password-stdin") {
			expected = strings.Replace(expected, "--password example_password", "--password-stdin", 1)
		}

		if actual != expected {
			t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", actual, expected)
		}
	}
}

func Test_mergeEnv(t *testing.T) {
	actual := env2map(mergeEnv([]string{"A=1", "B=c=d", "E=2"}, map[string]string{"B": "3", "F": "4"}))
	expected := map[string]string{"A": "1", "B": "3", "E": "2", "F": "4"}
	if !reflect.DeepEqual(actual, expected) {
		t.Errorf("mergeEnv()\nactual = %v\nexpect = %v", actual, expected)
	}
}

func Test_Template(t *testing.T) {
	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm, err := MockExecer(logger, "config", "dev")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	err = helm.TemplateRelease("release", "path/to/chart", "--values", "file.yml")
	expected := `Templating release=release, chart=path/to/chart
exec: helm --kubeconfig config --kube-context dev template release path/to/chart --values file.yml
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.Template()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}

	buffer.Reset()
	err = helm.TemplateRelease("release", "https://example_user:example_password@repo.example.com/chart.tgz", "--values", "file.yml")
	expected = `Templating release=release, chart=https://example_user:xxxxx@repo.example.com/chart.tgz
exec: helm --kubeconfig config --kube-context dev template release https://example_user:example_password@repo.example.com/chart.tgz --values file.yml
`
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if buffer.String() != expected {
		t.Errorf("helmexec.Template()\nactual = %v\nexpect = %v", buffer.String(), expected)
	}
}

func Test_Template_PostRendererWithOutputDir(t *testing.T) {
	tests := []struct {
		name             string
		postRendererFlag string
	}{
		{"separate flags", "--post-renderer"},
		{"combined flag", "--post-renderer=/bin/echo"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tmpDir := t.TempDir()
			var buffer bytes.Buffer
			logger := NewLogger(&buffer, "debug")

			// Use Helm 3 version for the version probe so the Helm 3 workaround is applied.
			// The workaround is not needed for Helm 4, which natively applies --post-renderer to --output-dir output.
			runner := &mockRunner{versionOutput: []byte("v3.20.0")}
			helm, err := New("helm", HelmExecOptions{}, logger, "config", "dev", runner)
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}

			runner.output = []byte("apiVersion: v1\nkind: Namespace\n")

			var flags []string
			if tt.postRendererFlag == "--post-renderer" {
				flags = []string{"--post-renderer", "/bin/echo", "--output-dir", tmpDir, "--values", "file.yml"}
			} else {
				flags = []string{tt.postRendererFlag, "--output-dir", tmpDir, "--values", "file.yml"}
			}

			err = helm.TemplateRelease("myrelease", "path/to/chart", flags...)
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}

			outputPath := filepath.Join(tmpDir, "templates", "myrelease.yaml")
			data, err := os.ReadFile(outputPath)
			if err != nil {
				t.Fatalf("expected output file %s to exist: %v", outputPath, err)
			}

			expected := "apiVersion: v1\nkind: Namespace\n\n"
			if string(data) != expected {
				t.Errorf("output file content:\nactual=%q\nexpect=%q", string(data), expected)
			}

			outputLog := buffer.String()
			if strings.Contains(outputLog, "--output-dir") {
				t.Errorf("helm should NOT have been called with --output-dir, got: %s", outputLog)
			}
			if !strings.Contains(outputLog, "--post-renderer") {
				t.Errorf("helm should have been called with --post-renderer, got: %s", outputLog)
			}
		})
	}
}

func Test_IsHelm3(t *testing.T) {
	helm3Runner := mockRunner{output: []byte("v3.0.0+ge29ce2a\n")}
	helm, err := New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm3Runner)
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if !helm.IsHelm3() {
		t.Error("helmexec.IsHelm3() - Failed to detect Helm 3")
	}

	helm4Runner := mockRunner{output: []byte("v4.0.1+g12500dd\n")}
	helm, err = New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm4Runner)
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if helm.IsHelm3() {
		t.Error("helmexec.IsHelm3() - Detected Helm 3 with Helm 4 version")
	}
}

func Test_IsHelm4(t *testing.T) {
	helm3Runner := mockRunner{output: []byte("v3.0.0+ge29ce2a\n")}
	helm, err := New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm3Runner)
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if helm.IsHelm4() {
		t.Error("helmexec.IsHelm4() - Detected Helm 4 with Helm 3 version")
	}

	helm4Runner := mockRunner{output: []byte("v4.0.1+g12500dd\n")}
	helm, err = New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm4Runner)
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if !helm.IsHelm4() {
		t.Error("helmexec.IsHelm4() - Failed to detect Helm 4")
	}
}

func Test_GetPluginVersion(t *testing.T) {
	v3ExpectedVersion := "3.15.0"
	v4ExpectedVersion := "4.7.4"
	v3PluginDirPath := "../../test/plugins/secrets/3.15.0"
	v4PluginDirPath := "../../test/plugins/secrets/4.7.4"

	v3SecretPluginVersion, err := GetPluginVersion("secrets", v3PluginDirPath)
	require.NoError(t, err)

	if v3SecretPluginVersion.String() != v3ExpectedVersion {
		t.Errorf("secrets v3 plugin version is %v, expected %v", v3SecretPluginVersion.String(), v3ExpectedVersion)
	}

	v4SecretPluginVersion, err := GetPluginVersion("secrets", v4PluginDirPath)
	require.NoError(t, err)
	if v4SecretPluginVersion.String() != v4ExpectedVersion {
		t.Errorf("secrets v4 plugin version is %v, expected %v", v4SecretPluginVersion.String(), v4ExpectedVersion)
	}
}

func Test_GetPluginVersion_XDGPaths(t *testing.T) {
	v3ExpectedVersion := "3.15.0"
	v4ExpectedVersion := "4.7.4"
	v3PluginDirPath := "../../test/plugins/secrets/3.15.0"
	v4PluginDirPath := "../../test/plugins/secrets/4.7.4"

	sep := string(os.PathListSeparator)
	xdgPaths := "nonexistent/path" + sep + v3PluginDirPath + sep + "another/nonexistent"

	pluginVersion, err := GetPluginVersion("secrets", xdgPaths)
	require.NoError(t, err)
	assert.Equal(t, v3ExpectedVersion, pluginVersion.String())

	xdgPathsV4 := v4PluginDirPath + sep + "nonexistent/path"
	pluginVersion, err = GetPluginVersion("secrets", xdgPathsV4)
	require.NoError(t, err)
	assert.Equal(t, v4ExpectedVersion, pluginVersion.String())

	_, err = GetPluginVersion("nonexistent-plugin", xdgPaths)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "plugin nonexistent-plugin not installed")
}

func Test_GetPluginVersion_Symlink(t *testing.T) {
	// Simulate a nix/devbox environment where the plugin directory is a symlink
	tmpDir := t.TempDir()

	// Create a real plugin directory with plugin.yaml
	realPluginDir := filepath.Join(tmpDir, "real", "helm-diff")
	require.NoError(t, os.MkdirAll(realPluginDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(realPluginDir, "plugin.yaml"), []byte(`name: "diff"
version: "3.12.0"
`), 0o644))

	// Create a plugins directory where helm-diff is a symlink (like nix/devbox)
	symlinkPluginsDir := filepath.Join(tmpDir, "plugins")
	require.NoError(t, os.MkdirAll(symlinkPluginsDir, 0o755))
	require.NoError(t, os.Symlink(realPluginDir, filepath.Join(symlinkPluginsDir, "helm-diff")))

	version, err := GetPluginVersion("diff", symlinkPluginsDir)
	require.NoError(t, err)
	assert.Equal(t, "3.12.0", version.String())
}

func Test_GetVersion(t *testing.T) {
	helm2Runner := mockRunner{output: []byte("Client: v2.16.1+ge13bc94\n")}
	helm, err := New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm2Runner)
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	ver := helm.GetVersion()
	if ver.Major != 2 || ver.Minor != 16 || ver.Patch != 1 {
		t.Errorf("helmexec.GetVersion - did not detect correct Helm2 version; it was: %+v", ver)
	}

	helm3Runner := mockRunner{output: []byte("v3.2.4+ge29ce2a\n")}
	helm, err = New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm3Runner)
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	ver = helm.GetVersion()
	if ver.Major != 3 || ver.Minor != 2 || ver.Patch != 4 {
		t.Errorf("helmexec.GetVersion - did not detect correct Helm3 version; it was: %+v", ver)
	}
}

func Test_IsVersionAtLeast(t *testing.T) {
	helm2Runner := mockRunner{output: []byte("Client: v2.16.1+ge13bc94\n")}
	helm, err := New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm2Runner)
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if !helm.IsVersionAtLeast("2.1.0") {
		t.Error("helmexec.IsVersionAtLeast - 2.16.1 not atleast 2.1")
	}

	if helm.IsVersionAtLeast("2.19.0") {
		t.Error("helmexec.IsVersionAtLeast - 2.16.1 is atleast 2.19")
	}

	if helm.IsVersionAtLeast("3.2.0") {
		t.Error("helmexec.IsVersionAtLeast - 2.16.1 is atleast 3.2")
	}
}

func Test_resolveOciChart(t *testing.T) {
	tests := []struct {
		name        string
		chartPath   string
		ociChartURL string
		ociChartTag string
	}{
		{
			name:        "normal",
			chartPath:   "chart/nginx:v1",
			ociChartURL: "oci://chart/nginx",
			ociChartTag: "v1",
		},
		{
			name:        "contains the port",
			chartPath:   "chart:5000/nginx:v1",
			ociChartURL: "oci://chart:5000/nginx",
			ociChartTag: "v1",
		},
		{
			name:        "no tag",
			chartPath:   "chart:5000/nginx",
			ociChartURL: "oci://chart:5000/nginx",
			ociChartTag: "",
		},
		{
			name:        "digest only",
			chartPath:   "ghcr.io/nginxinc/charts/nginx-ingress@sha256:87ad282a8e7cc31913ce0543de2933ddb3f3eba80d6e5285f33b62ed720fc085",
			ociChartURL: "oci://ghcr.io/nginxinc/charts/nginx-ingress@sha256:87ad282a8e7cc31913ce0543de2933ddb3f3eba80d6e5285f33b62ed720fc085",
			ociChartTag: "",
		},
		{
			name:        "version and digest",
			chartPath:   "ghcr.io/nginxinc/charts/nginx-ingress:2.0.0@sha256:87ad282a8e7cc31913ce0543de2933ddb3f3eba80d6e5285f33b62ed720fc085",
			ociChartURL: "oci://ghcr.io/nginxinc/charts/nginx-ingress@sha256:87ad282a8e7cc31913ce0543de2933ddb3f3eba80d6e5285f33b62ed720fc085",
			ociChartTag: "2.0.0",
		},
		{
			name:        "port with digest",
			chartPath:   "registry:5000/chart@sha256:abc123",
			ociChartURL: "oci://registry:5000/chart@sha256:abc123",
			ociChartTag: "",
		},
		{
			name:        "port with version and digest",
			chartPath:   "registry:5000/chart:1.0.0@sha256:abc123",
			ociChartURL: "oci://registry:5000/chart@sha256:abc123",
			ociChartTag: "1.0.0",
		},
	}
	for i := range tests {
		tt := tests[i]
		t.Run(tt.name, func(t *testing.T) {
			url, tag := resolveOciChart(tt.chartPath)
			if tt.ociChartURL != url || tt.ociChartTag != tag {
				actual := fmt.Sprintf("ociChartURL->%s  ociChartTag->%s", url, tag)
				expected := fmt.Sprintf("ociChartURL->%s ociChartTag->%s", tt.ociChartURL, tt.ociChartTag)
				t.Errorf("resolveOciChart()\nactual = %v\nexpect = %v", actual, expected)
			}
		})
	}
}

func Test_ShowChart(t *testing.T) {
	showChartRunner := mockRunner{output: []byte("name: my-chart\nversion: 3.2.0\n")}
	helm := &execer{
		helmBinary:  "helm",
		version:     semver.MustParse("3.3.2"),
		logger:      NewLogger(os.Stdout, "info"),
		kubeContext: "dev",
		runner:      &showChartRunner,
	}

	metadata, err := helm.ShowChart("my-chart")
	if err != nil {
		t.Errorf("helmexec.ShowChart() - unexpected error: %v", err)
	}
	if metadata.Name != "my-chart" {
		t.Errorf("helmexec.ShowChart() - expected chart name was %s, received: %s", "my-chart", metadata.Name)
	}
	if metadata.Version != "3.2.0" {
		t.Errorf("helmexec.ShowChart() - expected chart version was %s, received: %s", "3.2.0", metadata.Version)
	}
}

func Test_SetDisableForceUpdate(t *testing.T) {
	helm, err := MockExecer(NewLogger(os.Stdout, "info"), "config", "dev")
	if err != nil {
		t.Error(err)
	}
	if helm.options.DisableForceUpdate {
		t.Error("helmexec.options.DisableForceUpdate should not be enabled by default")
	}
	helm.SetDisableForceUpdate(true)
	if !helm.options.DisableForceUpdate {
		t.Errorf("helmexec.SetDisableForceUpdate() - actual = %t expect = true", helm.options.DisableForceUpdate)
	}
}

func TestParseHelmVersion(t *testing.T) {
	tests := []struct {
		name    string
		version string
		want    *semver.Version
		wantErr bool
	}{
		{
			name:    "helm 2",
			version: "Client: v2.16.1+ge13bc94\n",
			want:    semver.MustParse("v2.16.1+ge13bc94"),
			wantErr: false,
		},
		{
			name:    "helm 3",
			version: "Client: v3.2.4+ge29ce2a\n",
			want:    semver.MustParse("v3.2.4+ge29ce2a"),
			wantErr: false,
		},
		{
			name:    "helm 3 with os arch and build info",
			version: "Client v3.7.1+7.el8+g8f33223\n",
			want:    semver.MustParse("v3.7.1+7.el8"),
			wantErr: false,
		},
		{
			name:    "empty version",
			version: "",
			want:    nil,
			wantErr: true,
		},
		{
			name:    "invalid version",
			version: "oooooo",
			want:    nil,
			wantErr: true,
		},
		{
			name:    "version without v prefix",
			version: "3.2.4",
			want:    semver.MustParse("v3.2.4"),
			wantErr: false,
		},
		{
			name:    "version without v prefix with build info",
			version: "3.2.4+ge29ce2a",
			want:    semver.MustParse("v3.2.4+ge29ce2a"),
			wantErr: false,
		},
		{
			name:    "version without v prefix with spaces",
			version: "  3.2.4  ",
			want:    semver.MustParse("v3.2.4"),
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := parseHelmVersion(tt.version)
			if (err != nil) != tt.wantErr {
				t.Errorf("parseHelmVersion() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("parseHelmVersion() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_helmSecretsRequiresSplitInstall(t *testing.T) {
	tests := []struct {
		name    string
		version string
		want    bool
	}{
		{name: "below threshold", version: "v4.6.9", want: false},
		{name: "exactly at threshold", version: "v4.7.0", want: true},
		{name: "above threshold single digit minor", version: "v4.8.0", want: true},
		{name: "above threshold double digit minor (v4.10.0+)", version: "v4.10.0", want: true},
		{name: "pre-release below threshold", version: "v4.7.0-beta.1", want: false},
		{name: "invalid version string", version: "not-a-version", want: false},
		{name: "empty string", version: "", want: false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := helmSecretsRequiresSplitInstall(tt.version)
			assert.Equal(t, tt.want, got)
		})
	}
}

type funcRunner struct {
	execute func(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error)
}

func (r *funcRunner) ExecuteStdIn(cmd string, args []string, env map[string]string, stdin io.Reader) ([]byte, error) {
	return r.execute(cmd, args, env, false)
}

func (r *funcRunner) Execute(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
	return r.execute(cmd, args, env, enableLiveOutput)
}

func Test_UpdatePlugin_Helm4SecretsUsesUninstallReinstall(t *testing.T) {
	var calledArgs [][]string
	runner := &funcRunner{
		execute: func(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
			calledArgs = append(calledArgs, append([]string(nil), args...))
			return []byte{}, nil
		},
	}

	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm := &execer{
		helmBinary:  "helm",
		version:     semver.MustParse("4.0.0"),
		logger:      logger,
		kubeconfig:  "config",
		kubeContext: "dev",
		runner:      runner,
	}

	err := helm.UpdatePlugin("secrets", "https://github.com/jkroepke/helm-secrets", "v4.7.0")
	require.NoError(t, err)

	// Verify that "plugin update" was NOT called (the Helm 4 secrets path should skip it).
	for _, args := range calledArgs {
		for i, a := range args {
			if a == pluginCmd && i+1 < len(args) && args[i+1] == "update" {
				t.Errorf("expected 'plugin update' to not be called for Helm 4 secrets, but it was: %v", args)
			}
		}
	}

	// Verify that uninstall was called for all three split plugins.
	checkUninstall := func(name string) {
		for _, args := range calledArgs {
			for i, a := range args {
				if a == pluginCmd && i+2 < len(args) && args[i+1] == "uninstall" && args[i+2] == name {
					return
				}
			}
		}
		t.Errorf("expected 'plugin uninstall %s' to be called", name)
	}
	checkUninstall("secrets")
	checkUninstall("secrets-getter")
	checkUninstall("secrets-post-renderer")

	// Verify that install was called for all three split plugin tarballs.
	checkInstall := func(urlSubstring string) {
		for _, args := range calledArgs {
			for i, a := range args {
				if a == pluginCmd && i+2 < len(args) && args[i+1] == installCmd && strings.Contains(args[i+2], urlSubstring) {
					return
				}
			}
		}
		t.Errorf("expected 'plugin install' for %q to be called", urlSubstring)
	}
	checkInstall("secrets-4.7.0.tgz")
	checkInstall("secrets-getter-4.7.0.tgz")
	checkInstall("secrets-post-renderer-4.7.0.tgz")
}

// Test_UpdatePlugin_GeneralPathUsesUninstallReinstall verifies that for a regular
// plugin, UpdatePlugin does NOT rely on the unreliable `helm plugin update`
// command. Instead it must uninstall the existing plugin and reinstall the exact
// pinned version. See issues #2726 and #2548: `helm plugin update` reports
// success but leaves `helm plugin list` showing the old version because it
// re-installs from the cached source without the --version flag.
func Test_UpdatePlugin_GeneralPathUsesUninstallReinstall(t *testing.T) {
	var calledArgs [][]string
	runner := &funcRunner{
		execute: func(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
			calledArgs = append(calledArgs, append([]string(nil), args...))
			return []byte{}, nil
		},
	}

	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm := &execer{
		helmBinary: "helm",
		version:    semver.MustParse("3.16.4"),
		logger:     logger,
		runner:     runner,
	}

	err := helm.UpdatePlugin("diff", "https://github.com/databus23/helm-diff", "v3.15.10")
	require.NoError(t, err)

	// `plugin update` must never be used: it does not honor --version and silently
	// leaves the old version installed.
	for _, args := range calledArgs {
		for i, a := range args {
			if a == pluginCmd && i+1 < len(args) && args[i+1] == "update" {
				t.Errorf("expected 'plugin update' to not be called, but it was: %v", args)
			}
		}
	}

	// `plugin uninstall diff` must be called to clear the stale version.
	uninstalled := false
	for _, args := range calledArgs {
		for i, a := range args {
			if a == pluginCmd && i+2 < len(args) && args[i+1] == "uninstall" && args[i+2] == "diff" {
				uninstalled = true
			}
		}
	}
	require.True(t, uninstalled, "expected 'plugin uninstall diff' to be called")

	// `plugin install <repo> --version <pinned>` must be called with the exact
	// requested version so the installed plugin is updated to it.
	installed := false
	for _, args := range calledArgs {
		for i, a := range args {
			if a == pluginCmd && i+1 < len(args) && args[i+1] == installCmd {
				hasRepo := false
				hasVersion := false
				for _, arg := range args[i+2:] {
					if arg == "https://github.com/databus23/helm-diff" {
						hasRepo = true
					}
					if arg == "v3.15.10" {
						hasVersion = true
					}
				}
				if hasRepo && hasVersion {
					installed = true
				}
			}
		}
	}
	require.True(t, installed, "expected 'plugin install' with the pinned version to be called")
}

// Test_UpdatePlugin_NotFoundUninstallProceedsToInstall ensures that when the
// plugin is already absent (helm reports its "plugin not found" message),
// UpdatePlugin still proceeds to install the pinned version rather than treating
// it as an error. Both helm major formats are covered.
func Test_UpdatePlugin_NotFoundUninstallProceedsToInstall(t *testing.T) {
	// Helm 4 reports "plugin: <name> not found"; Helm 3 reports "Plugin: <name> not found".
	for _, tc := range []struct {
		name    string
		version string
		msg     string
	}{
		{name: "helm4", version: "4.2.4", msg: "plugin: diff not found"},
		{name: "helm3", version: "3.16.4", msg: "Plugin: diff not found"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			var calledArgs [][]string
			runner := &funcRunner{
				execute: func(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
					calledArgs = append(calledArgs, append([]string(nil), args...))
					if len(args) >= 2 && args[0] == pluginCmd && args[1] == "uninstall" {
						return nil, ExitError{Message: tc.msg, Code: 1}
					}
					return []byte{}, nil
				},
			}

			var buffer bytes.Buffer
			logger := NewLogger(&buffer, "debug")
			helm := &execer{
				helmBinary: "helm",
				version:    semver.MustParse(tc.version),
				logger:     logger,
				runner:     runner,
			}

			err := helm.UpdatePlugin("diff", "https://github.com/databus23/helm-diff", "v3.15.10")
			require.NoError(t, err, "a plugin-absent uninstall error should be ignored and install should proceed")

			// install with the pinned version must still be attempted
			installed := false
			for _, args := range calledArgs {
				for i, a := range args {
					if a == pluginCmd && i+1 < len(args) && args[i+1] == installCmd {
						for _, arg := range args[i+2:] {
							if arg == "v3.15.10" {
								installed = true
							}
						}
					}
				}
			}
			require.True(t, installed, "expected install to proceed after a plugin-absent uninstall")
		})
	}
}

// Test_UpdatePlugin_RealUninstallFailureReturnsError ensures that a genuine
// uninstall failure (permissions, broken Helm, etc.) is surfaced instead of
// being ignored, which would otherwise mask the root cause behind a confusing
// "plugin already exists" error from the subsequent install.
func Test_UpdatePlugin_RealUninstallFailureReturnsError(t *testing.T) {
	installCalled := false
	runner := &funcRunner{
		execute: func(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
			if len(args) >= 2 && args[0] == pluginCmd && args[1] == "uninstall" {
				return nil, ExitError{Message: "permission denied", Code: 1}
			}
			if len(args) >= 2 && args[0] == pluginCmd && args[1] == installCmd {
				installCalled = true
			}
			return []byte{}, nil
		},
	}

	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm := &execer{
		helmBinary: "helm",
		version:    semver.MustParse("3.16.4"),
		logger:     logger,
		runner:     runner,
	}

	err := helm.UpdatePlugin("diff", "https://github.com/databus23/helm-diff", "v3.15.10")
	require.Error(t, err, "a real uninstall failure should be returned")
	assert.Contains(t, err.Error(), "uninstall")
	assert.False(t, installCalled, "install must not be attempted after a real uninstall failure")
}

// Test_UpdatePlugin_ExecutableNotFoundUninstallErrorIsNotSwallowed guards against
// an overly broad "not found" check: when helm itself is missing the runner
// surfaces an error containing "executable file not found", which must NOT be
// mistaken for an absent plugin (otherwise UpdatePlugin would silently log and
// proceed to install, masking the real problem). Only the helm-specific
// "plugin: <name> not found" message is tolerated.
func Test_UpdatePlugin_ExecutableNotFoundUninstallErrorIsNotSwallowed(t *testing.T) {
	installCalled := false
	runner := &funcRunner{
		execute: func(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
			if len(args) >= 2 && args[0] == pluginCmd && args[1] == "uninstall" {
				// Mimics helmfile's ShellRunner when the helm binary is absent.
				return nil, fmt.Errorf("unexpected error: exec: %q: executable file not found in $PATH", cmd)
			}
			if len(args) >= 2 && args[0] == pluginCmd && args[1] == installCmd {
				installCalled = true
			}
			return []byte{}, nil
		},
	}

	var buffer bytes.Buffer
	logger := NewLogger(&buffer, "debug")
	helm := &execer{
		helmBinary: "helm",
		version:    semver.MustParse("3.16.4"),
		logger:     logger,
		runner:     runner,
	}

	err := helm.UpdatePlugin("diff", "https://github.com/databus23/helm-diff", "v3.15.10")
	require.Error(t, err, "a missing-binary uninstall error must be returned, not swallowed")
	assert.Contains(t, err.Error(), "executable file not found")
	assert.False(t, installCalled, "install must not be attempted when the helm binary is missing")
}

func Test_dedupeWroteLines(t *testing.T) {
	cases := []struct {
		name string
		in   string
		want string
	}{
		{
			name: "empty",
			in:   "",
			want: "",
		},
		{
			name: "no wrote lines untouched",
			in:   "Found 25 resources in manifest for release system\n",
			want: "Found 25 resources in manifest for release system\n",
		},
		{
			name: "collapses duplicate wrote lines preserving first-seen order",
			in: "wrote /tmp/a/service_account.yaml\n" +
				"wrote /tmp/a/service_account.yaml\n" +
				"wrote /tmp/a/secrets.yaml\n" +
				"wrote /tmp/a/service_account.yaml\n" +
				"wrote /tmp/a/secrets.yaml\n" +
				"Found 25 resources in manifest for release system",
			want: "wrote /tmp/a/service_account.yaml\n" +
				"wrote /tmp/a/secrets.yaml\n" +
				"Found 25 resources in manifest for release system",
		},
		{
			name: "non-wrote duplicates kept",
			in:   "hello\nhello\n",
			want: "hello\nhello\n",
		},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			got := string(dedupeWroteLines([]byte(c.in)))
			if got != c.want {
				t.Errorf("dedupeWroteLines() = %q, want %q", got, c.want)
			}
		})
	}
}
