// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package encoder_test

import (
	"sync"
	"testing"

	"github.com/stretchr/testify/suite"
	yaml "go.yaml.in/yaml/v4"

	"github.com/siderolabs/talos/pkg/machinery/config/encoder"
)

type Config struct {
	Integer      int                  `yaml:"integer"`
	Slice        []string             `yaml:"slice"`
	ComplexSlice []*Endpoint          `yaml:"complex_slice"`
	Map          map[string]*Endpoint `yaml:"map"`

	Skip   string `yaml:"-"`
	Omit   int    `yaml:",omitempty"`
	Inline *Mixin `yaml:",inline"`

	CustomMarshaller *WithCustomMarshaller `yaml:",omitempty"`
	Bytes            []byte                `yaml:"bytes,flow,omitempty"`

	NilSlice Manifests `yaml:"nilslice,omitempty" talos:"omitonlyifnil"`

	unexported int
}

type FakeConfig struct {
	Machine Machine `yaml:"machine,omitempty"`
}

type Mixin struct {
	MixedIn string `yaml:"mixed_in"`
}

type Endpoint struct {
	Host string
	Port int `yaml:",omitempty"`
}

type Machine struct {
	State  int
	Config *MachineConfig `yaml:",omitempty"`
}

type MachineConfig struct {
	Version      string
	Capabilities []string
}

type Manifests []Manifest

type Manifest struct {
	Name string `yaml:"name"`
}

type WithCustomMarshaller struct {
	value string
}

// MarshalYAML implements custom marshaller.
func (cm *WithCustomMarshaller) MarshalYAML() (any, error) {
	node := &yaml.Node{}

	if err := node.Encode(map[string]string{"value": cm.value}); err != nil {
		return nil, err
	}

	node.HeadComment = "completely custom"

	return node, nil
}

// This is manually defined documentation data for Config.
// It is intended to be generated by `docgen` command.
var (
	configDoc        encoder.Doc
	endpointDoc      encoder.Doc
	mixinDoc         encoder.Doc
	machineDoc       encoder.Doc
	machineConfigDoc encoder.Doc
)

func init() {
	configDoc.Comments[encoder.LineComment] = "test configuration"
	configDoc.Fields = make([]encoder.Doc, 11)
	configDoc.Fields[1].Comments[encoder.LineComment] = "<<<"
	configDoc.Fields[2].Comments[encoder.HeadComment] = "complex slice"
	configDoc.Fields[3].Comments[encoder.FootComment] = "some text example for map"

	configDoc.Fields[2].AddExample("slice example", []*Endpoint{{
		Host: "127.0.0.1",
		Port: 5554,
	}})

	configDoc.Fields[9].Comments[encoder.LineComment] = "A nilslice field is really cool."
	configDoc.Fields[9].AddExample("nilslice example", Manifests{{
		Name: "foo",
	}})

	endpointDoc.Comments[encoder.LineComment] = "endpoint settings"
	endpointDoc.Fields = make([]encoder.Doc, 2)
	endpointDoc.Fields[0].Comments[encoder.LineComment] = "endpoint host"
	endpointDoc.Fields[1].Comments[encoder.LineComment] = "custom port"

	mixinDoc.Fields = make([]encoder.Doc, 1)
	mixinDoc.Fields[0].Comments[encoder.LineComment] = "was inlined"

	machineDoc.AddExample("uncomment me", &Machine{
		State: 100,
	})
	machineDoc.AddExample("second example", &Machine{
		State: -1,
	})

	machineDoc.Fields = make([]encoder.Doc, 2)
	machineDoc.Fields[1].AddExample("", &MachineConfig{
		Version: "0.0.2",
	})

	machineConfigDoc.Fields = make([]encoder.Doc, 2)
	machineConfigDoc.Fields[0].Comments[encoder.HeadComment] = "this is some version"
	machineConfigDoc.Fields[1].AddExample("",
		[]string{
			"reboot", "upgrade",
		},
	)
}

func (c Config) Doc() *encoder.Doc {
	return &configDoc
}

func (c Endpoint) Doc() *encoder.Doc {
	return &endpointDoc
}

func (c Mixin) Doc() *encoder.Doc {
	return &mixinDoc
}

func (c Machine) Doc() *encoder.Doc {
	return &machineDoc
}

