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

package proxycfg

import (
	"fmt"
	"time"

	"github.com/mitchellh/go-testing-interface"
	"github.com/stretchr/testify/assert"

	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/agent/connect"
	"github.com/hashicorp/consul/agent/consul/discoverychain"
	"github.com/hashicorp/consul/agent/structs"
	"github.com/hashicorp/consul/api"
	"github.com/hashicorp/consul/types"
)

// TestConfigSnapshot returns a fully populated snapshot
func TestConfigSnapshot(t testing.T, nsFn func(ns *structs.NodeService), extraUpdates []UpdateEvent) *ConfigSnapshot {
	roots, leaf := TestCerts(t)

	// no entries implies we'll get a default chain
	dbChain := discoverychain.TestCompileConfigEntries(t, "db", "default", "default", "dc1", connect.TestClusterID+".consul", nil, nil)
	assert.True(t, dbChain.Default)

	var (
		upstreams   = structs.TestUpstreams(t, false)
		dbUpstream  = upstreams[0]
		geoUpstream = upstreams[1]

		dbUID  = NewUpstreamID(&dbUpstream)
		geoUID = NewUpstreamID(&geoUpstream)

		webSN = structs.ServiceIDString("web", nil)
	)

	baseEvents := []UpdateEvent{
		{
			CorrelationID: rootsWatchID,
			Result:        roots,
		},
		{
			CorrelationID: leafWatchID,
			Result:        leaf,
		},
		{
			CorrelationID: intentionsWatchID,
			Result:        structs.SimplifiedIntentions{}, // no intentions defined
		},
		{
			CorrelationID: svcChecksWatchIDPrefix + webSN,
			Result:        []structs.CheckType{},
		},
		{
			CorrelationID: "upstream:" + geoUID.String(),
			Result: &structs.PreparedQueryExecuteResponse{
				Nodes: TestPreparedQueryNodes(t, "geo-cache"),
			},
		},
		{
			CorrelationID: "discovery-chain:" + dbUID.String(),
			Result: &structs.DiscoveryChainResponse{
				Chain: dbChain,
			},
		},
		{
			CorrelationID: "upstream-target:" + dbChain.ID() + ":" + dbUID.String(),
			Result: &structs.IndexedCheckServiceNodes{
				Nodes: TestUpstreamNodes(t, "db"),
			},
		},
	}

	return testConfigSnapshotFixture(t, &structs.NodeService{
		Kind:    structs.ServiceKindConnectProxy,
		Service: "web-sidecar-proxy",
		Port:    9999,
		Proxy: structs.ConnectProxyConfig{
			DestinationServiceID:   "web",
			DestinationServiceName: "web",
			LocalServiceAddress:    "127.0.0.1",
			LocalServicePort:       8080,
			Config: map[string]interface{}{
				"foo": "bar",
			},
			Upstreams: upstreams,
		},
		Meta:            nil,
		TaggedAddresses: nil,
	}, nsFn, nil, testSpliceEvents(baseEvents, extraUpdates))
}

