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

package agent

import (
	"bytes"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/sdk/testutil/retry"
	"github.com/hashicorp/consul/testrpc"
)

func TestEventFire(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("test"))
	url := "/v1/event/fire/test?node=Node&service=foo&tag=bar"
	req, _ := http.NewRequest("PUT", url, body)
	resp := httptest.NewRecorder()
	obj, err := a.srv.EventFire(resp, req)
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	event, ok := obj.(*UserEvent)
	if !ok {
		t.Fatalf("bad: %#v", obj)
	}

	if event.ID == "" {
		t.Fatalf("bad: %#v", event)
	}
	if event.Name != "test" {
		t.Fatalf("bad: %#v", event)
	}
	if string(event.Payload) != "test" {
		t.Fatalf("bad: %#v", event)
	}
	if event.NodeFilter != "Node" {
		t.Fatalf("bad: %#v", event)
	}
	if event.ServiceFilter != "foo" {
		t.Fatalf("bad: %#v", event)
	}
	if event.TagFilter != "bar" {
		t.Fatalf("bad: %#v", event)
	}
}

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

	t.Parallel()
	a := NewTestAgent(t, TestACLConfig()+`
		acl_default_policy = "deny"
	`)
	defer a.Shutdown()
	testrpc.WaitForLeader(t, a.RPC, "dc1")

	token := createToken(t, a, testEventPolicy)

	type tcase struct {
		event   string
		allowed bool
	}
	tcases := []tcase{
		{"foo", false},
		{"bar", false},
		{"baz", true},
	}
	for _, c := range tcases {
		// Try to fire the event over the HTTP interface
		url := fmt.Sprintf("/v1/event/fire/%s", c.event)
		req, _ := http.NewRequest("PUT", url, nil)
		req.Header.Add("X-Consul-Token", token)
		resp := httptest.NewRecorder()
		_, err := a.srv.EventFire(resp, req)

		// Check the result
		if c.allowed {
			body := resp.Body.String()
			if acl.IsErrPermissionDenied(errors.New(body)) {
				t.Fatalf("bad: %s", body)
			}
			if resp.Code != 200 {
				t.Fatalf("bad: %d", resp.Code)
			}
		} else {
			if !acl.IsErrPermissionDenied(err) {
				t.Fatalf("bad: %s", err.Error())
			}
			if err, ok := err.(HTTPError); ok {
				if err.StatusCode != 403 {
					t.Fatalf("Expected 403 but got %d", err.StatusCode)
				}
			} else {
				t.Fatalf("Expected HTTP Error %v", err)
			}
		}
	}
}

func TestEventList(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")

	p := &UserEvent{Name: "test"}
	if err := a.UserEvent("dc1", "root", p); err != nil {
		t.Fatalf("err: %v", err)
	}

	retry.Run(t, func(r *retry.R) {
		req, _ := http.NewRequest("GET", "/v1/event/list", nil)
		resp := httptest.NewRecorder()
		obj, err := a.srv.EventList(resp, req)
		if err != nil {
			r.Fatal(err)
		}

		list, ok := obj.([]*UserEvent)
		if !ok {
			r.Fatalf("bad: %#v", obj)
		}
		if len(list) != 1 || list[0].Name != "test" {
			r.Fatalf("bad: %#v", list)
		}
		header := resp.Header().Get("X-Consul-Index")
		if header == "" || header == "0" {
			r.Fatalf("bad: %#v", header)
		}
	})
}

