// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package router_test

import (
	"fmt"
	"math/rand"
	"net"
	"strings"
	"testing"

	"github.com/hashicorp/consul/agent/metadata"
	"github.com/hashicorp/consul/agent/router"
	"github.com/hashicorp/consul/sdk/testutil"
)

type fauxAddr struct {
	addr string
}

func (a *fauxAddr) Network() string {
	return "faux"
}

func (a *fauxAddr) String() string {
	return a.addr
}

type fauxConnPool struct {
	// failPct between 0.0 and 1.0 == pct of time a Ping should fail.
	failPct float64

	// failAddr fail whenever we see this address.
	failAddr net.Addr
}

func (cp *fauxConnPool) Ping(dc string, nodeName string, addr net.Addr) (bool, error) {
	var success bool

	successProb := rand.Float64()
	if successProb > cp.failPct {
		success = true
	}

	if cp.failAddr != nil && addr.String() == cp.failAddr.String() {
		success = false
	}

	return success, nil
}

type fauxSerf struct {
}

func (s *fauxSerf) NumNodes() int {
	return 16384
}

func testManager(t testing.TB) (m *router.Manager) {
	logger := testutil.Logger(t)
	shutdownCh := make(chan struct{})
	m = router.New(logger, shutdownCh, &fauxSerf{}, &fauxConnPool{}, "", noopRebalancer)
	return m
}

func noopRebalancer() {}

func testManagerFailProb(t testing.TB, failPct float64) (m *router.Manager) {
	logger := testutil.Logger(t)
	shutdownCh := make(chan struct{})
	m = router.New(logger, shutdownCh, &fauxSerf{}, &fauxConnPool{failPct: failPct}, "", noopRebalancer)
	return m
}

func testManagerFailAddr(t testing.TB, failAddr net.Addr) (m *router.Manager) {
	logger := testutil.Logger(t)
	shutdownCh := make(chan struct{})
	m = router.New(logger, shutdownCh, &fauxSerf{}, &fauxConnPool{failAddr: failAddr}, "", noopRebalancer)
	return m
}

// func (m *Manager) AddServer(server *metadata.Server) {
func TestServers_AddServer(t *testing.T) {
	m := testManager(t)
	var num int
	num = m.NumServers()
	if num != 0 {
		t.Fatalf("Expected zero servers to start")
	}

	s1 := &metadata.Server{Name: "s1"}
	m.AddServer(s1)
	num = m.NumServers()
	if num != 1 {
		t.Fatalf("Expected one server")
	}

	m.AddServer(s1)
	num = m.NumServers()
	if num != 1 {
		t.Fatalf("Expected one server (still)")
	}

	s2 := &metadata.Server{Name: "s2"}
	m.AddServer(s2)
	num = m.NumServers()
	if num != 2 {
		t.Fatalf("Expected two servers")
	}
}

// func (m *Manager) IsOffline() bool {
func TestServers_IsOffline(t *testing.T) {
	m := testManager(t)
	if !m.IsOffline() {
		t.Fatalf("bad")
	}

	s1 := &metadata.Server{Name: "s1"}
	m.AddServer(s1)
	if m.IsOffline() {
		t.Fatalf("bad")
	}
	m.RebalanceServers()
	if m.IsOffline() {
		t.Fatalf("bad")
	}
	m.RemoveServer(s1)
	m.RebalanceServers()
	if !m.IsOffline() {
		t.Fatalf("bad")
	}

	const failPct = 0.5
	m = testManagerFailProb(t, failPct)
	m.AddServer(s1)
	var on, off int
	for i := 0; i < 100; i++ {
		m.RebalanceServers()
		if m.IsOffline() {
			off++
		} else {
			on++
		}
	}
	if on == 0 || off == 0 {
		t.Fatalf("bad: %d %d", on, off)
	}
}

// func (m *Manager) FindServer() (server *metadata.Server) {
func TestServers_FindServer(t *testing.T) {
	m := testManager(t)

	if m.FindServer() != nil {
		t.Fatalf("Expected nil return")
	}

	m.AddServer(&metadata.Server{Name: "s1"})
	if m.NumServers() != 1 {
		t.Fatalf("Expected one server")
	}

	s1 := m.FindServer()
	if s1 == nil {
		t.Fatalf("Expected non-nil server")
	}
	if s1.Name != "s1" {
		t.Fatalf("Expected s1 server")
	}

	s1 = m.FindServer()
	if s1 == nil || s1.Name != "s1" {
		t.Fatalf("Expected s1 server (still)")
	}

	m.AddServer(&metadata.Server{Name: "s2"})
	if m.NumServers() != 2 {
		t.Fatalf("Expected two servers")
	}
	s1 = m.FindServer()
	if s1 == nil || s1.Name != "s1" {
		t.Fatalf("Expected s1 server (still)")
	}

	m.NotifyFailedServer(s1)
	s2 := m.FindServer()
	if s2 == nil || s2.Name != "s2" {
		t.Fatalf("Expected s2 server")
	}

	m.NotifyFailedServer(s2)
	s1 = m.FindServer()
	if s1 == nil || s1.Name != "s1" {
		t.Fatalf("Expected s1 server")
	}
}

