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

//go:build !consulent
// +build !consulent

package agent

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"sync/atomic"
	"testing"

	"github.com/hashicorp/consul/agent/structs"
	"github.com/hashicorp/consul/testrpc"
	"github.com/stretchr/testify/require"
)

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

	t.Parallel()

	var (
		lastHeadersSent atomic.Value
		backendCalled   atomic.Value
	)
	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		backendCalled.Store(true)
		lastHeadersSent.Store(r.Header)
		if r.URL.Path == "/some/prefix/ok" {
			w.Write([]byte("OK"))
			return
		}
		http.Error(w, "not found on backend", http.StatusNotFound)
	}))
	defer backend.Close()

	backendURL := backend.URL + "/some/prefix"

	a := NewTestAgent(t, TestACLConfig()+fmt.Sprintf(`
		ui_config {
			enabled = true
			metrics_proxy {
				base_url = %q
			}
		}
		http_config {
			response_headers {
				"Access-Control-Allow-Origin" = "*"
			}
		}
	`, backendURL))
	defer a.Shutdown()

	a.enableDebug.Store(true)

	h := a.srv.handler()

	testrpc.WaitForLeader(t, a.RPC, "dc1")

	const endpointPath = "/v1/internal/ui/metrics-proxy"

	// create some ACL things
	for name, rules := range map[string]string{
		"one-service":  `service "foo" { policy = "read" }`,
		"all-services": `service_prefix "" { policy = "read" }`,
		"one-node":     `node "bar" { policy = "read" }`,
		"all-nodes":    `node_prefix "" { policy = "read" }`,
	} {
		req := structs.ACLPolicySetRequest{
			Policy: structs.ACLPolicy{
				Name:  name,
				Rules: rules,
			},
			Datacenter:   "dc1",
			WriteRequest: structs.WriteRequest{Token: "root"},
		}
		var policy structs.ACLPolicy
		require.NoError(t, a.RPC(context.Background(), "ACL.PolicySet", &req, &policy))
	}

	makeToken := func(t *testing.T, policyNames []string) string {
		req := structs.ACLTokenSetRequest{
			ACLToken:     structs.ACLToken{},
			Datacenter:   "dc1",
			WriteRequest: structs.WriteRequest{Token: "root"},
		}
		for _, name := range policyNames {
			req.ACLToken.Policies = append(req.ACLToken.Policies, structs.ACLTokenPolicyLink{Name: name})
		}
		require.Len(t, req.ACLToken.Policies, len(policyNames))

		var token structs.ACLToken
		require.NoError(t, a.RPC(context.Background(), "ACL.TokenSet", &req, &token))
		return token.SecretID
	}

	type testcase struct {
		name     string
		token    string
		policies []string
		expect   int
	}

	for _, tc := range []testcase{
		{name: "no token", token: "", expect: http.StatusForbidden},
		{name: "root token", token: "root", expect: http.StatusOK},
		//
		{name: "one node", policies: []string{"one-node"}, expect: http.StatusForbidden},
		{name: "all nodes", policies: []string{"all-nodes"}, expect: http.StatusForbidden},
		//
		{name: "one service", policies: []string{"one-service"}, expect: http.StatusForbidden},
		{name: "all services", policies: []string{"all-services"}, expect: http.StatusForbidden},
		//
		{name: "one service one node", policies: []string{"one-service", "one-node"}, expect: http.StatusForbidden},
		{name: "all services one node", policies: []string{"all-services", "one-node"}, expect: http.StatusForbidden},
		//
		{name: "one service all nodes", policies: []string{"one-service", "one-node"}, expect: http.StatusForbidden},
		{name: "all services all nodes", policies: []string{"all-services", "all-nodes"}, expect: http.StatusOK},
	} {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			if tc.token == "" {
				tc.token = makeToken(t, tc.policies)
			}

			t.Run("via query param should not work", func(t *testing.T) {
				req := httptest.NewRequest("GET", endpointPath+"/ok?token="+tc.token, nil)
				rec := httptest.NewRecorder()
				backendCalled.Store(false)
				h.ServeHTTP(rec, req)
				require.Equal(t, http.StatusForbidden, rec.Code)

				require.False(t, backendCalled.Load().(bool))
			})

			for _, headerName := range []string{"x-consul-token", "authorization"} {
				headerVal := tc.token
				if headerName == "authorization" {
					headerVal = "bearer " + tc.token
				}

				t.Run("via header "+headerName, func(t *testing.T) {
					req := httptest.NewRequest("GET", endpointPath+"/ok", nil)
					req.Header.Set(headerName, headerVal)
					rec := httptest.NewRecorder()
					backendCalled.Store(false)
					h.ServeHTTP(rec, req)
					require.Equal(t, tc.expect, rec.Code)

					headersSent, _ := lastHeadersSent.Load().(http.Header)
					if tc.expect == http.StatusOK {
						require.True(t, backendCalled.Load().(bool))
						// Ensure we didn't accidentally ship our consul token to the proxy.
						require.Empty(t, headersSent.Get("X-Consul-Token"))
						require.Empty(t, headersSent.Get("Authorization"))
					} else {
						require.False(t, backendCalled.Load().(bool))
					}
				})
			}
		})
	}
}
