// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package agent

import (
	"bytes"
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/hashicorp/consul/testrpc"
	"github.com/hashicorp/raft"
	autopilot "github.com/hashicorp/raft-autopilot"
	"github.com/stretchr/testify/require"

	"github.com/hashicorp/consul/agent/structs"
	"github.com/hashicorp/consul/api"
	"github.com/hashicorp/consul/sdk/testutil/retry"
)

func TestOperator_RaftConfiguration(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	a := NewTestAgent(t, "")
	defer a.Shutdown()

	body := bytes.NewBuffer(nil)
	req, _ := http.NewRequest("GET", "/v1/operator/raft/configuration", body)
	resp := httptest.NewRecorder()
	obj, err := a.srv.OperatorRaftConfiguration(resp, req)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	if resp.Code != 200 {
		t.Fatalf("bad code: %d", resp.Code)
	}
	out, ok := obj.(structs.RaftConfigurationResponse)
	if !ok {
		t.Fatalf("unexpected: %T", obj)
	}
	if len(out.Servers) != 1 ||
		!out.Servers[0].Leader ||
		!out.Servers[0].Voter {
		t.Fatalf("bad: %v", out)
	}
}

func TestOperator_RaftPeer(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	t.Run("", func(t *testing.T) {
		a := NewTestAgent(t, "")
		defer a.Shutdown()

		body := bytes.NewBuffer(nil)
		req, _ := http.NewRequest("DELETE", "/v1/operator/raft/peer?address=nope", body)
		// If we get this error, it proves we sent the address all the
		// way through.
		resp := httptest.NewRecorder()
		_, err := a.srv.OperatorRaftPeer(resp, req)
		if err == nil || !strings.Contains(err.Error(),
			"address \"nope\" was not found in the Raft configuration") {
			t.Fatalf("err: %v", err)
		}
	})

	t.Run("", func(t *testing.T) {
		a := NewTestAgent(t, "")
		defer a.Shutdown()

		body := bytes.NewBuffer(nil)
		req, _ := http.NewRequest("DELETE", "/v1/operator/raft/peer?id=nope", body)
		// If we get this error, it proves we sent the ID all the
		// way through.
		resp := httptest.NewRecorder()
		_, err := a.srv.OperatorRaftPeer(resp, req)
		if err == nil || !strings.Contains(err.Error(),
			"id \"nope\" was not found in the Raft configuration") {
			t.Fatalf("err: %v", err)
		}
	})
}

func TestOperator_KeyringInstall(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	oldKey := "H3/9gBxcKKRf45CaI2DlRg=="
	newKey := "z90lFx3sZZLtTOkutXcwYg=="
	a := NewTestAgent(t, `
		encrypt = "`+oldKey+`"
	`)
	defer a.Shutdown()

	body := bytes.NewBufferString(fmt.Sprintf("{\"Key\":\"%s\"}", newKey))
	req, _ := http.NewRequest("POST", "/v1/operator/keyring", body)
	resp := httptest.NewRecorder()
	_, err := a.srv.OperatorKeyringEndpoint(resp, req)
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	listResponse, err := a.ListKeys("", false, 0)
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if len(listResponse.Responses) != 2 {
		t.Fatalf("bad: %d", len(listResponse.Responses))
	}

	for _, response := range listResponse.Responses {
		count, ok := response.Keys[newKey]
		if !ok {
			t.Fatalf("bad: %v", response.Keys)
		}
		if count != response.NumNodes {
			t.Fatalf("bad: %d, %d", count, response.NumNodes)
		}
	}
}

