package token

import (
	"io/ioutil"
	"path/filepath"
	"testing"

	"github.com/hashicorp/consul/sdk/testutil"
	"github.com/hashicorp/go-hclog"
	"github.com/stretchr/testify/require"
)

func TestStore_Load(t *testing.T) {
	dataDir := testutil.TempDir(t, "datadir")
	tokenFile := filepath.Join(dataDir, tokensPath)
	logger := hclog.New(nil)
	store := new(Store)

	t.Run("with empty store", func(t *testing.T) {
		cfg := Config{
			DataDir:             dataDir,
			ACLAgentToken:       "alfa",
			ACLAgentMasterToken: "bravo",
			ACLDefaultToken:     "charlie",
			ACLReplicationToken: "delta",
		}
		require.NoError(t, store.Load(cfg, logger))
		require.Equal(t, "alfa", store.AgentToken())
		require.Equal(t, "bravo", store.AgentMasterToken())
		require.Equal(t, "charlie", store.UserToken())
		require.Equal(t, "delta", store.ReplicationToken())
	})

	t.Run("updated from Config", func(t *testing.T) {
		cfg := Config{
			DataDir:             dataDir,
			ACLDefaultToken:     "echo",
			ACLAgentToken:       "foxtrot",
			ACLAgentMasterToken: "golf",
			ACLReplicationToken: "hotel",
		}
		// ensures no error for missing persisted tokens file
		require.NoError(t, store.Load(cfg, logger))
		require.Equal(t, "echo", store.UserToken())
		require.Equal(t, "foxtrot", store.AgentToken())
		require.Equal(t, "golf", store.AgentMasterToken())
		require.Equal(t, "hotel", store.ReplicationToken())
	})

	t.Run("with persisted tokens", func(t *testing.T) {
		cfg := Config{
			DataDir:             dataDir,
			ACLDefaultToken:     "echo",
			ACLAgentToken:       "foxtrot",
			ACLAgentMasterToken: "golf",
			ACLReplicationToken: "hotel",
		}

		tokens := `{
			"agent" : "india",
			"agent_master" : "juliett",
			"default": "kilo",
			"replication" : "lima"
		}`

		require.NoError(t, ioutil.WriteFile(tokenFile, []byte(tokens), 0600))
		require.NoError(t, store.Load(cfg, logger))

		// no updates since token persistence is not enabled
		require.Equal(t, "echo", store.UserToken())
		require.Equal(t, "foxtrot", store.AgentToken())
		require.Equal(t, "golf", store.AgentMasterToken())
		require.Equal(t, "hotel", store.ReplicationToken())

		cfg.EnablePersistence = true
		require.NoError(t, store.Load(cfg, logger))

		require.Equal(t, "india", store.AgentToken())
		require.Equal(t, "juliett", store.AgentMasterToken())
		require.Equal(t, "kilo", store.UserToken())
		require.Equal(t, "lima", store.ReplicationToken())

		// check store persistence was enabled
		require.NotNil(t, store.persistence)
	})

	t.Run("with persisted tokens, persisted tokens override config", func(t *testing.T) {
		tokens := `{
			"agent" : "mike",
			"agent_master" : "november",
			"default": "oscar",
			"replication" : "papa"
		}`

		cfg := Config{
			EnablePersistence:   true,
			DataDir:             dataDir,
			ACLDefaultToken:     "quebec",
			ACLAgentToken:       "romeo",
			ACLAgentMasterToken: "sierra",
			ACLReplicationToken: "tango",
		}

		require.NoError(t, ioutil.WriteFile(tokenFile, []byte(tokens), 0600))
		require.NoError(t, store.Load(cfg, logger))

		require.Equal(t, "mike", store.AgentToken())
		require.Equal(t, "november", store.AgentMasterToken())
		require.Equal(t, "oscar", store.UserToken())
		require.Equal(t, "papa", store.ReplicationToken())
	})

	t.Run("with some persisted tokens", func(t *testing.T) {
		tokens := `{
			"agent" : "uniform",
			"agent_master" : "victor"
		}`

		cfg := Config{
			EnablePersistence:   true,
			DataDir:             dataDir,
			ACLDefaultToken:     "whiskey",
			ACLAgentToken:       "xray",
			ACLAgentMasterToken: "yankee",
			ACLReplicationToken: "zulu",
		}

		require.NoError(t, ioutil.WriteFile(tokenFile, []byte(tokens), 0600))
		require.NoError(t, store.Load(cfg, logger))

		require.Equal(t, "uniform", store.AgentToken())
		require.Equal(t, "victor", store.AgentMasterToken())
		require.Equal(t, "whiskey", store.UserToken())
		require.Equal(t, "zulu", store.ReplicationToken())
	})

	t.Run("persisted file contains invalid data", func(t *testing.T) {
		cfg := Config{
			EnablePersistence:   true,
			DataDir:             dataDir,
			ACLDefaultToken:     "one",
			ACLAgentToken:       "two",
			ACLAgentMasterToken: "three",
			ACLReplicationToken: "four",
		}

		require.NoError(t, ioutil.WriteFile(tokenFile, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, 0600))
		err := store.Load(cfg, logger)
		require.Error(t, err)
		require.Contains(t, err.Error(), "failed to decode tokens file")

		require.Equal(t, "one", store.UserToken())
		require.Equal(t, "two", store.AgentToken())
		require.Equal(t, "three", store.AgentMasterToken())
		require.Equal(t, "four", store.ReplicationToken())
	})

	t.Run("persisted file contains invalid json", func(t *testing.T) {
		cfg := Config{
			EnablePersistence:   true,
			DataDir:             dataDir,
			ACLDefaultToken:     "alfa",
			ACLAgentToken:       "bravo",
			ACLAgentMasterToken: "charlie",
			ACLReplicationToken: "foxtrot",
		}

		require.NoError(t, ioutil.WriteFile(tokenFile, []byte("[1,2,3]"), 0600))
		err := store.Load(cfg, logger)
		require.Error(t, err)
		require.Contains(t, err.Error(), "failed to decode tokens file")

		require.Equal(t, "alfa", store.UserToken())
		require.Equal(t, "bravo", store.AgentToken())
		require.Equal(t, "charlie", store.AgentMasterToken())
		require.Equal(t, "foxtrot", store.ReplicationToken())
	})
}

func TestStore_WithPersistenceLock(t *testing.T) {
	dataDir := testutil.TempDir(t, "datadir")
	store := new(Store)
	cfg := Config{
		EnablePersistence:   true,
		DataDir:             dataDir,
		ACLDefaultToken:     "default-token",
		ACLAgentToken:       "agent-token",
		ACLAgentMasterToken: "master-token",
		ACLReplicationToken: "replication-token",
	}
	err := store.Load(cfg, hclog.New(nil))
	require.NoError(t, err)

	f := func() error {
		updated := store.UpdateUserToken("the-new-token", TokenSourceAPI)
		require.True(t, updated)

		updated = store.UpdateAgentMasterToken("the-new-master-token", TokenSourceAPI)
		require.True(t, updated)
		return nil
	}

	err = store.WithPersistenceLock(f)
	require.NoError(t, err)

	tokens, err := readPersistedFromFile(filepath.Join(dataDir, tokensPath))
	require.NoError(t, err)
	expected := persistedTokens{
		Default:     "the-new-token",
		AgentMaster: "the-new-master-token",
	}
	require.Equal(t, expected, tokens)
}
