package vault

import (
	"errors"
	"sort"
	"strings"
	"testing"
	"time"

	"github.com/armon/go-metrics"
	logicalKv "github.com/hashicorp/vault-plugin-secrets-kv"
	"github.com/hashicorp/vault/helper/namespace"
	"github.com/hashicorp/vault/sdk/logical"
)

func TestCoreMetrics_KvSecretGauge(t *testing.T) {
	// Use the real KV implementation instead of Passthrough
	AddTestLogicalBackend("kv", logicalKv.Factory)
	// Clean up for the next test-- is there a better way?
	defer func() {
		delete(testLogicalBackends, "kv")
	}()
	core, _, root := TestCoreUnsealed(t)

	testMounts := []struct {
		Path          string
		Type          string
		Version       string
		ExpectedCount int
	}{
		{"secret/", "kv", "2", 0},
		{"secret1/", "kv", "1", 3},
		{"secret2/", "kv", "1", 0},
		{"secret3/", "kv", "2", 4},
		{"prefix/secret3/", "kv", "2", 0},
		{"prefix/secret4/", "kv", "2", 5},
		{"generic/", "generic", "1", 3},
	}
	ctx := namespace.RootContext(nil)

	// skip 0, secret/ is already mounted
	for _, tm := range testMounts[1:] {
		me := &MountEntry{
			Table:   mountTableType,
			Path:    sanitizePath(tm.Path),
			Type:    tm.Type,
			Options: map[string]string{"version": tm.Version},
		}
		err := core.mount(ctx, me)
		if err != nil {
			t.Fatalf("err: %v", err)
		}
	}

	v1secrets := []string{
		"secret1/a", // 3
		"secret1/b",
		"secret1/c/d",
		"generic/a",
		"generic/b",
		"generic/c",
	}
	v2secrets := []string{
		"secret3/data/a", // 4
		"secret3/data/b",
		"secret3/data/c/d",
		"secret3/data/c/e",
		"prefix/secret4/data/a/secret", // 5
		"prefix/secret4/data/a/secret2",
		"prefix/secret4/data/a/b/c/secret",
		"prefix/secret4/data/a/b/c/secret2",
		"prefix/secret4/data/a/b/c/d/secret3",
	}
	for _, p := range v1secrets {
		req := logical.TestRequest(t, logical.CreateOperation, p)
		req.Data["foo"] = "bar"
		req.ClientToken = root
		resp, err := core.HandleRequest(ctx, req)
		if err != nil {
			t.Fatalf("err: %v", err)
		}
		if resp != nil {
			t.Fatalf("bad: %#v", resp)
		}
	}
	for _, p := range v2secrets {
		for i := 0; i < 50; i++ {
			req := logical.TestRequest(t, logical.CreateOperation, p)
			req.Data["data"] = map[string]interface{}{"foo": "bar"}
			req.ClientToken = root
			resp, err := core.HandleRequest(ctx, req)
			if err != nil {
				if errors.Is(err, logical.ErrInvalidRequest) {
					// Handle scenario where KVv2 upgrade is ongoing
					time.Sleep(100 * time.Millisecond)
					continue
				}
				t.Fatalf("err: %v", err)
			}
			if resp.Error() != nil {
				t.Fatalf("bad: %#v", resp)
			}
			break
		}
	}

	values, err := core.kvSecretGaugeCollector(ctx)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	if len(values) != len(testMounts) {
		t.Errorf("Got %v values but expected %v mounts", len(values), len(testMounts))
	}

	for _, glv := range values {
		mountPoint := ""
		for _, l := range glv.Labels {
			if l.Name == "mount_point" {
				mountPoint = l.Value
			} else if l.Name == "namespace" {
				if l.Value != "root" {
					t.Errorf("Namespace is %v, not root", l.Value)
				}
			} else {
				t.Errorf("Unexpected label %v", l.Name)
			}
		}
		if mountPoint == "" {
			t.Errorf("No mount point in labels %v", glv.Labels)
			continue
		}
		found := false
		for _, tm := range testMounts {
			if tm.Path == mountPoint {
				found = true
				if glv.Value != float32(tm.ExpectedCount) {
					t.Errorf("Mount %v reported %v, not %v",
						tm.Path, glv.Value, tm.ExpectedCount)
				}
				break
			}
		}
		if !found {
			t.Errorf("Unexpected mount point %v", mountPoint)
		}
	}
}

func TestCoreMetrics_KvSecretGauge_BadPath(t *testing.T) {
	// Use the real KV implementation instead of Passthrough
	AddTestLogicalBackend("kv", logicalKv.Factory)
	// Clean up for the next test.
	defer func() {
		delete(testLogicalBackends, "kv")
	}()
	core, _, _ := TestCoreUnsealed(t)

	me := &MountEntry{
		Table:   mountTableType,
		Path:    sanitizePath("kv1"),
		Type:    "kv",
		Options: map[string]string{"version": "1"},
	}
	ctx := namespace.RootContext(nil)
	err := core.mount(ctx, me)
	if err != nil {
		t.Fatalf("mount error: %v", err)
	}

	// I don't think there's any remaining way to create a zero-length
	// key via the API, so we'll fake it by talking to the storage layer directly.
	fake_entry := &logical.StorageEntry{
		Key:   "logical/" + me.UUID + "/foo/",
		Value: []byte{1},
	}
	err = core.barrier.Put(ctx, fake_entry)
	if err != nil {
		t.Fatalf("put error: %v", err)
	}

	values, err := core.kvSecretGaugeCollector(ctx)
	if err != nil {
		t.Fatalf("collector error: %v", err)
	}
	t.Logf("Values: %v", values)
	found := false
	var count float32 = -1
	for _, glv := range values {
		for _, l := range glv.Labels {
			if l.Name == "mount_point" && l.Value == "kv1/" {
				found = true
				count = glv.Value
				break
			}
		}
	}
	if found {
		if count != 1.0 {
			t.Errorf("bad secret count for kv1/")
		}
	} else {
		t.Errorf("no secret count for kv1/")
	}
}

