/*
Copyright 2019 The Rook Authors. All rights reserved.

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 test

/*
The goal here is not to test every individual specification of the pod/container. Testing that the
generated pod spec has each piece set in the Rook code isn't a particularly effective use of unit
tests. Any time the Rook code changes the pod spec intentionally, the unit test changes in the
exact same way, which doesn't really help prevent against errors where devs are changing the wrong
spec values.

Instead, the unit tests should focus on testing things that are universal truths about
Ceph pod specifications that can help catch when pods ...
 - do not have the minimum requirements for running Ceph tools/daemons
 - have vestigial values set that are no longer needed
 - have references to absent resources (e.g., a volume mount without a volume source)

In this way, unit tests for pod specifications can be consistent and shared between all Ceph pods
created by the Rook operator. With this consistency between unit tests, there should be increased
consistency between the Ceph pods that Rook creates, ensuring a consistent user experience
interacting with pods.
*/

import (
	"fmt"
	"testing"

	"github.com/rook/rook/pkg/operator/ceph/config"
	optest "github.com/rook/rook/pkg/operator/test"
	"github.com/stretchr/testify/assert"
	v1 "k8s.io/api/core/v1"
)

// A PodSpecTester is a helper exposing methods for testing required Ceph specifications common for
// all Ceph PodSpecs.
type PodSpecTester struct {
	t    *testing.T
	spec *v1.PodSpec
}

// Spec creates a PodSpecTester from a parent PodTemplateSpecTester.
func (pt *PodTemplateSpecTester) Spec() *PodSpecTester {
	return NewPodSpecTester(pt.t, &pt.template.Spec)
}

// NewPodSpecTester creates a new tester to test the given PodSpec.
func NewPodSpecTester(t *testing.T, spec *v1.PodSpec) *PodSpecTester {
	return &PodSpecTester{t: t, spec: spec}
}

// AssertVolumesMeetCephRequirements asserts that all the required Ceph volumes exist in the pod
// spec under test, Volumes list.
func (ps *PodSpecTester) AssertVolumesMeetCephRequirements(
	daemonType config.DaemonType, daemonID string,
) {
	keyringSecretName := fmt.Sprintf("rook-ceph-%s-%s-keyring", daemonType, daemonID)
	if daemonType == config.MonType {
		keyringSecretName = "rook-ceph-mons-keyring" // mons share a keyring
	}
	requiredVols := []string{"rook-config-override", keyringSecretName}
	if daemonType != config.RbdMirrorType {
		requiredVols = append(requiredVols, "ceph-daemon-data")
	}
	vols := []string{}

	for _, v := range ps.spec.Volumes {
		vols = append(vols, v.Name)
		switch v.Name {
		case "ceph-daemon-data":
			switch daemonType {
			case config.MonType:
				// mons may be host path or pvc
				assert.True(ps.t,
					v.VolumeSource.HostPath != nil || v.VolumeSource.PersistentVolumeClaim != nil,
					string(daemonType)+" daemon should be host path or pvc:", v)
			case config.OsdType:
				// osds MUST be host path
				assert.NotNil(ps.t, v.VolumeSource.HostPath,
					string(daemonType)+" daemon should be host path:", v)
			case config.MgrType, config.MdsType, config.RgwType:
				// mgrs, mdses, and rgws MUST be host path
				assert.NotNil(ps.t, v.VolumeSource.EmptyDir,
					string(daemonType)+" daemon should be empty dir:", v)
			}
		case "rook-ceph-config":
			assert.Equal(ps.t, "rook-ceph-config", v.VolumeSource.ConfigMap.LocalObjectReference.Name,
				"Ceph config volume source is wrong path:", v)
		case keyringSecretName:
			assert.Equal(ps.t, keyringSecretName, v.VolumeSource.Secret.SecretName,
				"daemon keyring volume source is wrong path:", v)
		}
	}
	assert.Subset(ps.t, vols, requiredVols,
		"required volumes don't exist in pod spec's volume list:", ps.spec.Volumes)
}

// AssertRestartPolicyAlways asserts that the pod spec is set to always restart on failure.
func (ps *PodSpecTester) AssertRestartPolicyAlways() {
	assert.Equal(ps.t, v1.RestartPolicyAlways, ps.spec.RestartPolicy)
}

// AssertChownContainer ensures that the init container to chown the Ceph data dir is present for
// Ceph daemons.
func (ps *PodSpecTester) AssertChownContainer(daemonType config.DaemonType) {
	switch daemonType {
	case config.MonType, config.MgrType, config.OsdType, config.MdsType, config.RgwType, config.RbdMirrorType:
		assert.True(ps.t, containerExists("chown-container-data-dir", ps.spec))
	}
}

// AssertPriorityClassNameMatch asserts that the pod spec has priorityClassName set to be the same
func (ps *PodSpecTester) AssertPriorityClassNameMatch(name string) {
	assert.Equal(ps.t, name, ps.spec.PriorityClassName)
}

// RunFullSuite runs all assertion tests for the PodSpec under test and its sub-resources.
func (ps *PodSpecTester) RunFullSuite(
	daemonType config.DaemonType, resourceName, cephImage,
	cpuResourceLimit, cpuResourceRequest, memoryResourceLimit, memoryResourceRequest string, priorityClassName string,
) {
	resourceExpectations := optest.ResourceLimitExpectations{
		CPUResourceLimit:      cpuResourceLimit,
		MemoryResourceLimit:   memoryResourceLimit,
		CPUResourceRequest:    cpuResourceRequest,
		MemoryResourceRequest: memoryResourceRequest,
	}
	ops := optest.NewPodSpecTester(ps.t, ps.spec)
	ops.RunFullSuite(resourceExpectations)

	ps.AssertVolumesMeetCephRequirements(daemonType, resourceName)
	ps.AssertRestartPolicyAlways()
	ps.AssertChownContainer(daemonType)
	ps.AssertPriorityClassNameMatch(priorityClassName)
	ps.Containers().RunFullSuite(cephImage, cpuResourceLimit, cpuResourceRequest, memoryResourceLimit, memoryResourceRequest)
}

func allContainers(p *v1.PodSpec) []v1.Container {
	return append(p.InitContainers, p.Containers...)
}

func containerExists(containerName string, p *v1.PodSpec) bool {
	for _, c := range p.InitContainers {
		if c.Name == containerName {
			return true
		}
	}
	return false
}
