/*
Copyright 2020 The Flux 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 controller

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/intstr"

	flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
	"github.com/fluxcd/flagger/pkg/notifier"
)

func TestScheduler_DeploymentInit(t *testing.T) {
	mocks := newDeploymentFixture(nil)
	mocks.ctrl.advanceCanary("podinfo", "default")

	_, err := mocks.kubeClient.AppsV1().Deployments("default").Get(context.TODO(), "podinfo-primary", metav1.GetOptions{})
	require.NoError(t, err)
}

func TestScheduler_DeploymentNewRevision(t *testing.T) {
	mocks := newDeploymentFixture(nil)

	// initializing ...
	mocks.ctrl.advanceCanary("podinfo", "default")

	// make primary ready
	mocks.makePrimaryReady(t)

	// initialization done
	mocks.ctrl.advanceCanary("podinfo", "default")

	// check if ScaleToZero was performed
	dp, err := mocks.kubeClient.AppsV1().Deployments("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	assert.Equal(t, int32(0), *dp.Spec.Replicas)

	// update
	dep2 := newDeploymentTestDeploymentV2()
	_, err = mocks.kubeClient.AppsV1().Deployments("default").Update(context.TODO(), dep2, metav1.UpdateOptions{})
	require.NoError(t, err)

	// detect changes
	mocks.ctrl.advanceCanary("podinfo", "default")

	c, err := mocks.kubeClient.AppsV1().Deployments("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	assert.Equal(t, int32(1), *c.Spec.Replicas)
}

func TestScheduler_DeploymentRollback(t *testing.T) {
	mocks := newDeploymentFixture(nil)
	// initializing
	mocks.ctrl.advanceCanary("podinfo", "default")

	// make primary ready
	mocks.makePrimaryReady(t)

	// initialized
	mocks.ctrl.advanceCanary("podinfo", "default")

	// update failed checks to max
	err := mocks.deployer.SyncStatus(mocks.canary, flaggerv1.CanaryStatus{Phase: flaggerv1.CanaryPhaseProgressing, FailedChecks: 10})
	require.NoError(t, err)

	// set a metric check to fail
	c, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	cd := c.DeepCopy()
	cd.Spec.Analysis.Metrics = append(c.Spec.Analysis.Metrics, flaggerv1.CanaryMetric{
		Name:     "fail",
		Interval: "1m",
		ThresholdRange: &flaggerv1.CanaryThresholdRange{
			Min: toFloatPtr(0),
			Max: toFloatPtr(50),
		},
		Query: "fail",
	})
	_, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Update(context.TODO(), cd, metav1.UpdateOptions{})
	require.NoError(t, err)

	// run metric checks
	mocks.ctrl.advanceCanary("podinfo", "default")

	// finalise analysis
	mocks.ctrl.advanceCanary("podinfo", "default")

	// check status
	c, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)

	assert.Equal(t, flaggerv1.CanaryPhaseFailed, c.Status.Phase)
}

// when the primary fails to become ready during promotion, the healthy canary
// must be kept instead of rolled back to the broken primary (#1898)
func TestScheduler_DeploymentPromotionPrimaryNotReady(t *testing.T) {
	mocks := newDeploymentFixture(nil)

	// initializing
	mocks.ctrl.advanceCanary("podinfo", "default")
	mocks.makePrimaryReady(t)

	// initialized
	mocks.ctrl.advanceCanary("podinfo", "default")

	// update
	dep2 := newDeploymentTestDeploymentV2()
	_, err := mocks.kubeClient.AppsV1().Deployments("default").Update(context.TODO(), dep2, metav1.UpdateOptions{})
	require.NoError(t, err)

	// detect changes -> progressing, canary scaled up
	mocks.ctrl.advanceCanary("podinfo", "default")
	mocks.makeCanaryReady(t)
	require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseProgressing))

	canaryDep, err := mocks.kubeClient.AppsV1().Deployments("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	require.NotNil(t, canaryDep.Spec.Replicas)
	canaryReplicas := *canaryDep.Spec.Replicas
	require.Greater(t, canaryReplicas, int32(0))

	// simulate: analysis succeeded, spec promoted to primary, now finishing promotion
	cd, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	err = mocks.deployer.SetStatusPhase(cd, flaggerv1.CanaryPhasePromoting)
	require.NoError(t, err)

	// known routing state at promotion time (split between primary and canary)
	require.NoError(t, mocks.router.SetRoutes(mocks.canary, 50, 50, false))

	// the promoted primary fails to roll out
	mocks.makePrimaryNotReady(t)

	// advance: Flagger observes the primary is stuck
	mocks.ctrl.advanceCanary("podinfo", "default")

	// the canary must NOT be scaled to zero - it is the only healthy copy serving traffic
	canaryDep, err = mocks.kubeClient.AppsV1().Deployments("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	require.NotNil(t, canaryDep.Spec.Replicas)
	assert.Equal(t, canaryReplicas, *canaryDep.Spec.Replicas, "canary must not be scaled to zero when promotion fails")

	// traffic must be routed to the healthy canary, off the broken primary
	primaryWeight, canaryWeight, _, err := mocks.router.GetRoutes(mocks.canary)
	require.NoError(t, err)
	assert.Equal(t, 0, primaryWeight, "no traffic must remain on the unhealthy primary")
	assert.Equal(t, 100, canaryWeight, "all traffic must be routed to the healthy canary")

	// the rollout is reported as failed so it stops advancing and alerts
	c, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	assert.Equal(t, flaggerv1.CanaryPhaseFailed, c.Status.Phase)
	assert.Equal(t, 100, c.Status.CanaryWeight, "reported canary weight must match the traffic on the canary")
}

// when the primary fails after promotion traffic has already been shifted to it
// (Finalising phase), traffic must be routed back to the healthy canary (#1898)
func TestScheduler_DeploymentPromotionFailedAfterTrafficShift(t *testing.T) {
	mocks := newDeploymentFixture(nil)

	// initializing
	mocks.ctrl.advanceCanary("podinfo", "default")
	mocks.makePrimaryReady(t)

	// initialized
	mocks.ctrl.advanceCanary("podinfo", "default")

	// update
	dep2 := newDeploymentTestDeploymentV2()
	_, err := mocks.kubeClient.AppsV1().Deployments("default").Update(context.TODO(), dep2, metav1.UpdateOptions{})
	require.NoError(t, err)

	// detect changes -> progressing, canary scaled up
	mocks.ctrl.advanceCanary("podinfo", "default")
	mocks.makeCanaryReady(t)

	canaryDep, err := mocks.kubeClient.AppsV1().Deployments("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	require.NotNil(t, canaryDep.Spec.Replicas)
	canaryReplicas := *canaryDep.Spec.Replicas
	require.Greater(t, canaryReplicas, int32(0))

	// simulate: promotion already shifted all traffic to the primary (Finalising)
	cd, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	err = mocks.deployer.SetStatusPhase(cd, flaggerv1.CanaryPhaseFinalising)
	require.NoError(t, err)
	require.NoError(t, mocks.router.SetRoutes(mocks.canary, 100, 0, false))

	// the promoted primary then fails to stay ready
	mocks.makePrimaryNotReady(t)

	// advance: Flagger observes the primary is stuck
	mocks.ctrl.advanceCanary("podinfo", "default")

	// traffic must be routed back to the healthy canary, off the broken primary
	primaryWeight, canaryWeight, _, err := mocks.router.GetRoutes(mocks.canary)
	require.NoError(t, err)
	assert.Equal(t, 0, primaryWeight, "no traffic must remain on the unhealthy primary")
	assert.Equal(t, 100, canaryWeight, "all traffic must be routed to the healthy canary")

	// the canary must not be scaled to zero
	canaryDep, err = mocks.kubeClient.AppsV1().Deployments("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	require.NotNil(t, canaryDep.Spec.Replicas)
	assert.Equal(t, canaryReplicas, *canaryDep.Spec.Replicas, "canary must not be scaled to zero when promotion fails")

	// reported canary weight must match the routing (not zeroed)
	c, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	assert.Equal(t, flaggerv1.CanaryPhaseFailed, c.Status.Phase)
	assert.Equal(t, 100, c.Status.CanaryWeight, "reported canary weight must match the traffic on the canary")
}

func TestScheduler_DeploymentSkipAnalysis(t *testing.T) {
	mocks := newDeploymentFixture(nil)
	// initializing
	mocks.ctrl.advanceCanary("podinfo", "default")

	// make primary ready
	mocks.makePrimaryReady(t)

	// initialized
	mocks.ctrl.advanceCanary("podinfo", "default")

	// enable skip
	cd, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	cd.Spec.SkipAnalysis = true
	_, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Update(context.TODO(), cd, metav1.UpdateOptions{})
	require.NoError(t, err)

	// update
	dep2 := newDeploymentTestDeploymentV2()
	_, err = mocks.kubeClient.AppsV1().Deployments("default").Update(context.TODO(), dep2, metav1.UpdateOptions{})
	require.NoError(t, err)

	// detect changes
	mocks.ctrl.advanceCanary("podinfo", "default")
	mocks.makeCanaryReady(t)

	// advance
	mocks.ctrl.advanceCanary("podinfo", "default")

	c, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	assert.True(t, c.Spec.SkipAnalysis)
	assert.Equal(t, flaggerv1.CanaryPhaseSucceeded, c.Status.Phase)
}

func TestScheduler_DeploymentAnalysisPhases(t *testing.T) {
	cd := newDeploymentTestCanary()
	cd.Spec.Analysis = &flaggerv1.CanaryAnalysis{
		Interval:            "1m",
		StepWeight:          100,
		StepWeightPromotion: 50,
	}
	mocks := newDeploymentFixture(cd)

	// initializing
	mocks.ctrl.advanceCanary("podinfo", "default")

	// make primary ready
	mocks.makePrimaryReady(t)

	// initialized
	mocks.ctrl.advanceCanary("podinfo", "default")
	require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseInitialized))

	// update
	dep2 := newDeploymentTestDeploymentV2()
	_, err := mocks.kubeClient.AppsV1().Deployments("default").Update(context.TODO(), dep2, metav1.UpdateOptions{})
	require.NoError(t, err)

	// detect changes
	mocks.ctrl.advanceCanary("podinfo", "default")
	require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseProgressing))
	require.NoError(t, assertCanaryWeight(mocks.flaggerClient, "podinfo", 0))
	mocks.makeCanaryReady(t)

	// progressing
	mocks.ctrl.advanceCanary("podinfo", "default")
	require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseProgressing))
	require.NoError(t, assertCanaryWeight(mocks.flaggerClient, "podinfo", 100))

	// start promotion
	mocks.ctrl.advanceCanary("podinfo", "default")
	require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhasePromoting))
	require.NoError(t, assertCanaryWeight(mocks.flaggerClient, "podinfo", 100))

	// end promotion
	mocks.ctrl.advanceCanary("podinfo", "default")
	require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhasePromoting))
	require.NoError(t, assertCanaryWeight(mocks.flaggerClient, "podinfo", 50))

	// finalising
	mocks.ctrl.advanceCanary("podinfo", "default")
	require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseFinalising))
	require.NoError(t, assertCanaryWeight(mocks.flaggerClient, "podinfo", 0))

	// succeeded
	mocks.ctrl.advanceCanary("podinfo", "default")
	require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseSucceeded))
	require.NoError(t, assertCanaryWeight(mocks.flaggerClient, "podinfo", 0))
}

func TestScheduler_DeploymentBlueGreenAnalysisPhases(t *testing.T) {
	cd := newDeploymentTestCanary()
	cd.Spec.Analysis = &flaggerv1.CanaryAnalysis{
		Interval:   "1m",
		Iterations: 1,
	}
	mocks := newDeploymentFixture(cd)

	// initializing
	mocks.ctrl.advanceCanary("podinfo", "default")

	// make primary ready
	mocks.makePrimaryReady(t)

	// initialized
	mocks.ctrl.advanceCanary("podinfo", "default")
	require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseInitialized))

	// update
	dep2 := newDeploymentTestDeploymentV2()
	_, err := mocks.kubeClient.AppsV1().Deployments("default").Update(context.TODO(), dep2, metav1.UpdateOptions{})
	require.NoError(t, err)

	// detect changes (progressing)
	mocks.ctrl.advanceCanary("podinfo", "default")
	require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseProgressing))
	mocks.makeCanaryReady(t)

	// advance (progressing)
	mocks.ctrl.advanceCanary("podinfo", "default")
	require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseProgressing))

	// route traffic to primary (progressing)
	mocks.ctrl.advanceCanary("podinfo", "default")
	require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseProgressing))

	// promoting
	mocks.ctrl.advanceCanary("podinfo", "default")
	require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhasePromoting))

	// finalising
	mocks.ctrl.advanceCanary("podinfo", "default")
	require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseFinalising))

	// succeeded
	mocks.ctrl.advanceCanary("podinfo", "default")
	require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseSucceeded))
}

func TestScheduler_DeploymentNewRevisionReset(t *testing.T) {
	mocks := newDeploymentFixture(nil)
	// init
	// initializing
	mocks.ctrl.advanceCanary("podinfo", "default")

	// make primary ready
	mocks.makePrimaryReady(t)

	// initialized
	mocks.ctrl.advanceCanary("podinfo", "default")

	// first update
	dep2 := newDeploymentTestDeploymentV2()
	_, err := mocks.kubeClient.AppsV1().Deployments("default").Update(context.TODO(), dep2, metav1.UpdateOptions{})
	require.NoError(t, err)

	// detect changes
	mocks.ctrl.advanceCanary("podinfo", "default")
	mocks.makeCanaryReady(t)

	// advance
	mocks.ctrl.advanceCanary("podinfo", "default")

	primaryWeight, canaryWeight, mirrored, err := mocks.router.GetRoutes(mocks.canary)
	require.NoError(t, err)
	assert.Equal(t, 90, primaryWeight)
	assert.Equal(t, 10, canaryWeight)
	assert.False(t, mirrored)

	// second update
	dep2.Spec.Template.Spec.ServiceAccountName = "test"
	_, err = mocks.kubeClient.AppsV1().Deployments("default").Update(context.TODO(), dep2, metav1.UpdateOptions{})
	require.NoError(t, err)

	// detect changes
	mocks.ctrl.advanceCanary("podinfo", "default")

	primaryWeight, canaryWeight, mirrored, err = mocks.router.GetRoutes(mocks.canary)
	require.NoError(t, err)
	assert.Equal(t, 100, primaryWeight)
	assert.Equal(t, 0, canaryWeight)
	assert.False(t, mirrored)
}

func TestScheduler_DeploymentPromotion(t *testing.T) {
	mocks := newDeploymentFixture(nil)

	// initializing
	mocks.ctrl.advanceCanary("podinfo", "default")

	// make primary ready
	mocks.makePrimaryReady(t)

	// initialized
	mocks.ctrl.advanceCanary("podinfo", "default")

	// check initialized status
	c, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	assert.Equal(t, flaggerv1.CanaryPhaseInitialized, c.Status.Phase)

	// update
	dep2 := newDeploymentTestDeploymentV2()
	_, err = mocks.kubeClient.AppsV1().Deployments("default").Update(context.TODO(), dep2, metav1.UpdateOptions{})
	require.NoError(t, err)

	// detect pod spec changes
	mocks.ctrl.advanceCanary("podinfo", "default")
	mocks.makeCanaryReady(t)

	config2 := newDeploymentTestConfigMapV2()
	_, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Update(context.TODO(), config2, metav1.UpdateOptions{})
	require.NoError(t, err)

	secret2 := newDeploymentTestSecretV2()
	_, err = mocks.kubeClient.CoreV1().Secrets("default").Update(context.TODO(), secret2, metav1.UpdateOptions{})
	require.NoError(t, err)

	// detect configs changes
	mocks.ctrl.advanceCanary("podinfo", "default")

	_, _, _, err = mocks.router.GetRoutes(mocks.canary)
	require.NoError(t, err)

	primaryWeight := 60
	canaryWeight := 40
	err = mocks.router.SetRoutes(mocks.canary, primaryWeight, canaryWeight, false)
	require.NoError(t, err)

	// advance
	mocks.ctrl.advanceCanary("podinfo", "default")

	// check progressing status
	c, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	assert.Equal(t, flaggerv1.CanaryPhaseProgressing, c.Status.Phase)

	// promote
	mocks.ctrl.advanceCanary("podinfo", "default")

	// check promoting status
	c, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	assert.Equal(t, flaggerv1.CanaryPhasePromoting, c.Status.Phase)

	// finalise
	mocks.ctrl.advanceCanary("podinfo", "default")

	primaryWeight, canaryWeight, mirrored, err := mocks.router.GetRoutes(mocks.canary)
	require.NoError(t, err)
	assert.Equal(t, 100, primaryWeight)
	assert.Equal(t, 0, canaryWeight)
	assert.False(t, mirrored)

	primaryDep, err := mocks.kubeClient.AppsV1().Deployments("default").Get(context.TODO(), "podinfo-primary", metav1.GetOptions{})
	require.NoError(t, err)

	primaryImage := primaryDep.Spec.Template.Spec.Containers[0].Image
	canaryImage := dep2.Spec.Template.Spec.Containers[0].Image
	assert.Equal(t, canaryImage, primaryImage)

	configPrimary, err := mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-env-primary", metav1.GetOptions{})
	require.NoError(t, err)
	assert.Equal(t, config2.Data["color"], configPrimary.Data["color"])

	secretPrimary, err := mocks.kubeClient.CoreV1().Secrets("default").Get(context.TODO(), "podinfo-secret-env-primary", metav1.GetOptions{})
	require.NoError(t, err)
	assert.Equal(t, string(secret2.Data["apiKey"]), string(secretPrimary.Data["apiKey"]))

	// check finalising status
	c, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	assert.Equal(t, flaggerv1.CanaryPhaseFinalising, c.Status.Phase)

	// scale canary to zero
	mocks.ctrl.advanceCanary("podinfo", "default")

	c, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	assert.Equal(t, flaggerv1.CanaryPhaseSucceeded, c.Status.Phase)
}

func TestScheduler_DeploymentMirroring(t *testing.T) {
	mocks := newDeploymentFixture(newDeploymentTestCanaryMirror())

	// initializing
	mocks.ctrl.advanceCanary("podinfo", "default")

	// make primary ready
	mocks.makePrimaryReady(t)

	// initialized
	mocks.ctrl.advanceCanary("podinfo", "default")

	// update
	dep2 := newDeploymentTestDeploymentV2()
	_, err := mocks.kubeClient.AppsV1().Deployments("default").Update(context.TODO(), dep2, metav1.UpdateOptions{})
	require.NoError(t, err)

	// detect pod spec changes
	mocks.ctrl.advanceCanary("podinfo", "default")
	mocks.makeCanaryReady(t)

	// advance
	mocks.ctrl.advanceCanary("podinfo", "default")

	// check if traffic is mirrored to canary
	primaryWeight, canaryWeight, mirrored, err := mocks.router.GetRoutes(mocks.canary)
	require.NoError(t, err)
	assert.Equal(t, 100, primaryWeight)
	assert.Equal(t, 0, canaryWeight)
	assert.True(t, mirrored)

	// advance
	mocks.ctrl.advanceCanary("podinfo", "default")

	// check if traffic is mirrored to canary
	primaryWeight, canaryWeight, mirrored, err = mocks.router.GetRoutes(mocks.canary)
	require.NoError(t, err)
	assert.Equal(t, 90, primaryWeight)
	assert.Equal(t, 10, canaryWeight)
	assert.False(t, mirrored)
}

func TestScheduler_DeploymentABTesting(t *testing.T) {
	mocks := newDeploymentFixture(newDeploymentTestCanaryAB())
	// initializing
	mocks.ctrl.advanceCanary("podinfo", "default")

	// make primary ready
	mocks.makePrimaryReady(t)

	// initialized
	mocks.ctrl.advanceCanary("podinfo", "default")

	// update
	dep2 := newDeploymentTestDeploymentV2()
	_, err := mocks.kubeClient.AppsV1().Deployments("default").Update(context.TODO(), dep2, metav1.UpdateOptions{})
	require.NoError(t, err)

	// detect pod spec changes
	mocks.ctrl.advanceCanary("podinfo", "default")
	mocks.makeCanaryReady(t)

	// advance
	mocks.ctrl.advanceCanary("podinfo", "default")

	// check if traffic is routed to canary
	primaryWeight, canaryWeight, mirrored, err := mocks.router.GetRoutes(mocks.canary)
	require.NoError(t, err)
	assert.Equal(t, 0, primaryWeight)
	assert.Equal(t, 100, canaryWeight)
	assert.False(t, mirrored)

	cd, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)

	// set max iterations
	err = mocks.deployer.SetStatusIterations(cd, 10)
	require.NoError(t, err)

	// advance
	mocks.ctrl.advanceCanary("podinfo", "default")

	// finalising
	mocks.ctrl.advanceCanary("podinfo", "default")

	// check finalising status
	c, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	assert.Equal(t, flaggerv1.CanaryPhaseFinalising, c.Status.Phase)

	// check if the container image tag was updated
	primaryDep, err := mocks.kubeClient.AppsV1().Deployments("default").Get(context.TODO(), "podinfo-primary", metav1.GetOptions{})
	require.NoError(t, err)

	primaryImage := primaryDep.Spec.Template.Spec.Containers[0].Image
	canaryImage := dep2.Spec.Template.Spec.Containers[0].Image
	assert.Equal(t, canaryImage, primaryImage)

	// shutdown canary
	mocks.ctrl.advanceCanary("podinfo", "default")

	// check rollout status
	c, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	assert.Equal(t, flaggerv1.CanaryPhaseSucceeded, c.Status.Phase)
}

func TestScheduler_DeploymentPortDiscovery(t *testing.T) {
	mocks := newDeploymentFixture(nil)

	// enable port discovery
	cd, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	cd.Spec.Service.PortDiscovery = true
	_, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Update(context.TODO(), cd, metav1.UpdateOptions{})
	require.NoError(t, err)

	mocks.ctrl.advanceCanary("podinfo", "default")

	canarySvc, err := mocks.kubeClient.CoreV1().Services("default").Get(context.TODO(), "podinfo-canary", metav1.GetOptions{})
	require.NoError(t, err)
	require.Len(t, canarySvc.Spec.Ports, 3)

	matchPorts := func(lookup string) bool {
		switch lookup {
		case
			"http 9898",
			"http-metrics 8080",
			"tcp-podinfo-2 8888":
			return true
		}
		return false
	}

	for _, port := range canarySvc.Spec.Ports {
		require.True(t, matchPorts(fmt.Sprintf("%s %v", port.Name, port.Port)))
	}
}

func TestScheduler_DeploymentTargetPortNumber(t *testing.T) {
	mocks := newDeploymentFixture(nil)

	cd, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	cd.Spec.Service.Port = 80
	cd.Spec.Service.TargetPort = intstr.FromInt(9898)
	cd.Spec.Service.PortDiscovery = true
	_, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Update(context.TODO(), cd, metav1.UpdateOptions{})
	require.NoError(t, err)

	mocks.ctrl.advanceCanary("podinfo", "default")

	canarySvc, err := mocks.kubeClient.CoreV1().Services("default").Get(context.TODO(), "podinfo-canary", metav1.GetOptions{})
	require.NoError(t, err)
	require.Len(t, canarySvc.Spec.Ports, 3)

	matchPorts := func(lookup string) bool {
		switch lookup {
		case
			"http 80",
			"http-metrics 8080",
			"tcp-podinfo-2 8888":
			return true
		}
		return false
	}

	for _, port := range canarySvc.Spec.Ports {
		require.True(t, matchPorts(fmt.Sprintf("%s %v", port.Name, port.Port)))
	}
}

func TestScheduler_DeploymentTargetPortName(t *testing.T) {
	mocks := newDeploymentFixture(nil)

	cd, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
	require.NoError(t, err)
	cd.Spec.Service.Port = 8080
	cd.Spec.Service.TargetPort = intstr.FromString("http")
	cd.Spec.Service.PortDiscovery = true
	_, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Update(context.TODO(), cd, metav1.UpdateOptions{})
	require.NoError(t, err)

	mocks.ctrl.advanceCanary("podinfo", "default")

	canarySvc, err := mocks.kubeClient.CoreV1().Services("default").Get(context.TODO(), "podinfo-canary", metav1.GetOptions{})
	require.NoError(t, err)
	require.Len(t, canarySvc.Spec.Ports, 3)

	matchPorts := func(lookup string) bool {
		switch lookup {
		case
			"http 8080",
			"http-metrics 8080",
			"tcp-podinfo-2 8888":
			return true
		}
		return false
	}

	for _, port := range canarySvc.Spec.Ports {
		require.True(t, matchPorts(fmt.Sprintf("%s %v", port.Name, port.Port)))
	}
}

func TestScheduler_DeploymentAlerts(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		b, err := io.ReadAll(r.Body)
		require.NoError(t, err)
		var payload = notifier.SlackPayload{}
		err = json.Unmarshal(b, &payload)
		require.NoError(t, err)
		require.Equal(t, "podinfo.default", payload.Attachments[0].AuthorName)
	}))
	defer ts.Close()

	canary := newDeploymentTestCanary()
	canary.Spec.Analysis.Alerts = []flaggerv1.CanaryAlert{
		{
			Name:     "slack-dev",
			Severity: "info",
			ProviderRef: flaggerv1.CrossNamespaceObjectReference{
				Name:      "slack",
				Namespace: "default",
			},
		},
		{
			Name:     "slack-prod",
			Severity: "info",
			ProviderRef: flaggerv1.CrossNamespaceObjectReference{
				Name: "slack",
			},
		},
	}
	mocks := newDeploymentFixture(canary)

	secret := newDeploymentTestAlertProviderSecret()
	secret.Data = map[string][]byte{
		"address": []byte(ts.URL),
	}
	_, err := mocks.kubeClient.CoreV1().Secrets("default").Update(context.TODO(), secret, metav1.UpdateOptions{})
	require.NoError(t, err)

	// init canary
	mocks.ctrl.advanceCanary("podinfo", "default")

	// make primary ready
	mocks.makePrimaryReady(t)

	// initialization done - now send alert
	mocks.ctrl.advanceCanary("podinfo", "default")
}
