package clibase_test

import (
	"testing"

	"github.com/spf13/pflag"
	"github.com/stretchr/testify/require"
	"golang.org/x/exp/slices"
	"gopkg.in/yaml.v3"

	"github.com/coder/coder/v2/cli/clibase"
)

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

	t.Run("RequireKey", func(t *testing.T) {
		t.Parallel()
		var workspaceName clibase.String
		os := clibase.OptionSet{
			clibase.Option{
				Name:    "Workspace Name",
				Value:   &workspaceName,
				Default: "billie",
			},
		}

		node, err := os.MarshalYAML()
		require.NoError(t, err)
		require.Len(t, node.(*yaml.Node).Content, 0)
	})

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

		var workspaceName clibase.String

		os := clibase.OptionSet{
			clibase.Option{
				Name:        "Workspace Name",
				Value:       &workspaceName,
				Default:     "billie",
				Description: "The workspace's name.",
				Group:       &clibase.Group{YAML: "names"},
				YAML:        "workspaceName",
			},
		}

		err := os.SetDefaults()
		require.NoError(t, err)

		n, err := os.MarshalYAML()
		require.NoError(t, err)
		// Visually inspect for now.
		byt, err := yaml.Marshal(n)
		require.NoError(t, err)
		t.Logf("Raw YAML:\n%s", string(byt))
	})
}

func TestOptionSet_YAMLUnknownOptions(t *testing.T) {
	t.Parallel()
	os := clibase.OptionSet{
		{
			Name:        "Workspace Name",
			Default:     "billie",
			Description: "The workspace's name.",
			YAML:        "workspaceName",
			Value:       new(clibase.String),
		},
	}

	const yamlDoc = `something: else`
	err := yaml.Unmarshal([]byte(yamlDoc), &os)
	require.Error(t, err)
	require.Empty(t, os[0].Value.String())

	os[0].YAML = "something"

	err = yaml.Unmarshal([]byte(yamlDoc), &os)
	require.NoError(t, err)

	require.Equal(t, "else", os[0].Value.String())
}

// TestOptionSet_YAMLIsomorphism tests that the YAML representations of an
// OptionSet converts to the same OptionSet when read back in.
func TestOptionSet_YAMLIsomorphism(t *testing.T) {
	t.Parallel()
	// This is used to form a generic.
	//nolint:unused
	type kid struct {
		Name string `yaml:"name"`
		Age  int    `yaml:"age"`
	}

	for _, tc := range []struct {
		name      string
		os        clibase.OptionSet
		zeroValue func() pflag.Value
	}{
		{
			name: "SimpleString",
			os: clibase.OptionSet{
				{
					Name:        "Workspace Name",
					Default:     "billie",
					Description: "The workspace's name.",
					Group:       &clibase.Group{YAML: "names"},
					YAML:        "workspaceName",
				},
			},
			zeroValue: func() pflag.Value {
				return clibase.StringOf(new(string))
			},
		},
		{
			name: "Array",
			os: clibase.OptionSet{
				{
					YAML:    "names",
					Default: "jill,jack,joan",
				},
			},
			zeroValue: func() pflag.Value {
				return clibase.StringArrayOf(&[]string{})
			},
		},
		{
			name: "ComplexObject",
			os: clibase.OptionSet{
				{
					YAML: "kids",
					Default: `- name: jill
  age: 12
- name: jack
  age: 13`,
				},
			},
			zeroValue: func() pflag.Value {
				return &clibase.Struct[[]kid]{}
			},
		},
		{
			name: "DeepGroup",
			os: clibase.OptionSet{
				{
					YAML:    "names",
					Default: "jill,jack,joan",
					Group:   &clibase.Group{YAML: "kids", Parent: &clibase.Group{YAML: "family"}},
				},
			},
			zeroValue: func() pflag.Value {
				return clibase.StringArrayOf(&[]string{})
			},
		},
	} {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			// Set initial values.
			for i := range tc.os {
				tc.os[i].Value = tc.zeroValue()
			}
			err := tc.os.SetDefaults()
			require.NoError(t, err)

			y, err := tc.os.MarshalYAML()
			require.NoError(t, err)

			toByt, err := yaml.Marshal(y)
			require.NoError(t, err)

			t.Logf("Raw YAML:\n%s", string(toByt))

			var y2 yaml.Node
			err = yaml.Unmarshal(toByt, &y2)
			require.NoError(t, err)

			os2 := slices.Clone(tc.os)
			for i := range os2 {
				os2[i].Value = tc.zeroValue()
				os2[i].ValueSource = clibase.ValueSourceNone
			}

			// os2 values should be zeroed whereas tc.os should be
			// set to defaults.
			// This check makes sure we aren't mixing pointers.
			require.NotEqual(t, tc.os, os2)
			err = os2.UnmarshalYAML(&y2)
			require.NoError(t, err)

			want := tc.os
			for i := range want {
				want[i].ValueSource = clibase.ValueSourceYAML
			}

			require.Equal(t, tc.os, os2)
		})
	}
}
