package state

import (
	"fmt"
	"reflect"
	"strings"
	"testing"

	"github.com/go-test/deep"
	"go.uber.org/zap"

	"github.com/helmfile/helmfile/pkg/environment"
	"github.com/helmfile/helmfile/pkg/filesystem"
	"github.com/helmfile/helmfile/pkg/runtime"
	"github.com/helmfile/helmfile/pkg/yaml"
)

func boolPtrToString(ptr *bool) string {
	if ptr == nil {
		return "<nil>"
	}
	return fmt.Sprintf("&%t", *ptr)
}

func TestHelmState_executeTemplates(t *testing.T) {
	tests := []struct {
		name  string
		input ReleaseSpec
		want  ReleaseSpec
	}{
		{
			name: "Has template expressions in chart, values, secrets, version, labels",
			input: ReleaseSpec{
				Chart:          "test-charts/{{ .Release.Name }}",
				Version:        "{{ .Release.Name }}-0.1",
				Name:           "test-app",
				Namespace:      "test-namespace-{{ .Release.Name }}",
				ValuesTemplate: []any{"config/{{ .Environment.Name }}/{{ .Release.Name }}/{{ .Release.ChartVersion }}/values.yaml"},
				Secrets:        []any{"config/{{ .Environment.Name }}/{{ .Release.Name }}/{{ .Release.ChartVersion }}/secrets.yaml"},
				Labels:         map[string]string{"id": "{{ .Release.Name }}"},
			},
			want: ReleaseSpec{
				Chart:     "test-charts/test-app",
				Version:   "test-app-0.1",
				Name:      "test-app",
				Namespace: "test-namespace-test-app",
				Values:    []any{"config/test_env/test-app/test-app-0.1/values.yaml"},
				Secrets:   []any{"config/test_env/test-app/test-app-0.1/secrets.yaml"},
				Labels:    map[string]string{"id": "test-app"},
			},
		},
		{
			name: "Has template expressions in name with recursive refs",
			input: ReleaseSpec{
				Chart:     "test-chart",
				Name:      "{{ .Release.Labels.id }}-{{ .Release.Namespace }}",
				Namespace: "dev",
				Labels:    map[string]string{"id": "{{ .Release.Chart }}"},
			},
			want: ReleaseSpec{
				Chart:     "test-chart",
				Name:      "test-chart-dev",
				Namespace: "dev",
				Labels:    map[string]string{"id": "test-chart"},
			},
		},
		{
			name: "Has template expressions in boolean values",
			input: ReleaseSpec{
				Chart:             "test-chart",
				Name:              "app-dev",
				Namespace:         "dev",
				Labels:            map[string]string{"id": "app"},
				ConditionTemplate: func(i string) *string { return &i }(`{{ eq .Release.Labels.id "app" | ternary "true" "false" }}`),
				InstalledTemplate: func(i string) *string { return &i }(`{{ eq .Release.Labels.id "app" | ternary "true" "false" }}`),
				VerifyTemplate:    func(i string) *string { return &i }(`{{ true }}`),
				Verify:            func(i bool) *bool { return &i }(false),
				WaitTemplate:      func(i string) *string { return &i }(`{{ false }}`),
			},
			want: ReleaseSpec{
				Chart:     "test-chart",
				Name:      "app-dev",
				Namespace: "dev",
				Labels:    map[string]string{"id": "app"},
				Condition: "true",
				Installed: func(i bool) *bool { return &i }(true),
				Verify:    func(i bool) *bool { return &i }(true),
				Wait:      func(i bool) *bool { return &i }(false),
			},
		},
		{
			name: "Condition template renders false",
			input: ReleaseSpec{
				Chart:             "test-chart",
				Name:              "app-dev",
				Namespace:         "dev",
				ConditionTemplate: func(i string) *string { return &i }(`{{ false }}`),
			},
			want: ReleaseSpec{
				Chart:     "test-chart",
				Name:      "app-dev",
				Namespace: "dev",
				Condition: "false",
			},
		},
		{
			name: "Condition template takes precedence over condition",
			input: ReleaseSpec{
				Chart:             "test-chart",
				Name:              "app-dev",
				Namespace:         "dev",
				Condition:         "foo.enabled",
				ConditionTemplate: func(i string) *string { return &i }(`{{ true }}`),
			},
			want: ReleaseSpec{
				Chart:     "test-chart",
				Name:      "app-dev",
				Namespace: "dev",
				Condition: "true",
			},
		},
		{
			name: "Has template in set-values",
			input: ReleaseSpec{
				Chart:     "test-charts/chart",
				Name:      "test-app",
				Namespace: "dev",
				Version:   "1.5",
				SetValuesTemplate: []SetValue{
					{Name: "val1", Value: "{{ .Release.Name }}-val1"},
					{Name: "val2", File: "{{ .Release.Name }}.yml"},
					{Name: "val3", Values: []any{"{{ .Release.Name }}-val2", "{{ .Release.Name }}-val3"}},
					{Name: "val4", Value: "{{ .Release.Chart }}-{{ .Release.ChartVersion}}"},
				},
			},
			want: ReleaseSpec{
				Chart:     "test-charts/chart",
				Name:      "test-app",
				Namespace: "dev",
				Version:   "1.5",
				SetValues: []SetValue{
					{Name: "val1", Value: "test-app-val1"},
					{Name: "val2", File: "test-app.yml"},
					{Name: "val3", Values: []any{"test-app-val2", "test-app-val3"}},
					{Name: "val4", Value: "test-charts/chart-1.5"},
				},
			},
		},
		{
			name: "Has template in values (map)",
			input: ReleaseSpec{
				Chart:          "test-charts/chart",
				Verify:         nil,
				Name:           "app",
				Namespace:      "dev",
				ValuesTemplate: []any{map[string]string{"key": "{{ .Release.Name }}-val0"}},
			},
			want: ReleaseSpec{
				Chart:     "test-charts/chart",
				Verify:    nil,
				Name:      "app",
				Namespace: "dev",
				Values:    []any{map[string]any{"key": "app-val0"}},
			},
		},
		{
			name: "Has template expressions in post renderer args",
			input: ReleaseSpec{
				Chart: "test-chart",
				PostRendererArgs: []string{
					"--release",
					"{{ .Release.Name }}",
					"--chart",
					"{{ .Release.Chart }}",
				},
				Name: "test-release",
			},
			want: ReleaseSpec{
				Chart: "test-chart",
				Name:  "test-release",
				PostRendererArgs: []string{
					"--release",
					"test-chart-dev",
					"--chart",
					"test-chart",
				},
			},
		},
		{
			name: "Version is empty but used in templates (render as empty string)",
			input: ReleaseSpec{
				Name:           "test-app",
				Chart:          "test-charts/{{ .Release.Name }}",
				ValuesTemplate: []any{"config/values-{{ .Release.ChartVersion }}.yaml"},
			},
			want: ReleaseSpec{
				Name:   "test-app",
				Chart:  "test-charts/test-app",
				Values: []any{"config/values-.yaml"},
			},
		},
	}

	for i := range tests {
		tt := tests[i]
		t.Run(tt.name, func(t *testing.T) {
			state := &HelmState{
				fs: &filesystem.FileSystem{
					Glob: func(s string) ([]string, error) { return nil, nil }},
				basePath: ".",
				ReleaseSetSpec: ReleaseSetSpec{
					HelmDefaults: HelmSpec{
						KubeContext: "test_context",
					},
					Env:               environment.Environment{Name: "test_env"},
					OverrideNamespace: "test-namespace_",
					Repositories:      nil,
					Releases: []ReleaseSpec{
						tt.input,
					},
				},
				RenderedValues: map[string]any{},
			}

			r, err := state.ExecuteTemplates()
			if err != nil {
				t.Errorf("Unexpected error: %v", err)
				t.FailNow()
			}

			actual := r.Releases[0]

			if !reflect.DeepEqual(actual.Name, tt.want.Name) {
				t.Errorf("expected Name %+v, got %+v", tt.want.Name, actual.Name)
			}
			if !reflect.DeepEqual(actual.Chart, tt.want.Chart) {
				t.Errorf("expected Chart %+v, got %+v", tt.want.Chart, actual.Chart)
			}
			if !reflect.DeepEqual(actual.Namespace, tt.want.Namespace) {
				t.Errorf("expected Namespace %+v, got %+v", tt.want.Namespace, actual.Namespace)
			}
			if diff := deep.Equal(actual.Values, tt.want.Values); diff != nil && len(actual.Values) > 0 {
				t.Errorf("Values differs \n%+v", strings.Join(diff, "\n"))
			}
			if diff := deep.Equal(actual.Secrets, tt.want.Secrets); diff != nil && len(actual.Secrets) > 0 {
				t.Errorf("Secrets differs \n%+v", strings.Join(diff, "\n"))
			}
			if diff := deep.Equal(actual.SetValues, tt.want.SetValues); diff != nil && len(actual.SetValues) > 0 {
				t.Errorf("SetValues differs \n%+v", strings.Join(diff, "\n"))
			}
			if diff := deep.Equal(actual.Labels, tt.want.Labels); diff != nil && len(actual.Labels) > 0 {
				t.Errorf("Labels differs \n%+v", strings.Join(diff, "\n"))
			}
			if !reflect.DeepEqual(actual.Version, tt.want.Version) {
				t.Errorf("expected Version %+v, got %+v", tt.want.Version, actual.Version)
			}
			if !reflect.DeepEqual(actual.Installed, tt.want.Installed) {
				t.Errorf("expected actual.Installed %+v, got %+v",
					boolPtrToString(tt.want.Installed), boolPtrToString(actual.Installed),
				)
			}
			if !reflect.DeepEqual(actual.Verify, tt.want.Verify) {
				t.Errorf("expected actual.Verify %+v, got %+v",
					boolPtrToString(tt.want.Verify), boolPtrToString(actual.Verify),
				)
			}
			if !reflect.DeepEqual(actual.Wait, tt.want.Wait) {
				t.Errorf("expected actual.Wait %+v, got %+v",
					boolPtrToString(tt.want.Wait), boolPtrToString(actual.Wait),
				)
			}
			if !reflect.DeepEqual(actual.Condition, tt.want.Condition) {
				t.Errorf("expected Condition %+v, got %+v", tt.want.Condition, actual.Condition)
			}
			if actual.ConditionTemplate != nil {
				t.Errorf("expected ConditionTemplate to be nil, got %q", *actual.ConditionTemplate)
			}
		})
	}
}

