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

package resource_test

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"google.golang.org/protobuf/proto"

	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/agent/grpc-external/testutils"
	"github.com/hashicorp/consul/internal/resource"
	"github.com/hashicorp/consul/internal/resource/demo"
	"github.com/hashicorp/consul/proto-public/pbresource"
	pbdemov1 "github.com/hashicorp/consul/proto/private/pbdemo/v1"
	demov2 "github.com/hashicorp/consul/proto/private/pbdemo/v2"
)

func TestRegister(t *testing.T) {
	r := resource.NewRegistry()

	// register success
	reg := resource.Registration{
		Type:  demo.TypeV2Artist,
		Proto: &demov2.Artist{},
		Scope: resource.ScopeNamespace,
	}
	r.Register(reg)
	actual, ok := r.Resolve(demo.TypeV2Artist)
	require.True(t, ok)
	require.True(t, proto.Equal(demo.TypeV2Artist, actual.Type))

	// register existing should panic
	require.PanicsWithValue(t, "resource type demo.v2.Artist already registered", func() {
		r.Register(reg)
	})

	// register success when scope undefined and type exempt from scope
	// skip: can't test this because tombstone type is registered as part of NewRegistry()
}

func TestRegister_Defaults(t *testing.T) {
	r := resource.NewRegistry()
	r.Register(resource.Registration{
		Type:  demo.TypeV2Artist,
		Proto: &demov2.Artist{},
		Scope: resource.ScopeNamespace,
	})
	artist, err := demo.GenerateV2Artist()
	require.NoError(t, err)

	reg, ok := r.Resolve(demo.TypeV2Artist)
	require.True(t, ok)

	// verify default read hook requires operator:read
	require.NoError(t, reg.ACLs.Read(testutils.ACLOperatorRead(t), nil, artist.Id, nil))
	require.True(t, acl.IsErrPermissionDenied(reg.ACLs.Read(testutils.ACLNoPermissions(t), nil, artist.Id, nil)))

	// verify default write hook requires operator:write
	require.NoError(t, reg.ACLs.Write(testutils.ACLOperatorWrite(t), nil, artist))
	require.True(t, acl.IsErrPermissionDenied(reg.ACLs.Write(testutils.ACLNoPermissions(t), nil, artist)))

	// verify default list hook requires operator:read
	require.NoError(t, reg.ACLs.List(testutils.ACLOperatorRead(t), nil))
	require.True(t, acl.IsErrPermissionDenied(reg.ACLs.List(testutils.ACLNoPermissions(t), nil)))

	// verify default validate is a no-op
	require.NoError(t, reg.Validate(nil))

	// verify default mutate is a no-op
	require.NoError(t, reg.Mutate(nil))
}

func TestNewRegistry(t *testing.T) {
	r := resource.NewRegistry()

	// verify tombstone type registered implicitly
	_, ok := r.Resolve(resource.TypeV1Tombstone)
	require.True(t, ok)
}

func TestResolve(t *testing.T) {
	r := resource.NewRegistry()

	// not found
	_, ok := r.Resolve(demo.TypeV1Album)
	assert.False(t, ok)

	// found
	r.Register(resource.Registration{
		Type:  demo.TypeV1Album,
		Proto: &pbdemov1.Album{},
		Scope: resource.ScopeNamespace,
	})
	registration, ok := r.Resolve(demo.TypeV1Album)
	assert.True(t, ok)
	assert.Equal(t, registration.Type, demo.TypeV1Album)
}

func TestRegister_TypeValidation(t *testing.T) {
	registry := resource.NewRegistry()

	testCases := map[string]struct {
		fn    func(*pbresource.Type)
		valid bool
	}{
		"Valid": {valid: true},
		"Group empty": {
			fn:    func(t *pbresource.Type) { t.Group = "" },
			valid: false,
		},
		"Group PascalCase": {
			fn:    func(t *pbresource.Type) { t.Group = "Foo" },
			valid: false,
		},
		"Group kebab-case": {
			fn:    func(t *pbresource.Type) { t.Group = "foo-bar" },
			valid: false,
		},
		"Group snake_case": {
			fn:    func(t *pbresource.Type) { t.Group = "foo_bar" },
			valid: true,
		},
		"GroupVersion empty": {
			fn:    func(t *pbresource.Type) { t.GroupVersion = "" },
			valid: false,
		},
		"GroupVersion snake_case": {
			fn:    func(t *pbresource.Type) { t.GroupVersion = "v_1" },
			valid: false,
		},
		"GroupVersion kebab-case": {
			fn:    func(t *pbresource.Type) { t.GroupVersion = "v-1" },
			valid: false,
		},
		"GroupVersion no leading v": {
			fn:    func(t *pbresource.Type) { t.GroupVersion = "1" },
			valid: false,
		},
		"GroupVersion no trailing number": {
			fn:    func(t *pbresource.Type) { t.GroupVersion = "OnePointOh" },
			valid: false,
		},
		"Kind PascalCase with numbers": {
			fn:    func(t *pbresource.Type) { t.Kind = "Number1" },
			valid: true,
		},
		"Kind camelCase": {
			fn:    func(t *pbresource.Type) { t.Kind = "barBaz" },
			valid: false,
		},
		"Kind snake_case": {
			fn:    func(t *pbresource.Type) { t.Kind = "bar_baz" },
			valid: false,
		},
		"Kind empty": {
			fn:    func(t *pbresource.Type) { t.Kind = "" },
			valid: false,
		},
	}
	for desc, tc := range testCases {
		t.Run(desc, func(t *testing.T) {
			reg := func() {
				typ := &pbresource.Type{
					Group:        "foo",
					GroupVersion: "v1",
					Kind:         "Bar",
				}
				if tc.fn != nil {
					tc.fn(typ)
				}
				registry.Register(resource.Registration{
					Type: typ,
					// Just pass anything since proto is a required field.
					Proto: &pbdemov1.Artist{},
					// Scope is also required
					Scope: resource.ScopeNamespace,
				})
			}

			if tc.valid {
				require.NotPanics(t, reg)
			} else {
				require.Panics(t, reg)
			}
		})
	}
}
