package helmcli

import (
	"context"
	"errors"
	"io"
	"testing"

	helmclient "github.com/mittwald/go-helm-client"
	"github.com/stretchr/testify/assert"
	"helm.sh/helm/v3/pkg/action"
	helmfake "helm.sh/helm/v3/pkg/kube/fake"
	"helm.sh/helm/v3/pkg/release"
	"helm.sh/helm/v3/pkg/repo"
	"helm.sh/helm/v3/pkg/storage"
	"helm.sh/helm/v3/pkg/storage/driver"
	"k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/client-go/discovery"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type fakeHelmClient struct {
	helmclient.Client

	addOrUpdateChartRepoFunc    func(repo.Entry) error
	getReleaseFunc              func(string) (*release.Release, error)
	installOrUpgradeFunc        func(context.Context, *helmclient.ChartSpec, *helmclient.GenericHelmOptions) (*release.Release, error)
	uninstallReleaseFunc        func(string) error
	listReleasesByStateMaskFunc func(action.ListStates) ([]*release.Release, error)
}

func (c *fakeHelmClient) AddOrUpdateChartRepo(entry repo.Entry) error {
	return c.addOrUpdateChartRepoFunc(entry)
}

func (c *fakeHelmClient) GetRelease(name string) (*release.Release, error) {
	return c.getReleaseFunc(name)
}

func (c *fakeHelmClient) InstallOrUpgradeChart(ctx context.Context, spec *helmclient.ChartSpec, opts *helmclient.GenericHelmOptions) (*release.Release, error) {
	return c.installOrUpgradeFunc(ctx, spec, opts)
}

func (c *fakeHelmClient) UninstallReleaseByName(name string) error {
	return c.uninstallReleaseFunc(name)
}

func (c *fakeHelmClient) ListReleasesByStateMask(states action.ListStates) ([]*release.Release, error) {
	return c.listReleasesByStateMaskFunc(states)
}

type fakeClientGetter struct {
	helmConfig *action.Configuration
	err        error
}

func (g *fakeClientGetter) GetClient(context.Context) (client.Client, error) {
	return nil, nil
}

func (g *fakeClientGetter) GetHelmConfig(context.Context) (*action.Configuration, error) {
	return g.helmConfig, g.err
}

type fakeRESTClientGetter struct {
	err error
}

func (g *fakeRESTClientGetter) ToRESTConfig() (*rest.Config, error) {
	return nil, g.err
}

func (g *fakeRESTClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
	return nil, nil
}

func (g *fakeRESTClientGetter) ToRESTMapper() (meta.RESTMapper, error) {
	return nil, nil
}

func (g *fakeRESTClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig {
	return nil
}

func newMemoryHelmClient(t *testing.T, releases ...*release.Release) *helmclient.HelmClient {
	releaseStorage := storage.Init(driver.NewMemory())
	releaseStorage.Log = t.Logf
	for _, rel := range releases {
		assert.NoError(t, releaseStorage.Create(rel))
	}

	return &helmclient.HelmClient{
		ActionConfig: &action.Configuration{
			Releases: releaseStorage,
			KubeClient: &helmfake.PrintingKubeClient{
				Out:       io.Discard,
				LogOutput: io.Discard,
			},
			Log: t.Logf,
		},
		DebugLog: t.Logf,
	}
}

func TestBuildHelmClient(t *testing.T) {
	tests := []struct {
		name    string
		getter  *fakeClientGetter
		wantErr string
	}{
		{
			name: "returns helm config error",
			getter: &fakeClientGetter{
				err: errors.New("get helm config failed"),
			},
			wantErr: "failed to get helm config: get helm config failed",
		},
		{
			name: "returns rest config error",
			getter: &fakeClientGetter{
				helmConfig: &action.Configuration{
					RESTClientGetter: &fakeRESTClientGetter{err: errors.New("rest config failed")},
				},
			},
			wantErr: "failed to build rest config: rest config failed",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := BuildHelmClient(context.Background(), tt.getter, "monitoring")

			assert.Nil(t, got)
			assert.Error(t, err)
			assert.Contains(t, err.Error(), tt.wantErr)
		})
	}
}

func TestAddOrUpdateChartRepo(t *testing.T) {
	tests := []struct {
		name    string
		err     error
		wantErr string
	}{
		{
			name: "success",
		},
		{
			name:    "wraps error",
			err:     errors.New("repo update failed"),
			wantErr: "failed to add or update chart repo: repo update failed",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			helm := &fakeHelmClient{
				addOrUpdateChartRepoFunc: func(entry repo.Entry) error {
					assert.Equal(t, "victoria-metrics", entry.Name)
					assert.Equal(t, "https://example.com/charts", entry.URL)
					return tt.err
				},
			}

			err := AddOrUpdateChartRepo(helm, "victoria-metrics", "https://example.com/charts")
			if tt.wantErr != "" {
				assert.Error(t, err)
				assert.Equal(t, tt.wantErr, err.Error())
				return
			}
			assert.NoError(t, err)
		})
	}
}