func TestHelmState_recursiveRefsTemplates(t *testing.T) {
	tests := []struct {
		name  string
		input ReleaseSpec
	}{
		{
			name: "Has reqursive references",
			input: ReleaseSpec{
				Chart:     "test-charts/{{ .Release.Name }}",
				Verify:    nil,
				Name:      "{{ .Release.Labels.id }}",
				Namespace: "dev",
				Labels:    map[string]string{"id": "app-{{ .Release.Name }}"},
			},
		},
		{
			name: "Has unresolvable wait templates",
			input: ReleaseSpec{
				Name:         "app-dev",
				Chart:        "test-charts/app",
				Verify:       nil,
				Namespace:    "dev",
				WaitTemplate: func(i string) *string { return &i }("hi"),
			},
		},
		{
			name: "Has unresolvable condition template",
			input: ReleaseSpec{
				Name:              "app-dev",
				Chart:             "test-charts/app",
				Verify:            nil,
				Namespace:         "dev",
				ConditionTemplate: func(i string) *string { return &i }(`{{ "maybe" }}`),
			},
		},
	}

	for i := range tests {
		tt := tests[i]
		t.Run(tt.name, func(t *testing.T) {
			state := &HelmState{
				basePath: ".",
				fs: &filesystem.FileSystem{
					Glob: func(s string) ([]string, error) { return nil, nil },
				},
				ReleaseSetSpec: ReleaseSetSpec{
					HelmDefaults: HelmSpec{
						KubeContext: "test_context",
					},
					Env:               environment.Environment{Name: "test_env"},
					OverrideNamespace: "test-namespace_",
					Repositories:      nil,
					Releases: []ReleaseSpec{
						tt.input,
					},
				},
				RenderedValues: map[string]any{},
			}

			r, err := state.ExecuteTemplates()
			if err == nil {
				t.Errorf("Expected error, got valid response: %v", r)
				t.FailNow()
			}
		})
	}
}

