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

package nomad

import (
	"fmt"
	"testing"
	"time"

	"github.com/hashicorp/nomad/ci"
	"github.com/hashicorp/nomad/helper"
	"github.com/hashicorp/nomad/helper/pointer"
	"github.com/hashicorp/nomad/helper/testlog"
	"github.com/hashicorp/nomad/nomad/mock"
	"github.com/hashicorp/nomad/nomad/structs"
	"github.com/shoenig/test/must"
	"github.com/stretchr/testify/require"
)

func TestJobEndpointConnect_isSidecarForService(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		task    *structs.Task
		sidecar string
		exp     bool
	}{
		{
			&structs.Task{},
			"api",
			false,
		},
		{
			&structs.Task{
				Kind: "connect-proxy:api",
			},
			"api",
			true,
		},
		{
			&structs.Task{
				Kind: "connect-proxy:api",
			},
			"db",
			false,
		},
		{
			&structs.Task{
				Kind: "api",
			},
			"api",
			false,
		},
	}

	for _, c := range cases {
		require.Equal(t, c.exp, isSidecarForService(c.task, c.sidecar))
	}
}

func TestJobEndpointConnect_groupConnectGuessTaskDriver(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		name    string
		drivers []string
		exp     string
	}{
		{
			name:    "none",
			drivers: nil,
			exp:     "docker",
		},
		{
			name:    "neither",
			drivers: []string{"exec", "raw_exec", "rkt"},
			exp:     "docker",
		},
		{
			name:    "docker only",
			drivers: []string{"docker"},
			exp:     "docker",
		},
		{
			name:    "podman only",
			drivers: []string{"podman"},
			exp:     "podman",
		},
		{
			name:    "mix with docker",
			drivers: []string{"podman", "docker", "exec"},
			exp:     "docker",
		},
		{
			name:    "mix without docker",
			drivers: []string{"exec", "podman", "raw_exec"},
			exp:     "podman",
		},
	}

	for _, tc := range cases {
		tasks := helper.ConvertSlice(tc.drivers, func(driver string) *structs.Task {
			return &structs.Task{Driver: driver}
		})
		tg := &structs.TaskGroup{Tasks: tasks}
		result := groupConnectGuessTaskDriver(tg)
		must.Eq(t, tc.exp, result)
	}
}

func TestJobEndpointConnect_newConnectSidecarTask(t *testing.T) {
	ci.Parallel(t)

	task := newConnectSidecarTask("redis", "podman")
	must.Eq(t, "connect-proxy-redis", task.Name)
	must.Eq(t, "podman", task.Driver)

	task2 := newConnectSidecarTask("db", "docker")
	must.Eq(t, "connect-proxy-db", task2.Name)
	must.Eq(t, "docker", task2.Driver)
}

func TestJobEndpointConnect_groupConnectHook(t *testing.T) {
	ci.Parallel(t)

	// Test that connect-proxy task is inserted for backend service
	job := mock.Job()

	job.Meta = map[string]string{
		"backend_name": "backend",
		"admin_name":   "admin",
	}

	job.TaskGroups[0] = &structs.TaskGroup{
		Networks: structs.Networks{{
			Mode: "bridge",
		}},
		Services: []*structs.Service{{
			Name:      "${NOMAD_META_backend_name}",
			PortLabel: "8080",
			Connect: &structs.ConsulConnect{
				SidecarService: &structs.ConsulSidecarService{},
			},
		}, {
			Name:      "${NOMAD_META_admin_name}",
			PortLabel: "9090",
			Connect: &structs.ConsulConnect{
				SidecarService: &structs.ConsulSidecarService{},
			},
		}},
	}

	// Expected tasks
	tgExp := job.TaskGroups[0].Copy()
	tgExp.Tasks = []*structs.Task{
		newConnectSidecarTask("backend", "docker"),
		newConnectSidecarTask("admin", "docker"),
	}
	tgExp.Services[0].Name = "backend"
	tgExp.Services[1].Name = "admin"

	// Expect sidecar tasks to be in canonical form.
	tgExp.Tasks[0].Canonicalize(job, tgExp)
	tgExp.Tasks[1].Canonicalize(job, tgExp)
	tgExp.Networks[0].DynamicPorts = []structs.Port{{
		Label: fmt.Sprintf("%s-%s", structs.ConnectProxyPrefix, "backend"),
		To:    -1,
	}, {
		Label: fmt.Sprintf("%s-%s", structs.ConnectProxyPrefix, "admin"),
		To:    -1,
	}}
	tgExp.Networks[0].Canonicalize()

	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
	require.Exactly(t, tgExp, job.TaskGroups[0])

	// Test that hook is idempotent
	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
	require.Exactly(t, tgExp, job.TaskGroups[0])
}

