// Copyright © 2019 The Knative 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 service

import (
	"fmt"
	"strconv"
	"strings"
	"testing"
	"time"

	"gotest.tools/assert"
	"gotest.tools/assert/cmp"
	v1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"knative.dev/pkg/apis"
	duckv1 "knative.dev/pkg/apis/duck/v1"
	"knative.dev/serving/pkg/apis/autoscaling"
	api_serving "knative.dev/serving/pkg/apis/serving"
	servingv1 "knative.dev/serving/pkg/apis/serving/v1"

	client_serving "knative.dev/client/pkg/serving"
	knclient "knative.dev/client/pkg/serving/v1"
	"knative.dev/client/pkg/util"
	"knative.dev/pkg/ptr"
)

const (
	imageDigest = "sha256:1234567890123456789012345678901234567890123456789012345678901234"
)

func TestServiceDescribeBasic(t *testing.T) {

	// New mock client
	client := knclient.NewMockKnServiceClient(t)

	// Recording:
	r := client.Recorder()
	// Prepare service
	expectedService := createTestServiceWithServiceAccount("foo", []string{"rev1"}, "default-sa", goodConditions())

	// Get service & revision
	r.GetService("foo", &expectedService, nil)
	rev1 := createTestRevision("rev1", 1, goodConditions())
	r.GetRevision("rev1", &rev1, nil)

	// Testing:
	output, err := executeServiceCommand(client, "describe", "foo")
	assert.NilError(t, err)

	validateServiceOutput(t, "foo", output)
	assert.Assert(t, util.ContainsAll(output, "123456"))
	assert.Assert(t, util.ContainsAll(output, "Annotations:", "anno1=aval1, anno2=aval2, anno3="))
	assert.Assert(t, cmp.Regexp(`(?m)\s*Annotations:.*\.\.\.$`, output))
	assert.Assert(t, util.ContainsAll(output, "Labels:", "label1=lval1, label2=lval2\n"))
	assert.Assert(t, util.ContainsAll(output, "[1]"))
	assert.Assert(t, cmp.Regexp("Service Account: \\s+default-sa", output))

	assert.Equal(t, strings.Count(output, "rev1"), 1)

	// Validate that all recorded API methods have been called
	r.Validate()
}

func TestServiceDescribeWithMultipleNames(t *testing.T) {
	client := knclient.NewMockKnServiceClient(t)
	r := client.Recorder()
	createTestService("foo", []string{"rev1"}, goodConditions())
	_, err := executeServiceCommand(client, "describe", "foo", "foo1")

	assert.Assert(t, util.ContainsAll(err.Error(), "'service describe' requires the service name given as single argument"))
	r.Validate()
}

func TestServiceDescribeSad(t *testing.T) {
	client := knclient.NewMockKnServiceClient(t)
	r := client.Recorder()

	expectedService := createTestService("foo", []string{"rev1"}, goodConditions())
	expectedService.Status.Conditions[0].Status = v1.ConditionFalse
	r.GetService("foo", &expectedService, nil)
	rev1 := createTestRevision("rev1", 1, goodConditions())
	r.GetRevision("rev1", &rev1, nil)

	output, err := executeServiceCommand(client, "describe", "foo")
	assert.NilError(t, err)
	validateServiceOutput(t, "foo", output)
	assert.Assert(t, util.ContainsAll(output, "!!", "Ready"))

	r.Validate()
}

func TestServiceDescribeLatest(t *testing.T) {

	// New mock client
	client := knclient.NewMockKnServiceClient(t)
	r := client.Recorder()

	expectedService := createTestService("foo", []string{"rev1"}, goodConditions())
	expectedService.Status.Traffic[0].LatestRevision = ptr.Bool(true)

	// Get service & revision
	r.GetService("foo", &expectedService, nil)
	rev1 := createTestRevision("rev1", 1, goodConditions())
	r.GetRevision("rev1", &rev1, nil)

	output, err := executeServiceCommand(client, "describe", "foo")
	assert.NilError(t, err)
	validateServiceOutput(t, "foo", output)
	assert.Assert(t, util.ContainsAll(output, "@latest (rev1)"))

	// Validate that all recorded API methods have been called
	r.Validate()
}

