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

package write

import (
	"bytes"
	"io"
	"strings"
	"testing"

	"github.com/mitchellh/cli"
	"github.com/stretchr/testify/require"

	"github.com/hashicorp/consul/agent"
	"github.com/hashicorp/consul/agent/netutil"
	"github.com/hashicorp/consul/api"
	"github.com/hashicorp/consul/command/config"
	"github.com/hashicorp/consul/sdk/testutil"
)

func TestConfigWrite_noTabs(t *testing.T) {
	t.Parallel()

	require.NotContains(t, New(cli.NewMockUi()).Help(), "\t")
}

func TestConfigWrite(t *testing.T) {
	netutil.GetAgentBindAddrFunc = netutil.GetMockGetAgentBindAddrFunc("0.0.0.0")
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	a := agent.NewTestAgent(t, ``)
	defer a.Shutdown()
	client := a.Client()

	t.Run("File", func(t *testing.T) {
		ui := cli.NewMockUi()
		c := New(ui)

		f := testutil.TempFile(t, "config-write-svc-web.hcl")
		_, err := f.WriteString(`
      Kind = "service-defaults"
      Name = "web"
      Protocol = "tcp"
      `)

		require.NoError(t, err)

		args := []string{
			"-http-addr=" + a.HTTPAddr(),
			f.Name(),
		}

		code := c.Run(args)
		require.Empty(t, ui.ErrorWriter.String())
		require.Contains(t, ui.OutputWriter.String(),
			`Config entry written: service-defaults/web`)
		require.Equal(t, 0, code)

		entry, _, err := client.ConfigEntries().Get("service-defaults", "web", nil)
		require.NoError(t, err)
		svc, ok := entry.(*api.ServiceConfigEntry)
		require.True(t, ok)
		require.Equal(t, api.ServiceDefaults, svc.Kind)
		require.Equal(t, "web", svc.Name)
		require.Equal(t, "tcp", svc.Protocol)
	})

	t.Run("Stdin", func(t *testing.T) {
		stdinR, stdinW := io.Pipe()

		ui := cli.NewMockUi()
		c := New(ui)
		c.testStdin = stdinR

		go func() {
			stdinW.Write([]byte(`{
            "Kind": "proxy-defaults",
            "Name": "global",
            "Config": {
               "foo": "bar",
               "bar": 1.0
            }
         }`))
			stdinW.Close()
		}()

		args := []string{
			"-http-addr=" + a.HTTPAddr(),
			"-",
		}

		code := c.Run(args)
		require.Empty(t, ui.ErrorWriter.String())
		require.Contains(t, ui.OutputWriter.String(),
			`Config entry written: proxy-defaults/global`)
		require.Equal(t, 0, code)

		entry, _, err := client.ConfigEntries().Get(api.ProxyDefaults, api.ProxyConfigGlobal, nil)
		require.NoError(t, err)
		proxy, ok := entry.(*api.ProxyConfigEntry)
		require.True(t, ok)
		require.Equal(t, api.ProxyDefaults, proxy.Kind)
		require.Equal(t, api.ProxyConfigGlobal, proxy.Name)
		require.Equal(t, map[string]interface{}{"foo": "bar", "bar": 1.0}, proxy.Config)
	})

	t.Run("No config", func(t *testing.T) {
		ui := cli.NewMockUi()
		c := New(ui)

		code := c.Run([]string{})
		require.NotEqual(t, 0, code)
		require.NotEmpty(t, ui.ErrorWriter.String())
	})

	t.Run("mesh config entry", func(t *testing.T) {
		stdin := new(bytes.Buffer)
		stdin.WriteString(`
kind = "mesh"
meta {
	"foo" = "bar"
	"gir" = "zim"
}
transparent_proxy {
	mesh_destinations_only = true
}
http {
    sanitize_x_forwarded_client_cert = true
}
`)

		ui := cli.NewMockUi()
		c := New(ui)
		c.testStdin = stdin

		code := c.Run([]string{"-http-addr=" + a.HTTPAddr(), "-"})
		require.Empty(t, ui.ErrorWriter.String())
		require.Contains(t, ui.OutputWriter.String(),
			`Config entry written: mesh/mesh`)
		require.Equal(t, 0, code)

		entry, _, err := client.ConfigEntries().Get(api.MeshConfig, api.MeshConfigMesh, nil)
		require.NoError(t, err)
		proxy, ok := entry.(*api.MeshConfigEntry)
		require.True(t, ok)
		require.Equal(t, map[string]string{"foo": "bar", "gir": "zim"}, proxy.Meta)
		require.True(t, proxy.TransparentProxy.MeshDestinationsOnly)

		require.True(t, proxy.HTTP.SanitizeXForwardedClientCert)
	})

	// Test that if name isn't set (which isn't required for proxy-defaults because the name defaults to
	// "global"), the CLI response still says "config entry written proxy-defaults/global".
	t.Run("proxy defaults config entry without name set", func(t *testing.T) {
		stdin := new(bytes.Buffer)
		stdin.WriteString(`
kind = "proxy-defaults"
`)

		ui := cli.NewMockUi()
		c := New(ui)
		c.testStdin = stdin

		code := c.Run([]string{"-http-addr=" + a.HTTPAddr(), "-"})
		require.Empty(t, ui.ErrorWriter.String())
		require.Contains(t, ui.OutputWriter.String(),
			`Config entry written: proxy-defaults/global`)
		require.Equal(t, 0, code)
	})

	// Test that protocol field is first normalized and then validated
	// before writing the config entry
	t.Run("service defaults config entry mixed case in protocol field", func(t *testing.T) {
		stdin := new(bytes.Buffer)
		stdin.WriteString(`
		Kind = "service-defaults"
		Name = "web"
		Protocol = "TcP"
`)

		ui := cli.NewMockUi()
		c := New(ui)
		c.testStdin = stdin

		code := c.Run([]string{"-http-addr=" + a.HTTPAddr(), "-"})
		require.Empty(t, ui.ErrorWriter.String())
		require.Contains(t, ui.OutputWriter.String(),
			`Config entry written: service-defaults/web`)
		require.Equal(t, 0, code)
	})
}