// TestConfigSnapshotDiscoveryChain returns a fully populated snapshot using a discovery chain
func TestConfigSnapshotDiscoveryChain(
	t testing.T,
	variation string,
	enterprise bool,
	nsFn func(ns *structs.NodeService),
	extraUpdates []UpdateEvent,
	additionalEntries ...structs.ConfigEntry,
) *ConfigSnapshot {
	roots, leaf := TestCerts(t)

	var entMeta acl.EnterpriseMeta
	if enterprise {
		entMeta = acl.NewEnterpriseMetaWithPartition("ap1", "ns1")
	}

	var (
		upstreams   = structs.TestUpstreams(t, enterprise)
		geoUpstream = upstreams[1]

		geoUID = NewUpstreamID(&geoUpstream)

		webSN = structs.ServiceIDString("web", &entMeta)
	)

	baseEvents := testSpliceEvents([]UpdateEvent{
		{
			CorrelationID: rootsWatchID,
			Result:        roots,
		},
		{
			CorrelationID: leafWatchID,
			Result:        leaf,
		},
		{
			CorrelationID: intentionsWatchID,
			Result:        structs.SimplifiedIntentions{}, // no intentions defined
		},
		{
			CorrelationID: meshConfigEntryID,
			Result: &structs.ConfigEntryResponse{
				Entry: nil,
			},
		},
		{
			CorrelationID: svcChecksWatchIDPrefix + webSN,
			Result:        []structs.CheckType{},
		},
		{
			CorrelationID: "upstream:" + geoUID.String(),
			Result: &structs.PreparedQueryExecuteResponse{
				Nodes: TestPreparedQueryNodes(t, "geo-cache"),
			},
		},
	}, setupTestVariationConfigEntriesAndSnapshot(
		t, variation, enterprise, upstreams, additionalEntries...,
	))

	return testConfigSnapshotFixture(t, &structs.NodeService{
		Kind:    structs.ServiceKindConnectProxy,
		Service: "web-sidecar-proxy",
		Port:    9999,
		Proxy: structs.ConnectProxyConfig{
			DestinationServiceID:   "web",
			DestinationServiceName: "web",
			LocalServiceAddress:    "127.0.0.1",
			LocalServicePort:       8080,
			Config: map[string]interface{}{
				"foo": "bar",
			},
			Upstreams: upstreams,
		},
		Meta:            nil,
		TaggedAddresses: nil,
		EnterpriseMeta:  entMeta,
	}, nsFn, nil, testSpliceEvents(baseEvents, extraUpdates))
}

func TestConfigSnapshotExposeConfig(t testing.T, nsFn func(ns *structs.NodeService)) *ConfigSnapshot {
	roots, leaf := TestCerts(t)

	var (
		webSN = structs.ServiceIDString("web", nil)
	)

	baseEvents := []UpdateEvent{
		{
			CorrelationID: rootsWatchID,
			Result:        roots,
		},
		{
			CorrelationID: leafWatchID, Result: leaf,
		},
		{
			CorrelationID: intentionsWatchID,
			Result:        structs.SimplifiedIntentions{}, // no intentions defined
		},
		{
			CorrelationID: svcChecksWatchIDPrefix + webSN,
			Result:        []structs.CheckType{},
		},
	}

	return testConfigSnapshotFixture(t, &structs.NodeService{
		Kind:    structs.ServiceKindConnectProxy,
		Service: "web-sidecar-proxy",
		Address: "1.2.3.4",
		Port:    8080,
		Proxy: structs.ConnectProxyConfig{
			DestinationServiceID:   "web",
			DestinationServiceName: "web",
			LocalServicePort:       8080,
			Expose: structs.ExposeConfig{
				Checks: false,
				Paths: []structs.ExposePath{
					{
						LocalPathPort: 8080,
						Path:          "/health1",
						ListenerPort:  21500,
					},
					{
						LocalPathPort: 8080,
						Path:          "/health2",
						ListenerPort:  21501,
					},
				},
			},
		},
		Meta:            nil,
		TaggedAddresses: nil,
	}, nsFn, nil, baseEvents)
}

func TestConfigSnapshotExposeChecks(t testing.T) *ConfigSnapshot {
	return testConfigSnapshotExposedChecks(t, false)
}

func TestConfigSnapshotExposeChecksWithBindOverride(t testing.T) *ConfigSnapshot {
	return testConfigSnapshotExposedChecks(t, true)
}

func testConfigSnapshotExposedChecks(t testing.T, overrideBind bool) *ConfigSnapshot {
	return TestConfigSnapshot(t,
		func(ns *structs.NodeService) {
			ns.Address = "1.2.3.4"
			ns.Port = 8080
			ns.Proxy.Upstreams = nil
			ns.Proxy.Expose = structs.ExposeConfig{
				Checks: true,
			}
			if overrideBind {
				if ns.Proxy.Config == nil {
					ns.Proxy.Config = map[string]any{}
				}
				ns.Proxy.Config["bind_address"] = "6.7.8.9"
			}
		},
		[]UpdateEvent{
			{
				CorrelationID: svcChecksWatchIDPrefix + structs.ServiceIDString("web", nil),
				Result: []structs.CheckType{{
					CheckID:   types.CheckID("http"),
					Name:      "http",
					HTTP:      "http://127.0.0.1:8181/debug",
					ProxyHTTP: "http://:21500/debug",
					Method:    "GET",
					Interval:  10 * time.Second,
					Timeout:   1 * time.Second,
				}},
			},
		},
	)
}