func TestServiceDescribeLatestNotInTraffic(t *testing.T) {

	// New mock client
	client := knclient.NewMockKnServiceClient(t)

	// Recording:
	r := client.Recorder()
	// Prepare service
	expectedService := createTestService("foo", []string{"rev1", "rev2"}, goodConditions())
	expectedService.Status.Traffic = expectedService.Status.Traffic[:1]
	expectedService.Status.Traffic[0].LatestRevision = ptr.Bool(false)
	expectedService.Status.Traffic[0].Percent = ptr.Int64(int64(100))

	// Get service & revision
	r.GetService("foo", &expectedService, nil)
	rev1 := createTestRevision("rev1", 1, goodConditions())
	rev2 := createTestRevision("rev2", 2, goodConditions())
	r.GetRevision("rev1", &rev1, nil)
	r.GetRevision("rev2", &rev2, nil)

	// Testing:
	output, err := executeServiceCommand(client, "describe", "foo")
	assert.NilError(t, err)

	validateServiceOutput(t, "foo", output)
	assert.Assert(t, util.ContainsAll(output, "rev2 (current @latest)"))

	// Validate that all recorded API methods have been called
	r.Validate()
}

func TestServiceDescribeEachNamedOnce(t *testing.T) {

	// New mock client
	client := knclient.NewMockKnServiceClient(t)

	// Recording:
	r := client.Recorder()
	// Prepare service
	expectedService := createTestService("foo", []string{"rev1", "rev2"}, goodConditions())
	expectedService.Status.Traffic = expectedService.Status.Traffic[:1]
	expectedService.Status.Traffic[0].LatestRevision = ptr.Bool(false)
	expectedService.Status.Traffic[0].Percent = ptr.Int64(int64(100))

	// Get service & revision
	r.GetService("foo", &expectedService, nil)
	rev1 := createTestRevision("rev1", 1, goodConditions())
	rev2 := createTestRevision("rev2", 2, goodConditions())
	r.GetRevision("rev1", &rev1, nil)
	r.GetRevision("rev2", &rev2, nil)

	// Testing:
	output, err := executeServiceCommand(client, "describe", "foo")
	assert.NilError(t, err)

	validateServiceOutput(t, "foo", output)
	assert.Assert(t, util.ContainsAll(output, "rev1", "rev2"))
	assert.Equal(t, strings.Count(output, "rev2"), 1)
	assert.Equal(t, strings.Count(output, "rev1"), 1)

	// Validate that all recorded API methods have been called
	r.Validate()
}

func TestServiceDescribeLatestAndCurrentBothHaveTrafficEntries(t *testing.T) {
	// New mock client
	client := knclient.NewMockKnServiceClient(t)

	// Recording:
	r := client.Recorder()
	// Prepare service
	expectedService := createTestService("foo", []string{"rev1", "rev1"}, goodConditions())
	expectedService.Status.Traffic[0].LatestRevision = ptr.Bool(true)
	expectedService.Status.Traffic[0].Tag = "latest"
	expectedService.Status.Traffic[1].Tag = "current"

	// Get service & revision
	r.GetService("foo", &expectedService, nil)
	rev1 := createTestRevision("rev1", 1, goodConditions())
	r.GetRevision("rev1", &rev1, nil)
	r.GetRevision("rev1", &rev1, nil)

	// Testing:
	output, err := executeServiceCommand(client, "describe", "foo")
	assert.NilError(t, err)

	validateServiceOutput(t, "foo", output)
	assert.Assert(t, util.ContainsAll(output, "@latest (rev1) #latest", "rev1 (current @latest) #current", "50%"))

	// Validate that all recorded API methods have been called
	r.Validate()
}

