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

package structs

import (
	"encoding/json"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/hashicorp/consul/api"
)

func TestConnectProxyConfig_ToAPI(t *testing.T) {
	tests := []struct {
		name string
		in   ConnectProxyConfig
		want *api.AgentServiceConnectProxyConfig
	}{
		{
			name: "service",
			in: ConnectProxyConfig{
				DestinationServiceName: "web",
				DestinationServiceID:   "web1",
				LocalServiceAddress:    "127.0.0.2",
				LocalServicePort:       5555,
				Config: map[string]interface{}{
					"foo": "bar",
				},
				MeshGateway: MeshGatewayConfig{
					Mode: MeshGatewayModeLocal,
				},
				Upstreams: Upstreams{
					{
						DestinationType: UpstreamDestTypeService,
						DestinationName: "foo",
						Datacenter:      "dc1",
						LocalBindPort:   1234,
						MeshGateway: MeshGatewayConfig{
							Mode: MeshGatewayModeLocal,
						},
					},
					{
						DestinationType:  UpstreamDestTypePreparedQuery,
						DestinationName:  "foo",
						Datacenter:       "dc1",
						LocalBindPort:    2345,
						LocalBindAddress: "127.10.10.10",
					},
				},
				Mode: ProxyModeTransparent,
				TransparentProxy: TransparentProxyConfig{
					OutboundListenerPort: 808,
				},
			},
			want: &api.AgentServiceConnectProxyConfig{
				DestinationServiceName: "web",
				DestinationServiceID:   "web1",
				LocalServiceAddress:    "127.0.0.2",
				LocalServicePort:       5555,
				Config: map[string]interface{}{
					"foo": "bar",
				},
				MeshGateway: api.MeshGatewayConfig{
					Mode: api.MeshGatewayModeLocal,
				},
				Upstreams: []api.Upstream{
					{
						DestinationType: UpstreamDestTypeService,
						DestinationName: "foo",
						Datacenter:      "dc1",
						LocalBindPort:   1234,
						MeshGateway: api.MeshGatewayConfig{
							Mode: api.MeshGatewayModeLocal,
						},
					},
					{
						DestinationType:  UpstreamDestTypePreparedQuery,
						DestinationName:  "foo",
						Datacenter:       "dc1",
						LocalBindPort:    2345,
						LocalBindAddress: "127.10.10.10",
					},
				},
				Mode: api.ProxyModeTransparent,
				TransparentProxy: &api.TransparentProxyConfig{
					OutboundListenerPort: 808,
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			require.Equal(t, tt.want, tt.in.ToAPI())
		})
	}
}

func TestConnectProxyConfig_MarshalJSON(t *testing.T) {
	tests := []struct {
		name    string
		in      ConnectProxyConfig
		want    string
		wantErr bool
	}{
		{
			name: "direct proxy",
			in: ConnectProxyConfig{
				DestinationServiceName: "api",
				DestinationServiceID:   "api-1",
				LocalServiceAddress:    "127.0.0.1",
				LocalServicePort:       8080,
				Mode:                   ProxyModeDirect,
				Config: map[string]interface{}{
					"connect_timeout_ms": 5000,
				},
				Upstreams: Upstreams{
					Upstream{
						DestinationType: UpstreamDestTypeService,
						DestinationName: "db",
						Datacenter:      "dc1",
						LocalBindPort:   1234,
					},
				},
				MeshGateway: MeshGatewayConfig{Mode: MeshGatewayModeLocal},
				Expose:      ExposeConfig{Checks: true},

				// No transparent proxy config, since proxy is set to "direct" mode.
				// Field should be omitted from json output.
				// TransparentProxy: TransparentProxyConfig{},

				// No access logs config provided, so this should be omitted.
				// AccessLogs: AccessLogsConfig{},
			},
			want: `{
				"DestinationServiceName": "api",
				"DestinationServiceID": "api-1",
				"LocalServiceAddress": "127.0.0.1",
				"LocalServicePort": 8080,
				"Mode": "direct",
				"Config": {
					"connect_timeout_ms": 5000
				},
				"Upstreams": [
					{
						"DestinationType": "service",
						"DestinationName": "db",
						"Datacenter": "dc1",
						"LocalBindPort": 1234,
						"MeshGateway": {}
					}
				],
				"MeshGateway": {
					"Mode": "local"
				},
				"Expose": {
					"Checks": true
				}
			}`,
			wantErr: false,
		},
		{
			name: "transparent proxy",
			in: ConnectProxyConfig{
				DestinationServiceName: "billing",
				DestinationServiceID:   "billing-1",
				LocalServiceAddress:    "127.0.0.1",
				LocalServicePort:       8080,
				Mode:                   ProxyModeTransparent,
				Config: map[string]interface{}{
					"connect_timeout_ms": 5000,
				},
				MeshGateway: MeshGatewayConfig{Mode: MeshGatewayModeLocal},
				Expose:      ExposeConfig{Checks: true},
				TransparentProxy: TransparentProxyConfig{
					DialedDirectly:       true,
					OutboundListenerPort: 16001,
				},
				AccessLogs: AccessLogsConfig{
					Enabled:             true,
					DisableListenerLogs: true,
					Type:                FileLogSinkType,
					Path:                "/var/log/access.log",
				},
			},
			want: `{
				"DestinationServiceName": "billing",
				"DestinationServiceID": "billing-1",
				"LocalServiceAddress": "127.0.0.1",
				"LocalServicePort": 8080,
				"Mode": "transparent",
				"Config": {
					"connect_timeout_ms": 5000
				},
				"TransparentProxy": {
					"DialedDirectly": true,
					"OutboundListenerPort": 16001
				},
				"MeshGateway": {
					"Mode": "local"
				},
				"Expose": {
					"Checks": true
				},
				"AccessLogs": {
					"Enabled": true,
					"DisableListenerLogs": true,
  					"Type": "file",
					"Path": "/var/log/access.log"
				}
			}`,
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := tt.in.MarshalJSON()
			if tt.wantErr {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
			require.JSONEq(t, tt.want, string(got))
		})
	}
}

func TestUpstream_MarshalJSON(t *testing.T) {
	tests := []struct {
		name    string
		in      Upstream
		want    string
		wantErr bool
	}{
		{
			name: "service",
			in: Upstream{
				DestinationType: UpstreamDestTypeService,
				DestinationName: "foo",
				Datacenter:      "dc1",
				LocalBindPort:   1234,
				// Test IngressHosts does not marshal
				IngressHosts: []string{"test.example.com"},
			},
			want: `{
				"DestinationType": "service",
				"DestinationName": "foo",
				"Datacenter": "dc1",
				"LocalBindPort": 1234,
				"MeshGateway": {}
			}`,
			wantErr: false,
		},
		{
			name: "pq",
			in: Upstream{
				DestinationType: UpstreamDestTypePreparedQuery,
				DestinationName: "foo",
				Datacenter:      "dc1",
				LocalBindPort:   1234,
			},
			want: `{
				"DestinationType": "prepared_query",
				"DestinationName": "foo",
				"Datacenter": "dc1",
				"LocalBindPort": 1234,
				"MeshGateway": {}
			}`,
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := json.Marshal(tt.in)
			if tt.wantErr {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
			require.JSONEq(t, tt.want, string(got))
		})
	}
}

func TestUpstream_UnmarshalJSON(t *testing.T) {
	tests := []struct {
		name      string
		json      string
		jsonSnake string
		want      Upstream
		wantErr   bool
	}{
		{
			name: "service",
			json: `{
				"DestinationType": "service",
				"DestinationName": "foo",
				"Datacenter": "dc1"
			}`,
			want: Upstream{
				DestinationType: UpstreamDestTypeService,
				DestinationName: "foo",
				Datacenter:      "dc1",
			},
			wantErr: false,
		},
		{
			name: "pq",
			json: `{
				"DestinationType": "prepared_query",
				"DestinationName": "foo",
				"Datacenter": "dc1"
			}`,
			want: Upstream{
				DestinationType: UpstreamDestTypePreparedQuery,
				DestinationName: "foo",
				Datacenter:      "dc1",
			},
			wantErr: false,
		},
		{
			name: "ingress-hosts-do-not-unmarshal",
			json: `{
				"DestinationType": "service",
				"DestinationName": "foo",
				"Datacenter": "dc1",
				"IngressHosts": ["asdf"]
			}`,
			want: Upstream{
				DestinationType: UpstreamDestTypeService,
				DestinationName: "foo",
				Datacenter:      "dc1",
				IngressHosts:    nil, // Make sure this doesn't get parsed
			},
			wantErr: true,
		},
		{
			name: "kitchen sink",
			json: `
				{
				  "DestinationType": "service",
				  "DestinationNamespace": "default",
				  "DestinationName": "bar1",
				  "Datacenter": "dc1",
				  "LocalBindAddress": "127.0.0.2",
				  "LocalBindPort": 6060,
				  "Config": {
					"x": "y",
					"z": -2
				  },
				  "MeshGateway": {
					"Mode": "local"
				  }
				}
			`,
			jsonSnake: `
				{
				  "destination_type": "service",
				  "destination_namespace": "default",
				  "destination_name": "bar1",
				  "datacenter": "dc1",
				  "local_bind_address": "127.0.0.2",
				  "local_bind_port": 6060,
				  "config": {
					"x": "y",
					"z": -2
				  },
				  "mesh_gateway": {
					"mode": "local"
				  }
				}
			`,
			want: Upstream{
				DestinationType:      UpstreamDestTypeService,
				DestinationNamespace: "default",
				DestinationName:      "bar1",
				Datacenter:           "dc1",
				LocalBindAddress:     "127.0.0.2",
				LocalBindPort:        6060,
				Config: map[string]interface{}{
					"x": "y",
					"z": float64(-2),
				},
				MeshGateway: MeshGatewayConfig{
					Mode: "local",
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Run("camel", func(t *testing.T) {
				var got Upstream
				err := json.Unmarshal([]byte(tt.json), &got)
				if tt.wantErr {
					require.Error(t, err)
				} else {
					require.NoError(t, err)
					require.Equal(t, tt.want, got, "%+v", got)
				}
			})

			if tt.jsonSnake != "" {
				t.Run("snake", func(t *testing.T) {
					var got Upstream
					err := json.Unmarshal([]byte(tt.jsonSnake), &got)
					if tt.wantErr {
						require.Error(t, err)
					} else {
						require.NoError(t, err)
						require.Equal(t, tt.want, got)
					}
				})
			}
		})
	}
}

func TestConnectProxyConfig_UnmarshalJSON(t *testing.T) {
	tests := []struct {
		name      string
		json      string
		jsonSnake string
		want      ConnectProxyConfig
		wantErr   bool
	}{
		{
			name: "kitchen sink",
			json: `
				{
				  "DestinationServiceName": "foo-name",
				  "DestinationServiceID": "foo-id",
				  "LocalServiceAddress": "127.0.0.1",
				  "LocalServicePort": 5050,
				  "Config": {
					"a": "b",
					"v": 42
				  },
				  "Upstreams": [
					{
					  "DestinationType": "service",
					  "DestinationNamespace": "default",
					  "DestinationName": "bar1",
					  "Datacenter": "dc1",
					  "LocalBindAddress": "127.0.0.2",
					  "LocalBindPort": 6060,
					  "Config": {
						"x": "y",
						"z": -2
					  },
					  "MeshGateway": {
						"Mode": "local"
					  }
					},
					{
					  "DestinationType": "service",
					  "DestinationNamespace": "default",
					  "DestinationName": "bar2",
					  "Datacenter": "dc2",
					  "LocalBindAddress": "127.0.0.2",
					  "LocalBindPort": 6161
					}
				  ],
				  "MeshGateway": {
					"Mode": "remote"
				  },
				  "Expose": {
					"Checks": true,
					"Paths": [
					  {
						"ListenerPort": 8080,
						"Path": "/foo",
						"LocalPathPort": 7070,
						"Protocol": "http2",
						"ParsedFromCheck": true
					  },
					  {
						"ListenerPort": 8181,
						"Path": "/foo2",
						"LocalPathPort": 7171,
						"Protocol": "http",
						"ParsedFromCheck": false
					  }
					]
				  }
				}
			`,
			jsonSnake: `
				{
				  "destination_service_name": "foo-name",
				  "destination_service_id": "foo-id",
				  "local_service_address": "127.0.0.1",
				  "local_service_port": 5050,
				  "config": {
					"a": "b",
					"v": 42
				  },
				  "upstreams": [
					{
					  "destination_type": "service",
					  "destination_namespace": "default",
					  "destination_name": "bar1",
					  "datacenter": "dc1",
					  "local_bind_address": "127.0.0.2",
					  "local_bind_port": 6060,
					  "config": {
						"x": "y",
						"z": -2
					  },
					  "mesh_gateway": {
						"mode": "local"
					  }
					},
					{
					  "destination_type": "service",
					  "destination_namespace": "default",
					  "destination_name": "bar2",
					  "datacenter": "dc2",
					  "local_bind_address": "127.0.0.2",
					  "local_bind_port": 6161
					}
				  ],
				  "mesh_gateway": {
					"mode": "remote"
				  },
				  "expose": {
					"checks": true,
					"paths": [
					  {
						"listener_port": 8080,
						"path": "/foo",
						"local_path_port": 7070,
						"protocol": "http2",
						"parsed_from_check": true
					  },
					  {
						"listener_port": 8181,
						"path": "/foo2",
						"local_path_port": 7171,
						"protocol": "http",
						"parsed_from_check": false
					  }
					]
				  }
				}
			`,
			want: ConnectProxyConfig{
				DestinationServiceName: "foo-name",
				DestinationServiceID:   "foo-id",
				LocalServiceAddress:    "127.0.0.1",
				LocalServicePort:       5050,
				Config: map[string]interface{}{
					"a": "b",
					"v": float64(42),
				},
				Upstreams: []Upstream{
					{
						DestinationType:      UpstreamDestTypeService,
						DestinationNamespace: "default",
						DestinationName:      "bar1",
						Datacenter:           "dc1",
						LocalBindAddress:     "127.0.0.2",
						LocalBindPort:        6060,
						Config: map[string]interface{}{
							"x": "y",
							"z": float64(-2),
						},
						MeshGateway: MeshGatewayConfig{
							Mode: "local",
						},
					},

					{
						DestinationType:      UpstreamDestTypeService,
						DestinationNamespace: "default",
						DestinationName:      "bar2",
						Datacenter:           "dc2",
						LocalBindAddress:     "127.0.0.2",
						LocalBindPort:        6161,
					},
				},

				MeshGateway: MeshGatewayConfig{
					Mode: "remote",
				},
				Expose: ExposeConfig{
					Checks: true,
					Paths: []ExposePath{
						{
							ListenerPort:    8080,
							Path:            "/foo",
							LocalPathPort:   7070,
							Protocol:        "http2",
							ParsedFromCheck: true,
						},
						{
							ListenerPort:    8181,
							Path:            "/foo2",
							LocalPathPort:   7171,
							Protocol:        "http",
							ParsedFromCheck: false,
						},
					},
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Run("camel", func(t *testing.T) {
				//
				var got ConnectProxyConfig
				err := json.Unmarshal([]byte(tt.json), &got)
				if tt.wantErr {
					require.Error(t, err)
				} else {
					require.NoError(t, err)
					require.Equal(t, tt.want, got)
				}
			})
			if tt.jsonSnake != "" {
				t.Run("snake", func(t *testing.T) {
					//
					var got ConnectProxyConfig
					err := json.Unmarshal([]byte(tt.json), &got)
					if tt.wantErr {
						require.Error(t, err)
					} else {
						require.NoError(t, err)
						require.Equal(t, tt.want, got)
					}
				})
			}
		})
	}
}

func TestValidateMeshGatewayMode(t *testing.T) {
	for _, tc := range []struct {
		modeConstant string
		modeExplicit string
		expect       MeshGatewayMode
		ok           bool
	}{
		{string(MeshGatewayModeNone), "none", MeshGatewayModeNone, true},
		{string(MeshGatewayModeDefault), "", MeshGatewayModeDefault, true},
		{string(MeshGatewayModeLocal), "local", MeshGatewayModeLocal, true},
		{string(MeshGatewayModeRemote), "remote", MeshGatewayModeRemote, true},
	} {
		tc := tc

		t.Run(tc.modeConstant+" (constant)", func(t *testing.T) {
			got, err := ValidateMeshGatewayMode(tc.modeConstant)
			if tc.ok {
				require.NoError(t, err)
				require.Equal(t, tc.expect, got)
			} else {
				require.Error(t, err)
			}
		})
		t.Run(tc.modeExplicit+" (explicit)", func(t *testing.T) {
			got, err := ValidateMeshGatewayMode(tc.modeExplicit)
			if tc.ok {
				require.NoError(t, err)
				require.Equal(t, tc.expect, got)
			} else {
				require.Error(t, err)
			}
		})
	}
}

func TestValidateProxyMode(t *testing.T) {
	for _, tc := range []struct {
		modeConstant string
		modeExplicit string
		expect       ProxyMode
		ok           bool
	}{
		{string(ProxyModeDefault), "", ProxyModeDefault, true},
		{string(ProxyModeDirect), "direct", ProxyModeDirect, true},
		{string(ProxyModeTransparent), "transparent", ProxyModeTransparent, true},
	} {
		tc := tc

		t.Run(tc.modeConstant+" (constant)", func(t *testing.T) {
			got, err := ValidateProxyMode(tc.modeConstant)
			if tc.ok {
				require.NoError(t, err)
				require.Equal(t, tc.expect, got)
			} else {
				require.Error(t, err)
			}
		})
		t.Run(tc.modeExplicit+" (explicit)", func(t *testing.T) {
			got, err := ValidateProxyMode(tc.modeExplicit)
			if tc.ok {
				require.NoError(t, err)
				require.Equal(t, tc.expect, got)
			} else {
				require.Error(t, err)
			}
		})
	}
}