func TestOperator_KeyringList(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	key := "H3/9gBxcKKRf45CaI2DlRg=="
	a := NewTestAgent(t, `
		encrypt = "`+key+`"
	`)
	defer a.Shutdown()

	req, _ := http.NewRequest("GET", "/v1/operator/keyring", nil)
	resp := httptest.NewRecorder()
	r, err := a.srv.OperatorKeyringEndpoint(resp, req)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	responses, ok := r.([]*structs.KeyringResponse)
	if !ok {
		t.Fatalf("err: %v", !ok)
	}

	// Check that we get both a LAN and WAN response, and that they both only
	// contain the original key
	if len(responses) != 2 {
		t.Fatalf("bad: %d", len(responses))
	}

	// WAN
	if len(responses[0].Keys) != 1 {
		t.Fatalf("bad: %d", len(responses[0].Keys))
	}
	if !responses[0].WAN {
		t.Fatalf("bad: %v", responses[0].WAN)
	}
	if _, ok := responses[0].Keys[key]; !ok {
		t.Fatalf("bad: %v", ok)
	}

	// LAN
	if len(responses[1].Keys) != 1 {
		t.Fatalf("bad: %d", len(responses[1].Keys))
	}
	if responses[1].WAN {
		t.Fatalf("bad: %v", responses[1].WAN)
	}
	if _, ok := responses[1].Keys[key]; !ok {
		t.Fatalf("bad: %v", ok)
	}
}
func TestOperator_KeyringListLocalOnly(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	key := "H3/9gBxcKKRf45CaI2DlRg=="
	a := NewTestAgent(t, `
		encrypt = "`+key+`"
	`)
	defer a.Shutdown()

	req, _ := http.NewRequest("GET", "/v1/operator/keyring?local-only=1", nil)
	resp := httptest.NewRecorder()
	r, err := a.srv.OperatorKeyringEndpoint(resp, req)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	responses, ok := r.([]*structs.KeyringResponse)
	if !ok {
		t.Fatalf("err: %v", !ok)
	}

	// Check that we only get a LAN response with the original key
	if len(responses) != 1 {
		for _, r := range responses {
			fmt.Println(r)
		}
		t.Fatalf("bad: %d", len(responses))
	}

	// LAN
	if len(responses[0].Keys) != 1 {
		t.Fatalf("bad: %d", len(responses[1].Keys))
	}
	if responses[0].WAN {
		t.Fatalf("bad: %v", responses[1].WAN)
	}
	if _, ok := responses[0].Keys[key]; !ok {
		t.Fatalf("bad: %v", ok)
	}
}

func TestOperator_KeyringRemove(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	key := "H3/9gBxcKKRf45CaI2DlRg=="
	tempKey := "z90lFx3sZZLtTOkutXcwYg=="
	a := NewTestAgent(t, `
		encrypt = "`+key+`"
	`)
	defer a.Shutdown()

	_, err := a.InstallKey(tempKey, "", 0)
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	// Make sure the temp key is installed
	list, err := a.ListKeys("", false, 0)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	responses := list.Responses
	if len(responses) != 2 {
		t.Fatalf("bad: %d", len(responses))
	}
	for _, response := range responses {
		if len(response.Keys) != 2 {
			t.Fatalf("bad: %d", len(response.Keys))
		}
		if _, ok := response.Keys[tempKey]; !ok {
			t.Fatalf("bad: %v", ok)
		}
	}

	body := bytes.NewBufferString(fmt.Sprintf("{\"Key\":\"%s\"}", tempKey))
	req, _ := http.NewRequest("DELETE", "/v1/operator/keyring", body)
	resp := httptest.NewRecorder()
	if _, err := a.srv.OperatorKeyringEndpoint(resp, req); err != nil {
		t.Fatalf("err: %s", err)
	}

	// Make sure the temp key has been removed
	list, err = a.ListKeys("", false, 0)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	responses = list.Responses
	if len(responses) != 2 {
		t.Fatalf("bad: %d", len(responses))
	}
	for _, response := range responses {
		if len(response.Keys) != 1 {
			t.Fatalf("bad: %d", len(response.Keys))
		}
		if _, ok := response.Keys[tempKey]; ok {
			t.Fatalf("bad: %v", ok)
		}
	}
}