func TestServiceDescribeLatestCreatedIsBroken(t *testing.T) {
	// New mock client
	client := knclient.NewMockKnServiceClient(t)

	// Recording:
	r := client.Recorder()
	// Prepare service
	expectedService := createTestService("foo", []string{"rev1"}, goodConditions())
	expectedService.Status.Traffic[0].LatestRevision = ptr.Bool(true)
	expectedService.Status.LatestCreatedRevisionName = "rev2"

	// Get service & revision
	r.GetService("foo", &expectedService, nil)
	rev1 := createTestRevision("rev1", 1, goodConditions())
	rev2 := createTestRevision("rev2", 2, goodConditions())
	rev2.Status.Conditions[0].Status = v1.ConditionFalse
	r.GetRevision("rev1", &rev1, nil)
	r.GetRevision("rev2", &rev2, nil)

	// Testing:
	output, err := executeServiceCommand(client, "describe", "foo")
	assert.NilError(t, err)

	validateServiceOutput(t, "foo", output)
	assert.Assert(t, util.ContainsAll(output, "!", "rev2", "100%", "@latest (rev1)"))

	// Validate that all recorded API methods have been called
	r.Validate()
}

func TestServiceDescribeScaling(t *testing.T) {

	for _, data := range []struct {
		minScale, maxScale, limit, target string
		scaleOut, utilization             string
	}{
		{"", "", "", "", "", ""},
		{"", "10", "", "", "0 ... 10", ""},
		{"10", "", "", "", "10 ... ∞", ""},
		{"5", "20", "10", "", "5 ... 20", ""},
		{"", "", "20", "30", "", "50"},
	} {
		// New mock client
		client := knclient.NewMockKnServiceClient(t)

		// Recording:
		r := client.Recorder()

		// Prepare service
		expectedService := createTestService("foo", []string{"rev1"}, goodConditions())

		// Get service & revision
		r.GetService("foo", &expectedService, nil)
		rev1 := createTestRevision("rev1", 1, goodConditions())
		addScaling(&rev1, data.minScale, data.maxScale, data.target, data.limit, data.utilization)
		r.GetRevision("rev1", &rev1, nil)

		revList := servingv1.RevisionList{
			TypeMeta: metav1.TypeMeta{
				Kind:       "RevisionList",
				APIVersion: "serving.knative.dev/v1",
			},
			Items: []servingv1.Revision{
				rev1,
			},
		}

		// Return the list of all revisions
		r.ListRevisions(knclient.HasLabelSelector(api_serving.ServiceLabelKey, "foo"), &revList, nil)

		// Testing:
		output, err := executeServiceCommand(client, "describe", "foo", "--verbose")
		assert.NilError(t, err)

		validateServiceOutput(t, "foo", output)

		if data.limit != "" || data.target != "" {
			assert.Assert(t, util.ContainsAll(output, "Concurrency:"))
		} else {
			assert.Assert(t, !strings.Contains(output, "Concurrency:"))
		}
		assert.Assert(t, cmp.Regexp("Cluster:\\s+https://foo.default.svc.cluster.local", output))

		validateOutputLine(t, output, "Scale", data.scaleOut)
		validateOutputLine(t, output, "Limit", data.limit)
		validateOutputLine(t, output, "Target", data.target)

		// Validate that all recorded API methods have been called
		r.Validate()
	}
}

func validateOutputLine(t *testing.T, output string, label string, value string) {
	if value != "" {
		assert.Assert(t, cmp.Regexp(fmt.Sprintf("%s:\\s*%s", label, value), output))
	} else {
		assert.Assert(t, !strings.Contains(output, label+":"))
	}
}

