/*
Copyright 2022 The Flux authors

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

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

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

package reconcile

import (
	"fmt"
	"testing"

	"github.com/go-logr/logr"
	. "github.com/onsi/gomega"
	"helm.sh/helm/v3/pkg/chart"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"github.com/fluxcd/pkg/apis/kustomize"
	"github.com/fluxcd/pkg/apis/meta"
	"github.com/fluxcd/pkg/runtime/conditions"

	v2 "github.com/fluxcd/helm-controller/api/v2"
	"github.com/fluxcd/helm-controller/internal/action"
)

const (
	mockReleaseName      = "mock-release"
	mockReleaseNamespace = "mock-ns"
)

var (
	commonMetadata = &v2.CommonMetadata{
		Labels: map[string]string{
			"common-label": "test-label-value",
		},
		Annotations: map[string]string{
			"common-annotation": "test-annotation-value",
		},
	}
	commonMetadata2 = &v2.CommonMetadata{
		Labels: map[string]string{
			"common-label": "test-label-value",
			"new-label":    "new-label-value",
		},
	}
	postRenderers = []v2.PostRenderer{
		{
			Kustomize: &v2.Kustomize{
				Patches: []kustomize.Patch{
					{
						Target: &kustomize.Selector{
							Kind: "Deployment",
							Name: "test",
						},
						Patch: `
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test
spec:
  replicas: 2
`,
					},
				},
			},
		},
	}

	postRenderers2 = []v2.PostRenderer{
		{
			Kustomize: &v2.Kustomize{
				Patches: []kustomize.Patch{
					{
						Target: &kustomize.Selector{
							Kind: "Deployment",
							Name: "test",
						},
						Patch: `
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test
spec:
  replicas: 3
`,
					},
				},
			},
		},
	}
)

func Test_summarize(t *testing.T) {
	tests := []struct {
		name           string
		generation     int64
		spec           *v2.HelmReleaseSpec
		status         v2.HelmReleaseStatus
		expectedStatus *v2.HelmReleaseStatus
	}{
		{
			name:       "summarize conditions",
			generation: 1,
			status: v2.HelmReleaseStatus{
				Conditions: []metav1.Condition{
					{
						Type:               v2.ReleasedCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.InstallSucceededReason,
						Message:            "Install complete",
						ObservedGeneration: 1,
					},
					{
						Type:               v2.TestSuccessCondition,
						Status:             metav1.ConditionFalse,
						Reason:             v2.TestFailedReason,
						Message:            "test hook(s) failure",
						ObservedGeneration: 1,
					},
				},
			},
			expectedStatus: &v2.HelmReleaseStatus{
				Conditions: []metav1.Condition{
					{
						Type:               meta.ReadyCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.InstallSucceededReason,
						Message:            "Install complete",
						ObservedGeneration: 1,
					},
					{
						Type:               v2.ReleasedCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.InstallSucceededReason,
						Message:            "Install complete",
						ObservedGeneration: 1,
					},
				},
			},
		},
		{
			name:       "with tests enabled",
			generation: 1,
			status: v2.HelmReleaseStatus{
				Conditions: []metav1.Condition{
					{
						Type:               v2.ReleasedCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.InstallSucceededReason,
						Message:            "Install complete",
						ObservedGeneration: 1,
					},
					{
						Type:               v2.TestSuccessCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.TestSucceededReason,
						Message:            "test hook(s) succeeded",
						ObservedGeneration: 1,
					},
				},
			},
			spec: &v2.HelmReleaseSpec{
				Test: &v2.Test{
					Enable: true,
				},
			},
			expectedStatus: &v2.HelmReleaseStatus{
				Conditions: []metav1.Condition{
					{
						Type:               meta.ReadyCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.TestSucceededReason,
						Message:            "test hook(s) succeeded",
						ObservedGeneration: 1,
					},
					{
						Type:               v2.ReleasedCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.InstallSucceededReason,
						Message:            "Install complete",
						ObservedGeneration: 1,
					},
					{
						Type:               v2.TestSuccessCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.TestSucceededReason,
						Message:            "test hook(s) succeeded",
						ObservedGeneration: 1,
					},
				},
			},
		},
		{
			name:       "with tests enabled and failure tests",
			generation: 1,
			status: v2.HelmReleaseStatus{
				Conditions: []metav1.Condition{
					{
						Type:               v2.ReleasedCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.InstallSucceededReason,
						Message:            "Install complete",
						ObservedGeneration: 1,
					},
					{
						Type:               v2.TestSuccessCondition,
						Status:             metav1.ConditionFalse,
						Reason:             v2.TestFailedReason,
						Message:            "test hook(s) failure",
						ObservedGeneration: 1,
					},
				},
			},
			spec: &v2.HelmReleaseSpec{
				Test: &v2.Test{
					Enable: true,
				},
			},
			expectedStatus: &v2.HelmReleaseStatus{
				Conditions: []metav1.Condition{
					{
						Type:               meta.ReadyCondition,
						Status:             metav1.ConditionFalse,
						Reason:             v2.TestFailedReason,
						Message:            "test hook(s) failure",
						ObservedGeneration: 1,
					},
					{
						Type:               v2.ReleasedCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.InstallSucceededReason,
						Message:            "Install complete",
						ObservedGeneration: 1,
					},
					{
						Type:               v2.TestSuccessCondition,
						Status:             metav1.ConditionFalse,
						Reason:             v2.TestFailedReason,
						Message:            "test hook(s) failure",
						ObservedGeneration: 1,
					},
				},
			},
		},
		{
			name: "with test hooks enabled and pending tests",
			status: v2.HelmReleaseStatus{
				Conditions: []metav1.Condition{
					{
						Type:               v2.ReleasedCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.InstallSucceededReason,
						Message:            "Install complete",
						ObservedGeneration: 1,
					},
					{
						Type:               v2.TestSuccessCondition,
						Status:             metav1.ConditionUnknown,
						Reason:             "AwaitingTests",
						Message:            "Release is awaiting tests",
						ObservedGeneration: 1,
					},
				},
			},
			spec: &v2.HelmReleaseSpec{
				Test: &v2.Test{
					Enable: true,
				},
			},
			expectedStatus: &v2.HelmReleaseStatus{
				Conditions: []metav1.Condition{
					{
						Type:               meta.ReadyCondition,
						Status:             metav1.ConditionUnknown,
						Reason:             "AwaitingTests",
						Message:            "Release is awaiting tests",
						ObservedGeneration: 1,
					},
					{
						Type:               v2.ReleasedCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.InstallSucceededReason,
						Message:            "Install complete",
						ObservedGeneration: 1,
					},
					{
						Type:               v2.TestSuccessCondition,
						Status:             metav1.ConditionUnknown,
						Reason:             "AwaitingTests",
						Message:            "Release is awaiting tests",
						ObservedGeneration: 1,
					},
				},
			},
		},
		{
			name:       "with remediation failure",
			generation: 1,
			status: v2.HelmReleaseStatus{
				Conditions: []metav1.Condition{
					{
						Type:               v2.ReleasedCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.InstallSucceededReason,
						Message:            "Install complete",
						ObservedGeneration: 1,
					},
					{
						Type:               v2.TestSuccessCondition,
						Status:             metav1.ConditionFalse,
						Reason:             v2.TestFailedReason,
						Message:            "test hook(s) failure",
						ObservedGeneration: 1,
					},
					{
						Type:               v2.RemediatedCondition,
						Status:             metav1.ConditionFalse,
						Reason:             v2.UninstallFailedReason,
						Message:            "Uninstall failure",
						ObservedGeneration: 1,
					},
				},
			},
			spec: &v2.HelmReleaseSpec{
				Test: &v2.Test{
					Enable: true,
				},
			},
			expectedStatus: &v2.HelmReleaseStatus{
				Conditions: []metav1.Condition{
					{
						Type:               meta.ReadyCondition,
						Status:             metav1.ConditionFalse,
						Reason:             v2.UninstallFailedReason,
						Message:            "Uninstall failure",
						ObservedGeneration: 1,
					},
					{
						Type:               v2.ReleasedCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.InstallSucceededReason,
						Message:            "Install complete",
						ObservedGeneration: 1,
					},
					{
						Type:               v2.TestSuccessCondition,
						Status:             metav1.ConditionFalse,
						Reason:             v2.TestFailedReason,
						Message:            "test hook(s) failure",
						ObservedGeneration: 1,
					},
					{
						Type:               v2.RemediatedCondition,
						Status:             metav1.ConditionFalse,
						Reason:             v2.UninstallFailedReason,
						Message:            "Uninstall failure",
						ObservedGeneration: 1,
					},
				},
			},
		},
		{
			name:       "with remediation success",
			generation: 1,
			status: v2.HelmReleaseStatus{
				Conditions: []metav1.Condition{
					{
						Type:               v2.ReleasedCondition,
						Status:             metav1.ConditionFalse,
						Reason:             v2.UpgradeFailedReason,
						Message:            "Upgrade failure",
						ObservedGeneration: 1,
					},
					{
						Type:               v2.RemediatedCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.RollbackSucceededReason,
						Message:            "Uninstall complete",
						ObservedGeneration: 1,
					},
				},
			},
			expectedStatus: &v2.HelmReleaseStatus{
				Conditions: []metav1.Condition{
					{
						Type:               meta.ReadyCondition,
						Status:             metav1.ConditionFalse,
						Reason:             v2.RollbackSucceededReason,
						Message:            "Uninstall complete",
						ObservedGeneration: 1,
					},
					{
						Type:               v2.ReleasedCondition,
						Status:             metav1.ConditionFalse,
						Reason:             v2.UpgradeFailedReason,
						Message:            "Upgrade failure",
						ObservedGeneration: 1,
					},
					{
						Type:               v2.RemediatedCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.RollbackSucceededReason,
						Message:            "Uninstall complete",
						ObservedGeneration: 1,
					},
				},
			},
		},
		{
			name:       "with stale ready",
			generation: 1,
			status: v2.HelmReleaseStatus{
				Conditions: []metav1.Condition{
					{
						Type:    meta.ReadyCondition,
						Status:  metav1.ConditionFalse,
						Reason:  "ChartNotFound",
						Message: "chart not found",
					},
					{
						Type:               v2.ReleasedCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.UpgradeSucceededReason,
						Message:            "Upgrade finished",
						ObservedGeneration: 1,
					},
				},
			},
			expectedStatus: &v2.HelmReleaseStatus{
				Conditions: []metav1.Condition{
					{
						Type:               meta.ReadyCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.UpgradeSucceededReason,
						Message:            "Upgrade finished",
						ObservedGeneration: 1,
					},
					{
						Type:               v2.ReleasedCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.UpgradeSucceededReason,
						Message:            "Upgrade finished",
						ObservedGeneration: 1,
					},
				},
			},
		},
		{
			name:       "with stale observed generation",
			generation: 5,
			spec: &v2.HelmReleaseSpec{
				Test: &v2.Test{
					Enable: true,
				},
			},
			status: v2.HelmReleaseStatus{
				Conditions: []metav1.Condition{
					{
						Type:               v2.ReleasedCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.UpgradeSucceededReason,
						Message:            "Upgrade finished",
						ObservedGeneration: 4,
					},
					{
						Type:               v2.RemediatedCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.RollbackSucceededReason,
						Message:            "Rollback finished",
						ObservedGeneration: 3,
					},
					{
						Type:               v2.TestSuccessCondition,
						Status:             metav1.ConditionFalse,
						Reason:             v2.TestFailedReason,
						Message:            "test hook(s) failure",
						ObservedGeneration: 2,
					},
				},
			},
			expectedStatus: &v2.HelmReleaseStatus{
				Conditions: []metav1.Condition{
					{
						Type:               meta.ReadyCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.UpgradeSucceededReason,
						Message:            "Upgrade finished",
						ObservedGeneration: 5,
					},
					{
						Type:               v2.ReleasedCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.UpgradeSucceededReason,
						Message:            "Upgrade finished",
						ObservedGeneration: 4,
					},
					{
						Type:               v2.RemediatedCondition,
						Status:             metav1.ConditionTrue,
						Reason:             v2.RollbackSucceededReason,
						Message:            "Rollback finished",
						ObservedGeneration: 3,
					},
					{
						Type:               v2.TestSuccessCondition,
						Status:             metav1.ConditionFalse,
						Reason:             v2.TestFailedReason,
						Message:            "test hook(s) failure",
						ObservedGeneration: 2,
					},
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			g := NewWithT(t)

			obj := &v2.HelmRelease{
				ObjectMeta: metav1.ObjectMeta{
					Generation: tt.generation,
				},
				Status: tt.status,
			}
			if tt.spec != nil {
				obj.Spec = *tt.spec.DeepCopy()
			}
			summarize(&Request{Object: obj})

			g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.expectedStatus.Conditions))
			g.Expect(obj.Status.ObservedPostRenderersDigest).To(Equal(tt.expectedStatus.ObservedPostRenderersDigest))
		})
	}
}

func mockLogBuffer(size int, lines int) *action.LogBuffer {
	log := action.NewLogBuffer(action.NewDebugLog(logr.Discard()), size)
	for i := 0; i < lines; i++ {
		log.Log("line %d", i+1)
	}
	return log
}

func Test_RecordOnObject(t *testing.T) {
	tests := []struct {
		name     string
		obj      *v2.HelmRelease
		r        observedReleases
		mutate   bool
		testFunc func(*v2.HelmRelease) error
	}{
		{
			name: "record observed releases",
			obj: &v2.HelmRelease{
				ObjectMeta: metav1.ObjectMeta{
					Name:      mockReleaseName,
					Namespace: mockReleaseNamespace,
				},
			},
			r: observedReleases{
				1: {
					Name:    mockReleaseName,
					Version: 1,
					ChartMetadata: chart.Metadata{
						Name:    mockReleaseName,
						Version: "1.0.0",
					},
				},
			},
			testFunc: func(obj *v2.HelmRelease) error {
				if len(obj.Status.History) != 1 {
					return fmt.Errorf("history length is not 1")
				}
				if obj.Status.History[0].Name != mockReleaseName {
					return fmt.Errorf("release name is not %s", mockReleaseName)
				}
				return nil
			},
		},
		{
			name: "record observed releases with multiple versions",
			obj: &v2.HelmRelease{
				ObjectMeta: metav1.ObjectMeta{
					Name:      mockReleaseName,
					Namespace: mockReleaseNamespace,
				},
			},
			r: observedReleases{
				1: {
					Name:    mockReleaseName,
					Version: 1,
					ChartMetadata: chart.Metadata{
						Name:    mockReleaseName,
						Version: "1.0.0",
					},
				},
				2: {
					Name:    mockReleaseName,
					Version: 2,
					ChartMetadata: chart.Metadata{
						Name:    mockReleaseName,
						Version: "2.0.0",
					},
				},
			},
			testFunc: func(obj *v2.HelmRelease) error {
				if len(obj.Status.History) != 1 {
					return fmt.Errorf("want history length 1, got %d", len(obj.Status.History))
				}
				if obj.Status.History[0].Name != mockReleaseName {
					return fmt.Errorf("release name is not %s", mockReleaseName)
				}
				if obj.Status.History[0].ChartVersion != "2.0.0" {
					return fmt.Errorf("want chart version %s, got %s", "2.0.0", obj.Status.History[0].ChartVersion)
				}
				return nil
			},
		},
		{
			name: "record observed releases with status digest",
			obj: &v2.HelmRelease{
				ObjectMeta: metav1.ObjectMeta{
					Name:      mockReleaseName,
					Namespace: mockReleaseNamespace,
				},
				Status: v2.HelmReleaseStatus{
					LastAttemptedRevisionDigest: "sha256:123456",
				},
			},
			r: observedReleases{
				1: {
					Name:    mockReleaseName,
					Version: 1,
					ChartMetadata: chart.Metadata{
						Name:    mockReleaseName,
						Version: "1.0.0",
					},
				},
			},
			mutate: true,
			testFunc: func(obj *v2.HelmRelease) error {
				h := obj.Status.History.Latest()
				if h.Name != mockReleaseName {
					return fmt.Errorf("release name is not %s", mockReleaseName)
				}
				if h.ChartVersion != "1.0.0" {
					return fmt.Errorf("want chart version %s, got %s", "1.0.0", h.ChartVersion)
				}
				if h.OCIDigest != obj.Status.LastAttemptedRevisionDigest {
					return fmt.Errorf("want digest %s, got %s", obj.Status.LastAttemptedRevisionDigest, h.OCIDigest)
				}
				return nil
			},
		},
		{
			name: "record observed releases with multiple versions and status digest",
			obj: &v2.HelmRelease{
				ObjectMeta: metav1.ObjectMeta{
					Name:      mockReleaseName,
					Namespace: mockReleaseNamespace,
				},
				Status: v2.HelmReleaseStatus{
					LastAttemptedRevisionDigest: "sha256:123456",
				},
			},
			r: observedReleases{
				1: {
					Name:    mockReleaseName,
					Version: 1,
					ChartMetadata: chart.Metadata{
						Name:    mockReleaseName,
						Version: "1.0.0",
					},
				},
				2: {
					Name:    mockReleaseName,
					Version: 2,
					ChartMetadata: chart.Metadata{
						Name:    mockReleaseName,
						Version: "2.0.0",
					},
				},
			},
			mutate: true,
			testFunc: func(obj *v2.HelmRelease) error {
				if len(obj.Status.History) != 1 {
					return fmt.Errorf("want history length 1, got %d", len(obj.Status.History))
				}
				h := obj.Status.History.Latest()
				if h.Name != mockReleaseName {
					return fmt.Errorf("release name is not %s", mockReleaseName)
				}
				if h.ChartVersion != "2.0.0" {
					return fmt.Errorf("want chart version %s, got %s", "2.0.0", h.ChartVersion)
				}
				if h.OCIDigest != obj.Status.LastAttemptedRevisionDigest {
					return fmt.Errorf("want digest %s, got %s", obj.Status.LastAttemptedRevisionDigest, h.OCIDigest)
				}
				return nil
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			g := NewWithT(t)

			if tt.mutate {
				tt.r.recordOnObject(tt.obj, mutateOCIDigest)
			} else {
				tt.r.recordOnObject(tt.obj)
			}
			err := tt.testFunc(tt.obj)
			g.Expect(err).ToNot(HaveOccurred())
		})
	}

}
