/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package viperutil

import (
	"fmt"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/hyperledger/fabric-lib-go/bccsp/factory"
	"github.com/hyperledger/fabric/orderer/mocks/util"
	"github.com/stretchr/testify/require"
)

const (
	testConfigName = "viperutil"
	testEnvPrefix  = "VIPERUTIL"
)

func TestEnvSlice(t *testing.T) {
	type testSlice struct {
		Inner struct {
			Slice []string
		}
	}

	envVar := testEnvPrefix + "_INNER_SLICE"
	t.Setenv(envVar, "[a, b, c]")

	data := "---\nInner:\n    Slice: [d,e,f]"

	config := New()
	config.SetConfigName(testConfigName)
	err := config.ReadConfig(strings.NewReader(data))
	require.NoError(t, err, "error reading %s plugin config", testConfigName)

	var uconf testSlice
	err = config.EnhancedExactUnmarshal(&uconf)
	require.NoError(t, err, "failed to unmarshal")

	expected := []string{"a", "b", "c"}
	require.Exactly(t, expected, uconf.Inner.Slice, "did not get the expected slice")
}

type testByteSize struct {
	Inner struct {
		ByteSize uint32
	}
}

func TestByteSize(t *testing.T) {
	testCases := []struct {
		data     string
		expected uint32
	}{
		{"", 0},
		{"42", 42},
		{"42k", 42 * 1024},
		{"42kb", 42 * 1024},
		{"42K", 42 * 1024},
		{"42KB", 42 * 1024},
		{"42 K", 42 * 1024},
		{"42 KB", 42 * 1024},
		{"42m", 42 * 1024 * 1024},
		{"42mb", 42 * 1024 * 1024},
		{"42M", 42 * 1024 * 1024},
		{"42MB", 42 * 1024 * 1024},
		{"42 M", 42 * 1024 * 1024},
		{"42 MB", 42 * 1024 * 1024},
		{"3g", 3 * 1024 * 1024 * 1024},
		{"3gb", 3 * 1024 * 1024 * 1024},
		{"3G", 3 * 1024 * 1024 * 1024},
		{"3GB", 3 * 1024 * 1024 * 1024},
		{"3 G", 3 * 1024 * 1024 * 1024},
		{"3 GB", 3 * 1024 * 1024 * 1024},
	}

	for _, tc := range testCases {
		t.Run(tc.data, func(t *testing.T) {
			data := fmt.Sprintf("---\nInner:\n    ByteSize: %s", tc.data)

			config := New()
			err := config.ReadConfig(strings.NewReader(data))
			require.NoError(t, err, "error reading config")

			var uconf testByteSize
			err = config.EnhancedExactUnmarshal(&uconf)
			require.NoError(t, err, "failed to unmarshal")
			require.Exactly(t, tc.expected, uconf.Inner.ByteSize, "incorrect byte size")
		})
	}
}

func TestByteSizeOverflow(t *testing.T) {
	data := "---\nInner:\n    ByteSize: 4GB"

	config := New()
	err := config.ReadConfig(strings.NewReader(data))
	require.NoError(t, err, "error reading config")

	var uconf testByteSize
	err = config.EnhancedExactUnmarshal(&uconf)
	require.Error(t, err)
	require.Contains(t, err.Error(), "Inner.ByteSize")
	require.Contains(t, err.Error(), "value '4GB' overflows uint32")
}

type stringFromFileConfig struct {
	Inner struct {
		Single   string
		Multiple []string
	}
}

func TestStringNotFromFile(t *testing.T) {
	yaml := "---\nInner:\n  Single: expected_value\n"

	config := New()
	err := config.ReadConfig(strings.NewReader(yaml))
	require.NoError(t, err, "error reading config")

	var uconf stringFromFileConfig
	err = config.EnhancedExactUnmarshal(&uconf)
	require.NoError(t, err, "failed to unmarshal")
	require.Equal(t, "expected_value", uconf.Inner.Single)
}

func TestStringFromFile(t *testing.T) {
	file, err := os.CreateTemp(os.TempDir(), "test")
	require.NoError(t, err, "failed to create temp file")
	defer os.Remove(file.Name())

	expectedValue := "this is the text in the file"

	err = os.WriteFile(file.Name(), []byte(expectedValue), 0o644)
	require.NoError(t, err, "uname to write temp file")

	yaml := fmt.Sprintf("---\nInner:\n  Single:\n    File: %s", file.Name())

	config := New()
	err = config.ReadConfig(strings.NewReader(yaml))
	require.NoError(t, err, "error reading config")

	var uconf stringFromFileConfig
	err = config.EnhancedExactUnmarshal(&uconf)
	require.NoError(t, err, "unmarshal failed")
	require.Equal(t, expectedValue, uconf.Inner.Single)
}

func TestPEMBlocksFromFile(t *testing.T) {
	file, err := os.CreateTemp(os.TempDir(), "test")
	require.NoError(t, err, "failed to create temp file")
	defer os.Remove(file.Name())

	var pems []byte
	for i := 0; i < 3; i++ {
		publicKeyCert, _, _ := util.GenerateMockPublicPrivateKeyPairPEM(true)
		pems = append(pems, publicKeyCert...)
	}

	err = os.WriteFile(file.Name(), pems, 0o644)
	require.NoError(t, err, "failed to write temp file")

	yaml := fmt.Sprintf("---\nInner:\n  Multiple:\n    File: %s", file.Name())

	config := New()
	err = config.ReadConfig(strings.NewReader(yaml))
	require.NoError(t, err, "error reading config")

	var uconf stringFromFileConfig
	err = config.EnhancedExactUnmarshal(&uconf)
	require.NoError(t, err, "failed to unmarshal")
	require.Len(t, uconf.Inner.Multiple, 3)
}