func TestServiceDescribeResources(t *testing.T) {

	for _, data := range []struct {
		reqMem, limitMem, reqCPU, limitCPU string
		memoryOut, cpuOut                  string
	}{
		{"", "", "", "", "", ""},
		{"10Mi", "100Mi", "100m", "1", "10Mi ... 100Mi", "100m ... 1"},
		{"", "100Mi", "", "1", "100Mi", "1"},
		{"10Mi", "", "100m", "", "10Mi", "100m"},
	} {
		// New mock client
		client := knclient.NewMockKnServiceClient(t)

		// Recording:
		r := client.Recorder()

		// Prepare service
		expectedService := createTestService("foo", []string{"rev1"}, goodConditions())

		// Get service & revision
		r.GetService("foo", &expectedService, nil)
		rev1 := createTestRevision("rev1", 1, goodConditions())
		addResourceLimits(&rev1.Spec.Containers[0].Resources, data.reqMem, data.limitMem, data.reqCPU, data.limitCPU)
		r.GetRevision("rev1", &rev1, nil)

		revList := servingv1.RevisionList{
			TypeMeta: metav1.TypeMeta{
				Kind:       "RevisionList",
				APIVersion: "serving.knative.dev/v1",
			},
			Items: []servingv1.Revision{
				rev1,
			},
		}

		// Return the list of all revisions
		r.ListRevisions(knclient.HasLabelSelector(api_serving.ServiceLabelKey, "foo"), &revList, nil)

		// Testing:
		output, err := executeServiceCommand(client, "describe", "foo", "--verbose")
		assert.NilError(t, err)

		validateServiceOutput(t, "foo", output)

		assert.Assert(t, cmp.Regexp("Cluster:\\s+https://foo.default.svc.cluster.local", output))

		validateOutputLine(t, output, "Memory", data.memoryOut)
		validateOutputLine(t, output, "CPU", data.cpuOut)

		// Validate that all recorded API methods have been called
		r.Validate()
	}
}

func TestServiceDescribeUserImageVsImage(t *testing.T) {
	// New mock client
	client := knclient.NewMockKnServiceClient(t)

	// Recording:
	r := client.Recorder()

	// Prepare service
	expectedService := createTestService("foo", []string{"rev1", "rev2", "rev3", "rev4"}, goodConditions())
	r.GetService("foo", &expectedService, nil)

	rev1 := createTestRevision("rev1", 1, goodConditions())
	rev2 := createTestRevision("rev2", 2, goodConditions())
	rev3 := createTestRevision("rev3", 3, goodConditions())
	rev4 := createTestRevision("rev4", 4, goodConditions())

	// Different combinations of image annotations and not.
	rev1.Spec.Containers[0].Image = "gcr.io/test/image:latest"
	rev1.Annotations[client_serving.UserImageAnnotationKey] = "gcr.io/test/image:latest"
	rev2.Spec.Containers[0].Image = "gcr.io/test/image@" + imageDigest
	rev2.Annotations[client_serving.UserImageAnnotationKey] = "gcr.io/test/image:latest"
	// rev3 is as if we changed the image but didn't change the annotation
	rev3.Annotations[client_serving.UserImageAnnotationKey] = "gcr.io/test/image:latest"
	rev3.Spec.Containers[0].Image = "gcr.io/a/b"
	// rev4 is without the annotation at all and no hash
	rev4.Status.DeprecatedImageDigest = ""
	rev4.Spec.Containers[0].Image = "gcr.io/x/y"

	// Fetch the revisions
	r.GetRevision("rev1", &rev1, nil)
	r.GetRevision("rev2", &rev2, nil)
	r.GetRevision("rev3", &rev3, nil)
	r.GetRevision("rev4", &rev4, nil)

	// Testing:
	output, err := executeServiceCommand(client, "describe", "foo")
	assert.NilError(t, err)

	validateServiceOutput(t, "foo", output)

	assert.Assert(t, util.ContainsAll(output, "Image", "Name",
		"gcr.io/test/image:latest (pinned to 123456)", "gcr.io/a/b (at 123456)", "gcr.io/x/y"))
	assert.Assert(t, util.ContainsAll(output, "[1]", "[2]"))

	// Validate that all recorded API methods have been called
	r.Validate()

}