func TestJobEndpointConnect_groupConnectHook_IngressGateway_BridgeNetwork(t *testing.T) {
	ci.Parallel(t)

	// Test that the connect ingress gateway task is inserted if a gateway service
	// exists and since this is a bridge network, will rewrite the default gateway proxy
	// block with correct configuration.
	job := mock.ConnectIngressGatewayJob("bridge", false)
	job.Meta = map[string]string{
		"gateway_name": "my-gateway",
	}
	job.TaskGroups[0].Services[0].Name = "${NOMAD_META_gateway_name}"
	job.TaskGroups[0].Services[0].Connect.Gateway.Ingress.TLS = &structs.ConsulGatewayTLSConfig{
		Enabled:       true,
		TLSMinVersion: "TLSv1_2",
	}

	// setup expectations
	expTG := job.TaskGroups[0].Copy()
	expTG.Tasks = []*structs.Task{
		// inject the gateway task
		newConnectGatewayTask(structs.ConnectIngressPrefix, "my-gateway", false, true),
	}
	expTG.Services[0].Name = "my-gateway"
	expTG.Tasks[0].Canonicalize(job, expTG)
	expTG.Networks[0].Canonicalize()

	// rewrite the service gateway proxy configuration
	expTG.Services[0].Connect.Gateway.Proxy = gatewayProxy(expTG.Services[0].Connect.Gateway, "bridge")

	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
	require.Exactly(t, expTG, job.TaskGroups[0])

	// Test that the hook is idempotent
	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
	require.Exactly(t, expTG, job.TaskGroups[0])

	// Test that the hook populates the correct constraint for customized tls
	require.Contains(t, job.TaskGroups[0].Tasks[0].Constraints, &structs.Constraint{
		LTarget: "${attr.consul.version}",
		RTarget: ">= 1.11.2",
		Operand: structs.ConstraintSemver,
	})
}

func TestJobEndpointConnect_groupConnectHook_IngressGateway_HostNetwork(t *testing.T) {
	ci.Parallel(t)

	// Test that the connect ingress gateway task is inserted if a gateway service
	// exists. In host network mode, the default values are used.
	job := mock.ConnectIngressGatewayJob("host", false)
	job.Meta = map[string]string{
		"gateway_name": "my-gateway",
	}
	job.TaskGroups[0].Services[0].Name = "${NOMAD_META_gateway_name}"

	// setup expectations
	expTG := job.TaskGroups[0].Copy()
	expTG.Tasks = []*structs.Task{
		// inject the gateway task
		newConnectGatewayTask(structs.ConnectIngressPrefix, "my-gateway", true, false),
	}
	expTG.Services[0].Name = "my-gateway"
	expTG.Tasks[0].Canonicalize(job, expTG)
	expTG.Networks[0].Canonicalize()

	// rewrite the service gateway proxy configuration
	expTG.Services[0].Connect.Gateway.Proxy = gatewayProxy(expTG.Services[0].Connect.Gateway, "host")

	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
	require.Exactly(t, expTG, job.TaskGroups[0])

	// Test that the hook is idempotent
	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
	require.Exactly(t, expTG, job.TaskGroups[0])
}

func TestJobEndpointConnect_groupConnectHook_IngressGateway_CustomTask(t *testing.T) {
	ci.Parallel(t)

	// Test that the connect gateway task is inserted if a gateway service exists
	// and since this is a bridge network, will rewrite the default gateway proxy
	// block with correct configuration.
	job := mock.ConnectIngressGatewayJob("bridge", false)
	job.Meta = map[string]string{
		"gateway_name": "my-gateway",
	}
	job.TaskGroups[0].Services[0].Name = "${NOMAD_META_gateway_name}"
	job.TaskGroups[0].Services[0].Connect.SidecarTask = &structs.SidecarTask{
		Driver: "raw_exec",
		User:   "sidecars",
		Config: map[string]interface{}{
			"command": "/bin/sidecar",
			"args":    []string{"a", "b"},
		},
		Resources: &structs.Resources{
			CPU: 400,
			// Memory: inherit 128
		},
		KillSignal: "SIGHUP",
	}

	// setup expectations
	expTG := job.TaskGroups[0].Copy()
	expTG.Tasks = []*structs.Task{
		// inject merged gateway task
		{
			Name:   "connect-ingress-my-gateway",
			Kind:   structs.NewTaskKind(structs.ConnectIngressPrefix, "my-gateway"),
			Driver: "raw_exec",
			User:   "sidecars",
			Config: map[string]interface{}{
				"command": "/bin/sidecar",
				"args":    []string{"a", "b"},
			},
			Resources: &structs.Resources{
				CPU:      400,
				MemoryMB: 128,
			},
			LogConfig: &structs.LogConfig{
				MaxFiles:      2,
				MaxFileSizeMB: 2,
			},
			ShutdownDelay: 5 * time.Second,
			KillSignal:    "SIGHUP",
			Constraints: structs.Constraints{
				connectGatewayVersionConstraint(),
				connectListenerConstraint(),
			},
		},
	}
	expTG.Services[0].Name = "my-gateway"
	expTG.Tasks[0].Canonicalize(job, expTG)
	expTG.Networks[0].Canonicalize()

	// rewrite the service gateway proxy configuration
	expTG.Services[0].Connect.Gateway.Proxy = gatewayProxy(expTG.Services[0].Connect.Gateway, "bridge")

	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
	require.Exactly(t, expTG, job.TaskGroups[0])

	// Test that the hook is idempotent
	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
	require.Exactly(t, expTG, job.TaskGroups[0])
}

