/*
Copyright 2023 The K8sGPT 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 analyzer

import (
	"context"
	"sort"
	"testing"

	"github.com/k8sgpt-ai/k8sgpt/pkg/common"
	"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
	"github.com/stretchr/testify/require"
	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes/fake"
)

func TestPodAnalyzer(t *testing.T) {
	tests := []struct {
		name         string
		config       common.Analyzer
		expectations []struct {
			name          string
			failuresCount int
		}
	}{
		{
			name: "Pending pods, namespace filtering and readiness probe failure",
			config: common.Analyzer{
				Client: &kubernetes.Client{
					Client: fake.NewSimpleClientset(
						&v1.Pod{
							ObjectMeta: metav1.ObjectMeta{
								Name:      "Pod1",
								Namespace: "default",
							},
							Status: v1.PodStatus{
								Phase: v1.PodPending,
								Conditions: []v1.PodCondition{
									{
										// This condition will contribute to failures.
										Type:    v1.PodScheduled,
										Reason:  "Unschedulable",
										Message: "0/1 nodes are available: 1 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn't tolerate.",
									},
									{
										// This condition won't contribute to failures.
										Type:   v1.PodScheduled,
										Reason: "Unexpected failure",
									},
								},
							},
						},
						&v1.Pod{
							// This pod won't be selected because of namespace filtering.
							ObjectMeta: metav1.ObjectMeta{
								Name:      "Pod2",
								Namespace: "test",
							},
						},
						&v1.Pod{
							ObjectMeta: metav1.ObjectMeta{
								Name:      "Pod3",
								Namespace: "default",
							},
							Status: v1.PodStatus{
								// When pod is Running but its ReadinessProbe fails
								Phase: v1.PodRunning,
								ContainerStatuses: []v1.ContainerStatus{
									{
										Ready: false,
									},
								},
							},
						},
						&v1.Event{
							ObjectMeta: metav1.ObjectMeta{
								Name:      "Event1",
								Namespace: "default",
							},
							InvolvedObject: v1.ObjectReference{
								Kind:      "Pod",
								Name:      "Pod3",
								Namespace: "default",
							},
							Reason:  "Unhealthy",
							Message: "readiness probe failed: the detail reason here ...",
							Source:  v1.EventSource{Component: "eventTest"},
							Count:   1,
							Type:    v1.EventTypeWarning,
						},
					),
				},
				Context:   context.Background(),
				Namespace: "default",
			},
			expectations: []struct {
				name          string
				failuresCount int
			}{
				{
					name:          "default/Pod1",
					failuresCount: 1,
				},
				{
					name:          "default/Pod3",
					failuresCount: 1,
				},
			},
		},
		{
			name: "SchedulingGated pods with and without a message",
			config: common.Analyzer{
				Client: &kubernetes.Client{
					Client: fake.NewSimpleClientset(
						&v1.Pod{
							ObjectMeta: metav1.ObjectMeta{
								Name:      "GatedPod1",
								Namespace: "default",
							},
							Status: v1.PodStatus{
								Phase: v1.PodPending,
								Conditions: []v1.PodCondition{
									{
										// A gated pod usually has an empty message;
										// a default message is generated instead.
										Type:   v1.PodScheduled,
										Status: v1.ConditionFalse,
										Reason: v1.PodReasonSchedulingGated,
									},
								},
							},
						},
						&v1.Pod{
							ObjectMeta: metav1.ObjectMeta{
								Name:      "GatedPod2",
								Namespace: "default",
							},
							Status: v1.PodStatus{
								Phase: v1.PodPending,
								Conditions: []v1.PodCondition{
									{
										// When a message is present, it is surfaced as-is.
										Type:    v1.PodScheduled,
										Status:  v1.ConditionFalse,
										Reason:  v1.PodReasonSchedulingGated,
										Message: "Scheduling is blocked due to non-empty scheduling gates",
									},
								},
							},
						},
					),
				},
				Context:   context.Background(),
				Namespace: "default",
			},
			expectations: []struct {
				name          string
				failuresCount int
			}{
				{
					name:          "default/GatedPod1",
					failuresCount: 1,
				},
				{
					name:          "default/GatedPod2",
					failuresCount: 1,
				},
			},
		},
		{
			name: "Evicted pod with message",
			config: common.Analyzer{
				Client: &kubernetes.Client{
					Client: fake.NewSimpleClientset(
						&v1.Pod{
							ObjectMeta: metav1.ObjectMeta{
								Name:      "EvictedPod",
								Namespace: "default",
							},
							Status: v1.PodStatus{
								Reason:  "Evicted",
								Message: "Pod was rejected: The node had condition: [DiskPressure]. ",
							},
						},
					),
				},
				Context:   context.Background(),
				Namespace: "default",
			},
			expectations: []struct {
				name          string
				failuresCount int
			}{
				{
					name:          "default/EvictedPod",
					failuresCount: 1,
				},
			},
		},
		{
			name: "Evicted pod without message",
			config: common.Analyzer{
				Client: &kubernetes.Client{
					Client: fake.NewSimpleClientset(
						&v1.Pod{
							ObjectMeta: metav1.ObjectMeta{
								Name:      "EvictedPod",
								Namespace: "default",
							},
							Status: v1.PodStatus{
								Reason:  "Evicted",
								Message: "",
							},
						},
					),
				},
				Context:   context.Background(),
				Namespace: "default",
			},
			expectations: []struct {
				name          string
				failuresCount int
			}{
				{
					name:          "default/EvictedPod",
					failuresCount: 1,
				},
			},
		},
		{
			name: "readiness probe failure without any event",
			config: common.Analyzer{
				Client: &kubernetes.Client{
					Client: fake.NewSimpleClientset(
						&v1.Pod{
							ObjectMeta: metav1.ObjectMeta{
								Name:      "Pod1",
								Namespace: "default",
							},
							Status: v1.PodStatus{
								// When pod is Running but its ReadinessProbe fails
								// It won't contribute to any failures because
								// there's no event present.
								Phase: v1.PodRunning,
								ContainerStatuses: []v1.ContainerStatus{
									{
										Ready: false,
									},
								},
							},
						},
					),
				},
				Context:   context.Background(),
				Namespace: "default",
			},
		},
		{
			name: "Init container status state waiting",
			config: common.Analyzer{
				Client: &kubernetes.Client{
					Client: fake.NewSimpleClientset(
						&v1.Pod{
							ObjectMeta: metav1.ObjectMeta{
								Name:      "Pod1",
								Namespace: "default",
							},
							Status: v1.PodStatus{
								Phase: v1.PodPending,
								InitContainerStatuses: []v1.ContainerStatus{
									{
										Ready: true,
										State: v1.ContainerState{
											Running: &v1.ContainerStateRunning{
												StartedAt: metav1.Now(),
											},
										},
									},
									{
										Ready: false,
										State: v1.ContainerState{
											Waiting: &v1.ContainerStateWaiting{
												// This represents a container that is still being created or blocked due to conditions such as OOMKilled
												Reason: "ContainerCreating",
											},
										},
									},
								},
							},
						},
						&v1.Event{
							ObjectMeta: metav1.ObjectMeta{
								Name:      "Event1",
								Namespace: "default",
							},
							InvolvedObject: v1.ObjectReference{
								Kind:      "Pod",
								Name:      "Pod1",
								Namespace: "default",
							},
							Reason:  "FailedCreatePodSandBox",
							Message: "failed to create the pod sandbox ...",
							Type:    v1.EventTypeWarning,
						},
					),
				},
				Context:   context.Background(),
				Namespace: "default",
			},
			expectations: []struct {
				name          string
				failuresCount int
			}{
				{
					name:          "default/Pod1",
					failuresCount: 1,
				},
			},
		},
		{
			name: "Container status state waiting but no event reported",
			config: common.Analyzer{
				Client: &kubernetes.Client{
					Client: fake.NewSimpleClientset(
						&v1.Pod{
							ObjectMeta: metav1.ObjectMeta{
								Name:      "Pod1",
								Namespace: "default",
							},
							Status: v1.PodStatus{
								Phase: v1.PodPending,
								ContainerStatuses: []v1.ContainerStatus{
									{
										Ready: false,
										State: v1.ContainerState{
											Waiting: &v1.ContainerStateWaiting{
												// This represents a container that is still being created or blocked due to conditions such as OOMKilled
												Reason: "ContainerCreating",
											},
										},
									},
								},
							},
						},
					),
				},
				Context:   context.Background(),
				Namespace: "default",
			},
		},
		{
			name: "Container status state waiting",
			config: common.Analyzer{
				Client: &kubernetes.Client{
					Client: fake.NewSimpleClientset(
						&v1.Pod{
							ObjectMeta: metav1.ObjectMeta{
								Name:      "Pod1",
								Namespace: "default",
							},
							Status: v1.PodStatus{
								Phase: v1.PodPending,
								ContainerStatuses: []v1.ContainerStatus{
									{
										Name:  "Container1",
										Ready: false,
										State: v1.ContainerState{
											Waiting: &v1.ContainerStateWaiting{
												// This represents a container that is still being created or blocked due to conditions such as OOMKilled
												Reason: "ContainerCreating",
											},
										},
									},
									{
										Name:  "Container2",
										Ready: false,
										State: v1.ContainerState{
											Waiting: &v1.ContainerStateWaiting{
												// This represents container that is in CrashLoopBackOff state due to conditions such as OOMKilled
												Reason: "CrashLoopBackOff",
											},
										},
										LastTerminationState: v1.ContainerState{
											Terminated: &v1.ContainerStateTerminated{
												Reason: "test reason",
											},
										},
									},
									{
										Name:  "Container3",
										Ready: false,
										State: v1.ContainerState{
											Waiting: &v1.ContainerStateWaiting{
												// This won't contribute to failures.
												Reason:  "RandomReason",
												Message: "This container won't be present in the failures",
											},
										},
									},
									{
										Name:  "Container4",
										Ready: false,
										State: v1.ContainerState{
											Waiting: &v1.ContainerStateWaiting{
												// Valid error reason.
												Reason:  "PreStartHookError",
												Message: "Container4 encountered PreStartHookError",
											},
										},
									},
									{
										Name:  "Container5",
										Ready: false,
										State: v1.ContainerState{
											Waiting: &v1.ContainerStateWaiting{
												// Valid error reason.
												Reason:  "CrashLoopBackOff",
												Message: "Container4 encountered CrashLoopBackOff",
											},
										},
									},
								},
							},
						},
						&v1.Event{
							ObjectMeta: metav1.ObjectMeta{
								Name:      "Event1",
								Namespace: "default",
							},
							InvolvedObject: v1.ObjectReference{
								Kind:      "Pod",
								Name:      "Pod1",
								Namespace: "default",
							},
							// This reason won't contribute to failures.
							Reason: "RandomEvent",
							Type:   v1.EventTypeWarning,
						},
					),
				},
				Context:   context.Background(),
				Namespace: "default",
			},
			expectations: []struct {
				name          string
				failuresCount int
			}{
				{
					name:          "default/Pod1",
					failuresCount: 3,
				},
			},
		},
		{
			name: "Terminated container with non-zero exit code",
			config: common.Analyzer{
				Client: &kubernetes.Client{
					Client: fake.NewSimpleClientset(
						&v1.Pod{
							ObjectMeta: metav1.ObjectMeta{
								Name:      "Pod1",
								Namespace: "default",
							},
							Status: v1.PodStatus{
								Phase: v1.PodFailed,
								ContainerStatuses: []v1.ContainerStatus{
									{
										Name:  "Container1",
										Ready: false,
										State: v1.ContainerState{
											Terminated: &v1.ContainerStateTerminated{
												ExitCode: 1,
												Reason:   "Error",
											},
										},
									},
									{
										Name:  "Container2",
										Ready: false,
										State: v1.ContainerState{
											Terminated: &v1.ContainerStateTerminated{
												ExitCode: 2,
												Reason:   "",
											},
										},
									},
								},
							},
						},
					),
				},
				Context:   context.Background(),
				Namespace: "default",
			},
			expectations: []struct {
				name          string
				failuresCount int
			}{
				{
					name:          "default/Pod1",
					failuresCount: 2,
				},
			},
		},
	}

	podAnalyzer := PodAnalyzer{}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			results, err := podAnalyzer.Analyze(tt.config)
			require.NoError(t, err)

			if tt.expectations == nil {
				require.Equal(t, 0, len(results))
			} else {
				sort.Slice(results, func(i, j int) bool {
					return results[i].Name < results[j].Name
				})

				require.Equal(t, len(tt.expectations), len(results))

				for i, result := range results {
					require.Equal(t, tt.expectations[i].name, result.Name)
					require.Equal(t, tt.expectations[i].failuresCount, len(result.Error))
				}
			}
		})
	}
}