func TestApplyDefaultInherit(t *testing.T) {
	tests := []struct {
		name           string
		defaultInherit DefaultInherits
		releaseInherit Inherits
		want           Inherits
	}{
		{
			name:           "no default inherit",
			defaultInherit: nil,
			releaseInherit: Inherits{{Template: "foo"}},
			want:           Inherits{{Template: "foo"}},
		},
		{
			name:           "default inherit prepended",
			defaultInherit: DefaultInherits{"default"},
			releaseInherit: Inherits{{Template: "foo"}},
			want:           Inherits{{Template: "default"}, {Template: "foo"}},
		},
		{
			name:           "default inherit already in release inherit is not duplicated",
			defaultInherit: DefaultInherits{"default"},
			releaseInherit: Inherits{{Template: "default"}, {Template: "foo"}},
			want:           Inherits{{Template: "default"}, {Template: "foo"}},
		},
		{
			name:           "multiple default inherits",
			defaultInherit: DefaultInherits{"a", "b"},
			releaseInherit: Inherits{{Template: "c"}},
			want:           Inherits{{Template: "a"}, {Template: "b"}, {Template: "c"}},
		},
		{
			name:           "release inherit empty with defaults",
			defaultInherit: DefaultInherits{"default"},
			releaseInherit: nil,
			want:           Inherits{{Template: "default"}},
		},
		{
			name:           "default inherit deduplicates and skips empty values",
			defaultInherit: DefaultInherits{"default", " ", "default", "ops"},
			releaseInherit: Inherits{{Template: "foo"}},
			want:           Inherits{{Template: "default"}, {Template: "ops"}, {Template: "foo"}},
		},
		{
			// Whitespace-only template names in releaseInherit are used verbatim for dedup
			// (trimmed for map lookup), so the user's explicit entry is preserved in the output
			// and the default is not prepended again.
			name:           "release inherit with whitespace template is deduplicated correctly",
			defaultInherit: DefaultInherits{"default"},
			releaseInherit: Inherits{{Template: " default "}, {Template: "foo"}},
			want:           Inherits{{Template: " default "}, {Template: "foo"}},
		},
		{
			name:           "release inherit with blank template is skipped",
			defaultInherit: DefaultInherits{"default"},
			releaseInherit: Inherits{{Template: ""}, {Template: "foo"}},
			want:           Inherits{{Template: "default"}, {Template: "foo"}},
		},
	}

	for i := range tests {
		tt := tests[i]
		t.Run(tt.name, func(t *testing.T) {
			st := &HelmState{
				ReleaseSetSpec: ReleaseSetSpec{
					DefaultInherit: tt.defaultInherit,
				},
			}
			got := st.applyDefaultInherit(tt.releaseInherit)
			if len(got) != len(tt.want) {
				t.Fatalf("expected %d inherits, got %d", len(tt.want), len(got))
			}
			for j := range got {
				if got[j].Template != tt.want[j].Template {
					t.Errorf("inherit[%d]: expected template %q, got %q", j, tt.want[j].Template, got[j].Template)
				}
				if len(got[j].Except) != len(tt.want[j].Except) {
					t.Errorf("inherit[%d]: expected %d except, got %d", j, len(tt.want[j].Except), len(got[j].Except))
				}
			}
		})
	}
}