func TestJobEndpointConnect_groupConnectHook_TerminatingGateway(t *testing.T) {
	ci.Parallel(t)

	// Tests that the connect terminating gateway task is inserted if a gateway
	// service exists and since this is a bridge network, will rewrite the default
	// gateway proxy block with correct configuration.
	job := mock.ConnectTerminatingGatewayJob("bridge", false)
	job.Meta = map[string]string{
		"gateway_name": "my-gateway",
	}
	job.TaskGroups[0].Services[0].Name = "${NOMAD_META_gateway_name}"

	// setup expectations
	expTG := job.TaskGroups[0].Copy()
	expTG.Tasks = []*structs.Task{
		// inject the gateway task
		newConnectGatewayTask(structs.ConnectTerminatingPrefix, "my-gateway", false, false),
	}
	expTG.Services[0].Name = "my-gateway"
	expTG.Tasks[0].Canonicalize(job, expTG)
	expTG.Networks[0].Canonicalize()

	// rewrite the service gateway proxy configuration
	expTG.Services[0].Connect.Gateway.Proxy = gatewayProxy(expTG.Services[0].Connect.Gateway, "bridge")

	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
	require.Exactly(t, expTG, job.TaskGroups[0])

	// Test that the hook is idempotent
	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
	require.Exactly(t, expTG, job.TaskGroups[0])
}

func TestJobEndpointConnect_groupConnectHook_MeshGateway(t *testing.T) {
	ci.Parallel(t)

	// Test that the connect mesh gateway task is inserted if a gateway service
	// exists and since this is a bridge network, will rewrite the default gateway
	// proxy block with correct configuration, injecting a dynamic port for use
	// by the envoy lan listener.
	job := mock.ConnectMeshGatewayJob("bridge", false)
	job.Meta = map[string]string{
		"gateway_name": "my-gateway",
	}
	job.TaskGroups[0].Services[0].Name = "${NOMAD_META_gateway_name}"

	// setup expectations
	expTG := job.TaskGroups[0].Copy()
	expTG.Tasks = []*structs.Task{
		// inject the gateway task
		newConnectGatewayTask(structs.ConnectMeshPrefix, "my-gateway", false, false),
	}
	expTG.Services[0].Name = "my-gateway"
	expTG.Services[0].PortLabel = "public_port"
	expTG.Networks[0].DynamicPorts = []structs.Port{{
		Label:       "connect-mesh-my-gateway-lan",
		Value:       0,
		To:          -1,
		HostNetwork: "default",
	}}
	expTG.Tasks[0].Canonicalize(job, expTG)
	expTG.Networks[0].Canonicalize()

	// rewrite the service gateway proxy configuration
	expTG.Services[0].Connect.Gateway.Proxy = gatewayProxy(expTG.Services[0].Connect.Gateway, "bridge")

	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
	require.Exactly(t, expTG, job.TaskGroups[0])

	// Test that the hook is idempotent
	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
	require.Exactly(t, expTG, job.TaskGroups[0])
}

// TestJobEndpoint_ConnectInterpolation asserts that when a Connect sidecar
// proxy task is being created for a group service with an interpolated name,
// the service name is interpolated *before the task is created.
//
// See https://github.com/hashicorp/nomad/issues/6853
func TestJobEndpointConnect_ConnectInterpolation(t *testing.T) {
	ci.Parallel(t)

	server := &Server{logger: testlog.HCLogger(t)}
	jobEndpoint := NewJobEndpoints(server, nil)

	j := mock.ConnectJob()
	j.TaskGroups[0].Services[0].Name = "${JOB}-api"
	j, warnings, err := jobEndpoint.admissionMutators(j)
	require.NoError(t, err)
	require.Nil(t, warnings)

	require.Len(t, j.TaskGroups[0].Tasks, 2)
	require.Equal(t, "connect-proxy-my-job-api", j.TaskGroups[0].Tasks[1].Name)
}