func (c MachineConfig) Doc() *encoder.Doc {
	return &machineConfigDoc
}

// tests

type EncoderSuite struct {
	suite.Suite
}

func (suite *EncoderSuite) TestRun() {
	e := &Endpoint{
		Port: 8080,
	}
	tests := []struct {
		name         string
		value        any
		expectedYAML string
		incompatible bool
		options      []encoder.Option
	}{
		{
			name:  "default struct with all enabled",
			value: &Config{},
			expectedYAML: `integer: 0
# <<<
slice: []
# complex slice
complex_slice: []
#   # slice example
#   - host: 127.0.0.1 # endpoint host
#     port: 5554 # custom port

map: {}
# some text example for map
# # A nilslice field is really cool.

# # nilslice example
# nilslice:
#     - name: foo
`,
			options: []encoder.Option{
				encoder.WithComments(encoder.CommentsAll),
			},
			incompatible: true,
		},
		{
			name:  "default struct only with examples",
			value: &Config{},
			expectedYAML: `integer: 0
slice: []
complex_slice: []
#   # slice example
#   - host: 127.0.0.1
#     port: 5554

map: {}

# # nilslice example
# nilslice:
#     - name: foo
`,
			options: []encoder.Option{
				encoder.WithComments(encoder.CommentsExamples),
			},
			incompatible: true,
		},
		{
			name:  "default struct",
			value: &Config{},
			expectedYAML: `integer: 0
# <<<
slice: []
# complex slice
complex_slice: []
map: {}
# some text example for map
`,
			options: []encoder.Option{
				encoder.WithComments(encoder.CommentsDocs),
			},
		},
		{
			name: "struct with custom marshaller",
			value: &Config{
				CustomMarshaller: &WithCustomMarshaller{
					value: "abcd",
				},
			},
			expectedYAML: `integer: 0
# <<<
slice: []
# complex slice
complex_slice: []
map: {}
# some text example for map

custommarshaller:
    # completely custom
    value: abcd
`,
			options: []encoder.Option{
				encoder.WithComments(encoder.CommentsDocs),
			},
		},
		{
			name: "bytes flow",
			value: &Config{
				Bytes: []byte("..."),
			},
			expectedYAML: `integer: 0
# <<<
slice: []
# complex slice
complex_slice: []
map: {}
# some text example for map

bytes: [46, 46, 46]
`,
			options: []encoder.Option{
				encoder.WithComments(encoder.CommentsDocs),
			},
		},
		{
			name: "map check",
			value: &Config{
				Map: map[string]*Endpoint{
					"endpoint": new(Endpoint),
				},
				unexported: -1,
			},
			expectedYAML: `integer: 0
# <<<
slice: []
# complex slice
complex_slice: []
map:
    endpoint:
        host: "" # endpoint host
# some text example for map
`,
			options: []encoder.Option{
				encoder.WithComments(encoder.CommentsDocs),
			},
		},
		{
			name: "nil map element",
			value: &Config{
				Map: map[string]*Endpoint{
					"endpoint": nil,
				},
			},
			expectedYAML: `integer: 0
# <<<
slice: []
# complex slice
complex_slice: []
map:
    endpoint: null
# some text example for map
`,
			options: []encoder.Option{
				encoder.WithComments(encoder.CommentsDocs),
			},
		},
		{
			name: "nil map element",
			value: &Config{
				Map: map[string]*Endpoint{
					"endpoint": new(Endpoint),
				},
				ComplexSlice: []*Endpoint{e},
			},
			expectedYAML: `integer: 0
# <<<
slice: []
# complex slice
complex_slice:
    - host: "" # endpoint host
      port: 8080 # custom port
map:
    endpoint:
        host: "" # endpoint host
# some text example for map
`,
			options: []encoder.Option{
				encoder.WithComments(encoder.CommentsDocs),
			},
		},
		{
			name: "inline",
			value: &Config{
				Inline: &Mixin{
					MixedIn: "a",
				},
			},
			expectedYAML: `integer: 0
# <<<
slice: []
# complex slice
complex_slice: []
map: {}
# some text example for map

mixed_in: a # was inlined
`,
			options: []encoder.Option{
				encoder.WithComments(encoder.CommentsDocs),
			},
		},
		{
			name:  "comment example if zero",
			value: &FakeConfig{},
			expectedYAML: `# # uncomment me
# machine:
#     state: 100
#     config:
#         # this is some version
#         version: 0.0.2
#         capabilities:
#             - reboot
#             - upgrade
# # second example
# machine:
#     state: -1
#     config:
#         # this is some version
#         version: 0.0.2
#         capabilities:
#             - reboot
#             - upgrade
`,
			incompatible: true,
		},
		{
			name: "comment example if partially set",
			value: &FakeConfig{
				Machine{
					State: 1000,
				},
			},
			expectedYAML: `machine:
    state: 1000
    ` + `
    # config:
    #     # this is some version
    #     version: 0.0.2
    #     capabilities:
    #         - reboot
    #         - upgrade
`,
			incompatible: true,
		},
		{
			name: "populate map element's examples",
			value: map[string][]*MachineConfig{
				"first": {
					{},
				},
			},
			expectedYAML: `first:
    - # this is some version
      version: ""
      capabilities: []
      #   - reboot
      #   - upgrade
`,
			incompatible: true,
		},
		{
			name: "without comments",
			value: &FakeConfig{
				Machine{
					State: 1000,
				},
			},
			expectedYAML: `machine:
    state: 1000
`,
			incompatible: true,
			options: []encoder.Option{
				encoder.WithComments(encoder.CommentsDisabled),
			},
		},
		{
			name:  "only with docs",
			value: &FakeConfig{},
			expectedYAML: `{}
`,
			incompatible: true,
			options: []encoder.Option{
				encoder.WithComments(encoder.CommentsDocs),
			},
		},
		{
			name:  "only with examples",
			value: &FakeConfig{},
			expectedYAML: `# # uncomment me
# machine:
#     state: 100
#     config:
#         version: 0.0.2
#         capabilities:
#             - reboot
#             - upgrade
# # second example
# machine:
#     state: -1
#     config:
#         version: 0.0.2
#         capabilities:
#             - reboot
#             - upgrade
`,
			incompatible: true,
			options: []encoder.Option{
				encoder.WithComments(encoder.CommentsExamples),
			},
		},
		{
			name: "with onlyifnotnil tag",
			value: &Config{
				NilSlice: Manifests{},
			},
			expectedYAML: `integer: 0
# <<<
slice: []
# complex slice
complex_slice: []
#   # slice example
#   - host: 127.0.0.1 # endpoint host
#     port: 5554 # custom port

map: {}
# some text example for map

# A nilslice field is really cool.
nilslice: []
#   # nilslice example
#   - name: foo
`,
			incompatible: true,
			options: []encoder.Option{
				encoder.WithComments(encoder.CommentsAll),
			},
		},
	}

	for _, test := range tests {
		encoder := encoder.NewEncoder(test.value, test.options...)
		data, err := encoder.Encode()
		suite.Assert().NoError(err)

		// compare with expected string output
		suite.Assert().EqualValues(test.expectedYAML, string(data), test.name)

		// decode into raw map to strip all comments
		actualMap, err := decodeToMap(data)
		suite.Assert().NoError(err)

		// skip if marshaller output is not the same for our encoder and vanilla one
		// note: it is only incompatible if config contains nested structs stored as value
		// and if these nested structs are documented and you try to load generated yaml into map[interface{}]interface{}
		if !test.incompatible {
			// compare with regular yaml.Marshal call
			expected, err := yaml.Marshal(test.value)
			suite.Assert().NoError(err)

			expectedMap, err := decodeToMap(expected)
			suite.Assert().NoError(err)
			suite.Assert().EqualValues(expectedMap, actualMap)
		}
	}
}

func (suite *EncoderSuite) TestConcurrent() {
	value := &Machine{}

	var wg sync.WaitGroup

	for range 10 {
		wg.Go(func() {
			encoder := encoder.NewEncoder(value)
			_, err := encoder.Encode()
			suite.Assert().NoError(err)
		})
	}

	wg.Wait()
}

func decodeToMap(data []byte) (map[any]any, error) {
	raw := map[any]any{}
	err := yaml.Unmarshal(data, &raw)

	return raw, err
}

func TestEncoderSuite(t *testing.T) {
	suite.Run(t, &EncoderSuite{})
}