func TestServiceDescribeVerbose(t *testing.T) {

	// New mock client
	client := knclient.NewMockKnServiceClient(t)

	// Recording:
	r := client.Recorder()

	// Prepare service
	expectedService := createTestService("foo", []string{"rev1", "rev2", "rev3"}, goodConditions())
	expectedService.Status.Traffic = expectedService.Status.Traffic[2:]
	expectedService.Status.Traffic[0].LatestRevision = ptr.Bool(true)
	expectedService.Status.Traffic[0].Percent = ptr.Int64(int64(100))
	r.GetService("foo", &expectedService, nil)

	rev1 := createTestRevision("rev1", 1, goodConditions())
	rev2 := createTestRevision("rev2", 2, goodConditions())
	rev3 := createTestRevision("rev3", 3, goodConditions())

	revList := servingv1.RevisionList{
		TypeMeta: metav1.TypeMeta{
			Kind:       "RevisionList",
			APIVersion: "serving.knative.dev/v1",
		},
		Items: []servingv1.Revision{
			rev1, rev2, rev3,
		},
	}

	// Return the list of all revisions
	r.ListRevisions(knclient.HasLabelSelector(api_serving.ServiceLabelKey, "foo"), &revList, nil)

	// Fetch the revisions
	r.GetRevision("rev3", &rev3, nil)

	// Testing:
	output, err := executeServiceCommand(client, "describe", "foo", "--verbose")
	assert.NilError(t, err)

	validateServiceOutput(t, "foo", output)

	assert.Assert(t, cmp.Regexp("Cluster:\\s+https://foo.default.svc.cluster.local", output))
	assert.Assert(t, util.ContainsAll(output, "Image", "Name", "gcr.io/test/image (at 123456)", "100%", "(0s)"))
	assert.Assert(t, util.ContainsAll(output, "Env:", "env1=eval1\n", "env2=eval2\n"))
	assert.Assert(t, util.ContainsAll(output, "EnvFrom:", "cm:test1\n", "cm:test2\n"))
	assert.Assert(t, util.ContainsAll(output, "Annotations:", "anno1=aval1\n", "anno2=aval2\n"))
	assert.Assert(t, util.ContainsAll(output, "Labels:", "label1=lval1\n", "label2=lval2\n"))
	assert.Assert(t, util.ContainsAll(output, "[1]", "[2]"))
	assert.Assert(t, util.ContainsAll(output, "rev1", "rev2", "rev3"))
	assert.Equal(t, strings.Count(output, "rev3"), 1)
	assert.Equal(t, strings.Count(output, "rev2"), 1)
	assert.Equal(t, strings.Count(output, "rev1"), 1)

	// Validate that all recorded API methods have been called
	r.Validate()
}

func TestServiceDescribeWithWrongArguments(t *testing.T) {
	client := knclient.NewMockKnServiceClient(t)
	_, err := executeServiceCommand(client, "describe")
	assert.ErrorContains(t, err, "requires", "name", "service", "single", "argument")

	_, err = executeServiceCommand(client, "describe", "foo", "bar")
	assert.ErrorContains(t, err, "requires", "name", "service", "single", "argument")
}

func TestServiceDescribeMachineReadable(t *testing.T) {
	client := knclient.NewMockKnServiceClient(t)

	// Recording:
	r := client.Recorder()

	// Prepare service
	expectedService := createTestService("foo", []string{"rev1", "rev2"}, goodConditions())
	r.GetService("foo", &expectedService, nil)

	output, err := executeServiceCommand(client, "describe", "foo", "-o", "yaml")
	assert.NilError(t, err)
	assert.Assert(t, util.ContainsAll(output, "kind: Service", "spec:", "status:", "metadata:"))

	r.Validate()
}

