// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package token

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

	"github.com/hashicorp/go-hclog"
	"github.com/stretchr/testify/require"

	"github.com/hashicorp/consul/sdk/testutil"
)

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",
			ACLAgentRecoveryToken:          "bravo",
			ACLDefaultToken:                "charlie",
			ACLReplicationToken:            "delta",
			ACLConfigFileRegistrationToken: "echo",
			ACLDNSToken:                    "foxtrot",
		}
		require.NoError(t, store.Load(cfg, logger))
		require.Equal(t, "alfa", store.AgentToken())
		require.Equal(t, "bravo", store.AgentRecoveryToken())
		require.Equal(t, "charlie", store.UserToken())
		require.Equal(t, "delta", store.ReplicationToken())
		require.Equal(t, "echo", store.ConfigFileRegistrationToken())
		require.Equal(t, "foxtrot", store.DNSToken())
	})

	t.Run("updated from Config", func(t *testing.T) {
		cfg := Config{
			DataDir:                        dataDir,
			ACLDefaultToken:                "sierra",
			ACLAgentToken:                  "tango",
			ACLAgentRecoveryToken:          "uniform",
			ACLReplicationToken:            "victor",
			ACLConfigFileRegistrationToken: "xray",
			ACLDNSToken:                    "zulu",
		}
		// ensures no error for missing persisted tokens file
		require.NoError(t, store.Load(cfg, logger))
		require.Equal(t, "sierra", store.UserToken())
		require.Equal(t, "tango", store.AgentToken())
		require.Equal(t, "uniform", store.AgentRecoveryToken())
		require.Equal(t, "victor", store.ReplicationToken())
		require.Equal(t, "xray", store.ConfigFileRegistrationToken())
		require.Equal(t, "zulu", store.DNSToken())
	})

	t.Run("with persisted tokens", func(t *testing.T) {
		cfg := Config{
			DataDir:                        dataDir,
			ACLDefaultToken:                "alpha",
			ACLAgentToken:                  "bravo",
			ACLAgentRecoveryToken:          "charlie",
			ACLReplicationToken:            "delta",
			ACLConfigFileRegistrationToken: "echo",
			ACLDNSToken:                    "foxtrot",
		}

		tokens := `{
			"agent" : "golf",
			"agent_recovery" : "hotel",
			"default": "india",
			"replication": "juliet",
			"config_file_service_registration": "kilo",
			"dns": "lima"
		}`

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

		// no updates since token persistence is not enabled
		require.Equal(t, "alpha", store.UserToken())
		require.Equal(t, "bravo", store.AgentToken())
		require.Equal(t, "charlie", store.AgentRecoveryToken())
		require.Equal(t, "delta", store.ReplicationToken())
		require.Equal(t, "echo", store.ConfigFileRegistrationToken())
		require.Equal(t, "foxtrot", store.DNSToken())

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

		require.Equal(t, "golf", store.AgentToken())
		require.Equal(t, "hotel", store.AgentRecoveryToken())
		require.Equal(t, "india", store.UserToken())
		require.Equal(t, "juliet", store.ReplicationToken())
		require.Equal(t, "kilo", store.ConfigFileRegistrationToken())
		require.Equal(t, "lima", store.DNSToken())

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

	t.Run("persisted tokens include pre-1.11 agent_master naming", func(t *testing.T) {
		cfg := Config{
			EnablePersistence:     true,
			DataDir:               dataDir,
			ACLAgentRecoveryToken: "golf",
		}

		tokens := `{"agent_master": "juliett"}`
		require.NoError(t, os.WriteFile(tokenFile, []byte(tokens), 0600))
		require.NoError(t, store.Load(cfg, logger))

		require.Equal(t, "juliett", store.AgentRecoveryToken())
	})

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

		cfg := Config{
			EnablePersistence:              true,
			DataDir:                        dataDir,
			ACLDefaultToken:                "quebec",
			ACLAgentToken:                  "romeo",
			ACLAgentRecoveryToken:          "sierra",
			ACLReplicationToken:            "tango",
			ACLConfigFileRegistrationToken: "uniform",
			ACLDNSToken:                    "victor",
		}

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

		require.Equal(t, "mike", store.AgentToken())
		require.Equal(t, "november", store.AgentRecoveryToken())
		require.Equal(t, "oscar", store.UserToken())
		require.Equal(t, "papa", store.ReplicationToken())
		require.Equal(t, "lima", store.ConfigFileRegistrationToken())
		require.Equal(t, "kilo", store.DNSToken())
	})

	t.Run("with some persisted tokens", func(t *testing.T) {
		tokens := `{
			"agent" : "xray",
			"agent_recovery" : "zulu"
		}`

		cfg := Config{
			EnablePersistence:              true,
			DataDir:                        dataDir,
			ACLDefaultToken:                "alpha",
			ACLAgentToken:                  "bravo",
			ACLAgentRecoveryToken:          "charlie",
			ACLReplicationToken:            "delta",
			ACLConfigFileRegistrationToken: "echo",
			ACLDNSToken:                    "foxtrot",
		}

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

		require.Equal(t, "xray", store.AgentToken())
		require.Equal(t, "zulu", store.AgentRecoveryToken())

		require.Equal(t, "alpha", store.UserToken())
		require.Equal(t, "delta", store.ReplicationToken())
		require.Equal(t, "echo", store.ConfigFileRegistrationToken())
		require.Equal(t, "foxtrot", store.DNSToken())
	})

	t.Run("persisted file contains invalid data", func(t *testing.T) {
		cfg := Config{
			EnablePersistence:              true,
			DataDir:                        dataDir,
			ACLDefaultToken:                "alpha",
			ACLAgentToken:                  "bravo",
			ACLAgentRecoveryToken:          "charlie",
			ACLReplicationToken:            "delta",
			ACLConfigFileRegistrationToken: "echo",
			ACLDNSToken:                    "foxtrot",
		}

		require.NoError(t, os.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, "alpha", store.UserToken())
		require.Equal(t, "bravo", store.AgentToken())
		require.Equal(t, "charlie", store.AgentRecoveryToken())
		require.Equal(t, "delta", store.ReplicationToken())
		require.Equal(t, "echo", store.ConfigFileRegistrationToken())
		require.Equal(t, "foxtrot", store.DNSToken())
	})

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

		require.NoError(t, os.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.AgentRecoveryToken())
		require.Equal(t, "delta", store.ReplicationToken())
		require.Equal(t, "echo", store.ConfigFileRegistrationToken())
		require.Equal(t, "foxtrot", store.DNSToken())
	})
}