func TestCoreMetrics_KvSecretGaugeError(t *testing.T) {
	core, _, _, sink := TestCoreUnsealedWithMetrics(t)
	ctx := namespace.RootContext(nil)

	badKvMount := &kvMount{
		Namespace:  namespace.RootNamespace,
		MountPoint: "bad/path",
		Version:    "1",
		NumSecrets: 0,
	}

	core.walkKvMountSecrets(ctx, badKvMount)

	intervals := sink.Data()
	// Test crossed an interval boundary, don't try to deal with it.
	if len(intervals) > 1 {
		t.Skip("Detected interval crossing.")
	}

	// Should be an error
	keyPrefix := "metrics.collection.error"
	var counter *metrics.SampledValue = nil

	for _, c := range intervals[0].Counters {
		if strings.HasPrefix(c.Name, keyPrefix) {
			counter = &c
			break
		}
	}
	if counter == nil {
		t.Fatal("No metrics.collection.error counter found.")
	}
	if counter.Count != 1 {
		t.Errorf("Counter number of samples %v is not 1.", counter.Count)
	}
}

func metricLabelsMatch(t *testing.T, actual []metrics.Label, expected map[string]string) {
	t.Helper()

	if len(actual) != len(expected) {
		t.Errorf("Expected %v labels, got %v: %v", len(expected), len(actual), actual)
	}

	for _, l := range actual {
		if v, ok := expected[l.Name]; ok {
			if v != l.Value {
				t.Errorf("Mismatched value %v=%v, expected %v", l.Name, l.Value, v)
			}
		} else {
			t.Errorf("Unexpected label %v", l.Name)
		}
	}
}

func TestCoreMetrics_EntityGauges(t *testing.T) {
	ctx := namespace.RootContext(nil)
	is, ghAccessor, upAccessor, core := testIdentityStoreWithGithubUserpassAuth(ctx, t)

	// Create an entity
	alias1 := &logical.Alias{
		MountType:     "github",
		MountAccessor: ghAccessor,
		Name:          "githubuser",
	}

	entity, _, err := is.CreateOrFetchEntity(ctx, alias1)
	if err != nil {
		t.Fatal(err)
	}

	// Create a second alias for the same entity
	registerReq := &logical.Request{
		Operation: logical.UpdateOperation,
		Path:      "entity-alias",
		Data: map[string]interface{}{
			"name":           "userpassuser",
			"canonical_id":   entity.ID,
			"mount_accessor": upAccessor,
		},
	}
	resp, err := is.HandleRequest(ctx, registerReq)
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("err:%v resp:%#v", err, resp)
	}

	glv, err := core.entityGaugeCollector(ctx)
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if len(glv) != 1 {
		t.Fatalf("Wrong number of gauges %v, expected %v", len(glv), 1)
	}

	if glv[0].Value != 1.0 {
		t.Errorf("Entity count %v, expected %v", glv[0].Value, 1.0)
	}

	metricLabelsMatch(t, glv[0].Labels,
		map[string]string{
			"namespace": "root",
		})

	glv, err = core.entityGaugeCollectorByMount(ctx)
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if len(glv) != 2 {
		t.Fatalf("Wrong number of gauges %v, expected %v", len(glv), 1)
	}

	if glv[0].Value != 1.0 {
		t.Errorf("Alias count %v, expected %v", glv[0].Value, 1.0)
	}

	if glv[1].Value != 1.0 {
		t.Errorf("Alias count %v, expected %v", glv[0].Value, 1.0)
	}

	// Sort both metrics.Label slices by Name, causing the Label
	// with Name auth_method to be first in both arrays
	sort.Slice(glv[0].Labels, func(i, j int) bool { return glv[0].Labels[i].Name < glv[0].Labels[j].Name })
	sort.Slice(glv[1].Labels, func(i, j int) bool { return glv[1].Labels[i].Name < glv[1].Labels[j].Name })

	// Sort the GaugeLabelValues slice by the Value of the first metric,
	// in this case auth_method, in each metrics.Label slice
	sort.Slice(glv, func(i, j int) bool { return glv[i].Labels[0].Value < glv[j].Labels[0].Value })

	metricLabelsMatch(t, glv[0].Labels,
		map[string]string{
			"namespace":   "root",
			"auth_method": "github",
			"mount_point": "auth/github/",
		})

	metricLabelsMatch(t, glv[1].Labels,
		map[string]string{
			"namespace":   "root",
			"auth_method": "userpass",
			"mount_point": "auth/userpass/",
		})
}