func TestJobEndpointConnect_groupConnectSidecarValidate(t *testing.T) {
	ci.Parallel(t)

	// network validation

	makeService := func(name string) *structs.Service {
		return &structs.Service{Name: name, Connect: &structs.ConsulConnect{
			SidecarService: new(structs.ConsulSidecarService),
		}}
	}

	t.Run("sidecar 0 networks", func(t *testing.T) {
		require.EqualError(t, groupConnectSidecarValidate(&structs.TaskGroup{
			Name:     "g1",
			Networks: nil,
		}, makeService("connect-service")), `Consul Connect sidecars require exactly 1 network, found 0 in group "g1"`)
	})

	t.Run("sidecar non bridge", func(t *testing.T) {
		require.EqualError(t, groupConnectSidecarValidate(&structs.TaskGroup{
			Name: "g2",
			Networks: structs.Networks{{
				Mode: "host",
			}},
		}, makeService("connect-service")), `Consul Connect sidecar requires bridge network, found "host" in group "g2"`)
	})

	t.Run("sidecar okay", func(t *testing.T) {
		require.NoError(t, groupConnectSidecarValidate(&structs.TaskGroup{
			Name: "g3",
			Networks: structs.Networks{{
				Mode: "bridge",
			}},
		}, makeService("connect-service")))
	})

	// group and service name validation

	t.Run("non-connect service contains uppercase characters", func(t *testing.T) {
		err := groupConnectValidate(&structs.TaskGroup{
			Name:     "group",
			Networks: structs.Networks{{Mode: "bridge"}},
			Services: []*structs.Service{{
				Name: "Other-Service",
			}},
		})
		require.NoError(t, err)
	})

	t.Run("connect service contains uppercase characters", func(t *testing.T) {
		err := groupConnectValidate(&structs.TaskGroup{
			Name:     "group",
			Networks: structs.Networks{{Mode: "bridge"}},
			Services: []*structs.Service{{
				Name: "Other-Service",
			}, makeService("Connect-Service")},
		})
		require.EqualError(t, err, `Consul Connect service name "Connect-Service" in group "group" must not contain uppercase characters`)
	})

	t.Run("non-connect group contains uppercase characters", func(t *testing.T) {
		err := groupConnectValidate(&structs.TaskGroup{
			Name:     "Other-Group",
			Networks: structs.Networks{{Mode: "bridge"}},
			Services: []*structs.Service{{
				Name: "other-service",
			}},
		})
		require.NoError(t, err)
	})

	t.Run("connect-group contains uppercase characters", func(t *testing.T) {
		err := groupConnectValidate(&structs.TaskGroup{
			Name:     "Connect-Group",
			Networks: structs.Networks{{Mode: "bridge"}},
			Services: []*structs.Service{{
				Name: "other-service",
			}, makeService("connect-service")},
		})
		require.EqualError(t, err, `Consul Connect group "Connect-Group" with service "connect-service" must not contain uppercase characters`)
	})

	t.Run("connect group and service lowercase", func(t *testing.T) {
		err := groupConnectValidate(&structs.TaskGroup{
			Name:     "connect-group",
			Networks: structs.Networks{{Mode: "bridge"}},
			Services: []*structs.Service{{
				Name: "other-service",
			}, makeService("connect-service")},
		})
		require.NoError(t, err)
	})

	t.Run("connect group overlap upstreams", func(t *testing.T) {
		s1 := makeService("s1")
		s2 := makeService("s2")
		s1.Connect.SidecarService.Proxy = &structs.ConsulProxy{
			Upstreams: []structs.ConsulUpstream{{
				LocalBindPort: 8999,
			}},
		}
		s2.Connect.SidecarService.Proxy = &structs.ConsulProxy{
			Upstreams: []structs.ConsulUpstream{{
				LocalBindPort: 8999,
			}},
		}
		err := groupConnectValidate(&structs.TaskGroup{
			Name:     "connect-group",
			Networks: structs.Networks{{Mode: "bridge"}},
			Services: []*structs.Service{s1, s2},
		})
		require.EqualError(t, err, `Consul Connect services "s2" and "s1" in group "connect-group" using same address for upstreams (:8999)`)
	})
}