func TestOperator_KeyringUse(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	oldKey := "H3/9gBxcKKRf45CaI2DlRg=="
	newKey := "z90lFx3sZZLtTOkutXcwYg=="
	a := NewTestAgent(t, `
		encrypt = "`+oldKey+`"
	`)
	defer a.Shutdown()

	if _, err := a.InstallKey(newKey, "", 0); err != nil {
		t.Fatalf("err: %v", err)
	}

	body := bytes.NewBufferString(fmt.Sprintf("{\"Key\":\"%s\"}", newKey))
	req, _ := http.NewRequest("PUT", "/v1/operator/keyring", body)
	resp := httptest.NewRecorder()
	_, err := a.srv.OperatorKeyringEndpoint(resp, req)
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if _, err := a.RemoveKey(oldKey, "", 0); err != nil {
		t.Fatalf("err: %v", err)
	}

	// Make sure only the new key remains
	list, err := a.ListKeys("", false, 0)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	responses := list.Responses
	if len(responses) != 2 {
		t.Fatalf("bad: %d", len(responses))
	}
	for _, response := range responses {
		if len(response.Keys) != 1 {
			t.Fatalf("bad: %d", len(response.Keys))
		}
		if _, ok := response.Keys[newKey]; !ok {
			t.Fatalf("bad: %v", ok)
		}
	}
}

func TestOperator_Keyring_InvalidRelayFactor(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	key := "H3/9gBxcKKRf45CaI2DlRg=="
	a := NewTestAgent(t, `
		encrypt = "`+key+`"
	`)
	defer a.Shutdown()

	cases := map[string]string{
		"999":  "Relay factor must be in range",
		"asdf": "Error parsing relay factor",
	}
	for relayFactor, errString := range cases {
		req, err := http.NewRequest("GET", "/v1/operator/keyring?relay-factor="+relayFactor, nil)
		require.NoError(t, err)
		resp := httptest.NewRecorder()
		_, err = a.srv.OperatorKeyringEndpoint(resp, req)
		require.Error(t, err, "tc: "+relayFactor)
		require.Contains(t, err.Error(), errString, "tc: "+relayFactor)
	}
}

func TestOperator_Keyring_LocalOnly(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	key := "H3/9gBxcKKRf45CaI2DlRg=="
	a := NewTestAgent(t, `
		encrypt = "`+key+`"
	`)
	defer a.Shutdown()

	cases := []struct {
		description string
		method      string
		local       interface{}
		ok          bool
	}{
		{"all ok", "GET", true, true},
		{"garbage local-only value", "GET", "garbage", false},
		{"wrong method (DELETE)", "DELETE", true, false},
	}

	for _, tc := range cases {
		url := fmt.Sprintf("/v1/operator/keyring?local-only=%v", tc.local)
		req, err := http.NewRequest(tc.method, url, nil)
		require.NoError(t, err, "tc: "+tc.description)

		resp := httptest.NewRecorder()
		_, err = a.srv.OperatorKeyringEndpoint(resp, req)
		if tc.ok {
			require.NoError(t, err, "tc: "+tc.description)
		}
		if !tc.ok {
			require.Error(t, err, "tc: "+tc.description)
		}
	}
}

func TestOperator_AutopilotGetConfiguration(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	a := NewTestAgent(t, "")
	defer a.Shutdown()
	testrpc.WaitForTestAgent(t, a.RPC, "dc1")

	body := bytes.NewBuffer(nil)
	req, _ := http.NewRequest("GET", "/v1/operator/autopilot/configuration", body)
	resp := httptest.NewRecorder()
	obj, err := a.srv.OperatorAutopilotConfiguration(resp, req)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	if resp.Code != 200 {
		t.Fatalf("bad code: %d", resp.Code)
	}
	out, ok := obj.(api.AutopilotConfiguration)
	if !ok {
		t.Fatalf("unexpected: %T", obj)
	}
	if !out.CleanupDeadServers {
		t.Fatalf("bad: %#v", out)
	}
}