func TestStore_WithPersistenceLock(t *testing.T) {
	// ACLDefaultToken:                 alpha   --> sierra
	// ACLAgentToken:                   bravo   --> tango
	// ACLAgentRecoveryToken:           charlie --> uniform
	// ACLReplicationToken:             delta   --> victor
	// ACLConfigFileRegistrationToken:  echo    --> xray
	// ACLDNSToken:                     foxtrot --> zulu
	setupStore := func() (string, *Store) {
		dataDir := testutil.TempDir(t, "datadir")
		store := new(Store)
		cfg := Config{
			EnablePersistence:              true,
			DataDir:                        dataDir,
			ACLDefaultToken:                "alpha",
			ACLAgentToken:                  "bravo",
			ACLAgentRecoveryToken:          "charlie",
			ACLReplicationToken:            "delta",
			ACLConfigFileRegistrationToken: "echo",
			ACLDNSToken:                    "foxtrot",
		}
		err := store.Load(cfg, hclog.New(nil))
		require.NoError(t, err)

		return dataDir, store
	}

	requirePersistedTokens := func(t *testing.T, dataDir string, expected persistedTokens) {
		t.Helper()
		tokens, err := readPersistedFromFile(filepath.Join(dataDir, tokensPath))
		require.NoError(t, err)
		require.Equal(t, expected, tokens)
	}

	t.Run("persist some tokens", func(t *testing.T) {
		dataDir, store := setupStore()
		err := store.WithPersistenceLock(func() error {
			require.True(t, store.UpdateUserToken("sierra", TokenSourceAPI))
			require.True(t, store.UpdateAgentRecoveryToken("tango", TokenSourceAPI))
			return nil
		})
		require.NoError(t, err)

		// Only API-sourced tokens are persisted.
		requirePersistedTokens(t, dataDir, persistedTokens{
			Default:       "sierra",
			AgentRecovery: "tango",
		})
	})

	t.Run("persist all tokens", func(t *testing.T) {
		dataDir, store := setupStore()
		err := store.WithPersistenceLock(func() error {
			require.True(t, store.UpdateUserToken("sierra", TokenSourceAPI))
			require.True(t, store.UpdateAgentToken("tango", TokenSourceAPI))
			require.True(t, store.UpdateAgentRecoveryToken("uniform", TokenSourceAPI))
			require.True(t, store.UpdateReplicationToken("victor", TokenSourceAPI))
			require.True(t, store.UpdateConfigFileRegistrationToken("xray", TokenSourceAPI))
			require.True(t, store.UpdateDNSToken("zulu", TokenSourceAPI))
			return nil
		})
		require.NoError(t, err)

		requirePersistedTokens(t, dataDir, persistedTokens{
			Default:                "sierra",
			Agent:                  "tango",
			AgentRecovery:          "uniform",
			Replication:            "victor",
			ConfigFileRegistration: "xray",
			DNS:                    "zulu",
		})
	})

}
