package api_test

import (
	"encoding/base64"
	"strings"
	"testing"

	"github.com/hashicorp/vault/api"
	"github.com/hashicorp/vault/helper/testhelpers"
	vaulthttp "github.com/hashicorp/vault/http"
	"github.com/hashicorp/vault/shamir"
	"github.com/hashicorp/vault/vault"
)

func TestSysRekey_Verification(t *testing.T) {
	testSysRekey_Verification(t, false)
	testSysRekey_Verification(t, true)
}

func testSysRekey_Verification(t *testing.T, recovery bool) {
	cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
		HandlerFunc: vaulthttp.Handler,
	})
	cluster.Start()
	defer cluster.Cleanup()

	vault.TestWaitActive(t, cluster.Cores[0].Core)
	client := cluster.Cores[0].Client
	client.SetMaxRetries(0)

	initFunc := client.Sys().RekeyInit
	updateFunc := client.Sys().RekeyUpdate
	verificationUpdateFunc := client.Sys().RekeyVerificationUpdate
	verificationStatusFunc := client.Sys().RekeyVerificationStatus
	verificationCancelFunc := client.Sys().RekeyVerificationCancel
	if recovery {
		initFunc = client.Sys().RekeyRecoveryKeyInit
		updateFunc = client.Sys().RekeyRecoveryKeyUpdate
		verificationUpdateFunc = client.Sys().RekeyRecoveryKeyVerificationUpdate
		verificationStatusFunc = client.Sys().RekeyRecoveryKeyVerificationStatus
		verificationCancelFunc = client.Sys().RekeyRecoveryKeyVerificationCancel
	}

	sealAccess := cluster.Cores[0].Core.SealAccess()
	sealTestingParams := &vault.SealAccessTestingParams{}

	// This first block verifies that if we are using recovery keys to force a
	// rekey of a stored-shares barrier that verification is not allowed since
	// the keys aren't returned
	if !recovery {
		sealTestingParams.PretendToAllowRecoveryKeys = true
		sealTestingParams.PretendToAllowStoredShares = true
		if err := sealAccess.SetTestingParams(sealTestingParams); err != nil {
			t.Fatal(err)
		}

		_, err := initFunc(&api.RekeyInitRequest{
			StoredShares:        1,
			RequireVerification: true,
		})
		if err == nil {
			t.Fatal("expected error")
		}
		if !strings.Contains(err.Error(), "requiring verification not supported") {
			t.Fatalf("unexpected error: %v", err)
		}
		// Now we set things back and start a normal rekey with the verification process
		sealTestingParams.PretendToAllowRecoveryKeys = false
		sealTestingParams.PretendToAllowStoredShares = false
		if err := sealAccess.SetTestingParams(sealTestingParams); err != nil {
			t.Fatal(err)
		}
	} else {
		cluster.RecoveryKeys = cluster.BarrierKeys
		sealTestingParams.PretendToAllowRecoveryKeys = true
		recoveryKey, err := shamir.Combine(cluster.BarrierKeys)
		if err != nil {
			t.Fatal(err)
		}
		sealTestingParams.PretendRecoveryKey = recoveryKey
		if err := sealAccess.SetTestingParams(sealTestingParams); err != nil {
			t.Fatal(err)
		}
	}

	var verificationNonce string
	var newKeys []string
	doRekeyInitialSteps := func() {
		status, err := initFunc(&api.RekeyInitRequest{
			SecretShares:        5,
			SecretThreshold:     3,
			RequireVerification: true,
		})
		if err != nil {
			t.Fatal(err)
		}
		if status == nil {
			t.Fatal("nil status")
		}
		if !status.VerificationRequired {
			t.Fatal("expected verification required")
		}

		var resp *api.RekeyUpdateResponse
		for i := 0; i < 3; i++ {
			resp, err = updateFunc(base64.StdEncoding.EncodeToString(cluster.BarrierKeys[i]), status.Nonce)
			if err != nil {
				t.Fatal(err)
			}
		}
		switch {
		case !resp.Complete:
			t.Fatal("expected completion")
		case !resp.VerificationRequired:
			t.Fatal("expected verification required")
		case resp.VerificationNonce == "":
			t.Fatal("verification nonce expected")
		}
		verificationNonce = resp.VerificationNonce
		newKeys = resp.KeysB64
		t.Logf("verification nonce: %q", verificationNonce)
	}

	doRekeyInitialSteps()

	// We are still going, so should not be able to init again
	_, err := initFunc(&api.RekeyInitRequest{
		SecretShares:        5,
		SecretThreshold:     3,
		RequireVerification: true,
	})
	if err == nil {
		t.Fatal("expected error")
	}

	// Sealing should clear state, so after this we should be able to perform
	// the above again
	cluster.EnsureCoresSealed(t)
	cluster.UnsealCores(t)
	doRekeyInitialSteps()

	doStartVerify := func() {
		// Start the process
		for i := 0; i < 2; i++ {
			status, err := verificationUpdateFunc(newKeys[i], verificationNonce)
			if err != nil {
				t.Fatal(err)
			}
			switch {
			case status.Nonce != verificationNonce:
				t.Fatalf("unexpected nonce, expected %q, got %q", verificationNonce, status.Nonce)
			case status.Complete:
				t.Fatal("unexpected completion")
			}
		}

		// Check status
		vStatus, err := verificationStatusFunc()
		if err != nil {
			t.Fatal(err)
		}
		switch {
		case vStatus.Nonce != verificationNonce:
			t.Fatalf("unexpected nonce, expected %q, got %q", verificationNonce, vStatus.Nonce)
		case vStatus.T != 3:
			t.Fatal("unexpected threshold")
		case vStatus.N != 5:
			t.Fatal("unexpected number of new keys")
		case vStatus.Progress != 2:
			t.Fatal("unexpected progress")
		}
	}

	doStartVerify()

	// Cancel; this should still keep the rekey process going but just cancel
	// the verification operation
	err = verificationCancelFunc()
	if err != nil {
		t.Fatal(err)
	}
	// Verify cannot init again
	_, err = initFunc(&api.RekeyInitRequest{
		SecretShares:        5,
		SecretThreshold:     3,
		RequireVerification: true,
	})
	if err == nil {
		t.Fatal("expected error")
	}
	vStatus, err := verificationStatusFunc()
	if err != nil {
		t.Fatal(err)
	}
	switch {
	case vStatus.Nonce == verificationNonce:
		t.Fatalf("unexpected nonce, expected not-%q but got it", verificationNonce)
	case vStatus.T != 3:
		t.Fatal("unexpected threshold")
	case vStatus.N != 5:
		t.Fatal("unexpected number of new keys")
	case vStatus.Progress != 0:
		t.Fatal("unexpected progress")
	}

	verificationNonce = vStatus.Nonce
	doStartVerify()

	if !recovery {
		// Sealing should clear state, but we never actually finished, so it should
		// still be the old keys (which are still currently set)
		cluster.EnsureCoresSealed(t)
		cluster.UnsealCores(t)

		// Should be able to init again and get back to where we were
		doRekeyInitialSteps()
		doStartVerify()
	} else {
		// We haven't finished, so generating a root token should still be the
		// old keys (which are still currently set)
		testhelpers.GenerateRoot(t, cluster, false)
	}

	// Provide the final new key
	vuStatus, err := verificationUpdateFunc(newKeys[2], verificationNonce)
	if err != nil {
		t.Fatal(err)
	}
	switch {
	case vuStatus.Nonce != verificationNonce:
		t.Fatalf("unexpected nonce, expected %q, got %q", verificationNonce, vuStatus.Nonce)
	case !vuStatus.Complete:
		t.Fatal("expected completion")
	}

	if !recovery {
		// Seal and unseal -- it should fail to unseal because the key has now been
		// rotated
		cluster.EnsureCoresSealed(t)
		if err := cluster.UnsealCoresWithError(); err == nil {
			t.Fatal("expected error")
		}

		// Swap out the keys with our new ones and try again
		var newKeyBytes [][]byte
		for _, key := range newKeys {
			val, err := base64.StdEncoding.DecodeString(key)
			if err != nil {
				t.Fatal(err)
			}
			newKeyBytes = append(newKeyBytes, val)
		}
		cluster.BarrierKeys = newKeyBytes
		if err := cluster.UnsealCoresWithError(); err != nil {
			t.Fatal("expected error")
		}
	} else {
		// The old keys should no longer work
		_, err := testhelpers.GenerateRootWithError(t, cluster, false)
		if err == nil {
			t.Fatal("expected error")
		}

		// Put tne new keys in place and run again
		cluster.RecoveryKeys = nil
		for _, key := range newKeys {
			dec, err := base64.StdEncoding.DecodeString(key)
			if err != nil {
				t.Fatal(err)
			}
			cluster.RecoveryKeys = append(cluster.RecoveryKeys, dec)
		}
		if err := client.Sys().GenerateRootCancel(); err != nil {
			t.Fatal(err)
		}
		testhelpers.GenerateRoot(t, cluster, false)
	}
}