func TestJobEndpointConnect_groupConnectUpstreamsValidate(t *testing.T) {
	ci.Parallel(t)

	t.Run("no connect services", func(t *testing.T) {
		err := groupConnectUpstreamsValidate("group",
			[]*structs.Service{{Name: "s1"}, {Name: "s2"}})
		require.NoError(t, err)
	})

	t.Run("connect services no overlap", func(t *testing.T) {
		err := groupConnectUpstreamsValidate("group",
			[]*structs.Service{
				{
					Name: "s1",
					Connect: &structs.ConsulConnect{
						SidecarService: &structs.ConsulSidecarService{
							Proxy: &structs.ConsulProxy{
								Upstreams: []structs.ConsulUpstream{{
									LocalBindAddress: "127.0.0.1",
									LocalBindPort:    9001,
								}, {
									LocalBindAddress: "127.0.0.1",
									LocalBindPort:    9002,
								}},
							},
						},
					},
				},
				{
					Name: "s2",
					Connect: &structs.ConsulConnect{
						SidecarService: &structs.ConsulSidecarService{
							Proxy: &structs.ConsulProxy{
								Upstreams: []structs.ConsulUpstream{{
									LocalBindAddress: "10.0.0.1",
									LocalBindPort:    9001,
								}, {
									LocalBindAddress: "127.0.0.1",
									LocalBindPort:    9003,
								}},
							},
						},
					},
				},
			})
		require.NoError(t, err)
	})

	t.Run("connect services overlap port", func(t *testing.T) {
		err := groupConnectUpstreamsValidate("group",
			[]*structs.Service{
				{
					Name: "s1",
					Connect: &structs.ConsulConnect{
						SidecarService: &structs.ConsulSidecarService{
							Proxy: &structs.ConsulProxy{
								Upstreams: []structs.ConsulUpstream{{
									LocalBindAddress: "127.0.0.1",
									LocalBindPort:    9001,
								}, {
									LocalBindAddress: "127.0.0.1",
									LocalBindPort:    9002,
								}},
							},
						},
					},
				},
				{
					Name: "s2",
					Connect: &structs.ConsulConnect{
						SidecarService: &structs.ConsulSidecarService{
							Proxy: &structs.ConsulProxy{
								Upstreams: []structs.ConsulUpstream{{
									LocalBindAddress: "127.0.0.1",
									LocalBindPort:    9002,
								}, {
									LocalBindAddress: "127.0.0.1",
									LocalBindPort:    9003,
								}},
							},
						},
					},
				},
			})
		require.EqualError(t, err, `Consul Connect services "s2" and "s1" in group "group" using same address for upstreams (127.0.0.1:9002)`)
	})
}

func TestJobEndpointConnect_getNamedTaskForNativeService(t *testing.T) {
	ci.Parallel(t)

	t.Run("named exists", func(t *testing.T) {
		task, err := getNamedTaskForNativeService(&structs.TaskGroup{
			Name:  "g1",
			Tasks: []*structs.Task{{Name: "t1"}, {Name: "t2"}},
		}, "s1", "t2")
		require.NoError(t, err)
		require.Equal(t, "t2", task.Name)
	})

	t.Run("infer exists", func(t *testing.T) {
		task, err := getNamedTaskForNativeService(&structs.TaskGroup{
			Name:  "g1",
			Tasks: []*structs.Task{{Name: "t2"}},
		}, "s1", "")
		require.NoError(t, err)
		require.Equal(t, "t2", task.Name)
	})

	t.Run("infer ambiguous", func(t *testing.T) {
		task, err := getNamedTaskForNativeService(&structs.TaskGroup{
			Name:  "g1",
			Tasks: []*structs.Task{{Name: "t1"}, {Name: "t2"}},
		}, "s1", "")
		require.EqualError(t, err, "task for Consul Connect Native service g1->s1 is ambiguous and must be set")
		require.Nil(t, task)
	})

	t.Run("named absent", func(t *testing.T) {
		task, err := getNamedTaskForNativeService(&structs.TaskGroup{
			Name:  "g1",
			Tasks: []*structs.Task{{Name: "t1"}, {Name: "t2"}},
		}, "s1", "t3")
		require.EqualError(t, err, "task t3 named by Consul Connect Native service g1->s1 does not exist")
		require.Nil(t, task)
	})
}

func TestJobEndpointConnect_groupConnectGatewayValidate(t *testing.T) {
	ci.Parallel(t)

	t.Run("no group network", func(t *testing.T) {
		err := groupConnectGatewayValidate(&structs.TaskGroup{
			Name:     "g1",
			Networks: nil,
		})
		require.EqualError(t, err, `Consul Connect gateways require exactly 1 network, found 0 in group "g1"`)
	})

	t.Run("bad network mode", func(t *testing.T) {
		err := groupConnectGatewayValidate(&structs.TaskGroup{
			Name: "g1",
			Networks: structs.Networks{{
				Mode: "",
			}},
		})
		require.EqualError(t, err, `Consul Connect Gateway service requires Task Group with network mode of type "bridge" or "host"`)
	})
}

