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

package structs

import (
	"fmt"
	"strings"
	"testing"

	"github.com/hashicorp/consul/acl"

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

func TestStructs_ACLToken_PolicyIDs(t *testing.T) {

	t.Run("Basic", func(t *testing.T) {

		token := &ACLToken{
			Policies: []ACLTokenPolicyLink{
				{
					ID: "one",
				},
				{
					ID: "two",
				},
				{
					ID: "three",
				},
			},
		}

		policyIDs := token.PolicyIDs()
		require.Len(t, policyIDs, 3)
		require.Equal(t, "one", policyIDs[0])
		require.Equal(t, "two", policyIDs[1])
		require.Equal(t, "three", policyIDs[2])
	})

	t.Run("No Policies", func(t *testing.T) {

		token := &ACLToken{}

		policyIDs := token.PolicyIDs()
		require.Len(t, policyIDs, 0)
	})
}

func TestStructs_ACLServiceIdentity_SyntheticPolicy(t *testing.T) {

	cases := []struct {
		serviceName string
		datacenters []string
		expectRules string
	}{
		{"web", nil, aclServiceIdentityRules("web", nil)},
		{"companion-cube-99", []string{"dc1", "dc2"}, aclServiceIdentityRules("companion-cube-99", nil)},
	}

	for _, test := range cases {
		name := test.serviceName
		if len(test.datacenters) > 0 {
			name += " [" + strings.Join(test.datacenters, ", ") + "]"
		}
		t.Run(name, func(t *testing.T) {
			svcid := &ACLServiceIdentity{
				ServiceName: test.serviceName,
				Datacenters: test.datacenters,
			}

			expect := &ACLPolicy{
				Datacenters: test.datacenters,
				Rules:       test.expectRules,
			}

			got := svcid.SyntheticPolicy(nil)
			require.NotEmpty(t, got.ID)
			require.True(t, strings.HasPrefix(got.Name, "synthetic-policy-"))
			// strip irrelevant fields before equality
			got.ID = ""
			got.Name = ""
			got.Description = ""
			got.Hash = nil
			require.Equal(t, expect, got)
		})
	}
}

func TestStructs_ACLServiceIdentities_Deduplicate(t *testing.T) {
	identities := ACLServiceIdentities{
		{ServiceName: "web", Datacenters: []string{"dc1"}},
		{ServiceName: "web", Datacenters: []string{"dc2"}},
		{ServiceName: "db", Datacenters: []string{"dc3"}},
	}

	require.ElementsMatch(t, ACLServiceIdentities{
		{ServiceName: "web", Datacenters: []string{"dc1", "dc2"}},
		{ServiceName: "db", Datacenters: []string{"dc3"}},
	}, identities.Deduplicate())

	require.Len(t, identities, 3, "original slice shouldn't have been mutated")
}

func TestStructs_ACLNodeIdentities_Deduplicate(t *testing.T) {
	identities := ACLNodeIdentities{
		{NodeName: "web", Datacenter: "dc1"},
		{NodeName: "web", Datacenter: "dc2"},
		{NodeName: "web", Datacenter: "dc1"},
	}

	require.Equal(t, ACLNodeIdentities{
		{NodeName: "web", Datacenter: "dc1"},
		{NodeName: "web", Datacenter: "dc2"},
	}, identities.Deduplicate())

	require.Len(t, identities, 3, "original slice shouldn't have been mutated")
}

func TestStructs_ACLToken_SetHash(t *testing.T) {

	token := ACLToken{
		AccessorID:  "09d1c059-961a-46bd-a2e4-76adebe35fa5",
		SecretID:    "65e98e67-9b29-470c-8ffa-7c5a23cc67c8",
		Description: "test",
		Policies: []ACLTokenPolicyLink{
			{
				ID: "one",
			},
			{
				ID: "two",
			},
			{
				ID: "three",
			},
		},
	}

	t.Run("Nil Hash - Generate", func(t *testing.T) {
		require.Nil(t, token.Hash)
		h := token.SetHash(false)
		require.NotNil(t, h)
		require.NotEqual(t, []byte{}, h)
		require.Equal(t, h, token.Hash)
	})

	t.Run("Hash Set - Dont Generate", func(t *testing.T) {
		original := token.Hash
		h := token.SetHash(false)
		require.Equal(t, original, h)

		token.Description = "changed"
		h = token.SetHash(false)
		require.Equal(t, original, h)
	})

	t.Run("Hash Set - Generate", func(t *testing.T) {
		original := token.Hash
		h := token.SetHash(true)
		require.NotEqual(t, original, h)
	})
}

func TestStructs_ACLToken_EstimateSize(t *testing.T) {

	// estimated size here should
	token := ACLToken{
		AccessorID:  "09d1c059-961a-46bd-a2e4-76adebe35fa5",
		SecretID:    "65e98e67-9b29-470c-8ffa-7c5a23cc67c8",
		Description: "test",
		Policies: []ACLTokenPolicyLink{
			{
				ID: "one",
			},
			{
				ID: "two",
			},
			{
				ID: "three",
			},
		},
	}

	// this test is very contrived. Basically just tests that the
	// math is okay and returns the value.
	require.Equal(t, 128, token.EstimateSize())
}

func TestStructs_ACLToken_Stub(t *testing.T) {
	t.Run("Basic", func(t *testing.T) {

		token := ACLToken{
			AccessorID:  "09d1c059-961a-46bd-a2e4-76adebe35fa5",
			SecretID:    "65e98e67-9b29-470c-8ffa-7c5a23cc67c8",
			Description: "test",
			Policies: []ACLTokenPolicyLink{
				{
					ID: "one",
				},
				{
					ID: "two",
				},
				{
					ID: "three",
				},
			},
		}

		stub := token.Stub()

		require.Equal(t, token.AccessorID, stub.AccessorID)
		require.Equal(t, token.SecretID, stub.SecretID)
		require.Equal(t, token.Description, stub.Description)
		require.Equal(t, token.Policies, stub.Policies)
		require.Equal(t, token.Local, stub.Local)
		require.Equal(t, token.CreateTime, stub.CreateTime)
		require.Equal(t, token.Hash, stub.Hash)
		require.Equal(t, token.CreateIndex, stub.CreateIndex)
		require.Equal(t, token.ModifyIndex, stub.ModifyIndex)
	})
}

func TestStructs_ACLTokens_Sort(t *testing.T) {

	tokens := ACLTokens{
		&ACLToken{
			AccessorID: "9db509a9-c809-48c1-895d-99f845b7a9d5",
		},
		&ACLToken{
			AccessorID: "6bd01084-1695-43b8-898d-b2dd7874754d",
		},
		&ACLToken{
			AccessorID: "614a4cef-9149-4271-b878-7edb1ad661f8",
		},
		&ACLToken{
			AccessorID: "c9dd9980-8d54-472f-9e5e-74c02143e1f4",
		},
	}

	tokens.Sort()
	require.Equal(t, tokens[0].AccessorID, "614a4cef-9149-4271-b878-7edb1ad661f8")
	require.Equal(t, tokens[1].AccessorID, "6bd01084-1695-43b8-898d-b2dd7874754d")
	require.Equal(t, tokens[2].AccessorID, "9db509a9-c809-48c1-895d-99f845b7a9d5")
	require.Equal(t, tokens[3].AccessorID, "c9dd9980-8d54-472f-9e5e-74c02143e1f4")
}

func TestStructs_ACLTokenListStubs_Sort(t *testing.T) {

	tokens := ACLTokenListStubs{
		&ACLTokenListStub{
			AccessorID: "9db509a9-c809-48c1-895d-99f845b7a9d5",
		},
		&ACLTokenListStub{
			AccessorID: "6bd01084-1695-43b8-898d-b2dd7874754d",
		},
		&ACLTokenListStub{
			AccessorID: "614a4cef-9149-4271-b878-7edb1ad661f8",
		},
		&ACLTokenListStub{
			AccessorID: "c9dd9980-8d54-472f-9e5e-74c02143e1f4",
		},
	}

	tokens.Sort()
	require.Equal(t, tokens[0].AccessorID, "614a4cef-9149-4271-b878-7edb1ad661f8")
	require.Equal(t, tokens[1].AccessorID, "6bd01084-1695-43b8-898d-b2dd7874754d")
	require.Equal(t, tokens[2].AccessorID, "9db509a9-c809-48c1-895d-99f845b7a9d5")
	require.Equal(t, tokens[3].AccessorID, "c9dd9980-8d54-472f-9e5e-74c02143e1f4")
}

func TestStructs_ACLPolicy_Stub(t *testing.T) {

	policy := &ACLPolicy{
		ID:          "09d1c059-961a-46bd-a2e4-76adebe35fa5",
		Name:        "test",
		Description: "test",
		Rules:       `acl = "read"`,
	}

	stub := policy.Stub()

	require.Equal(t, policy.ID, stub.ID)
	require.Equal(t, policy.Name, stub.Name)
	require.Equal(t, policy.Description, stub.Description)
	require.Equal(t, policy.Datacenters, stub.Datacenters)
	require.Equal(t, policy.Hash, stub.Hash)
	require.Equal(t, policy.CreateIndex, stub.CreateIndex)
	require.Equal(t, policy.ModifyIndex, stub.ModifyIndex)
}

func TestStructs_ACLPolicy_SetHash(t *testing.T) {

	policy := &ACLPolicy{
		ID:          "09d1c059-961a-46bd-a2e4-76adebe35fa5",
		Name:        "test",
		Description: "test",
		Rules:       `acl = "read"`,
	}

	t.Run("Nil Hash - Generate", func(t *testing.T) {
		require.Nil(t, policy.Hash)
		h := policy.SetHash(false)
		require.NotNil(t, h)
		require.NotEqual(t, []byte{}, h)
		require.Equal(t, h, policy.Hash)
	})

	t.Run("Hash Set - Dont Generate", func(t *testing.T) {
		original := policy.Hash
		h := policy.SetHash(false)
		require.Equal(t, original, h)

		policy.Description = "changed"
		h = policy.SetHash(false)
		require.Equal(t, original, h)
	})

	t.Run("Hash Set - Generate", func(t *testing.T) {
		original := policy.Hash
		h := policy.SetHash(true)
		require.NotEqual(t, original, h)
	})
}

func TestStructs_ACLPolicy_EstimateSize(t *testing.T) {

	policy := ACLPolicy{
		ID:          "09d1c059-961a-46bd-a2e4-76adebe35fa5",
		Name:        "test",
		Description: "test",
		Rules:       `acl = "read"`,
	}

	// this test is very contrived. Basically just tests that the
	// math is okay and returns the value.
	require.Equal(t, 84, policy.EstimateSize())
	policy.Datacenters = []string{"dc1", "dc2"}
	require.Equal(t, 90, policy.EstimateSize())
}

func TestStructs_ACLPolicies_Sort(t *testing.T) {

	policies := ACLPolicies{
		&ACLPolicy{
			ID: "9db509a9-c809-48c1-895d-99f845b7a9d5",
		},
		&ACLPolicy{
			ID: "6bd01084-1695-43b8-898d-b2dd7874754d",
		},
		&ACLPolicy{
			ID: "614a4cef-9149-4271-b878-7edb1ad661f8",
		},
		&ACLPolicy{
			ID: "c9dd9980-8d54-472f-9e5e-74c02143e1f4",
		},
	}

	policies.Sort()
	require.Equal(t, policies[0].ID, "614a4cef-9149-4271-b878-7edb1ad661f8")
	require.Equal(t, policies[1].ID, "6bd01084-1695-43b8-898d-b2dd7874754d")
	require.Equal(t, policies[2].ID, "9db509a9-c809-48c1-895d-99f845b7a9d5")
	require.Equal(t, policies[3].ID, "c9dd9980-8d54-472f-9e5e-74c02143e1f4")
}

func TestStructs_ACLPolicyListStubs_Sort(t *testing.T) {

	policies := ACLPolicyListStubs{
		&ACLPolicyListStub{
			ID: "9db509a9-c809-48c1-895d-99f845b7a9d5",
		},
		&ACLPolicyListStub{
			ID: "6bd01084-1695-43b8-898d-b2dd7874754d",
		},
		&ACLPolicyListStub{
			ID: "614a4cef-9149-4271-b878-7edb1ad661f8",
		},
		&ACLPolicyListStub{
			ID: "c9dd9980-8d54-472f-9e5e-74c02143e1f4",
		},
	}

	policies.Sort()
	require.Equal(t, policies[0].ID, "614a4cef-9149-4271-b878-7edb1ad661f8")
	require.Equal(t, policies[1].ID, "6bd01084-1695-43b8-898d-b2dd7874754d")
	require.Equal(t, policies[2].ID, "9db509a9-c809-48c1-895d-99f845b7a9d5")
	require.Equal(t, policies[3].ID, "c9dd9980-8d54-472f-9e5e-74c02143e1f4")
}

func TestStructs_ACLPolicies_resolveWithCache(t *testing.T) {

	config := ACLCachesConfig{
		Identities:     0,
		Policies:       0,
		ParsedPolicies: 4,
		Authorizers:    0,
		Roles:          0,
	}
	cache, err := NewACLCaches(&config)
	require.NoError(t, err)

	testPolicies := ACLPolicies{
		&ACLPolicy{
			ID:          "5d5653a1-2c2b-4b36-b083-fc9f1398eb7b",
			Name:        "policy1",
			Description: "policy1",
			Rules:       `node_prefix "" { policy = "read", policy = "read", },`,
			RaftIndex: RaftIndex{
				CreateIndex: 1,
				ModifyIndex: 2,
			},
		},
		&ACLPolicy{
			ID:          "b35541f0-a88a-48da-bc66-43553c60b628",
			Name:        "policy2",
			Description: "policy2",
			Rules:       `agent_prefix "" { policy = "read" } `,
			RaftIndex: RaftIndex{
				CreateIndex: 3,
				ModifyIndex: 4,
			},
		},
		&ACLPolicy{
			ID:          "383abb79-94ca-46c6-89b7-8ecb69046de9",
			Name:        "policy3",
			Description: "policy3",
			Rules:       `key_prefix "" { policy = "read" }`,
			RaftIndex: RaftIndex{
				CreateIndex: 5,
				ModifyIndex: 6,
			},
		},
		&ACLPolicy{
			ID:          "8bf38965-95e5-4e86-9be7-f6070cc0708b",
			Name:        "policy4",
			Description: "policy4",
			//test should still pass even with the duplicate key since its resolving the cache
			Rules: `service_prefix "" { policy = "read"  policy = "read" }`,
			RaftIndex: RaftIndex{
				CreateIndex: 7,
				ModifyIndex: 8,
			},
		},
	}

	t.Run("Cache Misses", func(t *testing.T) {
		policies, err := testPolicies.resolveWithCache(cache, nil)
		require.NoError(t, err)
		require.Len(t, policies, 4)
		require.Len(t, policies[0].NodePrefixes, 1)
		require.Len(t, policies[1].AgentPrefixes, 1)
		require.Len(t, policies[2].KeyPrefixes, 1)
		require.Len(t, policies[3].ServicePrefixes, 1)
	})

	t.Run("Check Cache", func(t *testing.T) {
		for i := range testPolicies {
			entry := cache.GetParsedPolicy(fmt.Sprintf("%x", testPolicies[i].Hash))
			require.NotNil(t, entry)

			// set this to detect using from the cache next time
			testPolicies[i].Rules = "invalid"
		}
	})

	t.Run("Cache Hits", func(t *testing.T) {
		policies, err := testPolicies.resolveWithCache(cache, nil)
		require.NoError(t, err)
		require.Len(t, policies, 4)
		require.Len(t, policies[0].NodePrefixes, 1)
		require.Len(t, policies[1].AgentPrefixes, 1)
		require.Len(t, policies[2].KeyPrefixes, 1)
		require.Len(t, policies[3].ServicePrefixes, 1)
	})
}

func TestStructs_ACLPolicies_Compile(t *testing.T) {

	config := ACLCachesConfig{
		Identities:     0,
		Policies:       0,
		ParsedPolicies: 4,
		Authorizers:    2,
		Roles:          0,
	}
	cache, err := NewACLCaches(&config)
	require.NoError(t, err)

	testPolicies := ACLPolicies{
		&ACLPolicy{
			ID:          "5d5653a1-2c2b-4b36-b083-fc9f1398eb7b",
			Name:        "policy1",
			Description: "policy1",
			Rules:       `node_prefix "" { policy = "read" }`,
			RaftIndex: RaftIndex{
				CreateIndex: 1,
				ModifyIndex: 2,
			},
		},
		&ACLPolicy{
			ID:          "b35541f0-a88a-48da-bc66-43553c60b628",
			Name:        "policy2",
			Description: "policy2",
			Rules:       `agent_prefix "" { policy = "read" }`,
			RaftIndex: RaftIndex{
				CreateIndex: 3,
				ModifyIndex: 4,
			},
		},
		&ACLPolicy{
			ID:          "383abb79-94ca-46c6-89b7-8ecb69046de9",
			Name:        "policy3",
			Description: "policy3",
			Rules:       `key_prefix "" { policy = "read" }`,
			RaftIndex: RaftIndex{
				CreateIndex: 5,
				ModifyIndex: 6,
			},
		},
		&ACLPolicy{
			ID:          "8bf38965-95e5-4e86-9be7-f6070cc0708b",
			Name:        "policy4",
			Description: "policy4",
			Rules:       `service_prefix "" { policy = "read" }`,
			RaftIndex: RaftIndex{
				CreateIndex: 7,
				ModifyIndex: 8,
			},
		},
	}

	t.Run("Cache Miss", func(t *testing.T) {
		authz, err := testPolicies.Compile(cache, nil)
		require.NoError(t, err)
		require.NotNil(t, authz)

		require.Equal(t, acl.Allow, authz.NodeRead("foo", nil))
		require.Equal(t, acl.Allow, authz.AgentRead("foo", nil))
		require.Equal(t, acl.Allow, authz.KeyRead("foo", nil))
		require.Equal(t, acl.Allow, authz.ServiceRead("foo", nil))
		require.Equal(t, acl.Default, authz.ACLRead(nil))
	})

	t.Run("Check Cache", func(t *testing.T) {
		entry := cache.GetAuthorizer(testPolicies.HashKey())
		require.NotNil(t, entry)
		authz := entry.Authorizer
		require.NotNil(t, authz)

		require.Equal(t, acl.Allow, authz.NodeRead("foo", nil))
		require.Equal(t, acl.Allow, authz.AgentRead("foo", nil))
		require.Equal(t, acl.Allow, authz.KeyRead("foo", nil))
		require.Equal(t, acl.Allow, authz.ServiceRead("foo", nil))
		require.Equal(t, acl.Default, authz.ACLRead(nil))

		// setup the cache for the next test
		cache.PutAuthorizer(testPolicies.HashKey(), acl.DenyAll())
	})

	t.Run("Cache Hit", func(t *testing.T) {
		authz, err := testPolicies.Compile(cache, nil)
		require.NoError(t, err)
		require.NotNil(t, authz)

		// we reset the Authorizer in the cache so now everything should be defaulted
		require.Equal(t, acl.Deny, authz.NodeRead("foo", nil))
		require.Equal(t, acl.Deny, authz.AgentRead("foo", nil))
		require.Equal(t, acl.Deny, authz.KeyRead("foo", nil))
		require.Equal(t, acl.Deny, authz.ServiceRead("foo", nil))
		require.Equal(t, acl.Deny, authz.ACLRead(nil))
	})
}