func TestOperator_AutopilotSetConfiguration(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	a := NewTestAgent(t, "")
	defer a.Shutdown()

	// Provide a non-default value only for CleanupDeadServers.
	// Expect all other fields to be updated with default values
	// (except CreateIndex and ModifyIndex).
	body := bytes.NewBuffer([]byte(`{"CleanupDeadServers": false}`))
	expected := structs.AutopilotConfig{
		CleanupDeadServers:      false, // only non-default value
		LastContactThreshold:    200 * time.Millisecond,
		MaxTrailingLogs:         250,
		MinQuorum:               0,
		ServerStabilizationTime: 10 * time.Second,
		RedundancyZoneTag:       "",
		DisableUpgradeMigration: false,
		UpgradeVersionTag:       "",
	}

	req, _ := http.NewRequest("PUT", "/v1/operator/autopilot/configuration", body)
	resp := httptest.NewRecorder()
	if _, err := a.srv.OperatorAutopilotConfiguration(resp, req); err != nil {
		t.Fatalf("err: %v", err)
	}
	if resp.Code != 200 {
		t.Fatalf("bad code: %d", resp.Code)
	}

	args := structs.DCSpecificRequest{
		Datacenter: "dc1",
	}

	var reply structs.AutopilotConfig
	if err := a.RPC(context.Background(), "Operator.AutopilotGetConfiguration", &args, &reply); err != nil {
		t.Fatalf("err: %v", err)
	}

	// For equality comparison check, ignore CreateIndex and ModifyIndex
	expected.CreateIndex = reply.CreateIndex
	expected.ModifyIndex = reply.ModifyIndex
	require.Equal(t, expected, reply)
}

func TestOperator_AutopilotCASConfiguration(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	a := NewTestAgent(t, "")
	defer a.Shutdown()
	testrpc.WaitForTestAgent(t, a.RPC, "dc1")

	body := bytes.NewBuffer([]byte(`{"CleanupDeadServers": false}`))
	req, _ := http.NewRequest("PUT", "/v1/operator/autopilot/configuration", body)
	resp := httptest.NewRecorder()
	if _, err := a.srv.OperatorAutopilotConfiguration(resp, req); err != nil {
		t.Fatalf("err: %v", err)
	}
	if resp.Code != 200 {
		t.Fatalf("bad code: %d", resp.Code)
	}

	args := structs.DCSpecificRequest{
		Datacenter: "dc1",
	}

	var reply structs.AutopilotConfig
	if err := a.RPC(context.Background(), "Operator.AutopilotGetConfiguration", &args, &reply); err != nil {
		t.Fatalf("err: %v", err)
	}

	if reply.CleanupDeadServers {
		t.Fatalf("bad: %#v", reply)
	}

	// Create a CAS request, bad index
	{
		buf := bytes.NewBuffer([]byte(`{"CleanupDeadServers": true}`))
		req, _ := http.NewRequest("PUT", fmt.Sprintf("/v1/operator/autopilot/configuration?cas=%d", reply.ModifyIndex-1), buf)
		resp := httptest.NewRecorder()
		obj, err := a.srv.OperatorAutopilotConfiguration(resp, req)
		if err != nil {
			t.Fatalf("err: %v", err)
		}

		if res := obj.(bool); res {
			t.Fatalf("should NOT work")
		}
	}

	// Create a CAS request, good index
	{
		buf := bytes.NewBuffer([]byte(`{"CleanupDeadServers": true}`))
		req, _ := http.NewRequest("PUT", fmt.Sprintf("/v1/operator/autopilot/configuration?cas=%d", reply.ModifyIndex), buf)
		resp := httptest.NewRecorder()
		obj, err := a.srv.OperatorAutopilotConfiguration(resp, req)
		if err != nil {
			t.Fatalf("err: %v", err)
		}

		if res := obj.(bool); !res {
			t.Fatalf("should work")
		}
	}

	// Verify the update
	if err := a.RPC(context.Background(), "Operator.AutopilotGetConfiguration", &args, &reply); err != nil {
		t.Fatalf("err: %v", err)
	}
	if !reply.CleanupDeadServers {
		t.Fatalf("bad: %#v", reply)
	}
}

