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

package token

import (
	"testing"

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

func TestStore_RegularTokens(t *testing.T) {
	type tokens struct {
		userSource         TokenSource
		user               string
		agent              string
		agentSource        TokenSource
		recovery           string
		recoverySource     TokenSource
		repl               string
		replSource         TokenSource
		registration       string
		registrationSource TokenSource
		dns                string
		dnsSource          TokenSource
	}

	tests := []struct {
		name      string
		set       tokens
		raw       tokens
		effective tokens
	}{
		{
			name:      "set user - config",
			set:       tokens{user: "U", userSource: TokenSourceConfig},
			raw:       tokens{user: "U", userSource: TokenSourceConfig},
			effective: tokens{user: "U", agent: "U"},
		},
		{
			name:      "set user - api",
			set:       tokens{user: "U", userSource: TokenSourceAPI},
			raw:       tokens{user: "U", userSource: TokenSourceAPI},
			effective: tokens{user: "U", agent: "U"},
		},
		{
			name:      "set agent - config",
			set:       tokens{agent: "A", agentSource: TokenSourceConfig},
			raw:       tokens{agent: "A", agentSource: TokenSourceConfig},
			effective: tokens{agent: "A"},
		},
		{
			name:      "set agent - api",
			set:       tokens{agent: "A", agentSource: TokenSourceAPI},
			raw:       tokens{agent: "A", agentSource: TokenSourceAPI},
			effective: tokens{agent: "A"},
		},
		{
			name:      "set user and agent",
			set:       tokens{agent: "A", user: "U"},
			raw:       tokens{agent: "A", user: "U"},
			effective: tokens{agent: "A", user: "U"},
		},
		{
			name:      "set repl - config",
			set:       tokens{repl: "R", replSource: TokenSourceConfig},
			raw:       tokens{repl: "R", replSource: TokenSourceConfig},
			effective: tokens{repl: "R"},
		},
		{
			name:      "set repl - api",
			set:       tokens{repl: "R", replSource: TokenSourceAPI},
			raw:       tokens{repl: "R", replSource: TokenSourceAPI},
			effective: tokens{repl: "R"},
		},
		{
			name:      "set recovery - config",
			set:       tokens{recovery: "M", recoverySource: TokenSourceConfig},
			raw:       tokens{recovery: "M", recoverySource: TokenSourceConfig},
			effective: tokens{recovery: "M"},
		},
		{
			name:      "set recovery - api",
			set:       tokens{recovery: "M", recoverySource: TokenSourceAPI},
			raw:       tokens{recovery: "M", recoverySource: TokenSourceAPI},
			effective: tokens{recovery: "M"},
		},
		{
			name:      "set registration - config",
			set:       tokens{registration: "G", registrationSource: TokenSourceConfig},
			raw:       tokens{registration: "G", registrationSource: TokenSourceConfig},
			effective: tokens{registration: "G"},
		},
		{
			name:      "set registration - api",
			set:       tokens{registration: "G", registrationSource: TokenSourceAPI},
			raw:       tokens{registration: "G", registrationSource: TokenSourceAPI},
			effective: tokens{registration: "G"},
		},
		{
			name:      "set dns - config",
			set:       tokens{dns: "D", dnsSource: TokenSourceConfig},
			raw:       tokens{dns: "D", dnsSource: TokenSourceConfig},
			effective: tokens{dns: "D"},
		},
		{
			name:      "set dns - api",
			set:       tokens{dns: "D", dnsSource: TokenSourceAPI},
			raw:       tokens{dns: "D", dnsSource: TokenSourceAPI},
			effective: tokens{dns: "D"},
		},
		{
			name:      "set all",
			set:       tokens{user: "U", agent: "A", repl: "R", recovery: "M", registration: "G", dns: "D"},
			raw:       tokens{user: "U", agent: "A", repl: "R", recovery: "M", registration: "G", dns: "D"},
			effective: tokens{user: "U", agent: "A", repl: "R", recovery: "M", registration: "G", dns: "D"},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := new(Store)
			if tt.set.user != "" {
				require.True(t, s.UpdateUserToken(tt.set.user, tt.set.userSource))
			}

			if tt.set.agent != "" {
				require.True(t, s.UpdateAgentToken(tt.set.agent, tt.set.agentSource))
			}

			if tt.set.repl != "" {
				require.True(t, s.UpdateReplicationToken(tt.set.repl, tt.set.replSource))
			}

			if tt.set.recovery != "" {
				require.True(t, s.UpdateAgentRecoveryToken(tt.set.recovery, tt.set.recoverySource))
			}

			if tt.set.registration != "" {
				require.True(t, s.UpdateConfigFileRegistrationToken(tt.set.registration, tt.set.registrationSource))
			}

			if tt.set.dns != "" {
				require.True(t, s.UpdateDNSToken(tt.set.dns, tt.set.dnsSource))
			}

			// If they don't change then they return false.
			require.False(t, s.UpdateUserToken(tt.set.user, tt.set.userSource))
			require.False(t, s.UpdateAgentToken(tt.set.agent, tt.set.agentSource))
			require.False(t, s.UpdateReplicationToken(tt.set.repl, tt.set.replSource))
			require.False(t, s.UpdateAgentRecoveryToken(tt.set.recovery, tt.set.recoverySource))
			require.False(t, s.UpdateConfigFileRegistrationToken(tt.set.registration, tt.set.registrationSource))
			require.False(t, s.UpdateDNSToken(tt.set.dns, tt.set.dnsSource))

			require.Equal(t, tt.effective.user, s.UserToken())
			require.Equal(t, tt.effective.agent, s.AgentToken())
			require.Equal(t, tt.effective.recovery, s.AgentRecoveryToken())
			require.Equal(t, tt.effective.repl, s.ReplicationToken())
			require.Equal(t, tt.effective.registration, s.ConfigFileRegistrationToken())
			require.Equal(t, tt.effective.dns, s.DNSToken())

			tok, src := s.UserTokenAndSource()
			require.Equal(t, tt.raw.user, tok)
			require.Equal(t, tt.raw.userSource, src)

			tok, src = s.AgentTokenAndSource()
			require.Equal(t, tt.raw.agent, tok)
			require.Equal(t, tt.raw.agentSource, src)

			tok, src = s.AgentRecoveryTokenAndSource()
			require.Equal(t, tt.raw.recovery, tok)
			require.Equal(t, tt.raw.recoverySource, src)

			tok, src = s.ReplicationTokenAndSource()
			require.Equal(t, tt.raw.repl, tok)
			require.Equal(t, tt.raw.replSource, src)

			tok, src = s.ConfigFileRegistrationTokenAndSource()
			require.Equal(t, tt.raw.registration, tok)
			require.Equal(t, tt.raw.registrationSource, src)

			tok, src = s.DNSTokenAndSource()
			require.Equal(t, tt.raw.dns, tok)
			require.Equal(t, tt.raw.dnsSource, src)
		})
	}
}