func TestServiceDescribeURL(t *testing.T) {
	client := knclient.NewMockKnServiceClient(t)

	// Recording:
	r := client.Recorder()

	// Prepare service
	expectedService := createTestService("foo", []string{"rev1", "rev2"}, goodConditions())
	r.GetService("foo", &expectedService, nil)

	output, err := executeServiceCommand(client, "describe", "foo", "-o", "url")
	assert.NilError(t, err)
	assert.Assert(t, util.ContainsAll(output, "foo.default.example.com"))

	r.Validate()
}

func validateServiceOutput(t *testing.T, service string, output string) {
	assert.Assert(t, cmp.Regexp("Name:\\s+"+service, output))
	assert.Assert(t, cmp.Regexp("Namespace:\\s+default", output))
	assert.Assert(t, cmp.Regexp("URL:\\s+https://"+service+".default.example.com", output))

	assert.Assert(t, util.ContainsAll(output, "Age:", "Revisions:", "Conditions:", "Labels:", "Annotations:"))
	assert.Assert(t, util.ContainsAll(output, "Ready", "RoutesReady", "OK", "TYPE", "AGE", "REASON"))
}

func TestServiceDescribeRevisionUnknown(t *testing.T) {
	client := knclient.NewMockKnServiceClient(t)

	r := client.Recorder()

	expectedService := createTestServiceRevisionUnknown("foo", []string{"rev1"}, unknownConditions())

	r.GetService("foo", &expectedService, nil)
	rev1 := createTestRevision("rev1", 1, unknownConditions())
	r.GetRevision("rev1", &rev1, nil)

	output, err := executeServiceCommand(client, "describe", "foo")
	assert.NilError(t, err)

	validateServiceOutput(t, "foo", output)
	assert.Assert(t, util.ContainsAll(output, "123456"))
	assert.Assert(t, util.ContainsAll(output, "Annotations:", "anno1=aval1, anno2=aval2, anno3="))
	assert.Assert(t, cmp.Regexp(`(?m)\s*Annotations:.*\.\.\.$`, output))
	assert.Assert(t, util.ContainsAll(output, "Labels:", "label1=lval1, label2=lval2\n"))
	assert.Assert(t, util.ContainsAll(output, "[1]"))

	assert.Equal(t, strings.Count(output, "rev1"), 1)

	r.Validate()
}

func createTestService(name string, revisionNames []string, conditions duckv1.Conditions) servingv1.Service {

	labelMap := make(map[string]string)
	labelMap["label1"] = "lval1"
	labelMap["label2"] = "lval2"
	annoMap := make(map[string]string)
	annoMap["anno1"] = "aval1"
	annoMap["anno2"] = "aval2"
	annoMap["anno3"] = "very_long_value_which_should_be_truncated_in_normal_output_if_we_make_it_even_longer"

	serviceURL, _ := apis.ParseURL(fmt.Sprintf("https://%s.default.svc.cluster.local", name))
	addressURL, _ := apis.ParseURL(fmt.Sprintf("https://%s.default.example.com", name))
	service := servingv1.Service{
		TypeMeta: metav1.TypeMeta{
			Kind:       "Service",
			APIVersion: "serving.knative.dev/v1",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:              name,
			Namespace:         "default",
			Labels:            labelMap,
			Annotations:       annoMap,
			CreationTimestamp: metav1.Time{Time: time.Now().Add(-30 * time.Second)},
		},
		Status: servingv1.ServiceStatus{
			RouteStatusFields: servingv1.RouteStatusFields{
				URL:     addressURL,
				Address: &duckv1.Addressable{URL: serviceURL},
			},
			Status: duckv1.Status{
				Conditions: conditions,
			},
		},
	}
	service.Status.LatestCreatedRevisionName = revisionNames[len(revisionNames)-1]
	service.Status.LatestReadyRevisionName = revisionNames[len(revisionNames)-1]

	if len(revisionNames) > 0 {
		trafficTargets := make([]servingv1.TrafficTarget, 0)
		for _, rname := range revisionNames {
			url, _ := apis.ParseURL(fmt.Sprintf("https://%s", rname))
			target := servingv1.TrafficTarget{
				RevisionName:      rname,
				ConfigurationName: name,
				Percent:           ptr.Int64(int64(100 / len(revisionNames))),
				URL:               url,
			}
			trafficTargets = append(trafficTargets, target)
		}
		service.Status.Traffic = trafficTargets
	}

	return service
}

