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

package agent

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/hashicorp/consul/agent/grpc-external/limiter"
	"github.com/hashicorp/consul/agent/proxycfg"
	"github.com/hashicorp/consul/agent/structs"
	"github.com/hashicorp/consul/api"
	"github.com/hashicorp/consul/testrpc"
)

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

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

	token := generateUUID()

	svc := &structs.NodeService{
		ID:             "db",
		Service:        "db",
		Port:           5000,
		EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(),
	}
	require.NoError(t, a.State.AddServiceWithChecks(svc, nil, token, true))

	proxy := &structs.NodeService{
		Kind:    structs.ServiceKindConnectProxy,
		ID:      "db-sidecar-proxy",
		Service: "db-sidecar-proxy",
		Port:    5000,
		// Set this internal state that we expect sidecar registrations to have.
		LocallyRegisteredAsSidecar: true,
		Proxy: structs.ConnectProxyConfig{
			DestinationServiceName: "db",
			Upstreams:              structs.TestUpstreams(t, false),
		},
		EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(),
	}
	require.NoError(t, a.State.AddServiceWithChecks(proxy, nil, token, true))

	// This is a little gross, but this gives us the layered pair of
	// local/catalog sources for now.
	cfg := a.xdsServer.ProxyWatcher

	var (
		timer      = time.After(100 * time.Millisecond)
		timerFired = false
		finalTimer <-chan time.Time
	)

	var (
		firstTime = true
		ch        <-chan *proxycfg.ConfigSnapshot
		stc       limiter.SessionTerminatedChan
		cancel    context.CancelFunc
	)
	defer func() {
		if cancel != nil {
			cancel()
		}
	}()
	for {
		if ch == nil {
			// Sign up for a stream of config snapshots, in the same manner as the xds server.
			sid := proxy.CompoundServiceID()

			if firstTime {
				firstTime = false
			} else {
				t.Logf("re-creating watch")
			}

			// Prior to fixes in https://github.com/hashicorp/consul/pull/16497
			// this call to Watch() would deadlock.
			var err error
			ch, stc, _, cancel, err = cfg.Watch(sid, a.config.NodeName, token)
			require.NoError(t, err)
		}
		select {
		case <-stc:
			t.Fatal("session unexpectedly terminated")
		case snap, ok := <-ch:
			if !ok {
				t.Logf("channel is closed")
				cancel()
				ch, stc, cancel = nil, nil, nil
				continue
			}
			require.NotNil(t, snap)
			if !timerFired {
				t.Fatal("should not have gotten snapshot until after we manifested the token")
			}
			return
		case <-timer:
			timerFired = true
			finalTimer = time.After(1 * time.Second)

			// This simulates the eventual consistency of a token
			// showing up on a server after it's creation by
			// pre-creating the UUID and later using that as the
			// initial SecretID for a real token.
			gotToken := testWriteToken(t, a, &api.ACLToken{
				AccessorID:  generateUUID(),
				SecretID:    token,
				Description: "my token",
				ServiceIdentities: []*api.ACLServiceIdentity{{
					ServiceName: "db",
				}},
			})
			require.Equal(t, token, gotToken)
		case <-finalTimer:
			t.Fatal("did not receive a snapshot after the token manifested")
		}
	}

}

func testWriteToken(t *testing.T, a *TestAgent, tok *api.ACLToken) string {
	req, _ := http.NewRequest("PUT", "/v1/acl/token", jsonReader(tok))
	req.Header.Add("X-Consul-Token", "root")
	resp := httptest.NewRecorder()
	a.srv.h.ServeHTTP(resp, req)
	require.Equal(t, http.StatusOK, resp.Code)

	dec := json.NewDecoder(resp.Body)
	aclResp := &structs.ACLToken{}
	require.NoError(t, dec.Decode(aclResp))
	return aclResp.SecretID
}
