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

package resourcetest

import (
	"testing"

	"github.com/stretchr/testify/require"
	"google.golang.org/protobuf/reflect/protoreflect"

	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/agent/structs"
	"github.com/hashicorp/consul/internal/resource"
	"github.com/hashicorp/consul/proto-public/pbresource"
)

const (
	DENY    = "deny"
	ALLOW   = "allow"
	DEFAULT = "default"
)

var checkF = func(t *testing.T, expect string, got error) {
	switch expect {
	case ALLOW:
		if acl.IsErrPermissionDenied(got) {
			t.Fatal("should be allowed")
		}
	case DENY:
		if !acl.IsErrPermissionDenied(got) {
			t.Fatal("should be denied")
		}
	case DEFAULT:
		require.Nil(t, got, "expected fallthrough decision")
	default:
		t.Fatalf("unexpected expectation: %q", expect)
	}
}

type ACLTestCase struct {
	Rules string

	// AuthCtx is optional. If not provided an empty one will be used.
	AuthCtx *acl.AuthorizerContext

	// One of either Res or Data/Owner/Typ should be set.
	Res   *pbresource.Resource
	Data  protoreflect.ProtoMessage
	Owner *pbresource.ID
	Typ   *pbresource.Type

	ReadOK  string
	WriteOK string
	ListOK  string

	ReadHookRequiresResource bool
}

func RunACLTestCase(t *testing.T, tc ACLTestCase, registry resource.Registry) {
	var (
		typ *pbresource.Type
		res *pbresource.Resource
	)
	if tc.Res != nil {
		require.Nil(t, tc.Data)
		require.Nil(t, tc.Owner)
		require.Nil(t, tc.Typ)
		typ = tc.Res.Id.GetType()
		res = tc.Res
	} else {
		require.NotNil(t, tc.Data)
		require.NotNil(t, tc.Typ)
		typ = tc.Typ

		resolvedType, ok := registry.Resolve(typ)
		require.True(t, ok)

		res = Resource(tc.Typ, "test").
			WithTenancy(DefaultTenancyForType(t, resolvedType)).
			WithOwner(tc.Owner).
			WithData(t, tc.Data).
			Build()
	}

	reg, ok := registry.Resolve(typ)
	require.True(t, ok)

	ValidateAndNormalize(t, registry, res)

	config := acl.Config{
		WildcardName: structs.WildcardSpecifier,
	}
	authz, err := acl.NewAuthorizerFromRules(tc.Rules, &config, nil)
	require.NoError(t, err)
	authz = acl.NewChainedAuthorizer([]acl.Authorizer{authz, acl.DenyAll()})

	if tc.AuthCtx == nil {
		tc.AuthCtx = &acl.AuthorizerContext{}
	}

	if tc.ReadHookRequiresResource {
		err = reg.ACLs.Read(authz, tc.AuthCtx, res.Id, nil)
		require.ErrorIs(t, err, resource.ErrNeedResource, "read hook should require the data payload")
	}

	t.Run("read", func(t *testing.T) {
		err := reg.ACLs.Read(authz, tc.AuthCtx, res.Id, res)
		checkF(t, tc.ReadOK, err)
	})
	t.Run("write", func(t *testing.T) {
		err := reg.ACLs.Write(authz, tc.AuthCtx, res)
		checkF(t, tc.WriteOK, err)
	})
	t.Run("list", func(t *testing.T) {
		err := reg.ACLs.List(authz, tc.AuthCtx)
		checkF(t, tc.ListOK, err)
	})
}
