/*
Copyright 2020 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 annotations

import (
	"fmt"
	"net/http"
	"reflect"
	"strings"

	"github.com/onsi/ginkgo"
	"github.com/stretchr/testify/assert"

	"k8s.io/ingress-nginx/test/e2e/framework"
)

var _ = framework.DescribeAnnotation("affinitymode", func() {
	f := framework.NewDefaultFramework("affinity")

	ginkgo.It("Balanced affinity mode should balance", func() {
		deploymentName := "affinitybalanceecho"
		replicas := 5
		f.NewEchoDeploymentWithNameAndReplicas(deploymentName, replicas)

		host := "affinity-mode-balance.com"
		annotations := make(map[string]string)
		annotations["nginx.ingress.kubernetes.io/affinity"] = "cookie"
		annotations["ginx.ingress.kubernetes.io/session-cookie-name"] = "hello-cookie"
		annotations["nginx.ingress.kubernetes.io/session-cookie-expires"] = "172800"
		annotations["nginx.ingress.kubernetes.io/session-cookie-max-age"] = "172800"
		annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
		annotations["nginx.ingress.kubernetes.io/affinity-mode"] = "balanced"
		annotations["nginx.ingress.kubernetes.io/session-cookie-hash"] = "sha1"

		ing := framework.NewSingleIngress(host, "/", host, f.Namespace, deploymentName, 80, annotations)
		f.EnsureIngress(ing)

		f.WaitForNginxServer(host,
			func(server string) bool {
				return strings.Contains(server, fmt.Sprintf("server_name %s ;", host))
			})

		// Check configuration
		ingress := f.GetIngress(f.Namespace, host)
		returnedAnnotations := ingress.GetAnnotations()
		isItEqual := reflect.DeepEqual(annotations, returnedAnnotations)
		assert.Equal(ginkgo.GinkgoT(), isItEqual, true)
	})

	ginkgo.It("Check persistent affinity mode", func() {
		deploymentName := "affinitypersistentecho"
		replicas := 5
		f.NewEchoDeploymentWithNameAndReplicas(deploymentName, replicas)

		host := "affinity-mode-persistent.com"
		annotations := make(map[string]string)
		annotations["nginx.ingress.kubernetes.io/affinity"] = "cookie"
		annotations["ginx.ingress.kubernetes.io/session-cookie-name"] = "hello-cookie"
		annotations["nginx.ingress.kubernetes.io/session-cookie-expires"] = "172800"
		annotations["nginx.ingress.kubernetes.io/session-cookie-max-age"] = "172800"
		annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
		annotations["nginx.ingress.kubernetes.io/affinity-mode"] = "persistent"
		annotations["nginx.ingress.kubernetes.io/session-cookie-hash"] = "sha1"

		ing := framework.NewSingleIngress(host, "/", host, f.Namespace, deploymentName, 80, annotations)
		f.EnsureIngress(ing)

		f.WaitForNginxServer(host,
			func(server string) bool {
				return strings.Contains(server, fmt.Sprintf("server_name %s ;", host))
			})

		// Check configuration
		ingress := f.GetIngress(f.Namespace, host)
		returnedAnnotations := ingress.GetAnnotations()
		isItEqual := reflect.DeepEqual(annotations, returnedAnnotations)
		assert.Equal(ginkgo.GinkgoT(), isItEqual, true)

		// Make a request
		request := f.HTTPTestClient().GET("/").WithHeader("Host", host)
		response := request.Expect()

		// Get the responder host name
		originalHostName := getHostnameFromResponseBody(response.Body().Raw())

		// Send new requests and add new backends. Check which backend responded to the sent request
		cookies := getCookiesFromHeader(response.Header("Set-Cookie").Raw())
		for sendRequestNumber := 0; sendRequestNumber < 10; sendRequestNumber++ {
			replicas = replicas + 1
			err := framework.UpdateDeployment(f.KubeClientSet, f.Namespace, deploymentName, replicas, nil)
			assert.Nil(ginkgo.GinkgoT(), err)
			framework.Sleep()

			response = request.WithCookies(cookies).Expect()
			newHostName := getHostnameFromResponseBody(response.Body().Raw())
			assert.Equal(ginkgo.GinkgoT(), originalHostName, newHostName,
				fmt.Sprintf("Response number %v is not from the same host. Original host: %s, response returned: %s", sendRequestNumber, originalHostName, newHostName))

		}

		// remove all backends
		replicas = 0
		err := framework.UpdateDeployment(f.KubeClientSet, f.Namespace, deploymentName, replicas, nil)
		assert.Nil(ginkgo.GinkgoT(), err)
		framework.Sleep()

		// validate, there is no backend to serve the request
		response = request.WithCookies(cookies).Expect().Status(http.StatusServiceUnavailable)

		// create brand new backends
		replicas = 2
		err = framework.UpdateDeployment(f.KubeClientSet, f.Namespace, deploymentName, replicas, nil)
		assert.Nil(ginkgo.GinkgoT(), err)
		framework.Sleep()

		// wait brand new backends to spawn
		response = request.WithCookies(cookies).Expect()
		try := 0
		for (response.Raw().StatusCode == http.StatusServiceUnavailable) && (try < 30) {
			framework.Sleep()
			response = request.WithCookies(cookies).Expect()
			try++
		}
		assert.LessOrEqual(ginkgo.GinkgoT(), try, 29, "Tries reached it's maximum, backends did not deployed in time")

		// brand new backends equals new hostname
		newHostName := getHostnameFromResponseBody(response.Body().Raw())
		assert.NotEqual(ginkgo.GinkgoT(), originalHostName, newHostName,
			fmt.Sprintf("Response is from the same host (That should not be possible). Original host: %s, response returned: %s", originalHostName, newHostName))
	})
})

func getHostnameFromResponseBody(rawResponseBody string) string {
	lines := strings.Split(strings.TrimSpace(rawResponseBody), "\n")
	for _, line := range lines {
		if strings.Contains(line, "Hostname") {
			hostnameParts := strings.Split(strings.TrimSpace(line), ":")
			if len(hostnameParts) == 2 {
				return strings.TrimSpace(hostnameParts[1])
			}
			return ""
		}
	}
	return ""
}

func getCookiesFromHeader(rawheader string) map[string]string {
	cookies := make(map[string]string)
	parts := strings.Split(strings.TrimSpace(rawheader), ";")
	for _, part := range parts {
		subparts := strings.Split(strings.TrimSpace(part), "=")
		if len(subparts) == 2 {
			cookies[subparts[0]] = subparts[1]
		} else {
			cookies[subparts[0]] = ""
		}
	}
	return cookies
}