func TestIsReleaseNotFoundError(t *testing.T) {
	tests := []struct {
		name string
		err  error
		want bool
	}{
		{
			name: "driver not found",
			err:  driver.ErrReleaseNotFound,
			want: true,
		},
		{
			name: "wrapped driver not found",
			err:  errors.Join(errors.New("lookup failed"), driver.ErrReleaseNotFound),
			want: true,
		},
		{
			name: "message contains not found",
			err:  errors.New("release test not found"),
			want: true,
		},
		{
			name: "message contains no revision",
			err:  errors.New("no revision for release test"),
			want: true,
		},
		{
			name: "other error",
			err:  errors.New("permission denied"),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.want, isReleaseNotFoundError(tt.err))
		})
	}
}

func TestCleanupPendingRelease(t *testing.T) {
	tests := []struct {
		name    string
		helm    helmclient.Client
		wantErr string
	}{
		{
			name: "ignores missing release",
			helm: &fakeHelmClient{
				getReleaseFunc: func(name string) (*release.Release, error) {
					assert.Equal(t, "vmagent", name)
					return nil, driver.ErrReleaseNotFound
				},
			},
		},
		{
			name: "ignores non pending release",
			helm: &fakeHelmClient{
				getReleaseFunc: func(name string) (*release.Release, error) {
					assert.Equal(t, "vmagent", name)
					return &release.Release{
						Name: "vmagent",
						Info: &release.Info{Status: release.StatusDeployed},
					}, nil
				},
			},
		},
		{
			name: "wraps get release error",
			helm: &fakeHelmClient{
				getReleaseFunc: func(name string) (*release.Release, error) {
					assert.Equal(t, "vmagent", name)
					return nil, errors.New("storage failed")
				},
			},
			wantErr: "failed to get release vmagent: storage failed",
		},
		{
			name: "rejects pending release from unexpected client type",
			helm: &fakeHelmClient{
				getReleaseFunc: func(name string) (*release.Release, error) {
					assert.Equal(t, "vmagent", name)
					return &release.Release{
						Name:    "vmagent",
						Version: 1,
						Info:    &release.Info{Status: release.StatusPendingUpgrade},
					}, nil
				},
			},
			wantErr: "unexpected helm client type",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := CleanupPendingRelease(tt.helm, "vmagent")
			if tt.wantErr != "" {
				assert.Error(t, err)
				assert.Contains(t, err.Error(), tt.wantErr)
				return
			}
			assert.NoError(t, err)
		})
	}
}