func TestJobEndpointConnect_newConnectGatewayTask_host(t *testing.T) {
	ci.Parallel(t)

	t.Run("ingress", func(t *testing.T) {
		task := newConnectGatewayTask(structs.ConnectIngressPrefix, "foo", true, false)
		require.Equal(t, "connect-ingress-foo", task.Name)
		require.Equal(t, "connect-ingress:foo", string(task.Kind))
		require.Equal(t, ">= 1.8.0", task.Constraints[0].RTarget)
		require.Equal(t, "host", task.Config["network_mode"])
		require.Nil(t, task.Lifecycle)
	})

	t.Run("terminating", func(t *testing.T) {
		task := newConnectGatewayTask(structs.ConnectTerminatingPrefix, "bar", true, false)
		require.Equal(t, "connect-terminating-bar", task.Name)
		require.Equal(t, "connect-terminating:bar", string(task.Kind))
		require.Equal(t, ">= 1.8.0", task.Constraints[0].RTarget)
		require.Equal(t, "host", task.Config["network_mode"])
		require.Nil(t, task.Lifecycle)
	})
}

func TestJobEndpointConnect_newConnectGatewayTask_bridge(t *testing.T) {
	ci.Parallel(t)

	task := newConnectGatewayTask(structs.ConnectIngressPrefix, "service1", false, false)
	require.NotContains(t, task.Config, "network_mode")
}

func TestJobEndpointConnect_hasGatewayTaskForService(t *testing.T) {
	ci.Parallel(t)

	t.Run("no gateway task", func(t *testing.T) {
		result := hasGatewayTaskForService(&structs.TaskGroup{
			Name: "group",
			Tasks: []*structs.Task{{
				Name: "task1",
				Kind: "",
			}},
		}, "my-service")
		require.False(t, result)
	})

	t.Run("has ingress task", func(t *testing.T) {
		result := hasGatewayTaskForService(&structs.TaskGroup{
			Name: "group",
			Tasks: []*structs.Task{{
				Name: "ingress-gateway-my-service",
				Kind: structs.NewTaskKind(structs.ConnectIngressPrefix, "my-service"),
			}},
		}, "my-service")
		require.True(t, result)
	})

	t.Run("has terminating task", func(t *testing.T) {
		result := hasGatewayTaskForService(&structs.TaskGroup{
			Name: "group",
			Tasks: []*structs.Task{{
				Name: "terminating-gateway-my-service",
				Kind: structs.NewTaskKind(structs.ConnectTerminatingPrefix, "my-service"),
			}},
		}, "my-service")
		require.True(t, result)
	})

	t.Run("has mesh task", func(t *testing.T) {
		result := hasGatewayTaskForService(&structs.TaskGroup{
			Name: "group",
			Tasks: []*structs.Task{{
				Name: "mesh-gateway-my-service",
				Kind: structs.NewTaskKind(structs.ConnectMeshPrefix, "my-service"),
			}},
		}, "my-service")
		require.True(t, result)
	})
}

func TestJobEndpointConnect_gatewayProxyIsDefault(t *testing.T) {
	ci.Parallel(t)

	t.Run("nil", func(t *testing.T) {
		result := gatewayProxyIsDefault(nil)
		require.True(t, result)
	})

	t.Run("unrelated fields set", func(t *testing.T) {
		result := gatewayProxyIsDefault(&structs.ConsulGatewayProxy{
			ConnectTimeout: pointer.Of(2 * time.Second),
			Config:         map[string]interface{}{"foo": 1},
		})
		require.True(t, result)
	})

	t.Run("no-bind set", func(t *testing.T) {
		result := gatewayProxyIsDefault(&structs.ConsulGatewayProxy{
			EnvoyGatewayNoDefaultBind: true,
		})
		require.False(t, result)
	})

	t.Run("bind-tagged set", func(t *testing.T) {
		result := gatewayProxyIsDefault(&structs.ConsulGatewayProxy{
			EnvoyGatewayBindTaggedAddresses: true,
		})
		require.False(t, result)
	})

	t.Run("bind-addresses set", func(t *testing.T) {
		result := gatewayProxyIsDefault(&structs.ConsulGatewayProxy{
			EnvoyGatewayBindAddresses: map[string]*structs.ConsulGatewayBindAddress{
				"listener1": {
					Address: "1.1.1.1",
					Port:    9000,
				},
			},
		})
		require.False(t, result)
	})
}

