/*
 * Copyright 2019 gRPC 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 edsbalancer

import (
	"context"
	"reflect"
	"testing"

	"google.golang.org/grpc/balancer"
	"google.golang.org/grpc/balancer/roundrobin"
	"google.golang.org/grpc/connectivity"
	"google.golang.org/grpc/resolver"
)

var (
	rrBuilder        = balancer.Get(roundrobin.Name)
	testBalancerIDs  = []string{"b1", "b2", "b3"}
	testBackendAddrs = []resolver.Address{{Addr: "1.1.1.1:1"}, {Addr: "2.2.2.2:2"}, {Addr: "3.3.3.3:3"}, {Addr: "4.4.4.4:4"}}
)

// 1 balancer, 1 backend -> 2 backends -> 1 backend.
func TestBalancerGroup_OneRR_AddRemoveBackend(t *testing.T) {
	cc := newTestClientConn(t)
	bg := newBalancerGroup(cc)

	// Add one balancer to group.
	bg.add(testBalancerIDs[0], 1, rrBuilder)
	// Send one resolved address.
	bg.handleResolvedAddrs(testBalancerIDs[0], testBackendAddrs[0:1])

	// Send subconn state change.
	sc1 := <-cc.newSubConnCh
	bg.handleSubConnStateChange(sc1, connectivity.Connecting)
	bg.handleSubConnStateChange(sc1, connectivity.Ready)

	// Test pick with one backend.
	p1 := <-cc.newPickerCh
	for i := 0; i < 5; i++ {
		gotSC, _, _ := p1.Pick(context.Background(), balancer.PickOptions{})
		if !reflect.DeepEqual(gotSC, sc1) {
			t.Fatalf("picker.Pick, got %v, want %v", gotSC, sc1)
		}
	}

	// Send two addresses.
	bg.handleResolvedAddrs(testBalancerIDs[0], testBackendAddrs[0:2])
	// Expect one new subconn, send state update.
	sc2 := <-cc.newSubConnCh
	bg.handleSubConnStateChange(sc2, connectivity.Connecting)
	bg.handleSubConnStateChange(sc2, connectivity.Ready)

	// Test roundrobin pick.
	p2 := <-cc.newPickerCh
	want := []balancer.SubConn{sc1, sc2}
	if err := isRoundRobin(want, func() balancer.SubConn {
		sc, _, _ := p2.Pick(context.Background(), balancer.PickOptions{})
		return sc
	}); err != nil {
		t.Fatalf("want %v, got %v", want, err)
	}

	// Remove the first address.
	bg.handleResolvedAddrs(testBalancerIDs[0], testBackendAddrs[1:2])
	scToRemove := <-cc.removeSubConnCh
	if !reflect.DeepEqual(scToRemove, sc1) {
		t.Fatalf("RemoveSubConn, want %v, got %v", sc1, scToRemove)
	}
	bg.handleSubConnStateChange(scToRemove, connectivity.Shutdown)

	// Test pick with only the second subconn.
	p3 := <-cc.newPickerCh
	for i := 0; i < 5; i++ {
		gotSC, _, _ := p3.Pick(context.Background(), balancer.PickOptions{})
		if !reflect.DeepEqual(gotSC, sc2) {
			t.Fatalf("picker.Pick, got %v, want %v", gotSC, sc2)
		}
	}
}

// 2 balancers, each with 1 backend.
func TestBalancerGroup_TwoRR_OneBackend(t *testing.T) {
	cc := newTestClientConn(t)
	bg := newBalancerGroup(cc)

	// Add two balancers to group and send one resolved address to both
	// balancers.
	bg.add(testBalancerIDs[0], 1, rrBuilder)
	bg.handleResolvedAddrs(testBalancerIDs[0], testBackendAddrs[0:1])
	sc1 := <-cc.newSubConnCh

	bg.add(testBalancerIDs[1], 1, rrBuilder)
	bg.handleResolvedAddrs(testBalancerIDs[1], testBackendAddrs[0:1])
	sc2 := <-cc.newSubConnCh

	// Send state changes for both subconns.
	bg.handleSubConnStateChange(sc1, connectivity.Connecting)
	bg.handleSubConnStateChange(sc1, connectivity.Ready)
	bg.handleSubConnStateChange(sc2, connectivity.Connecting)
	bg.handleSubConnStateChange(sc2, connectivity.Ready)

	// Test roundrobin on the last picker.
	p1 := <-cc.newPickerCh
	want := []balancer.SubConn{sc1, sc2}
	if err := isRoundRobin(want, func() balancer.SubConn {
		sc, _, _ := p1.Pick(context.Background(), balancer.PickOptions{})
		return sc
	}); err != nil {
		t.Fatalf("want %v, got %v", want, err)
	}
}

// 2 balancers, each with more than 1 backends.
func TestBalancerGroup_TwoRR_MoreBackends(t *testing.T) {
	cc := newTestClientConn(t)
	bg := newBalancerGroup(cc)

	// Add two balancers to group and send one resolved address to both
	// balancers.
	bg.add(testBalancerIDs[0], 1, rrBuilder)
	bg.handleResolvedAddrs(testBalancerIDs[0], testBackendAddrs[0:2])
	sc1 := <-cc.newSubConnCh
	sc2 := <-cc.newSubConnCh

	bg.add(testBalancerIDs[1], 1, rrBuilder)
	bg.handleResolvedAddrs(testBalancerIDs[1], testBackendAddrs[2:4])
	sc3 := <-cc.newSubConnCh
	sc4 := <-cc.newSubConnCh

	// Send state changes for both subconns.
	bg.handleSubConnStateChange(sc1, connectivity.Connecting)
	bg.handleSubConnStateChange(sc1, connectivity.Ready)
	bg.handleSubConnStateChange(sc2, connectivity.Connecting)
	bg.handleSubConnStateChange(sc2, connectivity.Ready)
	bg.handleSubConnStateChange(sc3, connectivity.Connecting)
	bg.handleSubConnStateChange(sc3, connectivity.Ready)
	bg.handleSubConnStateChange(sc4, connectivity.Connecting)
	bg.handleSubConnStateChange(sc4, connectivity.Ready)

	// Test roundrobin on the last picker.
	p1 := <-cc.newPickerCh
	want := []balancer.SubConn{sc1, sc2, sc3, sc4}
	if err := isRoundRobin(want, func() balancer.SubConn {
		sc, _, _ := p1.Pick(context.Background(), balancer.PickOptions{})
		return sc
	}); err != nil {
		t.Fatalf("want %v, got %v", want, err)
	}

	// Turn sc2's connection down, should be RR between balancers.
	bg.handleSubConnStateChange(sc2, connectivity.TransientFailure)
	p2 := <-cc.newPickerCh
	// Expect two sc1's in the result, because balancer1 will be picked twice,
	// but there's only one sc in it.
	want = []balancer.SubConn{sc1, sc1, sc3, sc4}
	if err := isRoundRobin(want, func() balancer.SubConn {
		sc, _, _ := p2.Pick(context.Background(), balancer.PickOptions{})
		return sc
	}); err != nil {
		t.Fatalf("want %v, got %v", want, err)
	}

	// Remove sc3's addresses.
	bg.handleResolvedAddrs(testBalancerIDs[1], testBackendAddrs[3:4])
	scToRemove := <-cc.removeSubConnCh
	if !reflect.DeepEqual(scToRemove, sc3) {
		t.Fatalf("RemoveSubConn, want %v, got %v", sc3, scToRemove)
	}
	bg.handleSubConnStateChange(scToRemove, connectivity.Shutdown)
	p3 := <-cc.newPickerCh
	want = []balancer.SubConn{sc1, sc4}
	if err := isRoundRobin(want, func() balancer.SubConn {
		sc, _, _ := p3.Pick(context.Background(), balancer.PickOptions{})
		return sc
	}); err != nil {
		t.Fatalf("want %v, got %v", want, err)
	}

	// Turn sc1's connection down.
	bg.handleSubConnStateChange(sc1, connectivity.TransientFailure)
	p4 := <-cc.newPickerCh
	want = []balancer.SubConn{sc4}
	if err := isRoundRobin(want, func() balancer.SubConn {
		sc, _, _ := p4.Pick(context.Background(), balancer.PickOptions{})
		return sc
	}); err != nil {
		t.Fatalf("want %v, got %v", want, err)
	}

	// Turn last connection to connecting.
	bg.handleSubConnStateChange(sc4, connectivity.Connecting)
	p5 := <-cc.newPickerCh
	for i := 0; i < 5; i++ {
		if _, _, err := p5.Pick(context.Background(), balancer.PickOptions{}); err != balancer.ErrNoSubConnAvailable {
			t.Fatalf("want pick error %v, got %v", balancer.ErrNoSubConnAvailable, err)
		}
	}

	// Turn all connections down.
	bg.handleSubConnStateChange(sc4, connectivity.TransientFailure)
	p6 := <-cc.newPickerCh
	for i := 0; i < 5; i++ {
		if _, _, err := p6.Pick(context.Background(), balancer.PickOptions{}); err != balancer.ErrTransientFailure {
			t.Fatalf("want pick error %v, got %v", balancer.ErrTransientFailure, err)
		}
	}
}

// 2 balancers with different weights.
func TestBalancerGroup_TwoRR_DifferentWeight_MoreBackends(t *testing.T) {
	cc := newTestClientConn(t)
	bg := newBalancerGroup(cc)

	// Add two balancers to group and send two resolved addresses to both
	// balancers.
	bg.add(testBalancerIDs[0], 2, rrBuilder)
	bg.handleResolvedAddrs(testBalancerIDs[0], testBackendAddrs[0:2])
	sc1 := <-cc.newSubConnCh
	sc2 := <-cc.newSubConnCh

	bg.add(testBalancerIDs[1], 1, rrBuilder)
	bg.handleResolvedAddrs(testBalancerIDs[1], testBackendAddrs[2:4])
	sc3 := <-cc.newSubConnCh
	sc4 := <-cc.newSubConnCh

	// Send state changes for both subconns.
	bg.handleSubConnStateChange(sc1, connectivity.Connecting)
	bg.handleSubConnStateChange(sc1, connectivity.Ready)
	bg.handleSubConnStateChange(sc2, connectivity.Connecting)
	bg.handleSubConnStateChange(sc2, connectivity.Ready)
	bg.handleSubConnStateChange(sc3, connectivity.Connecting)
	bg.handleSubConnStateChange(sc3, connectivity.Ready)
	bg.handleSubConnStateChange(sc4, connectivity.Connecting)
	bg.handleSubConnStateChange(sc4, connectivity.Ready)

	// Test roundrobin on the last picker.
	p1 := <-cc.newPickerCh
	want := []balancer.SubConn{sc1, sc1, sc2, sc2, sc3, sc4}
	if err := isRoundRobin(want, func() balancer.SubConn {
		sc, _, _ := p1.Pick(context.Background(), balancer.PickOptions{})
		return sc
	}); err != nil {
		t.Fatalf("want %v, got %v", want, err)
	}
}

// totally 3 balancers, add/remove balancer.
func TestBalancerGroup_ThreeRR_RemoveBalancer(t *testing.T) {
	cc := newTestClientConn(t)
	bg := newBalancerGroup(cc)

	// Add three balancers to group and send one resolved address to both
	// balancers.
	bg.add(testBalancerIDs[0], 1, rrBuilder)
	bg.handleResolvedAddrs(testBalancerIDs[0], testBackendAddrs[0:1])
	sc1 := <-cc.newSubConnCh

	bg.add(testBalancerIDs[1], 1, rrBuilder)
	bg.handleResolvedAddrs(testBalancerIDs[1], testBackendAddrs[1:2])
	sc2 := <-cc.newSubConnCh

	bg.add(testBalancerIDs[2], 1, rrBuilder)
	bg.handleResolvedAddrs(testBalancerIDs[2], testBackendAddrs[1:2])
	sc3 := <-cc.newSubConnCh

	// Send state changes for both subconns.
	bg.handleSubConnStateChange(sc1, connectivity.Connecting)
	bg.handleSubConnStateChange(sc1, connectivity.Ready)
	bg.handleSubConnStateChange(sc2, connectivity.Connecting)
	bg.handleSubConnStateChange(sc2, connectivity.Ready)
	bg.handleSubConnStateChange(sc3, connectivity.Connecting)
	bg.handleSubConnStateChange(sc3, connectivity.Ready)

	p1 := <-cc.newPickerCh
	want := []balancer.SubConn{sc1, sc2, sc3}
	if err := isRoundRobin(want, func() balancer.SubConn {
		sc, _, _ := p1.Pick(context.Background(), balancer.PickOptions{})
		return sc
	}); err != nil {
		t.Fatalf("want %v, got %v", want, err)
	}

	// Remove the second balancer, while the others two are ready.
	bg.remove(testBalancerIDs[1])
	scToRemove := <-cc.removeSubConnCh
	if !reflect.DeepEqual(scToRemove, sc2) {
		t.Fatalf("RemoveSubConn, want %v, got %v", sc2, scToRemove)
	}
	p2 := <-cc.newPickerCh
	want = []balancer.SubConn{sc1, sc3}
	if err := isRoundRobin(want, func() balancer.SubConn {
		sc, _, _ := p2.Pick(context.Background(), balancer.PickOptions{})
		return sc
	}); err != nil {
		t.Fatalf("want %v, got %v", want, err)
	}

	// move balancer 3 into transient failure.
	bg.handleSubConnStateChange(sc3, connectivity.TransientFailure)
	// Remove the first balancer, while the third is transient failure.
	bg.remove(testBalancerIDs[0])
	scToRemove = <-cc.removeSubConnCh
	if !reflect.DeepEqual(scToRemove, sc1) {
		t.Fatalf("RemoveSubConn, want %v, got %v", sc1, scToRemove)
	}
	p3 := <-cc.newPickerCh
	for i := 0; i < 5; i++ {
		if _, _, err := p3.Pick(context.Background(), balancer.PickOptions{}); err != balancer.ErrTransientFailure {
			t.Fatalf("want pick error %v, got %v", balancer.ErrTransientFailure, err)
		}
	}
}

// 2 balancers, change balancer weight.
func TestBalancerGroup_TwoRR_ChangeWeight_MoreBackends(t *testing.T) {
	cc := newTestClientConn(t)
	bg := newBalancerGroup(cc)

	// Add two balancers to group and send two resolved addresses to both
	// balancers.
	bg.add(testBalancerIDs[0], 2, rrBuilder)
	bg.handleResolvedAddrs(testBalancerIDs[0], testBackendAddrs[0:2])
	sc1 := <-cc.newSubConnCh
	sc2 := <-cc.newSubConnCh

	bg.add(testBalancerIDs[1], 1, rrBuilder)
	bg.handleResolvedAddrs(testBalancerIDs[1], testBackendAddrs[2:4])
	sc3 := <-cc.newSubConnCh
	sc4 := <-cc.newSubConnCh

	// Send state changes for both subconns.
	bg.handleSubConnStateChange(sc1, connectivity.Connecting)
	bg.handleSubConnStateChange(sc1, connectivity.Ready)
	bg.handleSubConnStateChange(sc2, connectivity.Connecting)
	bg.handleSubConnStateChange(sc2, connectivity.Ready)
	bg.handleSubConnStateChange(sc3, connectivity.Connecting)
	bg.handleSubConnStateChange(sc3, connectivity.Ready)
	bg.handleSubConnStateChange(sc4, connectivity.Connecting)
	bg.handleSubConnStateChange(sc4, connectivity.Ready)

	// Test roundrobin on the last picker.
	p1 := <-cc.newPickerCh
	want := []balancer.SubConn{sc1, sc1, sc2, sc2, sc3, sc4}
	if err := isRoundRobin(want, func() balancer.SubConn {
		sc, _, _ := p1.Pick(context.Background(), balancer.PickOptions{})
		return sc
	}); err != nil {
		t.Fatalf("want %v, got %v", want, err)
	}

	bg.changeWeight(testBalancerIDs[0], 3)

	// Test roundrobin with new weight.
	p2 := <-cc.newPickerCh
	want = []balancer.SubConn{sc1, sc1, sc1, sc2, sc2, sc2, sc3, sc4}
	if err := isRoundRobin(want, func() balancer.SubConn {
		sc, _, _ := p2.Pick(context.Background(), balancer.PickOptions{})
		return sc
	}); err != nil {
		t.Fatalf("want %v, got %v", want, err)
	}
}
