package multi_master

import (
	"fmt"
	"testing"
	"time"

	"github.com/seaweedfs/seaweedfs/weed/pb"
)

var raftImplementations = []struct {
	name          string
	raftHashicorp bool
}{
	{"goraft", false},
	{"hashicorp", true},
}

// TestFreshClusterFormsWithoutALeader covers the other half of the bootstrap
// decision: with no leader anywhere, three masters starting together still have
// to mint a cluster between them.
func TestFreshClusterFormsWithoutALeader(t *testing.T) {
	for _, impl := range raftImplementations {
		t.Run(impl.name, func(t *testing.T) {
			mc := NewMasterCluster(t, impl.raftHashicorp)
			for i := range 3 {
				mc.StartNode(i)
			}

			if _, err := waitForCommonLeader(mc, waitTimeout); err != nil {
				mc.DumpLogs()
				t.Fatalf("fresh cluster did not converge: %v", err)
			}
			for i := range 3 {
				if err := waitForPeerCount(mc, i, 2, waitTimeout); err != nil {
					mc.DumpLogs()
					t.Fatalf("master %d does not see the full cluster: %v", i, err)
				}
			}
		})
	}
}

// TestScaleUpOntoExistingLeader mirrors a Kubernetes master StatefulSet whose
// replica count goes back from one to three: master 0 keeps running as the
// leader of a single-peer cluster while two fresh masters come up pointing at
// all three. The newcomers start with an empty raft log, so neither raft
// implementation lets them campaign — the sitting leader has to admit them.
func TestScaleUpOntoExistingLeader(t *testing.T) {
	for _, impl := range raftImplementations {
		t.Run(impl.name, func(t *testing.T) {
			mc := NewMasterCluster(t, impl.raftHashicorp)

			// Master 0 is alone in its peer list, the way the operator renders
			// -peers when spec.master.replicas is 1.
			mc.SetNodePeers(0, mc.NodeAddress(0))
			mc.StartNode(0)
			if err := mc.WaitForLeader(waitTimeout); err != nil {
				mc.DumpLogs()
				t.Fatalf("single master did not become leader: %v", err)
			}

			// Scale up. These two carry the full peer list; master 0 still
			// runs with the old one and has never heard of them.
			mc.StartNode(1)
			mc.StartNode(2)

			leader, err := waitForCommonLeader(mc, waitTimeout)
			if err != nil {
				mc.DumpLogs()
				t.Fatalf("masters did not converge after scaling up: %v", err)
			}
			if leader != mc.NodeAddress(0) {
				t.Fatalf("leader moved to %s, want the sitting leader %s", leader, mc.NodeAddress(0))
			}

			if err := waitForPeerCount(mc, 0, 2, waitTimeout); err != nil {
				mc.DumpLogs()
				t.Fatalf("leader did not admit both new masters: %v", err)
			}
		})
	}
}

// waitForCommonLeader waits until every running master names the same leader,
// and returns it.
func waitForCommonLeader(mc *MasterCluster, timeout time.Duration) (string, error) {
	var lastErr error
	deadline := time.Now().Add(timeout)
	for time.Now().Before(deadline) {
		leader, err := commonLeader(mc)
		if err == nil {
			return leader, nil
		}
		lastErr = err
		time.Sleep(waitTick)
	}
	return "", lastErr
}

func commonLeader(mc *MasterCluster) (string, error) {
	agreed := ""
	for i := range 3 {
		if !mc.IsNodeRunning(i) {
			continue
		}
		cs, err := mc.GetClusterStatus(i)
		if err != nil {
			return "", err
		}
		leader := pb.ServerAddress(cs.Leader).ToHttpAddress()
		if leader == "" {
			return "", fmt.Errorf("master %d has no leader", i)
		}
		if agreed == "" {
			agreed = leader
		} else if agreed != leader {
			return "", fmt.Errorf("masters disagree on the leader: %s and %s", agreed, leader)
		}
	}
	if agreed == "" {
		return "", fmt.Errorf("no master is running")
	}
	return agreed, nil
}

// waitForPeerCount waits until node i reports the given number of raft peers.
// The count excludes the node itself.
func waitForPeerCount(mc *MasterCluster, i, want int, timeout time.Duration) error {
	got := -1
	deadline := time.Now().Add(timeout)
	for time.Now().Before(deadline) {
		cs, err := mc.GetClusterStatus(i)
		if err == nil {
			got = peerCountExcludingSelf(cs.Peers, mc.NodeAddress(i))
			if got == want {
				return nil
			}
		}
		time.Sleep(waitTick)
	}
	return fmt.Errorf("master %d reports %d peers, want %d", i, got, want)
}

func peerCountExcludingSelf(peers []string, self string) int {
	count := 0
	for _, peer := range peers {
		if pb.ServerAddress(peer).ToHttpAddress() != self {
			count++
		}
	}
	return count
}

// TestRaftBootstrapKeepsExistingCluster covers a master restarting under
// -raftBootstrap, the way the helm chart renders it on every master on every
// roll. Bootstrapping is genesis: seeding a second cluster over committed raft
// state mints a rival TopologyId, and the split-brain guard then Fatals every
// master that still holds the first one.
func TestRaftBootstrapKeepsExistingCluster(t *testing.T) {
	for _, impl := range raftImplementations {
		t.Run(impl.name, func(t *testing.T) {
			mc := NewMasterCluster(t, impl.raftHashicorp)
			for i := range 3 {
				mc.SetRaftBootstrap(i)
				mc.StartNode(i)
			}
			before, err := mc.WaitForTopologyId(waitTimeout)
			if err != nil {
				mc.DumpLogs()
				t.Fatalf("cluster did not mint a TopologyId: %v", err)
			}

			for i := range 3 {
				mc.StopNode(i)
			}
			for i := range 3 {
				mc.StartNode(i)
			}

			after, err := mc.WaitForTopologyId(waitTimeout)
			if err != nil {
				mc.DumpLogs()
				t.Fatalf("cluster did not come back after a restart: %v", err)
			}
			if after != before {
				mc.DumpLogs()
				t.Fatalf("-raftBootstrap re-seeded the cluster: TopologyId %s became %s", before, after)
			}

			// The leader answers for the whole cluster, so a follower that
			// forked is only visible in its own log.
			seen, err := mc.WaitForNodeTopologyIds(waitTimeout)
			if err != nil {
				mc.DumpLogs()
				t.Fatal(err)
			}
			for i, ids := range seen {
				for _, id := range ids {
					if id != before {
						mc.DumpLogs()
						t.Fatalf("master %d saw TopologyId %s, want %s", i, id, before)
					}
				}
				if mc.LogContains(i, "Split-brain detected") {
					mc.DumpLogs()
					t.Fatalf("master %d hit the split-brain guard", i)
				}
			}
		})
	}
}