func TestServers_New(t *testing.T) {
	logger := testutil.Logger(t)
	shutdownCh := make(chan struct{})
	m := router.New(logger, shutdownCh, &fauxSerf{}, &fauxConnPool{}, "", noopRebalancer)
	if m == nil {
		t.Fatalf("Manager nil")
	}
}

// func (m *Manager) NotifyFailedServer(server *metadata.Server) {
func TestServers_NotifyFailedServer(t *testing.T) {
	m := testManager(t)

	if m.NumServers() != 0 {
		t.Fatalf("Expected zero servers to start")
	}

	s1 := &metadata.Server{Name: "s1"}
	s2 := &metadata.Server{Name: "s2"}

	// Try notifying for a server that is not managed by Manager
	m.NotifyFailedServer(s1)
	if m.NumServers() != 0 {
		t.Fatalf("Expected zero servers to start")
	}
	m.AddServer(s1)

	// Test again w/ a server not in the list
	m.NotifyFailedServer(s2)
	if m.NumServers() != 1 {
		t.Fatalf("Expected one server")
	}

	m.AddServer(s2)
	if m.NumServers() != 2 {
		t.Fatalf("Expected two servers")
	}

	s1 = m.FindServer()
	if s1 == nil || s1.Name != "s1" {
		t.Fatalf("Expected s1 server")
	}

	m.NotifyFailedServer(s2)
	s1 = m.FindServer()
	if s1 == nil || s1.Name != "s1" {
		t.Fatalf("Expected s1 server (still)")
	}

	m.NotifyFailedServer(s1)
	s2 = m.FindServer()
	if s2 == nil || s2.Name != "s2" {
		t.Fatalf("Expected s2 server")
	}

	m.NotifyFailedServer(s2)
	s1 = m.FindServer()
	if s1 == nil || s1.Name != "s1" {
		t.Fatalf("Expected s1 server")
	}
}

// func (m *Manager) NumServers() (numServers int) {
func TestServers_NumServers(t *testing.T) {
	m := testManager(t)
	var num int
	num = m.NumServers()
	if num != 0 {
		t.Fatalf("Expected zero servers to start")
	}

	s := &metadata.Server{}
	m.AddServer(s)
	num = m.NumServers()
	if num != 1 {
		t.Fatalf("Expected one server after AddServer")
	}
}

// func (m *Manager) RebalanceServers() {
func TestServers_RebalanceServers(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	const failPct = 0.5
	m := testManagerFailProb(t, failPct)
	const maxServers = 100
	const numShuffleTests = 100
	const uniquePassRate = 0.5

	// Make a huge list of nodes.
	for i := 0; i < maxServers; i++ {
		nodeName := fmt.Sprintf("s%02d", i)
		m.AddServer(&metadata.Server{Name: nodeName})
	}

	// Keep track of how many unique shuffles we get.
	uniques := make(map[string]struct{}, maxServers)
	for i := 0; i < numShuffleTests; i++ {
		m.RebalanceServers()

		var names []string
		for j := 0; j < maxServers; j++ {
			server := m.FindServer()
			m.NotifyFailedServer(server)
			names = append(names, server.Name)
		}
		key := strings.Join(names, "|")
		uniques[key] = struct{}{}
	}

	// We have to allow for the fact that there won't always be a unique
	// shuffle each pass, so we just look for smell here without the test
	// being flaky.
	if len(uniques) < int(maxServers*uniquePassRate) {
		t.Fatalf("unique shuffle ratio too low: %d/%d", len(uniques), maxServers)
	}
}