func TestHelmState_executeTemplatesWithDefaultTemplates(t *testing.T) {
	logger := zap.NewNop().Sugar()
	state := &HelmState{
		logger: logger,
		fs: &filesystem.FileSystem{
			Glob: func(s string) ([]string, error) { return nil, nil },
		},
		basePath: ".",
		ReleaseSetSpec: ReleaseSetSpec{
			HelmDefaults: HelmSpec{
				KubeContext: "test_context",
			},
			Env: environment.Environment{Name: "test_env"},
			Templates: map[string]TemplateSpec{
				"default": {
					ReleaseSpec: ReleaseSpec{
						Namespace: "default-ns",
						Labels:    map[string]string{"managed": "true"},
					},
				},
			},
			DefaultInherit: DefaultInherits{"default"},
			Releases: []ReleaseSpec{
				{
					Name:  "app1",
					Chart: "test-chart",
				},
				{
					Name:  "app2",
					Chart: "test-chart-2",
					Inherit: Inherits{
						{Template: "default", Except: []string{"labels"}},
					},
				},
			},
		},
		RenderedValues: map[string]any{},
	}

	r, err := state.ExecuteTemplates()
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	app1 := r.Releases[0]
	if app1.Namespace != "default-ns" {
		t.Errorf("app1: expected namespace %q, got %q", "default-ns", app1.Namespace)
	}
	if app1.Labels["managed"] != "true" {
		t.Errorf("app1: expected label managed=true, got %v", app1.Labels)
	}

	app2 := r.Releases[1]
	if app2.Namespace != "default-ns" {
		t.Errorf("app2: expected namespace %q, got %q", "default-ns", app2.Namespace)
	}
	if _, ok := app2.Labels["managed"]; ok {
		t.Errorf("app2: expected labels to be excluded, but got %v", app2.Labels)
	}
}

