package yamlpatch_test

import (
	"os"
	"path/filepath"
	"testing"

	"github.com/crowdsecurity/crowdsec/pkg/yamlpatch"
	"github.com/stretchr/testify/require"
)

// similar to the one in cstest, but with test number too. We cannot import
// cstest here because of circular dependency.
func requireErrorContains(t *testing.T, err error, expectedErr string) {
	t.Helper()

	if expectedErr != "" {
		require.ErrorContains(t, err, expectedErr)

		return
	}

	require.NoError(t, err)
}

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

	tests := []struct {
		name        string
		base        string
		patch       string
		expected    string
		expectedErr string
	}{
		{
			"invalid yaml in base",
			"notayaml",
			"",
			"",
			"config.yaml: yaml: unmarshal errors:",
		},
		{
			"invalid yaml in base (detailed message)",
			"notayaml",
			"",
			"",
			"cannot unmarshal !!str `notayaml`",
		},
		{
			"invalid yaml in patch",
			"",
			"notayaml",
			"",
			"config.yaml.local: yaml: unmarshal errors:",
		},
		{
			"invalid yaml in patch (detailed message)",
			"",
			"notayaml",
			"",
			"cannot unmarshal !!str `notayaml`",
		},
		{
			"basic merge",
			"{'first':{'one':1,'two':2},'second':{'three':3}}",
			"{'first':{'one':10,'dos':2}}",
			"{'first':{'one':10,'dos':2,'two':2},'second':{'three':3}}",
			"",
		},

		// bools and zero values; here the "mergo" package had issues
		// so we used something simpler.

		{
			"bool merge - off if false",
			"bool: on",
			"bool: off",
			"bool: false",
			"",
		},
		{
			"bool merge - on is true",
			"bool: off",
			"bool: on",
			"bool: true",
			"",
		},
		{
			"string is not a bool - on to off",
			"{'bool': 'on'}",
			"{'bool': 'off'}",
			"{'bool': 'off'}",
			"",
		},
		{
			"string is not a bool - off to on",
			"{'bool': 'off'}",
			"{'bool': 'on'}",
			"{'bool': 'on'}",
			"",
		},
		{
			"bool merge - true to false",
			"{'bool': true}",
			"{'bool': false}",
			"{'bool': false}",
			"",
		},
		{
			"bool merge - false to true",
			"{'bool': false}",
			"{'bool': true}",
			"{'bool': true}",
			"",
		},
		{
			"string merge - value to value",
			"{'string': 'value'}",
			"{'string': ''}",
			"{'string': ''}",
			"",
		},
		{
			"sequence merge - value to empty",
			"{'sequence': [1, 2]}",
			"{'sequence': []}",
			"{'sequence': []}",
			"",
		},
		{
			"map merge - value to value",
			"{'map': {'one': 1, 'two': 2}}",
			"{'map': {}}",
			"{'map': {'one': 1, 'two': 2}}",
			"",
		},

		// mismatched types

		{
			"can't merge a sequence into a mapping",
			"map: {'key': 'value'}",
			"map: ['value1', 'value2']",
			"",
			"can't merge a sequence into a mapping",
		},
		{
			"can't merge a scalar into a mapping",
			"map: {'key': 'value'}",
			"map: 3",
			"",
			"can't merge a scalar into a mapping",
		},
		{
			"can't merge a mapping into a sequence",
			"sequence: ['value1', 'value2']",
			"sequence: {'key': 'value'}",
			"",
			"can't merge a mapping into a sequence",
		},
		{
			"can't merge a scalar into a sequence",
			"sequence: ['value1', 'value2']",
			"sequence: 3",
			"",
			"can't merge a scalar into a sequence",
		},
		{
			"can't merge a sequence into a scalar",
			"scalar: true",
			"scalar: ['value1', 'value2']",
			"",
			"can't merge a sequence into a scalar",
		},
		{
			"can't merge a mapping into a scalar",
			"scalar: true",
			"scalar: {'key': 'value'}",
			"",
			"can't merge a mapping into a scalar",
		},
	}

	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			dirPath, err := os.MkdirTemp("", "yamlpatch")
			require.NoError(t, err)

			defer os.RemoveAll(dirPath)

			configPath := filepath.Join(dirPath, "config.yaml")
			patchPath := filepath.Join(dirPath, "config.yaml.local")
			err = os.WriteFile(configPath, []byte(tc.base), 0o600)
			require.NoError(t, err)

			err = os.WriteFile(patchPath, []byte(tc.patch), 0o600)
			require.NoError(t, err)

			patcher := yamlpatch.NewPatcher(configPath, ".local")
			patchedBytes, err := patcher.MergedPatchContent()
			requireErrorContains(t, err, tc.expectedErr)
			require.YAMLEq(t, tc.expected, string(patchedBytes))
		})
	}
}

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

	tests := []struct {
		name        string
		base        string
		patch       string
		expected    string
		expectedErr string
	}{
		// we test with scalars here, because YAMLeq does not work
		// with multi-document files, so we need char-to-char comparison
		// which is noisy with sequences and (unordered) mappings
		{
			"newlines are always appended, if missing, by yaml.Marshal()",
			"foo: bar",
			"",
			"foo: bar\n",
			"",
		},
		{
			"prepend empty document",
			"foo: bar\n",
			"",
			"foo: bar\n",
			"",
		},
		{
			"prepend a document to another",
			"foo: bar",
			"baz: qux",
			"baz: qux\n---\nfoo: bar\n",
			"",
		},
		{
			"prepend document with same key",
			"foo: true",
			"foo: false",
			"foo: false\n---\nfoo: true\n",
			"",
		},
		{
			"prepend multiple documents",
			"one: 1\n---\ntwo: 2\n---\none: 3",
			"four: 4\n---\none: 1.1",
			"four: 4\n---\none: 1.1\n---\none: 1\n---\ntwo: 2\n---\none: 3\n",
			"",
		},
		{
			"invalid yaml in base",
			"blablabla",
			"",
			"",
			"config.yaml: yaml: unmarshal errors:",
		},
		{
			"invalid yaml in base (detailed message)",
			"blablabla",
			"",
			"",
			"cannot unmarshal !!str `blablabla`",
		},
		{
			"invalid yaml in patch",
			"",
			"blablabla",
			"",
			"config.yaml.local: yaml: unmarshal errors:",
		},
		{
			"invalid yaml in patch (detailed message)",
			"",
			"blablabla",
			"",
			"cannot unmarshal !!str `blablabla`",
		},
	}

	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			dirPath, err := os.MkdirTemp("", "yamlpatch")
			require.NoError(t, err)

			defer os.RemoveAll(dirPath)

			configPath := filepath.Join(dirPath, "config.yaml")
			patchPath := filepath.Join(dirPath, "config.yaml.local")

			err = os.WriteFile(configPath, []byte(tc.base), 0o600)
			require.NoError(t, err)

			err = os.WriteFile(patchPath, []byte(tc.patch), 0o600)
			require.NoError(t, err)

			patcher := yamlpatch.NewPatcher(configPath, ".local")
			patchedBytes, err := patcher.PrependedPatchContent()
			requireErrorContains(t, err, tc.expectedErr)
			// YAMLeq does not handle multiple documents
			require.Equal(t, tc.expected, string(patchedBytes))
		})
	}
}
