package params_test

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"path"
	"path/filepath"
	"testing"

	validator "gopkg.in/go-playground/validator.v9"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/status-im/status-go/params"
	"github.com/status-im/status-go/t/utils"
)

func TestNewNodeConfigWithDefaults(t *testing.T) {
	c, err := params.NewNodeConfigWithDefaults(
		"/some/data/path",
		params.GoerliNetworkID,
		params.WithFleet(params.FleetProd),
		params.WithLES(),
		params.WithMailserver(),
	)
	require.NoError(t, err)
	assert.Equal(t, "/some/data/path", c.DataDir)
	assert.Equal(t, "/some/data/path/keystore", c.KeyStoreDir)
	// assert Whisper
	assert.Equal(t, true, c.WakuConfig.Enabled)
	assert.Equal(t, "/some/data/path/waku", c.WakuConfig.DataDir)
	// assert MailServer
	assert.Equal(t, false, c.WakuConfig.EnableMailServer)
	// assert cluster
	assert.Equal(t, false, c.NoDiscovery)
	assert.Equal(t, params.FleetProd, c.ClusterConfig.Fleet)
	assert.NotEmpty(t, c.ClusterConfig.BootNodes)
	assert.NotEmpty(t, c.ClusterConfig.StaticNodes)
	assert.NotEmpty(t, c.ClusterConfig.PushNotificationsServers)
	// assert LES
	assert.Equal(t, true, c.LightEthConfig.Enabled)
	// assert other
	assert.Equal(t, false, c.HTTPEnabled)
	assert.Equal(t, false, c.IPCEnabled)

	assert.Equal(t, "", c.RuntimeLogLevel)

	assert.Equal(t, "/some/data/path/archivedata", c.TorrentConfig.DataDir)
	assert.Equal(t, "/some/data/path/torrents", c.TorrentConfig.TorrentDir)
	assert.Equal(t, 9025, c.TorrentConfig.Port)
	assert.Equal(t, false, c.TorrentConfig.Enabled)

	assert.NoError(t, c.UpdateWithDefaults())
	assert.NotEmpty(t, c.ShhextConfig.DefaultPushNotificationsServers)
}

func TestNewConfigFromJSON(t *testing.T) {
	tmpDir := t.TempDir()
	json := `{
		"NetworkId": 3,
		"DataDir": "` + tmpDir + `",
		"KeyStoreDir": "` + tmpDir + `",
		"KeycardPairingDataFile": "` + path.Join(tmpDir, "keycard/pairings.json") + `",
		"NoDiscovery": true,
		"TorrentConfig": {
			"Port": 9025,
			"Enabled": false,
			"DataDir": "` + tmpDir + `/archivedata",
			"TorrentDir": "` + tmpDir + `/torrents"
		},
		"RuntimeLogLevel": "DEBUG"
	}`
	c, err := params.NewConfigFromJSON(json)
	require.NoError(t, err)
	require.Equal(t, uint64(3), c.NetworkID)
	require.Equal(t, tmpDir, c.DataDir)
	require.Equal(t, tmpDir, c.KeyStoreDir)
	require.Equal(t, false, c.TorrentConfig.Enabled)
	require.Equal(t, 9025, c.TorrentConfig.Port)
	require.Equal(t, tmpDir+"/archivedata", c.TorrentConfig.DataDir)
	require.Equal(t, tmpDir+"/torrents", c.TorrentConfig.TorrentDir)
	require.Equal(t, "DEBUG", c.RuntimeLogLevel)
}

func TestConfigWriteRead(t *testing.T) {
	tmpDir := t.TempDir()

	nodeConfig, err := utils.MakeTestNodeConfigWithDataDir("", tmpDir, params.GoerliNetworkID)
	require.Nil(t, err, "cannot create new config object")

	err = nodeConfig.Save()
	require.Nil(t, err, "cannot persist configuration")

	loadedConfigData, err := ioutil.ReadFile(filepath.Join(nodeConfig.DataDir, "config.json"))
	require.Nil(t, err, "cannot read configuration from disk")
	loadedConfig := string(loadedConfigData)
	require.Contains(t, loadedConfig, fmt.Sprintf(`"NetworkId": %d`, params.GoerliNetworkID))
	require.Contains(t, loadedConfig, fmt.Sprintf(`"DataDir": "%s"`, tmpDir))
}