func TestJobEndpointConnect_gatewayBindAddressesForBridge(t *testing.T) {
	ci.Parallel(t)

	t.Run("nil", func(t *testing.T) {

		result := gatewayBindAddressesIngressForBridge(nil)
		require.Empty(t, result)
	})

	t.Run("no listeners", func(t *testing.T) {
		result := gatewayBindAddressesIngressForBridge(&structs.ConsulIngressConfigEntry{Listeners: nil})
		require.Empty(t, result)
	})

	t.Run("simple", func(t *testing.T) {
		result := gatewayBindAddressesIngressForBridge(&structs.ConsulIngressConfigEntry{
			Listeners: []*structs.ConsulIngressListener{{
				Port:     3000,
				Protocol: "tcp",
				Services: []*structs.ConsulIngressService{{
					Name: "service1",
				}},
			}},
		})
		require.Equal(t, map[string]*structs.ConsulGatewayBindAddress{
			"service1": {
				Address: "0.0.0.0",
				Port:    3000,
			},
		}, result)
	})

	t.Run("complex", func(t *testing.T) {
		result := gatewayBindAddressesIngressForBridge(&structs.ConsulIngressConfigEntry{
			Listeners: []*structs.ConsulIngressListener{{
				Port:     3000,
				Protocol: "tcp",
				Services: []*structs.ConsulIngressService{{
					Name: "service1",
				}, {
					Name: "service2",
				}},
			}, {
				Port:     3001,
				Protocol: "http",
				Services: []*structs.ConsulIngressService{{
					Name: "service3",
				}},
			}},
		})
		require.Equal(t, map[string]*structs.ConsulGatewayBindAddress{
			"service1": {
				Address: "0.0.0.0",
				Port:    3000,
			},
			"service2": {
				Address: "0.0.0.0",
				Port:    3000,
			},
			"service3": {
				Address: "0.0.0.0",
				Port:    3001,
			},
		}, result)
	})
}