func TestStore_AgentRecoveryToken(t *testing.T) {
	s := new(Store)

	verify := func(want bool, toks ...string) {
		for _, tok := range toks {
			require.Equal(t, want, s.IsAgentRecoveryToken(tok))
		}
	}

	verify(false, "", "nope")

	s.UpdateAgentRecoveryToken("recovery", TokenSourceConfig)
	verify(true, "recovery")
	verify(false, "", "nope")

	s.UpdateAgentRecoveryToken("another", TokenSourceConfig)
	verify(true, "another")
	verify(false, "", "nope", "recovery")

	s.UpdateAgentRecoveryToken("", TokenSourceConfig)
	verify(false, "", "nope", "recovery", "another")
}

func TestStore_Notify(t *testing.T) {
	s := new(Store)

	newNotification := func(t *testing.T, s *Store, kind TokenKind) Notifier {
		n := s.Notify(kind)
		require.NotNil(t, n.Ch)
		return n
	}

	requireNotNotified := func(t *testing.T, ch <-chan struct{}) {
		require.Empty(t, ch)
	}

	requireNotifiedOnce := func(t *testing.T, ch <-chan struct{}) {
		require.Len(t, ch, 1)
		// drain the channel
		<-ch
		// just to be safe
		require.Empty(t, ch)
	}

	agentNotifier := newNotification(t, s, TokenKindAgent)
	userNotifier := newNotification(t, s, TokenKindUser)
	agentRecoveryNotifier := newNotification(t, s, TokenKindAgentRecovery)
	replicationNotifier := newNotification(t, s, TokenKindReplication)
	replicationNotifier2 := newNotification(t, s, TokenKindReplication)
	registrationNotifier := newNotification(t, s, TokenKindConfigFileRegistration)
	dnsNotifier := newNotification(t, s, TokenKindDNS)

	// perform an update of the user token
	require.True(t, s.UpdateUserToken("edcae2a2-3b51-4864-b412-c7a568f49cb1", TokenSourceConfig))
	// do it again to ensure it doesn't block even though nothing has read from the 1 buffered chan yet
	require.True(t, s.UpdateUserToken("47788919-f944-476a-bda5-446d64be1df8", TokenSourceAPI))

	// ensure notifications were sent to the user notifier and all other notifiers were not notified.
	requireNotNotified(t, agentNotifier.Ch)
	requireNotifiedOnce(t, userNotifier.Ch)
	requireNotNotified(t, replicationNotifier.Ch)
	requireNotNotified(t, agentRecoveryNotifier.Ch)
	requireNotNotified(t, replicationNotifier2.Ch)
	requireNotNotified(t, registrationNotifier.Ch)
	requireNotNotified(t, dnsNotifier.Ch)

	// update the agent token which should send a notification to the agent notifier.
	require.True(t, s.UpdateAgentToken("5d748ec2-d536-461f-8e2a-1f7eae98d559", TokenSourceAPI))

	requireNotifiedOnce(t, agentNotifier.Ch)
	requireNotNotified(t, userNotifier.Ch)
	requireNotNotified(t, replicationNotifier.Ch)
	requireNotNotified(t, agentRecoveryNotifier.Ch)
	requireNotNotified(t, replicationNotifier2.Ch)
	requireNotNotified(t, registrationNotifier.Ch)
	requireNotNotified(t, dnsNotifier.Ch)

	// update the agent recovery token which should send a notification to the agent recovery notifier.
	require.True(t, s.UpdateAgentRecoveryToken("789badc8-f850-43e1-8742-9b9f484957cc", TokenSourceAPI))

	requireNotNotified(t, agentNotifier.Ch)
	requireNotNotified(t, userNotifier.Ch)
	requireNotNotified(t, replicationNotifier.Ch)
	requireNotifiedOnce(t, agentRecoveryNotifier.Ch)
	requireNotNotified(t, replicationNotifier2.Ch)
	requireNotNotified(t, registrationNotifier.Ch)
	requireNotNotified(t, dnsNotifier.Ch)

	// update the replication token which should send a notification to the replication notifier.
	require.True(t, s.UpdateReplicationToken("789badc8-f850-43e1-8742-9b9f484957cc", TokenSourceAPI))

	requireNotNotified(t, agentNotifier.Ch)
	requireNotNotified(t, userNotifier.Ch)
	requireNotifiedOnce(t, replicationNotifier.Ch)
	requireNotNotified(t, agentRecoveryNotifier.Ch)
	requireNotifiedOnce(t, replicationNotifier2.Ch)
	requireNotNotified(t, registrationNotifier.Ch)
	requireNotNotified(t, dnsNotifier.Ch)

	s.StopNotify(replicationNotifier2)

	// update the replication token which should send a notification to the replication notifier.
	require.True(t, s.UpdateReplicationToken("eb0b56b9-fa65-4ae1-902a-c64457c62ac6", TokenSourceAPI))

	requireNotNotified(t, agentNotifier.Ch)
	requireNotNotified(t, userNotifier.Ch)
	requireNotifiedOnce(t, replicationNotifier.Ch)
	requireNotNotified(t, agentRecoveryNotifier.Ch)
	requireNotNotified(t, replicationNotifier2.Ch)
	requireNotNotified(t, registrationNotifier.Ch)
	requireNotNotified(t, dnsNotifier.Ch)

	// update the config file registration token which should send a notification to the replication notifier.
	require.True(t, s.UpdateConfigFileRegistrationToken("82fe7362-7d83-4f43-bb27-c35f1f15083c", TokenSourceAPI))

	requireNotNotified(t, agentNotifier.Ch)
	requireNotNotified(t, userNotifier.Ch)
	requireNotNotified(t, replicationNotifier.Ch)
	requireNotNotified(t, agentRecoveryNotifier.Ch)
	requireNotNotified(t, replicationNotifier2.Ch)
	requireNotifiedOnce(t, registrationNotifier.Ch)
	requireNotNotified(t, dnsNotifier.Ch)

	// update the dns token which should send a notification to the replication notifier.
	require.True(t, s.UpdateDNSToken("ce8e829f-dc45-4ba7-9dd3-1dbbe070f573", TokenSourceAPI))

	requireNotNotified(t, agentNotifier.Ch)
	requireNotNotified(t, userNotifier.Ch)
	requireNotNotified(t, replicationNotifier.Ch)
	requireNotNotified(t, agentRecoveryNotifier.Ch)
	requireNotNotified(t, replicationNotifier2.Ch)
	requireNotNotified(t, registrationNotifier.Ch)
	requireNotifiedOnce(t, dnsNotifier.Ch)

	// request updates that are not changes
	require.False(t, s.UpdateAgentToken("5d748ec2-d536-461f-8e2a-1f7eae98d559", TokenSourceAPI))
	require.False(t, s.UpdateAgentRecoveryToken("789badc8-f850-43e1-8742-9b9f484957cc", TokenSourceAPI))
	require.False(t, s.UpdateUserToken("47788919-f944-476a-bda5-446d64be1df8", TokenSourceAPI))
	require.False(t, s.UpdateReplicationToken("eb0b56b9-fa65-4ae1-902a-c64457c62ac6", TokenSourceAPI))
	require.False(t, s.UpdateConfigFileRegistrationToken("82fe7362-7d83-4f43-bb27-c35f1f15083c", TokenSourceAPI))
	require.False(t, s.UpdateDNSToken("ce8e829f-dc45-4ba7-9dd3-1dbbe070f573", TokenSourceAPI))

	// ensure that notifications were not sent
	requireNotNotified(t, agentNotifier.Ch)
	requireNotNotified(t, userNotifier.Ch)
	requireNotNotified(t, replicationNotifier.Ch)
	requireNotNotified(t, agentRecoveryNotifier.Ch)
	requireNotNotified(t, registrationNotifier.Ch)
	requireNotNotified(t, dnsNotifier.Ch)
}