// TestNodeConfigValidate checks validation of individual fields.
func TestNodeConfigValidate(t *testing.T) {
	testCases := []struct {
		Name        string
		Config      string
		Error       string
		FieldErrors map[string]string // map[Field]Tag
		CheckFunc   func(*testing.T, *params.NodeConfig)
	}{
		{
			Name: "Valid JSON config",
			Config: `{
				"NetworkId": 1,
				"RootDataDir": "/tmp/data",
				"DataDir": "/tmp/data",
				"KeyStoreDir": "/tmp/data",
				"KeycardPairingDataFile": "/tmp/data/keycard/pairings.json",
				"NoDiscovery": true
			}`,
		},
		{
			Name:   "Invalid JSON config",
			Config: `{"NetworkId": }`,
			Error:  "invalid character '}'",
		},
		{
			Name:   "Invalid field type",
			Config: `{"NetworkId": "abc"}`,
			Error:  "json: cannot unmarshal string into Go struct field",
		},
		{
			Name:   "Validate all required fields",
			Config: `{}`,
			FieldErrors: map[string]string{
				"NetworkID":              "required",
				"DataDir":                "required",
				"KeyStoreDir":            "required",
				"KeycardPairingDataFile": "required",
			},
		},
		{
			Name: "Validate that Name does not contain slash",
			Config: `{
				"NetworkId": 1,
				"DataDir": "/some/dir",
				"KeyStoreDir": "/some/dir",
				"KeycardPairingDataFile": "/some/dir/keycard/pairings.json",
				"Name": "invalid/name"
			}`,
			FieldErrors: map[string]string{
				"Name": "excludes",
			},
		},
		{
			Name: "Validate that NodeKey is checked for validity",
			Config: `{
				"NetworkId": 1,
				"DataDir": "/some/dir",
				"KeyStoreDir": "/some/dir",
				"KeycardPairingDataFile": "/some/dir/keycard/pairings.json",
				"NoDiscovery": true,
				"NodeKey": "foo"
			}`,
			Error: "NodeKey is invalid",
		},
		{
			Name: "Validate that UpstreamConfig.URL is validated if UpstreamConfig is enabled",
			Config: `{
				"NetworkId": 1,
				"DataDir": "/some/dir",
				"KeyStoreDir": "/some/dir",
				"KeycardPairingDataFile": "/some/dir/keycard/pairings.json",
				"NoDiscovery": true,
				"UpstreamConfig": {
					"Enabled": true,
					"URL": "[bad.url]"
				}
			}`,
			Error: "'[bad.url]' is invalid",
		},
		{
			Name: "Validate that UpstreamConfig.URL is not validated if UpstreamConfig is disabled",
			Config: `{
				"NetworkId": 1,
				"DataDir": "/some/dir",
				"KeyStoreDir": "/some/dir",
				"KeycardPairingDataFile": "/some/dir/keycard/pairings.json",
				"NoDiscovery": true,
				"UpstreamConfig": {
					"Enabled": false,
					"URL": "[bad.url]"
				}
			}`,
		},
		{
			Name: "Validate that UpstreamConfig.URL validation passes if UpstreamConfig.URL is valid",
			Config: `{
				"NetworkId": 1,
				"DataDir": "/some/dir",
				"KeyStoreDir": "/some/dir",
				"KeycardPairingDataFile": "/some/dir/keycard/pairings.json",
				"NoDiscovery": true,
				"UpstreamConfig": {
					"Enabled": true,
					"URL": "` + params.MainnetEthereumNetworkURL + `"
				}
			}`,
		},
		{
			Name: "Validate that ClusterConfig.BootNodes is verified to not be empty if discovery is disabled",
			Config: `{
				"NetworkId": 1,
				"DataDir": "/some/dir",
				"KeyStoreDir": "/some/dir",
				"KeycardPairingDataFile": "/some/dir/keycard/pairings.json",
				"NoDiscovery": false
			}`,
			Error: "NoDiscovery is false, but ClusterConfig.BootNodes is empty",
		},
		{
			Name: "Validate that PFSEnabled & InstallationID are checked for validity",
			Config: `{
				"NetworkId": 1,
				"DataDir": "/some/dir",
				"KeyStoreDir": "/some/dir",
				"KeycardPairingDataFile": "/some/dir/keycard/pairings.json",
				"NoDiscovery": true,
				"WakuConfig": {
					"Enabled": true,
					"DataDir": "/foo"
				},
				"ShhextConfig": {
					"PFSEnabled": true
				}
			}`,
			Error: "PFSEnabled is true, but InstallationID is empty",
		},
		{
			Name: "Default HTTP virtual hosts is localhost and CORS is empty",
			Config: `{
				"NetworkId": 1,
				"DataDir": "/some/dir",
				"KeyStoreDir": "/some/dir",
				"KeycardPairingDataFile": "/some/dir/keycard/pairings.json"
			}`,
			CheckFunc: func(t *testing.T, config *params.NodeConfig) {
				require.Equal(t, []string{"localhost"}, config.HTTPVirtualHosts)
				require.Nil(t, config.HTTPCors)
			},
		},
		{
			Name: "Set HTTP virtual hosts and CORS",
			Config: `{
				"NetworkId": 1,
				"DataDir": "/some/dir",
				"KeyStoreDir": "/some/dir",
				"KeycardPairingDataFile": "/some/dir/keycard/pairings.json",
				"HTTPVirtualHosts": ["my.domain.com"],
				"HTTPCors": ["http://my.domain.com:8080"]
			}`,
			CheckFunc: func(t *testing.T, config *params.NodeConfig) {
				require.Equal(t, []string{"my.domain.com"}, config.HTTPVirtualHosts)
				require.Equal(t, []string{"http://my.domain.com:8080"}, config.HTTPCors)
			},
		},
		{
			Name: "ShhextConfig is not required",
			Config: `{
				"NetworkId": 1,
				"DataDir": "/some/dir",
				"KeyStoreDir": "/some/dir",
				"KeycardPairingDataFile": "/some/dir/keycard/pairings.json"
			}`,
		},
		{
			Name:   "Missing APIModules",
			Config: `{"NetworkId": 1, "DataDir": "/tmp/data", "KeyStoreDir": "/tmp/data", "KeycardPairingDataFile": "/tmp/data/keycard/pairings.json", "APIModules" :""}`,
			FieldErrors: map[string]string{
				"APIModules": "required",
			},
		},
		{
			Name: "Validate that TorrentConfig.DataDir and TorrentConfig.TorrentDir can't be empty strings",
			Config: `{
				"NetworkId": 1,
				"DataDir": "/some/dir",
				"KeyStoreDir": "/some/dir",
				"KeycardPairingDataFile": "/some/dir/keycard/pairings.json",
        "TorrentConfig": {
          "Enabled": true,
          "Port": 9025,
          "DataDir": "",
          "TorrentDir": ""
        }
      }`,
			Error: `TorrentConfig.DataDir and TorrentConfig.TorrentDir cannot be ""`,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.Name, func(t *testing.T) {
			config, err := params.NewConfigFromJSON(tc.Config)
			switch err := err.(type) {
			case validator.ValidationErrors:
				for _, ve := range err {
					require.Contains(t, tc.FieldErrors, ve.Field())
					require.Equal(t, tc.FieldErrors[ve.Field()], ve.Tag())
				}
			case error:
				if tc.Error == "" {
					require.NoError(t, err)
				} else {
					fmt.Println(tc.Error)
					require.Contains(t, err.Error(), tc.Error)
				}
			case nil:
				if tc.Error != "" {
					require.Error(t, err, "Error should be '%v'", tc.Error)
				}
				require.Nil(t, tc.FieldErrors)
				if tc.CheckFunc != nil {
					tc.CheckFunc(t, config)
				}
			}
		})
	}
}

func TestMarshalWalletConfigJSON(t *testing.T) {
	walletConfig := params.WalletConfig{
		OpenseaAPIKey:        "some-key",
		RaribleMainnetAPIKey: "some-key2",
	}
	bytes, err := json.Marshal(walletConfig)
	require.NoError(t, err)
	// check if sensitive fields are not present
	require.NotContains(t, string(bytes), "OpenseaAPIKey")
	require.Contains(t, string(bytes), "StatusProxyEnabled")

	// check if deserializing are still working with sensitive fields
	walletConfig = params.WalletConfig{}
	err = json.Unmarshal([]byte(`{"OpenseaAPIKey":"some-key", "StatusProxyEnabled":true}`), &walletConfig)
	require.NoError(t, err)
	require.Equal(t, "some-key", walletConfig.OpenseaAPIKey)
	require.True(t, walletConfig.StatusProxyEnabled)
}