func createTestServiceRevisionUnknown(name string, revisionNames []string, conditions duckv1.Conditions) servingv1.Service {
	labelMap := make(map[string]string)
	labelMap["label1"] = "lval1"
	labelMap["label2"] = "lval2"
	annoMap := make(map[string]string)
	annoMap["anno1"] = "aval1"
	annoMap["anno2"] = "aval2"
	annoMap["anno3"] = "very_long_value_which_should_be_truncated_in_normal_output_if_we_make_it_even_longer"

	serviceURL, _ := apis.ParseURL(fmt.Sprintf("https://%s.default.svc.cluster.local", name))
	addressURL, _ := apis.ParseURL(fmt.Sprintf("https://%s.default.example.com", name))
	service := servingv1.Service{
		TypeMeta: metav1.TypeMeta{
			Kind:       "Service",
			APIVersion: "serving.knative.dev/v1",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:              name,
			Namespace:         "default",
			Labels:            labelMap,
			Annotations:       annoMap,
			CreationTimestamp: metav1.Time{Time: time.Now().Add(-30 * time.Second)},
		},
		Status: servingv1.ServiceStatus{
			RouteStatusFields: servingv1.RouteStatusFields{
				URL:     addressURL,
				Address: &duckv1.Addressable{URL: serviceURL},
			},
			Status: duckv1.Status{
				Conditions: conditions,
			},
		},
	}
	service.Status.LatestCreatedRevisionName = revisionNames[len(revisionNames)-1]
	// LatestReadyRevisionName == "" when Revision is unknown
	service.Status.LatestReadyRevisionName = ""

	if len(revisionNames) > 0 {
		trafficTargets := make([]servingv1.TrafficTarget, 0)
		for _, rname := range revisionNames {
			url, _ := apis.ParseURL(fmt.Sprintf("https://%s", rname))
			target := servingv1.TrafficTarget{
				RevisionName:      rname,
				ConfigurationName: name,
				Percent:           ptr.Int64(int64(100 / len(revisionNames))),
				URL:               url,
			}
			trafficTargets = append(trafficTargets, target)
		}
		service.Status.Traffic = trafficTargets
	}

	return service
}

func createTestServiceWithServiceAccount(name string, revisionNames []string, serviceAccountName string, conditions duckv1.Conditions) servingv1.Service {
	service := createTestService(name, revisionNames, conditions)

	if serviceAccountName != "" {
		template := servingv1.RevisionTemplateSpec{
			Spec: servingv1.RevisionSpec{
				PodSpec: v1.PodSpec{
					ServiceAccountName: serviceAccountName,
				},
			},
		}
		service.Spec.Template = template
	}

	return service
}

func addScaling(revision *servingv1.Revision, minScale, maxScale, concurrencyTarget, concurrencyLimit, utilization string) {
	annos := make(map[string]string)
	if minScale != "" {
		annos[autoscaling.MinScaleAnnotationKey] = minScale
	}
	if maxScale != "" {
		annos[autoscaling.MaxScaleAnnotationKey] = maxScale
	}
	if concurrencyTarget != "" {
		annos[autoscaling.TargetAnnotationKey] = concurrencyTarget
	}
	if utilization != "" {
		annos[autoscaling.TargetUtilizationPercentageKey] = utilization
	}
	revision.Annotations = annos
	if concurrencyLimit != "" {
		l, _ := strconv.ParseInt(concurrencyLimit, 10, 64)
		revision.Spec.ContainerConcurrency = ptr.Int64(l)
	}
}