func TestPEMBlocksFromFileEnv(t *testing.T) {
	file, err := os.CreateTemp(os.TempDir(), "test")
	require.NoError(t, err, "failed to create temp file")
	defer os.Remove(file.Name())

	var pems []byte
	for i := 0; i < 3; i++ {
		publicKeyCert, _, _ := util.GenerateMockPublicPrivateKeyPairPEM(true)
		pems = append(pems, publicKeyCert...)
	}

	err = os.WriteFile(file.Name(), pems, 0o644)
	require.NoError(t, err, "failed to write temp file")

	envVar := testEnvPrefix + "_INNER_MULTIPLE_FILE"
	t.Setenv(envVar, file.Name())

	testCases := []struct {
		name string
		data string
	}{
		{"Override", "---\nInner:\n  Multiple:\n    File: wrong_file"},
		{"NoFileElement", "---\nInner:\n  Multiple:\n"},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			config := New()
			config.SetConfigName(testConfigName)

			err := config.ReadConfig(strings.NewReader(tc.data))
			require.NoError(t, err, "error reading config")

			var uconf stringFromFileConfig
			err = config.EnhancedExactUnmarshal(&uconf)
			require.NoError(t, err, "failed to unmarshal")
			require.Len(t, uconf.Inner.Multiple, 3)
		})
	}
}

func TestStringFromFileNotSpecified(t *testing.T) {
	yaml := "---\nInner:\n  Single:\n    File:\n"

	config := New()
	err := config.ReadConfig(strings.NewReader(yaml))
	require.NoError(t, err, "error reading config")

	var uconf stringFromFileConfig
	err = config.EnhancedExactUnmarshal(&uconf)
	require.Error(t, err, "umarshal should fail")
}

func TestStringFromFileEnv(t *testing.T) {
	expectedValue := "this is the text in the file"

	file, err := os.CreateTemp(os.TempDir(), "test")
	require.NoError(t, err, "failed to create temp file")
	defer os.Remove(file.Name())

	err = os.WriteFile(file.Name(), []byte(expectedValue), 0o644)
	require.NoError(t, err, "failed to write temp file")

	envVar := testEnvPrefix + "_INNER_SINGLE_FILE"
	t.Setenv(envVar, file.Name())

	testCases := []struct {
		name string
		data string
	}{
		{"Override", "---\nInner:\n  Single:\n    File: wrong_file"},
		{"NoFileElement", "---\nInner:\n  Single:\n"},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			config := New()
			config.SetConfigName(testConfigName)

			err := config.ReadConfig(strings.NewReader(tc.data))
			require.NoError(t, err, "error reading config")

			var uconf stringFromFileConfig
			err = config.EnhancedExactUnmarshal(&uconf)
			require.NoError(t, err, "failed to unmarshal")
			require.Exactly(t, expectedValue, uconf.Inner.Single)
		})
	}
}

func TestDecodeOpaqueField(t *testing.T) {
	yaml := "---\nFoo: bar\nHello:\n  World: 42\n"

	config := New()
	err := config.ReadConfig(strings.NewReader(yaml))
	require.NoError(t, err, "error reading config")

	var conf struct {
		Foo   string
		Hello struct{ World int }
	}
	err = config.EnhancedExactUnmarshal(&conf)
	require.NoError(t, err, "failed to unmarshal")
	require.Equal(t, "bar", conf.Foo)
	require.Equal(t, 42, conf.Hello.World)
}

func TestBCCSPDecodeHookOverride(t *testing.T) {
	yaml := "---\nBCCSP:\n  Default: default-provider\n  SW:\n    Security: 999\n"

	overrideVar := testEnvPrefix + "_BCCSP_SW_SECURITY"
	t.Setenv(overrideVar, "1111")

	config := New()
	config.SetConfigName(testConfigName)
	err := config.ReadConfig(strings.NewReader(yaml))
	require.NoError(t, err, "error reading config")

	var tc struct {
		BCCSP *factory.FactoryOpts
	}
	err = config.EnhancedExactUnmarshal(&tc)
	require.NoError(t, err, "failed to unmarshal")
	require.NotNil(t, tc.BCCSP)
	require.NotNil(t, tc.BCCSP.SW)
	require.Equal(t, 1111, tc.BCCSP.SW.Security)
}

func TestDurationDecode(t *testing.T) {
	tests := []struct {
		input    string
		expected time.Duration
	}{
		{"", 0},
		{"100", 100 * time.Nanosecond},
		{"1s", time.Second},
		{"1m", time.Minute},
		{"1m1s", 61 * time.Second},
		{"90s", 90 * time.Second},
	}
	for _, tt := range tests {
		t.Run(tt.expected.String(), func(t *testing.T) {
			yaml := fmt.Sprintf("---\nDuration: %s\n", tt.input)

			config := New()
			config.SetConfigName(testConfigName)
			err := config.ReadConfig(strings.NewReader(yaml))
			require.NoError(t, err, "error reading config")

			var conf struct{ Duration time.Duration }
			err = config.EnhancedExactUnmarshal(&conf)
			require.NoError(t, err, "failed to unmarshal")
			require.Equal(t, tt.expected, conf.Duration)
		})
	}
}