func TestJobEndpointConnect_gatewayProxy(t *testing.T) {
	ci.Parallel(t)

	t.Run("nil", func(t *testing.T) {
		result := gatewayProxy(nil, "bridge")
		require.Nil(t, result)
	})

	t.Run("nil proxy", func(t *testing.T) {
		result := gatewayProxy(&structs.ConsulGateway{
			Ingress: &structs.ConsulIngressConfigEntry{
				Listeners: []*structs.ConsulIngressListener{{
					Port:     3000,
					Protocol: "tcp",
					Services: []*structs.ConsulIngressService{{
						Name: "service1",
					}},
				}},
			},
		}, "bridge")
		require.Equal(t, &structs.ConsulGatewayProxy{
			ConnectTimeout:                  pointer.Of(defaultConnectTimeout),
			EnvoyGatewayNoDefaultBind:       true,
			EnvoyGatewayBindTaggedAddresses: false,
			EnvoyGatewayBindAddresses: map[string]*structs.ConsulGatewayBindAddress{
				"service1": {
					Address: "0.0.0.0",
					Port:    3000,
				}},
		}, result)
	})

	t.Run("ingress set defaults", func(t *testing.T) {
		result := gatewayProxy(&structs.ConsulGateway{
			Proxy: &structs.ConsulGatewayProxy{
				ConnectTimeout: pointer.Of(2 * time.Second),
				Config:         map[string]interface{}{"foo": 1},
			},
			Ingress: &structs.ConsulIngressConfigEntry{
				Listeners: []*structs.ConsulIngressListener{{
					Port:     3000,
					Protocol: "tcp",
					Services: []*structs.ConsulIngressService{{
						Name: "service1",
					}},
				}},
			},
		}, "bridge")
		require.Equal(t, &structs.ConsulGatewayProxy{
			ConnectTimeout:                  pointer.Of(2 * time.Second),
			Config:                          map[string]interface{}{"foo": 1},
			EnvoyGatewayNoDefaultBind:       true,
			EnvoyGatewayBindTaggedAddresses: false,
			EnvoyGatewayBindAddresses: map[string]*structs.ConsulGatewayBindAddress{
				"service1": {
					Address: "0.0.0.0",
					Port:    3000,
				}},
		}, result)
	})

	t.Run("ingress leave as-is", func(t *testing.T) {
		result := gatewayProxy(&structs.ConsulGateway{
			Proxy: &structs.ConsulGatewayProxy{
				Config:                          map[string]interface{}{"foo": 1},
				EnvoyGatewayBindTaggedAddresses: true,
			},
			Ingress: &structs.ConsulIngressConfigEntry{
				Listeners: []*structs.ConsulIngressListener{{
					Port:     3000,
					Protocol: "tcp",
					Services: []*structs.ConsulIngressService{{
						Name: "service1",
					}},
				}},
			},
		}, "bridge")
		require.Equal(t, &structs.ConsulGatewayProxy{
			ConnectTimeout:                  nil,
			Config:                          map[string]interface{}{"foo": 1},
			EnvoyGatewayNoDefaultBind:       false,
			EnvoyGatewayBindTaggedAddresses: true,
			EnvoyGatewayBindAddresses:       nil,
		}, result)
	})

	t.Run("terminating set defaults", func(t *testing.T) {
		result := gatewayProxy(&structs.ConsulGateway{
			Proxy: &structs.ConsulGatewayProxy{
				ConnectTimeout:        pointer.Of(2 * time.Second),
				EnvoyDNSDiscoveryType: "STRICT_DNS",
			},
			Terminating: &structs.ConsulTerminatingConfigEntry{
				Services: []*structs.ConsulLinkedService{{
					Name:     "service1",
					CAFile:   "/cafile.pem",
					CertFile: "/certfile.pem",
					KeyFile:  "/keyfile.pem",
					SNI:      "",
				}},
			},
		}, "bridge")
		require.Equal(t, &structs.ConsulGatewayProxy{
			ConnectTimeout:                  pointer.Of(2 * time.Second),
			EnvoyGatewayNoDefaultBind:       true,
			EnvoyGatewayBindTaggedAddresses: false,
			EnvoyDNSDiscoveryType:           "STRICT_DNS",
			EnvoyGatewayBindAddresses: map[string]*structs.ConsulGatewayBindAddress{
				"default": {
					Address: "0.0.0.0",
					Port:    -1,
				},
			},
		}, result)
	})

	t.Run("terminating leave as-is", func(t *testing.T) {
		result := gatewayProxy(&structs.ConsulGateway{
			Proxy: &structs.ConsulGatewayProxy{
				Config:                          map[string]interface{}{"foo": 1},
				EnvoyGatewayBindTaggedAddresses: true,
			},
			Terminating: &structs.ConsulTerminatingConfigEntry{
				Services: []*structs.ConsulLinkedService{{
					Name: "service1",
				}},
			},
		}, "bridge")
		require.Equal(t, &structs.ConsulGatewayProxy{
			ConnectTimeout:                  nil,
			Config:                          map[string]interface{}{"foo": 1},
			EnvoyGatewayNoDefaultBind:       false,
			EnvoyGatewayBindTaggedAddresses: true,
			EnvoyGatewayBindAddresses:       nil,
		}, result)
	})

	t.Run("mesh set defaults in bridge", func(t *testing.T) {
		result := gatewayProxy(&structs.ConsulGateway{
			Proxy: &structs.ConsulGatewayProxy{
				ConnectTimeout: pointer.Of(2 * time.Second),
			},
			Mesh: &structs.ConsulMeshConfigEntry{
				// nothing
			},
		}, "bridge")
		require.Equal(t, &structs.ConsulGatewayProxy{
			ConnectTimeout:                  pointer.Of(2 * time.Second),
			EnvoyGatewayNoDefaultBind:       true,
			EnvoyGatewayBindTaggedAddresses: false,
			EnvoyGatewayBindAddresses: map[string]*structs.ConsulGatewayBindAddress{
				"lan": {
					Address: "0.0.0.0",
					Port:    -1,
				},
				"wan": {
					Address: "0.0.0.0",
					Port:    -1,
				},
			},
		}, result)
	})

	t.Run("mesh set defaults in host", func(t *testing.T) {
		result := gatewayProxy(&structs.ConsulGateway{
			Proxy: &structs.ConsulGatewayProxy{
				ConnectTimeout: pointer.Of(2 * time.Second),
			},
			Mesh: &structs.ConsulMeshConfigEntry{
				// nothing
			},
		}, "host")
		require.Equal(t, &structs.ConsulGatewayProxy{
			ConnectTimeout: pointer.Of(2 * time.Second),
		}, result)
	})

	t.Run("mesh leave as-is", func(t *testing.T) {
		result := gatewayProxy(&structs.ConsulGateway{
			Proxy: &structs.ConsulGatewayProxy{
				Config:                          map[string]interface{}{"foo": 1},
				EnvoyGatewayBindTaggedAddresses: true,
			},
			Mesh: &structs.ConsulMeshConfigEntry{
				// nothing
			},
		}, "bridge")
		require.Equal(t, &structs.ConsulGatewayProxy{
			ConnectTimeout:                  nil,
			Config:                          map[string]interface{}{"foo": 1},
			EnvoyGatewayNoDefaultBind:       false,
			EnvoyGatewayBindTaggedAddresses: true,
			EnvoyGatewayBindAddresses:       nil,
		}, result)
	})
}