func TestConfigWrite_Warning(t *testing.T) {
	netutil.GetAgentBindAddrFunc = netutil.GetMockGetAgentBindAddrFunc("0.0.0.0")
	t.Parallel()

	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	a := agent.NewTestAgent(t, ``)
	defer a.Shutdown()
	client := a.Client()

	cases := map[string]struct {
		entry   string
		warning string
	}{
		"service-defaults no warning": {
			entry: `
				Kind = "service-defaults"
				Name = "web"
				MutualTLSMode = "strict"
			`,
		},
		"proxy-defaults no warning": {
			entry: `
				Kind = "proxy-defaults"
				Name = "global"
				MutualTLSMode = "strict"
			`,
		},
		"mesh config entry no warning": {
			entry: `
				Kind = "mesh"
				AllowEnablingPermissiveMutualTLS = false
			`,
		},
		"service-defaults warning on MutualTLSMode=permissive": {
			entry: `
				Kind = "service-defaults"
				Name = "web"
				MutualTLSMode = "permissive"
			`,
			warning: config.WarningServiceDefaultsPermissiveMTLS,
		},
		"proxy-defaults warning on MutualTLSMode=permissive": {
			entry: `
				Kind = "proxy-defaults"
				Name = "global"
				MutualTLSMode = "permissive"
			`,
			warning: config.WarningProxyDefaultsPermissiveMTLS,
		},
		"mesh config entry warning on AllowEnablingPermissiveMutualTLS=true": {
			entry: `
				Kind = "mesh"
				AllowEnablingPermissiveMutualTLS = true
			`,
			warning: config.WarningMeshAllowEnablingPermissiveMutualTLS,
		},
	}
	for name, c := range cases {
		c := c
		t.Run(name, func(t *testing.T) {
			// Always reset this setting to avoid causing validation errors.
			_, _, err := client.ConfigEntries().Set(&api.MeshConfigEntry{
				AllowEnablingPermissiveMutualTLS: true,
			}, nil)
			require.NoError(t, err)

			f := testutil.TempFile(t, "config-write-warning-*.hcl")
			_, err = f.WriteString(c.entry)
			require.NoError(t, err)

			ui := cli.NewMockUi()
			code := New(ui).Run([]string{
				"-http-addr=" + a.HTTPAddr(),
				f.Name(),
			})

			require.Equal(t, 0, code)
			require.Contains(t, ui.OutputWriter.String(), `Config entry written`)

			errMsg := ui.ErrorWriter.String()
			if c.warning != "" {
				require.Contains(t, errMsg, c.warning)
			} else {
				require.Empty(t, errMsg)
			}
		})
	}
}

func requireContainsLower(t *testing.T, haystack, needle string) {
	t.Helper()
	require.Contains(t, strings.ToLower(haystack), strings.ToLower(needle))
}