func addResourceLimits(resources *v1.ResourceRequirements, reqMem, limitMem, reqCPU, limitCPU string) {
	(*resources).Requests = getResourceListQuantity(reqMem, reqCPU)
	(*resources).Limits = getResourceListQuantity(limitMem, limitCPU)
}

func getResourceListQuantity(mem string, cpu string) v1.ResourceList {
	list := v1.ResourceList{}
	if mem != "" {
		q, _ := resource.ParseQuantity(mem)
		list[v1.ResourceMemory] = q
	}
	if cpu != "" {
		q, _ := resource.ParseQuantity(cpu)
		list[v1.ResourceCPU] = q
	}
	return list
}

func createTestRevision(revision string, gen int64, conditions duckv1.Conditions) servingv1.Revision {
	labels := make(map[string]string)
	labels[api_serving.ConfigurationGenerationLabelKey] = fmt.Sprintf("%d", gen)

	return servingv1.Revision{
		TypeMeta: metav1.TypeMeta{
			Kind:       "Revision",
			APIVersion: "serving.knative.dev/v1",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:              revision,
			Namespace:         "default",
			Generation:        1,
			Labels:            labels,
			Annotations:       make(map[string]string),
			CreationTimestamp: metav1.Time{Time: time.Now()},
		},
		Spec: servingv1.RevisionSpec{
			PodSpec: v1.PodSpec{
				Containers: []v1.Container{
					{
						Image: "gcr.io/test/image",
						Env: []v1.EnvVar{
							{Name: "env1", Value: "eval1"},
							{Name: "env2", Value: "eval2"},
						},
						EnvFrom: []v1.EnvFromSource{
							{ConfigMapRef: &v1.ConfigMapEnvSource{LocalObjectReference: v1.LocalObjectReference{Name: "test1"}}},
							{ConfigMapRef: &v1.ConfigMapEnvSource{LocalObjectReference: v1.LocalObjectReference{Name: "test2"}}},
						},
						Ports: []v1.ContainerPort{
							{ContainerPort: 8080},
						},
					},
				},
			},
		},
		Status: servingv1.RevisionStatus{
			DeprecatedImageDigest: "gcr.io/test/image@" + imageDigest,
			Status: duckv1.Status{
				Conditions: conditions,
			},
		},
	}
}

func goodConditions() duckv1.Conditions {
	ret := make(duckv1.Conditions, 0)
	ret = append(ret, apis.Condition{
		Type:   apis.ConditionReady,
		Status: v1.ConditionTrue,
		LastTransitionTime: apis.VolatileTime{
			Inner: metav1.Time{Time: time.Now()},
		},
	})
	ret = append(ret, apis.Condition{
		Type:   servingv1.ServiceConditionRoutesReady,
		Status: v1.ConditionTrue,
		LastTransitionTime: apis.VolatileTime{
			Inner: metav1.Time{Time: time.Now()},
		},
	})
	return ret
}

func unknownConditions() duckv1.Conditions {
	ret := make(duckv1.Conditions, 0)
	ret = append(ret, apis.Condition{
		Type:   apis.ConditionReady,
		Status: v1.ConditionUnknown,
		LastTransitionTime: apis.VolatileTime{
			Inner: metav1.Time{Time: time.Now()},
		},
	})
	ret = append(ret, apis.Condition{
		Type:   servingv1.ServiceConditionRoutesReady,
		Status: v1.ConditionUnknown,
		LastTransitionTime: apis.VolatileTime{
			Inner: metav1.Time{Time: time.Now()},
		},
	})
	return ret
}