func TestServers_RebalanceServers_AvoidFailed(t *testing.T) {
	// Do a large number of rebalances with one failed server in the
	// list and make sure we never have that one selected afterwards.
	// This was added when fixing #3463, when we were just doing the
	// shuffle and not actually cycling servers. We do a large number
	// of trials with a small number of servers to try to make sure
	// the shuffle alone won't give the right answer.
	servers := []*metadata.Server{
		{Name: "s1", Addr: &fauxAddr{"s1"}},
		{Name: "s2", Addr: &fauxAddr{"s2"}},
		{Name: "s3", Addr: &fauxAddr{"s3"}},
	}
	for i := 0; i < 100; i++ {
		m := testManagerFailAddr(t, &fauxAddr{"s2"})
		for _, s := range servers {
			m.AddServer(s)
		}

		m.RebalanceServers()
		if front := m.FindServer().Name; front == "s2" {
			t.Fatalf("should have avoided the failed server")
		}
	}
}

// func (m *Manager) RemoveServer(server *metadata.Server) {
func TestManager_RemoveServer(t *testing.T) {
	const nodeNameFmt = "s%02d"
	m := testManager(t)

	if m.NumServers() != 0 {
		t.Fatalf("Expected zero servers to start")
	}

	// Test removing server before its added
	nodeName := fmt.Sprintf(nodeNameFmt, 1)
	s1 := &metadata.Server{Name: nodeName}
	m.RemoveServer(s1)
	m.AddServer(s1)

	nodeName = fmt.Sprintf(nodeNameFmt, 2)
	s2 := &metadata.Server{Name: nodeName}
	m.RemoveServer(s2)
	m.AddServer(s2)

	const maxServers = 19
	// Already added two servers above
	for i := maxServers; i > 2; i-- {
		nodeName := fmt.Sprintf(nodeNameFmt, i)
		server := &metadata.Server{Name: nodeName}
		m.AddServer(server)
	}
	if m.NumServers() != maxServers {
		t.Fatalf("Expected %d servers, received %d", maxServers, m.NumServers())
	}

	m.RebalanceServers()

	if m.NumServers() != maxServers {
		t.Fatalf("Expected %d servers, received %d", maxServers, m.NumServers())
	}

	findServer := func(server *metadata.Server) bool {
		for i := m.NumServers(); i > 0; i-- {
			s := m.FindServer()
			if s == server {
				return true
			}
		}
		return false
	}

	expectedNumServers := maxServers
	removedServers := make([]*metadata.Server, 0, maxServers)

	// Remove servers from the front of the list
	for i := 3; i > 0; i-- {
		server := m.FindServer()
		if server == nil {
			t.Fatalf("FindServer returned nil")
		}
		m.RemoveServer(server)
		expectedNumServers--
		if m.NumServers() != expectedNumServers {
			t.Fatalf("Expected %d servers (got %d)", expectedNumServers, m.NumServers())
		}
		if findServer(server) == true {
			t.Fatalf("Did not expect to find server %s after removal from the front", server.Name)
		}
		removedServers = append(removedServers, server)
	}

	// Remove server from the end of the list
	for i := 3; i > 0; i-- {
		server := m.FindServer()
		m.NotifyFailedServer(server)
		m.RemoveServer(server)
		expectedNumServers--
		if m.NumServers() != expectedNumServers {
			t.Fatalf("Expected %d servers (got %d)", expectedNumServers, m.NumServers())
		}
		if findServer(server) == true {
			t.Fatalf("Did not expect to find server %s", server.Name)
		}
		removedServers = append(removedServers, server)
	}

	// Remove server from the middle of the list
	for i := 3; i > 0; i-- {
		server := m.FindServer()
		m.NotifyFailedServer(server)
		server2 := m.FindServer()
		m.NotifyFailedServer(server2) // server2 now at end of the list

		m.RemoveServer(server)
		expectedNumServers--
		if m.NumServers() != expectedNumServers {
			t.Fatalf("Expected %d servers (got %d)", expectedNumServers, m.NumServers())
		}
		if findServer(server) == true {
			t.Fatalf("Did not expect to find server %s", server.Name)
		}
		removedServers = append(removedServers, server)
	}

	if m.NumServers()+len(removedServers) != maxServers {
		t.Fatalf("Expected %d+%d=%d servers", m.NumServers(), len(removedServers), maxServers)
	}

	// Drain the remaining servers from the middle
	for i := m.NumServers(); i > 0; i-- {
		server := m.FindServer()
		m.NotifyFailedServer(server)
		server2 := m.FindServer()
		m.NotifyFailedServer(server2) // server2 now at end of the list
		m.RemoveServer(server)
		removedServers = append(removedServers, server)
	}

	if m.NumServers() != 0 {
		t.Fatalf("Expected an empty server list")
	}
	if len(removedServers) != maxServers {
		t.Fatalf("Expected all servers to be in removed server list")
	}
}

// func (m *Manager) Start() {