func TestOperator_ServerHealth(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	a := NewTestAgent(t, `
		raft_protocol = 3
	`)
	defer a.Shutdown()

	body := bytes.NewBuffer(nil)
	req, _ := http.NewRequest("GET", "/v1/operator/autopilot/health", body)
	retry.Run(t, func(r *retry.R) {
		resp := httptest.NewRecorder()
		obj, err := a.srv.OperatorServerHealth(resp, req)
		if err != nil {
			r.Fatalf("err: %v", err)
		}
		if resp.Code != 200 {
			r.Fatalf("bad code: %d", resp.Code)
		}
		out, ok := obj.(*api.OperatorHealthReply)
		if !ok {
			r.Fatalf("unexpected: %T", obj)
		}
		if len(out.Servers) != 1 ||
			!out.Servers[0].Healthy ||
			out.Servers[0].Name != a.Config.NodeName ||
			out.Servers[0].SerfStatus != "alive" ||
			out.FailureTolerance != 0 {
			r.Fatalf("bad: %v", out)
		}
	})
}

func TestOperator_ServerHealth_Unhealthy(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	a := NewTestAgent(t, `
		raft_protocol = 3
		autopilot {
			last_contact_threshold = "-1s"
		}
	`)
	defer a.Shutdown()

	body := bytes.NewBuffer(nil)
	req, _ := http.NewRequest("GET", "/v1/operator/autopilot/health", body)
	retry.Run(t, func(r *retry.R) {
		resp := httptest.NewRecorder()
		obj, err := a.srv.OperatorServerHealth(resp, req)
		if err != nil {
			r.Fatalf("err: %v", err)
		}
		if resp.Code != 429 {
			r.Fatalf("bad code: %d", resp.Code)
		}
		out, ok := obj.(*api.OperatorHealthReply)
		if !ok {
			r.Fatalf("unexpected: %T", obj)
		}
		if len(out.Servers) != 1 ||
			out.Healthy ||
			out.Servers[0].Name != a.Config.NodeName {
			r.Fatalf("bad: %#v", out.Servers)
		}
	})
}

func TestOperator_AutopilotState(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	a := NewTestAgent(t, "")
	defer a.Shutdown()

	req, err := http.NewRequest("GET", "/v1/operator/autopilot/state", nil)
	require.NoError(t, err)
	retry.Run(t, func(r *retry.R) {
		resp := httptest.NewRecorder()
		obj, err := a.srv.OperatorAutopilotState(resp, req)
		require.NoError(r, err)
		require.Equal(r, 200, resp.Code)
		state, ok := obj.(*api.AutopilotState)
		require.True(r, ok)

		srv, ok := state.Servers[string(a.config.NodeID)]
		require.True(r, ok)
		require.True(r, srv.Healthy)
		require.Equal(r, a.config.NodeName, srv.Name)

	})
}

