package tree

import (
	"context"
	"encoding/hex"
	"fmt"
	"testing"

	"git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client"
	core "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
	frostfsidcore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/frostfsid"
	checkercore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/common/ape"
	containerSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory"
	nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/stretchr/testify/require"
)

var (
	containerID = "73tQMTYyUkTgmvPR1HWib6pndbhSoBovbnMF7Pws8Rcy"

	senderPrivateKey, _ = keys.NewPrivateKey()

	senderKey = hex.EncodeToString(senderPrivateKey.PublicKey().Bytes())

	rootCnr = &core.Container{Value: containerSDK.Container{}}
)

type frostfsIDProviderMock struct {
	subjects         map[util.Uint160]*client.Subject
	subjectsExtended map[util.Uint160]*client.SubjectExtended
}

func (f *frostfsIDProviderMock) GetSubject(key util.Uint160) (*client.Subject, error) {
	v, ok := f.subjects[key]
	if !ok {
		return nil, fmt.Errorf("%s", frostfsidcore.SubjectNotFoundErrorMessage)
	}
	return v, nil
}

func (f *frostfsIDProviderMock) GetSubjectExtended(key util.Uint160) (*client.SubjectExtended, error) {
	v, ok := f.subjectsExtended[key]
	if !ok {
		return nil, fmt.Errorf("%s", frostfsidcore.SubjectNotFoundErrorMessage)
	}
	return v, nil
}

var _ frostfsidcore.SubjectProvider = (*frostfsIDProviderMock)(nil)

func newFrostfsIDProviderMock(t *testing.T) *frostfsIDProviderMock {
	return &frostfsIDProviderMock{
		subjects: map[util.Uint160]*client.Subject{
			scriptHashFromSenderKey(t, senderKey): {
				Namespace: "testnamespace",
				Name:      "test",
				KV: map[string]string{
					"tag-attr1": "value1",
					"tag-attr2": "value2",
				},
			},
		},
		subjectsExtended: map[util.Uint160]*client.SubjectExtended{
			scriptHashFromSenderKey(t, senderKey): {
				Namespace: "testnamespace",
				Name:      "test",
				KV: map[string]string{
					"tag-attr1": "value1",
					"tag-attr2": "value2",
				},
				Groups: []*client.Group{
					{
						ID:        1,
						Name:      "test",
						Namespace: "testnamespace",
						KV: map[string]string{
							"attr1": "value1",
							"attr2": "value2",
						},
					},
				},
			},
		},
	}
}

func scriptHashFromSenderKey(t *testing.T, senderKey string) util.Uint160 {
	pk, err := keys.NewPublicKeyFromString(senderKey)
	require.NoError(t, err)
	return pk.GetScriptHash()
}

type stMock struct{}

func (m *stMock) CurrentEpoch() uint64 {
	return 8
}

func TestCheckAPE(t *testing.T) {
	cid := cid.ID{}
	_ = cid.DecodeString(containerID)

	t.Run("put non-tombstone rule won't affect tree remove", func(t *testing.T) {
		los := inmemory.NewInmemoryLocalStorage()
		mcs := inmemory.NewInmemoryMorphRuleChainStorage()
		fid := newFrostfsIDProviderMock(t)
		s := Service{
			cfg: cfg{
				frostfsidSubjectProvider: fid,
			},
			apeChecker: checkercore.New(los, mcs, fid, &stMock{}),
		}

		los.AddOverride(chain.Ingress, engine.ContainerTarget(containerID), &chain.Chain{
			Rules: []chain.Rule{
				{
					Status:  chain.AccessDenied,
					Actions: chain.Actions{Names: []string{nativeschema.MethodPutObject}},
					Resources: chain.Resources{
						Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, containerID)},
					},
					Condition: []chain.Condition{
						{
							Op:    chain.CondStringNotEquals,
							Kind:  chain.KindResource,
							Key:   nativeschema.PropertyKeyObjectType,
							Value: "TOMBSTONE",
						},
					},
				},
			},
			MatchType: chain.MatchTypeFirstMatch,
		})

		mcs.AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(containerID), &chain.Chain{
			Rules: []chain.Rule{
				{
					Status:  chain.Allow,
					Actions: chain.Actions{Names: []string{nativeschema.MethodDeleteObject}},
					Resources: chain.Resources{
						Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, containerID)},
					},
				},
			},
			MatchType: chain.MatchTypeFirstMatch,
		})

		err := s.checkAPE(context.Background(), nil, rootCnr, cid, acl.OpObjectDelete, acl.RoleOwner, senderPrivateKey.PublicKey())
		require.NoError(t, err)
	})

	t.Run("delete rule won't affect tree add", func(t *testing.T) {
		los := inmemory.NewInmemoryLocalStorage()
		mcs := inmemory.NewInmemoryMorphRuleChainStorage()
		fid := newFrostfsIDProviderMock(t)
		s := Service{
			cfg: cfg{
				frostfsidSubjectProvider: fid,
			},
			apeChecker: checkercore.New(los, mcs, fid, &stMock{}),
		}

		los.AddOverride(chain.Ingress, engine.ContainerTarget(containerID), &chain.Chain{
			Rules: []chain.Rule{
				{
					Status:  chain.AccessDenied,
					Actions: chain.Actions{Names: []string{nativeschema.MethodDeleteObject}},
					Resources: chain.Resources{
						Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, containerID)},
					},
				},
			},
			MatchType: chain.MatchTypeFirstMatch,
		})

		mcs.AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(containerID), &chain.Chain{
			Rules: []chain.Rule{
				{
					Status:  chain.Allow,
					Actions: chain.Actions{Names: []string{nativeschema.MethodPutObject}},
					Resources: chain.Resources{
						Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, containerID)},
					},
					Condition: []chain.Condition{
						{
							Op:    chain.CondStringNotEquals,
							Kind:  chain.KindResource,
							Key:   nativeschema.PropertyKeyObjectType,
							Value: "TOMBSTONE",
						},
					},
				},
			},
			MatchType: chain.MatchTypeFirstMatch,
		})

		err := s.checkAPE(context.Background(), nil, rootCnr, cid, acl.OpObjectPut, acl.RoleOwner, senderPrivateKey.PublicKey())
		require.NoError(t, err)
	})
}