func TestDefaultInherits_UnmarshalYAML(t *testing.T) {
	tests := []struct {
		name  string
		input string
		want  DefaultInherits
	}{
		{
			name:  "single string",
			input: `default`,
			want:  DefaultInherits{"default"},
		},
		{
			name:  "list of strings",
			input: `["a", "b"]`,
			want:  DefaultInherits{"a", "b"},
		},
		{
			name:  "null value",
			input: `null`,
			want:  nil,
		},
		{
			name:  "empty string value",
			input: `""`,
			want:  nil,
		},
		{
			name:  "list trims and drops empty names",
			input: `[" a ", "", " ", "b"]`,
			want:  DefaultInherits{"a", "b"},
		},
	}

	for _, enableGoYamlV3 := range []bool{true, false} {
		t.Run(fmt.Sprintf("GoYamlV3=%t", enableGoYamlV3), func(t *testing.T) {
			prev := runtime.GoYamlV3
			runtime.GoYamlV3 = enableGoYamlV3
			defer func() {
				runtime.GoYamlV3 = prev
			}()

			for _, tt := range tests {
				t.Run(tt.name, func(t *testing.T) {
					var got DefaultInherits
					err := yaml.Unmarshal([]byte(tt.input), &got)
					if err != nil {
						t.Fatalf("unexpected error: %v", err)
					}
					if len(got) != len(tt.want) {
						t.Fatalf("expected %d items, got %d", len(tt.want), len(got))
					}
					for i := range got {
						if got[i] != tt.want[i] {
							t.Errorf("item[%d]: expected %q, got %q", i, tt.want[i], got[i])
						}
					}
				})
			}
		})
	}
}
