/*
Copyright 2024 The Kubernetes 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 metricsutil

import (
	"testing"
	"time"

	"github.com/stretchr/testify/assert"

	v1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/cli-runtime/pkg/genericiooptions"
	metricsapi "k8s.io/metrics/pkg/apis/metrics"
)

func TestPrintNodeMetrics(t *testing.T) {

	tests := []struct {
		name           string
		nodeMetric     []metricsapi.NodeMetrics
		nodes          []*v1.Node
		noHeader       bool
		sortBy         string
		expectedErr    error
		expectedOutput string
	}{
		{
			name:  "Single node with default header",
			nodes: []*v1.Node{newNode("node1")},
			nodeMetric: []metricsapi.NodeMetrics{
				{
					ObjectMeta: metav1.ObjectMeta{Name: "node1", ResourceVersion: "10", Labels: map[string]string{"key": "value"}},
					Window:     metav1.Duration{Duration: time.Minute},
					Usage: v1.ResourceList{
						v1.ResourceCPU:    resource.MustParse("1"),
						v1.ResourceMemory: resource.MustParse("1Gi"),
					},
				},
			},
			expectedOutput: `NAME    CPU(cores)   CPU(%)   MEMORY(bytes)   MEMORY(%)   
node1   1000m        10%      1024Mi          10%         
`,
		},
		{
			name:  "Single node without header",
			nodes: []*v1.Node{newNode("node1")},
			nodeMetric: []metricsapi.NodeMetrics{
				{
					ObjectMeta: metav1.ObjectMeta{Name: "node1", ResourceVersion: "10", Labels: map[string]string{"key": "value"}},
					Window:     metav1.Duration{Duration: time.Minute},
					Usage: v1.ResourceList{
						v1.ResourceCPU:    resource.MustParse("1"),
						v1.ResourceMemory: resource.MustParse("1Gi"),
					},
				},
			},
			noHeader: true,
			expectedOutput: `node1   1000m   10%   1024Mi   10%   
`,
		},
		{
			name:  "Multiple nodes with one missing metrics",
			nodes: []*v1.Node{newNode("node1"), newNode("node2")},
			nodeMetric: []metricsapi.NodeMetrics{
				{
					ObjectMeta: metav1.ObjectMeta{Name: "node1", ResourceVersion: "10", Labels: map[string]string{"key": "value"}},
					Window:     metav1.Duration{Duration: time.Minute},
					Usage: v1.ResourceList{
						v1.ResourceCPU:    resource.MustParse("1"),
						v1.ResourceMemory: resource.MustParse("1Gi"),
					},
				},
			},
			expectedOutput: `NAME    CPU(cores)   CPU(%)      MEMORY(bytes)   MEMORY(%)   
node1   1000m        10%         1024Mi          10%         
node2   <unknown>    <unknown>   <unknown>       <unknown>   
`,
		},
		{
			name:  "Multiple nodes with metrics sorted by memory",
			nodes: []*v1.Node{newNode("node1"), newNode("node2")},
			nodeMetric: []metricsapi.NodeMetrics{
				{
					ObjectMeta: metav1.ObjectMeta{Name: "node1", ResourceVersion: "10", Labels: map[string]string{"key": "value"}},
					Window:     metav1.Duration{Duration: time.Minute},
					Usage: v1.ResourceList{
						v1.ResourceCPU:    resource.MustParse("1"),
						v1.ResourceMemory: resource.MustParse("1Gi"),
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{Name: "node2", ResourceVersion: "10", Labels: map[string]string{"key": "value"}},
					Window:     metav1.Duration{Duration: time.Minute},
					Usage: v1.ResourceList{
						v1.ResourceCPU:    resource.MustParse("2"),
						v1.ResourceMemory: resource.MustParse("5Gi"),
					},
				},
			},
			sortBy: "memory",
			expectedOutput: `NAME    CPU(cores)   CPU(%)   MEMORY(bytes)   MEMORY(%)   
node2   2000m        20%      5120Mi          50%         
node1   1000m        10%      1024Mi          10%         
`,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			// Create a new TopCmdPrinter with a test writer.
			_, _, out, _ := genericiooptions.NewTestIOStreams()

			availableResources := make(map[string]v1.ResourceList)
			for _, n := range test.nodes {
				availableResources[n.Name] = n.Status.Capacity
			}
			top := NewTopCmdPrinter(out)
			err := top.PrintNodeMetrics(test.nodeMetric, availableResources, test.noHeader, test.sortBy)
			assert.Equal(t, test.expectedErr, err)
			assert.Equal(t, test.expectedOutput, out.String())
		})
	}
}

func TestPrintPodMetrics(t *testing.T) {

	tests := []struct {
		name            string
		podMetric       []metricsapi.PodMetrics
		printContainers bool
		withNamespace   bool
		noHeader        bool
		sortBy          string
		sum             bool
		expectedErr     error
		expectedOutput  string
	}{
		{
			name: "Single Pod with Multiple Containers",
			podMetric: []metricsapi.PodMetrics{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test",
						Namespace: "default",
					},
					Timestamp: metav1.Time{Time: time.Now()},
					Window:    metav1.Duration{Duration: time.Minute},
					Containers: []metricsapi.ContainerMetrics{
						{
							Name: "container1",
							Usage: v1.ResourceList{
								v1.ResourceCPU:    resource.MustParse("0.2"),
								v1.ResourceMemory: resource.MustParse("1Gi"),
							},
						},
						{
							Name: "container2",
							Usage: v1.ResourceList{
								v1.ResourceCPU:    resource.MustParse("0.2"),
								v1.ResourceMemory: resource.MustParse("1Gi"),
							},
						},
					},
				},
			},
			expectedOutput: `NAME   CPU(cores)   MEMORY(bytes)   
test   400m         2048Mi          
`,
		},
		{
			name: "Single Pod with Multiple Containers - Print Containers",
			podMetric: []metricsapi.PodMetrics{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test",
						Namespace: "default",
					},
					Timestamp: metav1.Time{Time: time.Now()},
					Window:    metav1.Duration{Duration: time.Minute},
					Containers: []metricsapi.ContainerMetrics{
						{
							Name: "container1",
							Usage: v1.ResourceList{
								v1.ResourceCPU:    resource.MustParse("0.2"),
								v1.ResourceMemory: resource.MustParse("1Gi"),
							},
						},
						{
							Name: "container2",
							Usage: v1.ResourceList{
								v1.ResourceCPU:    resource.MustParse("0.2"),
								v1.ResourceMemory: resource.MustParse("1Gi"),
							},
						},
					},
				},
			},
			printContainers: true,
			expectedOutput: `POD    NAME         CPU(cores)   MEMORY(bytes)   
test   container1   200m         1024Mi          
test   container2   200m         1024Mi          
`,
		},
		{
			name: "Single Pod with Multiple Containers - Calculate Resource Usage Without Header",
			podMetric: []metricsapi.PodMetrics{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test",
						Namespace: "default",
					},
					Timestamp: metav1.Time{Time: time.Now()},
					Window:    metav1.Duration{Duration: time.Minute},
					Containers: []metricsapi.ContainerMetrics{
						{
							Name: "container1",
							Usage: v1.ResourceList{
								v1.ResourceCPU:    resource.MustParse("0.2"),
								v1.ResourceMemory: resource.MustParse("1Gi"),
							},
						},
						{
							Name: "container2",
							Usage: v1.ResourceList{
								v1.ResourceCPU:    resource.MustParse("0.2"),
								v1.ResourceMemory: resource.MustParse("1Gi"),
							},
						},
					},
				},
			},
			noHeader: true,
			expectedOutput: `test   400m   2048Mi   
`,
		},
		{
			name: "Multiple Pods with Multiple Containers - Sort by Memory Usage",
			podMetric: []metricsapi.PodMetrics{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test",
						Namespace: "default",
					},
					Timestamp: metav1.Time{Time: time.Now()},
					Window:    metav1.Duration{Duration: time.Minute},
					Containers: []metricsapi.ContainerMetrics{
						{
							Name: "container1",
							Usage: v1.ResourceList{
								v1.ResourceCPU:    resource.MustParse("0.2"),
								v1.ResourceMemory: resource.MustParse("1Gi"),
							},
						},
						{
							Name: "container2",
							Usage: v1.ResourceList{
								v1.ResourceCPU:    resource.MustParse("0.2"),
								v1.ResourceMemory: resource.MustParse("1Gi"),
							},
						},
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-1",
						Namespace: "default",
					},
					Timestamp: metav1.Time{Time: time.Now()},
					Window:    metav1.Duration{Duration: time.Minute},
					Containers: []metricsapi.ContainerMetrics{
						{
							Name: "container1",
							Usage: v1.ResourceList{
								v1.ResourceCPU:    resource.MustParse("0.2"),
								v1.ResourceMemory: resource.MustParse("3Gi"),
							},
						},
						{
							Name: "container2",
							Usage: v1.ResourceList{
								v1.ResourceCPU:    resource.MustParse("0.2"),
								v1.ResourceMemory: resource.MustParse("2Gi"),
							},
						},
					},
				},
			},
			sortBy: "memory",
			expectedOutput: `NAME     CPU(cores)   MEMORY(bytes)   
test-1   400m         5120Mi          
test     400m         2048Mi          
`,
		},
		{
			name: "Multiple Pods with Multiple Containers - Calculate Total Resource Usage",
			podMetric: []metricsapi.PodMetrics{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test",
						Namespace: "default",
					},
					Timestamp: metav1.Time{Time: time.Now()},
					Window:    metav1.Duration{Duration: time.Minute},
					Containers: []metricsapi.ContainerMetrics{
						{
							Name: "container1",
							Usage: v1.ResourceList{
								v1.ResourceCPU:    resource.MustParse("0.2"),
								v1.ResourceMemory: resource.MustParse("1Gi"),
							},
						},
						{
							Name: "container2",
							Usage: v1.ResourceList{
								v1.ResourceCPU:    resource.MustParse("0.2"),
								v1.ResourceMemory: resource.MustParse("1Gi"),
							},
						},
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-1",
						Namespace: "default",
					},
					Timestamp: metav1.Time{Time: time.Now()},
					Window:    metav1.Duration{Duration: time.Minute},
					Containers: []metricsapi.ContainerMetrics{
						{
							Name: "container1",
							Usage: v1.ResourceList{
								v1.ResourceCPU:    resource.MustParse("0.2"),
								v1.ResourceMemory: resource.MustParse("3Gi"),
							},
						},
						{
							Name: "container2",
							Usage: v1.ResourceList{
								v1.ResourceCPU:    resource.MustParse("0.2"),
								v1.ResourceMemory: resource.MustParse("2Gi"),
							},
						},
					},
				},
			},
			sum: true,
			expectedOutput: `NAME     CPU(cores)   MEMORY(bytes)   
test     400m         2048Mi          
test-1   400m         5120Mi          
         ________     ________        
         800m         7168Mi          
`,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			// Create a new TopCmdPrinter with a test writer.
			_, _, out, _ := genericiooptions.NewTestIOStreams()

			top := NewTopCmdPrinter(out)
			err := top.PrintPodMetrics(test.podMetric, test.printContainers,
				test.withNamespace, test.noHeader, test.sortBy, test.sum)
			assert.Equal(t, test.expectedErr, err)
			assert.Equal(t, test.expectedOutput, out.String())
		})
	}
}

func newNode(name string) *v1.Node {
	return &v1.Node{
		ObjectMeta: metav1.ObjectMeta{Name: name},
		Status: v1.NodeStatus{
			Capacity: v1.ResourceList{
				v1.ResourceName(v1.ResourceCPU):    resource.MustParse("10"),
				v1.ResourceName(v1.ResourceMemory): resource.MustParse("10Gi"),
			},
		},
	}
}
