/*
Copyright (c) Edgeless Systems GmbH

SPDX-License-Identifier: AGPL-3.0-only
*/

package cmd

import (
	"bytes"
	"context"
	"fmt"
	"testing"

	"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
	"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
	"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
	"github.com/edgelesssys/constellation/v2/internal/config"
	"github.com/edgelesssys/constellation/v2/internal/constants"
	"github.com/edgelesssys/constellation/v2/internal/constellation/kubecmd"
	"github.com/edgelesssys/constellation/v2/internal/file"
	updatev1alpha1 "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/api/v1alpha1"
	"github.com/spf13/afero"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const successOutput = targetVersions + versionsOutput + nodesUpToDateOutput + attestationConfigOutput

const inProgressOutput = targetVersions + versionsOutput + nodesInProgressOutput + attestationConfigOutput

const targetVersions = `Target versions:
	Image: v1.1.0
	Kubernetes: v1.2.3
`

const nodesUpToDateOutput = `Cluster status: Node version of every node is up to date
`

const nodesInProgressOutput = `Cluster status: Some node versions are out of date
	Image: 1/2
	Kubernetes: 1/2
`

const versionsOutput = `Service versions:
	Cilium: v1.0.0
	cert-manager: v1.0.0
	constellation-operators: v1.1.0
	constellation-services: v1.1.0
`

const attestationConfigOutput = `Attestation config:
	measurements:
	    15:
	        expected: "0000000000000000000000000000000000000000000000000000000000000000"
	        warnOnly: false
`

// TestStatus checks that the status function produces the correct strings.
func TestStatus(t *testing.T) {
	mustParseNodeVersion := func(nV updatev1alpha1.NodeVersion) kubecmd.NodeVersion {
		nodeVersion, err := kubecmd.NewNodeVersion(nV)
		require.NoError(t, err)
		return nodeVersion
	}

	testCases := map[string]struct {
		kubeClient     stubKubeClient
		expectedOutput string
		wantErr        bool
	}{
		"success": {
			kubeClient: stubKubeClient{
				status: map[string]kubecmd.NodeStatus{
					"outdated": kubecmd.NewNodeStatus(corev1.Node{
						ObjectMeta: metav1.ObjectMeta{
							Name: "node1",
							Annotations: map[string]string{
								"constellation.edgeless.systems/node-image": "v1.1.0",
							},
						},
						Status: corev1.NodeStatus{
							NodeInfo: corev1.NodeSystemInfo{
								KubeletVersion: "v1.2.3",
							},
						},
					}),
				},
				version: mustParseNodeVersion(updatev1alpha1.NodeVersion{
					Spec: updatev1alpha1.NodeVersionSpec{
						ImageVersion:             "v1.1.0",
						ImageReference:           "v1.1.0",
						KubernetesClusterVersion: "v1.2.3",
					},
					Status: updatev1alpha1.NodeVersionStatus{
						Conditions: []metav1.Condition{
							{
								Message: "Node version of every node is up to date",
							},
						},
					},
				}),
				attestation: &config.QEMUVTPM{
					Measurements: measurements.M{
						15: measurements.WithAllBytes(0, measurements.Enforce, measurements.PCRMeasurementLength),
					},
				},
			},
			expectedOutput: successOutput,
		},
		"one of two nodes not upgraded": {
			kubeClient: stubKubeClient{
				status: map[string]kubecmd.NodeStatus{
					"outdated": kubecmd.NewNodeStatus(corev1.Node{
						ObjectMeta: metav1.ObjectMeta{
							Name: "outdated",
							Annotations: map[string]string{
								"constellation.edgeless.systems/node-image": "v1.0.0",
							},
						},
						Status: corev1.NodeStatus{
							NodeInfo: corev1.NodeSystemInfo{
								KubeletVersion: "v1.2.2",
							},
						},
					}),
					"uptodate": kubecmd.NewNodeStatus(corev1.Node{
						ObjectMeta: metav1.ObjectMeta{
							Name: "uptodate",
							Annotations: map[string]string{
								"constellation.edgeless.systems/node-image": "v1.1.0",
							},
						},
						Status: corev1.NodeStatus{
							NodeInfo: corev1.NodeSystemInfo{
								KubeletVersion: "v1.2.3",
							},
						},
					}),
				},
				version: mustParseNodeVersion(updatev1alpha1.NodeVersion{
					Spec: updatev1alpha1.NodeVersionSpec{
						ImageVersion:             "v1.1.0",
						ImageReference:           "v1.1.0",
						KubernetesClusterVersion: "v1.2.3",
					},
					Status: updatev1alpha1.NodeVersionStatus{
						Conditions: []metav1.Condition{
							{
								Message: "Some node versions are out of date",
							},
						},
					},
				}),
				attestation: &config.QEMUVTPM{
					Measurements: measurements.M{
						15: measurements.WithAllBytes(0, measurements.Enforce, measurements.PCRMeasurementLength),
					},
				},
			},
			expectedOutput: inProgressOutput,
		},
		"error getting node status": {
			kubeClient: stubKubeClient{
				statusErr: assert.AnError,
				version: mustParseNodeVersion(updatev1alpha1.NodeVersion{
					Spec: updatev1alpha1.NodeVersionSpec{
						ImageVersion:             "v1.1.0",
						ImageReference:           "v1.1.0",
						KubernetesClusterVersion: "v1.2.3",
					},
					Status: updatev1alpha1.NodeVersionStatus{
						Conditions: []metav1.Condition{
							{
								Message: "Node version of every node is up to date",
							},
						},
					},
				}),
				attestation: &config.QEMUVTPM{
					Measurements: measurements.M{
						15: measurements.WithAllBytes(0, measurements.Enforce, measurements.PCRMeasurementLength),
					},
				},
			},
			expectedOutput: successOutput,
			wantErr:        true,
		},
		"error getting node version": {
			kubeClient: stubKubeClient{
				status: map[string]kubecmd.NodeStatus{
					"outdated": kubecmd.NewNodeStatus(corev1.Node{
						ObjectMeta: metav1.ObjectMeta{
							Name: "node1",
							Annotations: map[string]string{
								"constellation.edgeless.systems/node-image": "v1.1.0",
							},
						},
						Status: corev1.NodeStatus{
							NodeInfo: corev1.NodeSystemInfo{
								KubeletVersion: "v1.2.3",
							},
						},
					}),
				},
				versionErr: assert.AnError,
				attestation: &config.QEMUVTPM{
					Measurements: measurements.M{
						15: measurements.WithAllBytes(0, measurements.Enforce, measurements.PCRMeasurementLength),
					},
				},
			},
			expectedOutput: successOutput,
			wantErr:        true,
		},
		"error getting attestation config": {
			kubeClient: stubKubeClient{
				status: map[string]kubecmd.NodeStatus{
					"outdated": kubecmd.NewNodeStatus(corev1.Node{
						ObjectMeta: metav1.ObjectMeta{
							Name: "node1",
							Annotations: map[string]string{
								"constellation.edgeless.systems/node-image": "v1.1.0",
							},
						},
						Status: corev1.NodeStatus{
							NodeInfo: corev1.NodeSystemInfo{
								KubeletVersion: "v1.2.3",
							},
						},
					}),
				},
				version: mustParseNodeVersion(updatev1alpha1.NodeVersion{
					Spec: updatev1alpha1.NodeVersionSpec{
						ImageVersion:             "v1.1.0",
						ImageReference:           "v1.1.0",
						KubernetesClusterVersion: "v1.2.3",
					},
					Status: updatev1alpha1.NodeVersionStatus{
						Conditions: []metav1.Condition{
							{
								Message: "Node version of every node is up to date",
							},
						},
					},
				}),
				attestationErr: assert.AnError,
			},
			expectedOutput: successOutput,
			wantErr:        true,
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			require := require.New(t)
			assert := assert.New(t)

			cmd := NewStatusCmd()
			var out bytes.Buffer
			cmd.SetOut(&out)
			var errOut bytes.Buffer
			cmd.SetErr(&errOut)

			fileHandler := file.NewHandler(afero.NewMemMapFs())
			cfg, err := createConfigWithAttestationVariant(cloudprovider.Azure, "", variant.AzureSEVSNP{})
			require.NoError(err)
			modifyConfigForAzureToPassValidate(cfg)
			require.NoError(fileHandler.WriteYAML(constants.ConfigFilename, cfg))
			s := statusCmd{fileHandler: fileHandler}

			err = s.status(
				cmd,
				stubGetVersions(versionsOutput),
				tc.kubeClient,
				stubAttestationFetcher{},
			)
			if tc.wantErr {
				assert.Error(err)
				return
			}
			require.NoError(err)
			assert.Equal(tc.expectedOutput, out.String())
		})
	}
}