func TestCleanupPendingReleaseDeletesOnlyPendingRevision(t *testing.T) {
	helm := newMemoryHelmClient(t,
		&release.Release{
			Name:      "vmagent",
			Namespace: "monitoring",
			Version:   1,
			Info:      &release.Info{Status: release.StatusDeployed},
		},
		&release.Release{
			Name:      "vmagent",
			Namespace: "monitoring",
			Version:   2,
			Info:      &release.Info{Status: release.StatusPendingUpgrade},
		},
	)

	assert.NoError(t, CleanupPendingRelease(helm, "vmagent"))

	_, err := helm.ActionConfig.Releases.Get("vmagent", 2)
	assert.True(t, isReleaseNotFoundError(err))
	previous, err := helm.ActionConfig.Releases.Get("vmagent", 1)
	assert.NoError(t, err)
	assert.Equal(t, release.StatusDeployed, previous.Info.Status)
}

func TestInstallOrUpgradeChart(t *testing.T) {
	labels := map[string]string{"o11y.tidbcloud.com/managed-by": "kubernetesagent"}

	tests := []struct {
		name       string
		helm       func(t *testing.T) *fakeHelmClient
		labels     map[string]string
		wantErr    string
		wantNoCall bool
	}{
		{
			name:   "cleans missing release and installs chart with expected spec",
			labels: labels,
			helm: func(t *testing.T) *fakeHelmClient {
				calls := 0
				return &fakeHelmClient{
					getReleaseFunc: func(name string) (*release.Release, error) {
						calls++
						assert.Equal(t, 1, calls)
						assert.Equal(t, "vmagent", name)
						return nil, driver.ErrReleaseNotFound
					},
					installOrUpgradeFunc: func(_ context.Context, spec *helmclient.ChartSpec, opts *helmclient.GenericHelmOptions) (*release.Release, error) {
						calls++
						assert.Equal(t, 2, calls)
						assert.Nil(t, opts)
						assert.Equal(t, "vmagent", spec.ReleaseName)
						assert.Equal(t, "victoria-metrics/victoria-metrics-agent", spec.ChartName)
						assert.Equal(t, "monitoring", spec.Namespace)
						assert.Equal(t, "1.2.3", spec.Version)
						assert.Equal(t, "replicaCount: 1\n", spec.ValuesYaml)
						assert.True(t, spec.CreateNamespace)
						assert.Equal(t, 5, spec.MaxHistory)
						assert.Equal(t, labels, spec.Labels)
						return &release.Release{Name: "vmagent"}, nil
					},
				}
			},
		},
		{
			name:   "passes nil labels through unchanged",
			labels: nil,
			helm: func(t *testing.T) *fakeHelmClient {
				return &fakeHelmClient{
					getReleaseFunc: func(name string) (*release.Release, error) {
						return nil, driver.ErrReleaseNotFound
					},
					installOrUpgradeFunc: func(_ context.Context, spec *helmclient.ChartSpec, _ *helmclient.GenericHelmOptions) (*release.Release, error) {
						assert.Nil(t, spec.Labels)
						return &release.Release{Name: "vmagent"}, nil
					},
				}
			},
		},
		{
			name: "returns cleanup error before install",
			helm: func(t *testing.T) *fakeHelmClient {
				return &fakeHelmClient{
					getReleaseFunc: func(name string) (*release.Release, error) {
						assert.Equal(t, "vmagent", name)
						return nil, errors.New("get release failed")
					},
					installOrUpgradeFunc: func(context.Context, *helmclient.ChartSpec, *helmclient.GenericHelmOptions) (*release.Release, error) {
						t.Fatal("install should not run when cleanup fails")
						return nil, nil
					},
				}
			},
			wantErr:    "failed to clean up pending release (monitoring/vmagent): failed to get release vmagent: get release failed",
			wantNoCall: true,
		},
		{
			name: "wraps install error",
			helm: func(t *testing.T) *fakeHelmClient {
				return &fakeHelmClient{
					getReleaseFunc: func(name string) (*release.Release, error) {
						assert.Equal(t, "vmagent", name)
						return nil, driver.ErrReleaseNotFound
					},
					installOrUpgradeFunc: func(context.Context, *helmclient.ChartSpec, *helmclient.GenericHelmOptions) (*release.Release, error) {
						return nil, errors.New("install failed")
					},
				}
			},
			wantErr: "failed to install or upgrade chart (monitoring/victoria-metrics/victoria-metrics-agent/vmagent): install failed",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := InstallOrUpgradeChart(
				context.Background(),
				tt.helm(t),
				"vmagent",
				"victoria-metrics/victoria-metrics-agent",
				"monitoring",
				"1.2.3",
				"replicaCount: 1\n",
				tt.labels,
			)
			if tt.wantErr != "" {
				assert.Error(t, err)
				assert.Equal(t, tt.wantErr, err.Error())
				return
			}
			assert.NoError(t, err)
		})
	}
}