func TestConfigSnapshotExposeChecksGRPC(t testing.T) *ConfigSnapshot {
	return TestConfigSnapshot(t,
		func(ns *structs.NodeService) {
			ns.Address = "1.2.3.4"
			ns.Port = 9090
			ns.Proxy.Upstreams = nil
			ns.Proxy.Expose = structs.ExposeConfig{
				Checks: true,
			}
		},
		[]UpdateEvent{
			{
				CorrelationID: svcChecksWatchIDPrefix + structs.ServiceIDString("web", nil),
				Result: []structs.CheckType{{
					CheckID:   types.CheckID("grpc"),
					Name:      "grpc",
					GRPC:      "localhost:9090/v1.Health",
					ProxyGRPC: "localhost:21501/myservice",
					Interval:  10 * time.Second,
					Timeout:   1 * time.Second,
				}},
			},
		},
	)
}

func TestConfigSnapshotGRPCExposeHTTP1(t testing.T) *ConfigSnapshot {
	roots, leaf := TestCerts(t)

	return testConfigSnapshotFixture(t, &structs.NodeService{
		Kind:    structs.ServiceKindConnectProxy,
		Service: "grpc-proxy",
		Address: "1.2.3.4",
		Port:    8080,
		Proxy: structs.ConnectProxyConfig{
			DestinationServiceName: "grpc",
			DestinationServiceID:   "grpc",
			LocalServicePort:       8080,
			Config: map[string]interface{}{
				"protocol": "grpc",
			},
			Expose: structs.ExposeConfig{
				Checks: false,
				Paths: []structs.ExposePath{
					{
						LocalPathPort: 8090,
						Path:          "/healthz",
						ListenerPort:  21500,
						Protocol:      "http",
					},
				},
			},
		},
		Meta:            nil,
		TaggedAddresses: nil,
	}, nil, nil, []UpdateEvent{
		{
			CorrelationID: rootsWatchID,
			Result:        roots,
		},
		{
			CorrelationID: leafWatchID,
			Result:        leaf,
		},
		{
			CorrelationID: intentionsWatchID,
			Result:        structs.SimplifiedIntentions{}, // no intentions defined
		},
		{
			CorrelationID: svcChecksWatchIDPrefix + structs.ServiceIDString("grpc", nil),
			Result:        []structs.CheckType{},
		},
	})
}

// TestConfigSnapshotTelemetryCollector returns a fully populated snapshot using a discovery chain
func TestConfigSnapshotTelemetryCollector(t testing.T) *ConfigSnapshot {
	// DiscoveryChain without an UpstreamConfig should yield a
	// filter chain when in transparent proxy mode
	var (
		collector      = structs.NewServiceName(api.TelemetryCollectorName, nil)
		collectorUID   = NewUpstreamIDFromServiceName(collector)
		collectorChain = discoverychain.TestCompileConfigEntries(t, api.TelemetryCollectorName, "default", "default", "dc1", connect.TestClusterID+".consul", nil, nil)
	)

	return TestConfigSnapshot(t, func(ns *structs.NodeService) {
		ns.Proxy.Config = map[string]interface{}{
			"envoy_telemetry_collector_bind_socket_dir": "/tmp/consul/telemetry-collector",
		}
	}, []UpdateEvent{
		{
			CorrelationID: meshConfigEntryID,
			Result: &structs.ConfigEntryResponse{
				Entry: nil,
			},
		},
		{
			CorrelationID: "discovery-chain:" + collectorUID.String(),
			Result: &structs.DiscoveryChainResponse{
				Chain: collectorChain,
			},
		},
		{
			CorrelationID: fmt.Sprintf("upstream-target:%s.default.default.dc1:", api.TelemetryCollectorName) + collectorUID.String(),
			Result: &structs.IndexedCheckServiceNodes{
				Nodes: []structs.CheckServiceNode{
					{
						Node: &structs.Node{
							Address:    "8.8.8.8",
							Datacenter: "dc1",
						},
						Service: &structs.NodeService{
							Service: api.TelemetryCollectorName,
							Address: "9.9.9.9",
							Port:    9090,
						},
					},
				},
			},
		},
	})
}