func TestAutopilotStateToAPIConversion(t *testing.T) {
	var leaderID raft.ServerID = "79324811-9588-4311-b208-f272e38aaabf"
	var follower1ID raft.ServerID = "ef8aee9a-f9d6-4ec4-b383-aac956bdb80f"
	var follower2ID raft.ServerID = "ae84aefb-a303-4734-8739-5c102d4ee2d9"
	input := autopilot.State{
		Healthy:          true,
		FailureTolerance: 1,
		Leader:           leaderID,
		Voters: []raft.ServerID{
			leaderID,
			follower1ID,
			follower2ID,
		},
		Servers: map[raft.ServerID]*autopilot.ServerState{
			leaderID: {
				Server: autopilot.Server{
					ID:         leaderID,
					Name:       "node1",
					Address:    "198.18.0.1:8300",
					NodeStatus: autopilot.NodeAlive,
					Version:    "1.9.0",
					Meta: map[string]string{
						"foo": "bar",
					},
					NodeType: autopilot.NodeVoter,
				},
				State: autopilot.RaftLeader,
				Stats: autopilot.ServerStats{
					LastContact: 0,
					LastTerm:    3,
					LastIndex:   42,
				},
				Health: autopilot.ServerHealth{
					Healthy:     true,
					StableSince: time.Date(2020, 11, 6, 14, 51, 0, 0, time.UTC),
				},
			},
			follower1ID: {
				Server: autopilot.Server{
					ID:         follower1ID,
					Name:       "node2",
					Address:    "198.18.0.2:8300",
					NodeStatus: autopilot.NodeAlive,
					Version:    "1.9.0",
					Meta: map[string]string{
						"bar": "baz",
					},
					NodeType: autopilot.NodeVoter,
				},
				State: autopilot.RaftVoter,
				Stats: autopilot.ServerStats{
					LastContact: time.Millisecond,
					LastTerm:    3,
					LastIndex:   41,
				},
				Health: autopilot.ServerHealth{
					Healthy:     true,
					StableSince: time.Date(2020, 11, 6, 14, 52, 0, 0, time.UTC),
				},
			},
			follower2ID: {
				Server: autopilot.Server{
					ID:         follower2ID,
					Name:       "node3",
					Address:    "198.18.0.3:8300",
					NodeStatus: autopilot.NodeAlive,
					Version:    "1.9.0",
					Meta: map[string]string{
						"baz": "foo",
					},
					NodeType: autopilot.NodeVoter,
				},
				State: autopilot.RaftVoter,
				Stats: autopilot.ServerStats{
					LastContact: 2 * time.Millisecond,
					LastTerm:    3,
					LastIndex:   39,
				},
				Health: autopilot.ServerHealth{
					Healthy:     true,
					StableSince: time.Date(2020, 11, 6, 14, 53, 0, 0, time.UTC),
				},
			},
		},
	}

	expected := api.AutopilotState{
		Healthy:                    true,
		FailureTolerance:           1,
		OptimisticFailureTolerance: 1,
		Leader:                     string(leaderID),
		Voters: []string{
			string(leaderID),
			string(follower1ID),
			string(follower2ID),
		},
		Servers: map[string]api.AutopilotServer{
			string(leaderID): {
				ID:         string(leaderID),
				Name:       "node1",
				Address:    "198.18.0.1:8300",
				NodeStatus: "alive",
				Version:    "1.9.0",
				Meta: map[string]string{
					"foo": "bar",
				},
				NodeType:    api.AutopilotTypeVoter,
				Status:      api.AutopilotServerLeader,
				LastContact: api.NewReadableDuration(0),
				LastTerm:    3,
				LastIndex:   42,
				Healthy:     true,
				StableSince: time.Date(2020, 11, 6, 14, 51, 0, 0, time.UTC),
			},
			string(follower1ID): {
				ID:         string(follower1ID),
				Name:       "node2",
				Address:    "198.18.0.2:8300",
				NodeStatus: "alive",
				Version:    "1.9.0",
				Meta: map[string]string{
					"bar": "baz",
				},
				NodeType:    api.AutopilotTypeVoter,
				Status:      api.AutopilotServerVoter,
				LastContact: api.NewReadableDuration(time.Millisecond),
				LastTerm:    3,
				LastIndex:   41,
				Healthy:     true,
				StableSince: time.Date(2020, 11, 6, 14, 52, 0, 0, time.UTC),
			},
			string(follower2ID): {
				ID:         string(follower2ID),
				Name:       "node3",
				Address:    "198.18.0.3:8300",
				NodeStatus: "alive",
				Version:    "1.9.0",
				Meta: map[string]string{
					"baz": "foo",
				},
				NodeType:    api.AutopilotTypeVoter,
				Status:      api.AutopilotServerVoter,
				LastContact: api.NewReadableDuration(2 * time.Millisecond),
				LastTerm:    3,
				LastIndex:   39,
				Healthy:     true,
				StableSince: time.Date(2020, 11, 6, 14, 53, 0, 0, time.UTC),
			},
		},
	}

	require.Equal(t, &expected, autopilotToAPIState(&input))
}