func TestUninstallRelease(t *testing.T) {
	tests := []struct {
		name    string
		err     error
		wantErr string
	}{
		{
			name: "success",
		},
		{
			name: "ignores missing release",
			err:  driver.ErrReleaseNotFound,
		},
		{
			name:    "wraps uninstall error",
			err:     errors.New("delete failed"),
			wantErr: "failed to uninstall chart vmagent: delete failed",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			helm := &fakeHelmClient{
				uninstallReleaseFunc: func(name string) error {
					assert.Equal(t, "vmagent", name)
					return tt.err
				},
			}

			err := UninstallRelease(context.Background(), helm, "vmagent")
			if tt.wantErr != "" {
				assert.Error(t, err)
				assert.Equal(t, tt.wantErr, err.Error())
				return
			}
			assert.NoError(t, err)
		})
	}
}

func TestUninstallUnknownReleases(t *testing.T) {
	labels := map[string]string{"o11y.tidbcloud.com/managed-by": "kubernetesagent"}
	const ns = "monitoring"

	tests := []struct {
		name         string
		namespace    string
		matchLabels  map[string]string
		keep         map[string]struct{}
		listed       []*release.Release
		uninstallErr error
		listErr      error
		wantCalled   []string
		wantReturned []string
		wantErr      string
	}{
		{
			name:        "skips kept managed release",
			namespace:   ns,
			matchLabels: labels,
			keep:        map[string]struct{}{"vmagent": {}},
			listed: []*release.Release{
				{Name: "vmagent", Namespace: ns, Labels: labels},
			},
			wantCalled:   nil,
			wantReturned: nil,
		},
		{
			name:        "uninstalls managed release outside keep set",
			namespace:   ns,
			matchLabels: labels,
			keep:        map[string]struct{}{"vmagent": {}},
			listed: []*release.Release{
				{Name: "vector-operator", Namespace: ns, Labels: labels},
			},
			wantCalled:   []string{"vector-operator"},
			wantReturned: []string{"vector-operator"},
		},
		{
			name:        "skips release in a different namespace even if labels match",
			namespace:   ns,
			matchLabels: labels,
			keep:        map[string]struct{}{},
			listed: []*release.Release{
				{Name: "vmagent-other", Namespace: "observability", Labels: labels},
			},
			wantCalled:   nil,
			wantReturned: nil,
		},
		{
			name:        "skips release with mismatched label value",
			namespace:   ns,
			matchLabels: labels,
			keep:        map[string]struct{}{},
			listed: []*release.Release{
				{Name: "sharedpool-vector", Namespace: ns, Labels: map[string]string{"o11y.tidbcloud.com/managed-by": "sharedpoolagent"}},
			},
			wantCalled:   nil,
			wantReturned: nil,
		},
		{
			name:        "skips release from another system",
			namespace:   ns,
			matchLabels: labels,
			keep:        map[string]struct{}{},
			listed: []*release.Release{
				{Name: "terraform-managed", Namespace: ns, Labels: map[string]string{"app.kubernetes.io/managed-by": "Terraform"}},
			},
			wantCalled:   nil,
			wantReturned: nil,
		},
		{
			name:        "skips legacy release without managed-by label",
			namespace:   ns,
			matchLabels: labels,
			keep:        map[string]struct{}{},
			listed: []*release.Release{
				{Name: "legacy-operator", Namespace: ns},
			},
			wantCalled:   nil,
			wantReturned: nil,
		},
		{
			name:        "empty list is a no-op",
			namespace:   ns,
			matchLabels: labels,
			keep:        map[string]struct{}{},
			listed:      nil,
		},
		{
			name:        "rejects empty namespace to prevent cluster-wide prune",
			namespace:   "",
			matchLabels: labels,
			keep:        map[string]struct{}{},
			listed: []*release.Release{
				{Name: "vmagent", Namespace: ns, Labels: labels},
			},
			wantCalled: nil,
			wantErr:    "requires a non-empty namespace to avoid pruning unrelated releases",
		},
		{
			name:        "rejects nil matchLabels to prevent namespace-wide prune",
			namespace:   ns,
			matchLabels: nil,
			keep:        map[string]struct{}{},
			listed: []*release.Release{
				{Name: "vmagent", Namespace: ns, Labels: labels},
			},
			wantCalled: nil,
			wantErr:    "requires non-empty matchLabels to avoid pruning unrelated releases",
		},
		{
			name:        "rejects empty matchLabels to prevent namespace-wide prune",
			namespace:   ns,
			matchLabels: map[string]string{},
			keep:        map[string]struct{}{},
			listed: []*release.Release{
				{Name: "vmagent", Namespace: ns, Labels: labels},
			},
			wantCalled: nil,
			wantErr:    "requires non-empty matchLabels to avoid pruning unrelated releases",
		},
		{
			name:        "wraps list error",
			namespace:   ns,
			matchLabels: labels,
			listErr:     errors.New("storage down"),
			wantErr:     "failed to list releases: storage down",
		},
		{
			name:        "wraps uninstall error",
			namespace:   ns,
			matchLabels: labels,
			keep:        map[string]struct{}{},
			listed: []*release.Release{
				{Name: "stuck", Namespace: ns, Labels: labels},
			},
			uninstallErr: errors.New("hook timeout"),
			wantCalled:   []string{"stuck"},
			wantReturned: nil,
			wantErr:      "failed to uninstall stuck: failed to uninstall chart stuck: hook timeout",
		},
		{
			name:        "mixed batch uninstalls only managed non-kept in namespace",
			namespace:   ns,
			matchLabels: labels,
			keep:        map[string]struct{}{"vmagent": {}, "victoria-metrics-operator": {}},
			listed: []*release.Release{
				{Name: "vmagent", Namespace: ns, Labels: labels},
				{Name: "victoria-metrics-operator", Namespace: ns, Labels: labels},
				{Name: "vector-operator", Namespace: ns, Labels: labels},
				{Name: "vmagent-other", Namespace: "observability", Labels: labels},
				{Name: "vector", Namespace: ns, Labels: nil},
				{Name: "argocd-app", Namespace: ns, Labels: map[string]string{"app.kubernetes.io/managed-by": "argocd"}},
			},
			wantCalled:   []string{"vector-operator"},
			wantReturned: []string{"vector-operator"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var called []string
			helm := &fakeHelmClient{
				listReleasesByStateMaskFunc: func(states action.ListStates) ([]*release.Release, error) {
					assert.Equal(t, action.ListDeployed, states)
					return tt.listed, tt.listErr
				},
				uninstallReleaseFunc: func(name string) error {
					called = append(called, name)
					return tt.uninstallErr
				},
			}

			returned, err := UninstallUnknownReleases(context.Background(), helm, tt.namespace, tt.matchLabels, tt.keep)
			if tt.wantErr != "" {
				assert.Error(t, err)
				assert.Equal(t, tt.wantErr, err.Error())
			} else {
				assert.NoError(t, err)
			}
			assert.Equal(t, tt.wantCalled, called)
			assert.Equal(t, tt.wantReturned, returned)
		})
	}
}