func modifyConfigForAzureToPassValidate(c *config.Config) {
	c.RemoveProviderAndAttestationExcept(cloudprovider.Azure)
	c.Image = constants.BinaryVersion().String()
	c.Provider.Azure.SubscriptionID = "11111111-1111-1111-1111-111111111111"
	c.Provider.Azure.TenantID = "11111111-1111-1111-1111-111111111111"
	c.Provider.Azure.Location = "westus"
	c.Provider.Azure.ResourceGroup = "test"
	c.Provider.Azure.UserAssignedIdentity = "/subscriptions/11111111-1111-1111-1111-111111111111/resourcegroups/constellation-identity/providers/Microsoft.ManagedIdentity/userAssignedIdentities/constellation-identity"
	c.Attestation.AzureSEVSNP.Measurements = measurements.M{
		0: measurements.WithAllBytes(0x00, measurements.Enforce, measurements.PCRMeasurementLength),
	}
	c.NodeGroups = map[string]config.NodeGroup{
		constants.ControlPlaneDefault: {
			Role:            "control-plane",
			Zone:            "",
			InstanceType:    "Standard_DC4as_v5",
			StateDiskSizeGB: 30,
			StateDiskType:   "StandardSSD_LRS",
			InitialCount:    3,
		},
		constants.WorkerDefault: {
			Role:            "worker",
			Zone:            "",
			InstanceType:    "Standard_DC4as_v5",
			StateDiskSizeGB: 30,
			StateDiskType:   "StandardSSD_LRS",
			InitialCount:    3,
		},
	}
}

type stubKubeClient struct {
	status         map[string]kubecmd.NodeStatus
	statusErr      error
	version        kubecmd.NodeVersion
	versionErr     error
	attestation    config.AttestationCfg
	attestationErr error
}

func (s stubKubeClient) ClusterStatus(_ context.Context) (map[string]kubecmd.NodeStatus, error) {
	return s.status, s.statusErr
}

func (s stubKubeClient) GetConstellationVersion(_ context.Context) (kubecmd.NodeVersion, error) {
	return s.version, s.versionErr
}

func (s stubKubeClient) GetClusterAttestationConfig(_ context.Context, _ variant.Variant) (config.AttestationCfg, error) {
	return s.attestation, s.attestationErr
}

func stubGetVersions(output string) func() (fmt.Stringer, error) {
	return func() (fmt.Stringer, error) {
		return stubServiceVersions{output}, nil
	}
}

type stubServiceVersions struct {
	output string
}

func (s stubServiceVersions) String() string {
	return s.output
}