func TestEventList_Filter(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")

	p := &UserEvent{Name: "test"}
	if err := a.UserEvent("dc1", "root", p); err != nil {
		t.Fatalf("err: %v", err)
	}

	p = &UserEvent{Name: "foo"}
	if err := a.UserEvent("dc1", "root", p); err != nil {
		t.Fatalf("err: %v", err)
	}

	retry.Run(t, func(r *retry.R) {
		req, _ := http.NewRequest("GET", "/v1/event/list?name=foo", nil)
		resp := httptest.NewRecorder()
		obj, err := a.srv.EventList(resp, req)
		if err != nil {
			r.Fatal(err)
		}

		list, ok := obj.([]*UserEvent)
		if !ok {
			r.Fatalf("bad: %#v", obj)
		}
		if len(list) != 1 || list[0].Name != "foo" {
			r.Fatalf("bad: %#v", list)
		}
		header := resp.Header().Get("X-Consul-Index")
		if header == "" || header == "0" {
			r.Fatalf("bad: %#v", header)
		}
	})
}

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

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

	// Fire some events.
	events := []*UserEvent{
		{Name: "foo"},
		{Name: "bar"},
	}
	for _, e := range events {
		err := a.UserEvent("dc1", "root", e)
		require.NoError(t, err)
	}

	t.Run("no token", func(t *testing.T) {
		retry.Run(t, func(r *retry.R) {
			req := httptest.NewRequest("GET", "/v1/event/list", nil)
			resp := httptest.NewRecorder()

			obj, err := a.srv.EventList(resp, req)
			require.NoError(r, err)

			list, ok := obj.([]*UserEvent)
			require.True(r, ok)
			require.Empty(r, list)
			require.Empty(r, resp.Header().Get("X-Consul-Results-Filtered-By-ACLs"))
		})
	})

	t.Run("token with access to one event type", func(t *testing.T) {
		retry.Run(t, func(r *retry.R) {
			token := testCreateToken(r, a, `
				event "foo" {
					policy = "read"
				}
			`)

			req := httptest.NewRequest("GET", "/v1/event/list", nil)
			req.Header.Add("X-Consul-Token", token)
			resp := httptest.NewRecorder()

			obj, err := a.srv.EventList(resp, req)
			require.NoError(r, err)

			list, ok := obj.([]*UserEvent)
			require.True(r, ok)
			require.Len(r, list, 1)
			require.Equal(r, "foo", list[0].Name)
			require.NotEmpty(r, resp.Header().Get("X-Consul-Results-Filtered-By-ACLs"))
		})
	})

	t.Run("root token", func(t *testing.T) {
		retry.Run(t, func(r *retry.R) {
			req := httptest.NewRequest("GET", "/v1/event/list", nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()

			obj, err := a.srv.EventList(resp, req)
			require.NoError(r, err)

			list, ok := obj.([]*UserEvent)
			require.True(r, ok)
			require.Len(r, list, 2)

			var names []string
			for _, e := range list {
				names = append(names, e.Name)
			}
			require.ElementsMatch(r, []string{"foo", "bar"}, names)

			require.Empty(r, resp.Header().Get("X-Consul-Results-Filtered-By-ACLs"))
		})
	})
}

func TestEventList_Blocking(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")

	p := &UserEvent{Name: "test"}
	if err := a.UserEvent("dc1", "root", p); err != nil {
		t.Fatalf("err: %v", err)
	}

	var index string
	retry.Run(t, func(r *retry.R) {
		req, _ := http.NewRequest("GET", "/v1/event/list", nil)
		resp := httptest.NewRecorder()
		if _, err := a.srv.EventList(resp, req); err != nil {
			r.Fatal(err)
		}
		header := resp.Header().Get("X-Consul-Index")
		if header == "" || header == "0" {
			r.Fatalf("bad: %#v", header)
		}
		index = header
	})

	go func() {
		time.Sleep(50 * time.Millisecond)
		p := &UserEvent{Name: "second"}
		if err := a.UserEvent("dc1", "root", p); err != nil {
			t.Errorf("err: %v", err)
		}
	}()

	retry.Run(t, func(r *retry.R) {
		url := "/v1/event/list?index=" + index
		req, _ := http.NewRequest("GET", url, nil)
		resp := httptest.NewRecorder()
		obj, err := a.srv.EventList(resp, req)
		if err != nil {
			r.Fatal(err)
		}

		list, ok := obj.([]*UserEvent)
		if !ok {
			r.Fatalf("bad: %#v", obj)
		}
		if len(list) != 2 || list[1].Name != "second" {
			r.Fatalf("bad: %#v", list)
		}
	})
}

func TestEventList_EventBufOrder(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")

	// Fire some events in a non-sequential order
	expected := &UserEvent{Name: "foo"}

	for _, e := range []*UserEvent{
		{Name: "foo"},
		{Name: "bar"},
		{Name: "foo"},
		expected,
		{Name: "bar"},
	} {
		if err := a.UserEvent("dc1", "root", e); err != nil {
			t.Fatalf("err: %v", err)
		}
	}
	// Test that the event order is preserved when name
	// filtering on a list of > 1 matching event.
	retry.Run(t, func(r *retry.R) {
		url := "/v1/event/list?name=foo"
		req, _ := http.NewRequest("GET", url, nil)
		resp := httptest.NewRecorder()
		obj, err := a.srv.EventList(resp, req)
		if err != nil {
			r.Fatal(err)
		}
		list, ok := obj.([]*UserEvent)
		if !ok {
			r.Fatalf("bad: %#v", obj)
		}
		if len(list) != 3 || list[2].ID != expected.ID {
			r.Fatalf("bad: %#v", list)
		}
	})
}

func TestUUIDToUint64(t *testing.T) {
	t.Parallel()
	inp := "cb9a81ad-fff6-52ac-92a7-5f70687805ec"

	// Output value was computed using python
	if uuidToUint64(inp) != 6430540886266763072 {
		t.Fatalf("bad")
	}
}