package container

import (
	"context"
	"crypto/ecdsa"
	"encoding/hex"
	"errors"
	"fmt"
	"testing"

	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
	session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
	"git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client"
	containercore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
	apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
	cnrSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
	containertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/test"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
	sessiontest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session/test"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
	"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"
)

const (
	testDomainName = "testdomainname"
	testDomainZone = "testdomainname.ns"
)

func TestAPE(t *testing.T) {
	t.Parallel()
	t.Run("allow then deny get container", testAllowThenDenyGetContainerRuleDefined)
	t.Run("deny get container no rule found", testDenyGetContainerNoRuleFound)
	t.Run("deny get container for others", testDenyGetContainerForOthers)
	t.Run("deny set container eACL for IR", testDenySetContainerEACLForIR)
	t.Run("deny get container eACL for IR with session token", testDenyGetContainerEACLForIRSessionToken)
	t.Run("deny put container for others with session token", testDenyPutContainerForOthersSessionToken)
	t.Run("deny put container, read namespace from frostfsID", testDenyPutContainerReadNamespaceFromFrostfsID)
	t.Run("deny put container with invlaid namespace", testDenyPutContainerInvalidNamespace)
	t.Run("deny list containers for owner with PK", testDenyListContainersForPK)
	t.Run("deny list containers by namespace invalidation", testDenyListContainersValidationNamespaceError)
}

func testAllowThenDenyGetContainerRuleDefined(t *testing.T) {
	t.Parallel()
	srv := &srvStub{
		calls: map[string]int{},
	}
	router := inmemory.NewInMemory()
	contRdr := &containerStub{
		c: map[cid.ID]*containercore.Container{},
	}
	ir := &irStub{
		keys: [][]byte{},
	}
	nm := &netmapStub{}
	frostfsIDSubjectReader := &frostfsidStub{
		subjects: map[util.Uint160]*client.Subject{},
	}
	apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv)

	contID := cidtest.ID()
	testContainer := containertest.Container()
	pp := netmap.PlacementPolicy{}
	require.NoError(t, pp.DecodeString("REP 1"))
	testContainer.SetPlacementPolicy(pp)
	contRdr.c[contID] = &containercore.Container{Value: testContainer}

	nm.currentEpoch = 100
	nm.netmaps = map[uint64]*netmap.NetMap{}
	var testNetmap netmap.NetMap
	testNetmap.SetEpoch(nm.currentEpoch)
	testNetmap.SetNodes([]netmap.NodeInfo{{}})
	nm.netmaps[nm.currentEpoch] = &testNetmap
	nm.netmaps[nm.currentEpoch-1] = &testNetmap

	addDefaultAllowGetPolicy(t, router, contID)

	req := &container.GetRequest{}
	req.SetBody(&container.GetRequestBody{})
	var refContID refs.ContainerID
	contID.WriteToV2(&refContID)
	req.GetBody().SetContainerID(&refContID)

	pk, err := keys.NewPrivateKey()
	require.NoError(t, err)
	require.NoError(t, signature.SignServiceMessage(&pk.PrivateKey, req))

	_, err = apeSrv.Get(context.Background(), req)
	require.NoError(t, err)

	_, _, err = router.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(contID.EncodeToString()), &chain.Chain{
		Rules: []chain.Rule{
			{
				Status: chain.AccessDenied,
				Actions: chain.Actions{
					Names: []string{
						nativeschema.MethodGetContainer,
					},
				},
				Resources: chain.Resources{
					Names: []string{
						fmt.Sprintf(nativeschema.ResourceFormatRootContainer, contID.EncodeToString()),
					},
				},
			},
		},
	})
	require.NoError(t, err)

	resp, err := apeSrv.Get(context.Background(), req)
	require.Nil(t, resp)
	var errAccessDenied *apistatus.ObjectAccessDenied
	require.ErrorAs(t, err, &errAccessDenied)
	require.Contains(t, errAccessDenied.Reason(), chain.AccessDenied.String())
}

func testDenyGetContainerNoRuleFound(t *testing.T) {
	t.Parallel()
	srv := &srvStub{
		calls: map[string]int{},
	}
	router := inmemory.NewInMemory()
	contRdr := &containerStub{
		c: map[cid.ID]*containercore.Container{},
	}
	ir := &irStub{
		keys: [][]byte{},
	}
	nm := &netmapStub{}
	frostfsIDSubjectReader := &frostfsidStub{
		subjects: map[util.Uint160]*client.Subject{},
	}
	apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv)

	contID := cidtest.ID()
	testContainer := containertest.Container()
	pp := netmap.PlacementPolicy{}
	require.NoError(t, pp.DecodeString("REP 1"))
	testContainer.SetPlacementPolicy(pp)
	contRdr.c[contID] = &containercore.Container{Value: testContainer}

	nm.currentEpoch = 100
	nm.netmaps = map[uint64]*netmap.NetMap{}
	var testNetmap netmap.NetMap
	testNetmap.SetEpoch(nm.currentEpoch)
	testNetmap.SetNodes([]netmap.NodeInfo{{}})
	nm.netmaps[nm.currentEpoch] = &testNetmap
	nm.netmaps[nm.currentEpoch-1] = &testNetmap

	req := &container.GetRequest{}
	req.SetBody(&container.GetRequestBody{})
	var refContID refs.ContainerID
	contID.WriteToV2(&refContID)
	req.GetBody().SetContainerID(&refContID)

	pk, err := keys.NewPrivateKey()
	require.NoError(t, err)
	require.NoError(t, signature.SignServiceMessage(&pk.PrivateKey, req))

	resp, err := apeSrv.Get(context.Background(), req)
	require.Nil(t, resp)
	var errAccessDenied *apistatus.ObjectAccessDenied
	require.ErrorAs(t, err, &errAccessDenied)
	require.Contains(t, errAccessDenied.Reason(), chain.NoRuleFound.String())
}

func testDenyGetContainerForOthers(t *testing.T) {
	t.Parallel()
	srv := &srvStub{
		calls: map[string]int{},
	}
	router := inmemory.NewInMemory()
	contRdr := &containerStub{
		c: map[cid.ID]*containercore.Container{},
	}
	ir := &irStub{
		keys: [][]byte{},
	}
	nm := &netmapStub{}
	frostfsIDSubjectReader := &frostfsidStub{
		subjects: map[util.Uint160]*client.Subject{},
	}
	apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv)

	contID := cidtest.ID()
	testContainer := containertest.Container()
	pp := netmap.PlacementPolicy{}
	require.NoError(t, pp.DecodeString("REP 1"))
	testContainer.SetPlacementPolicy(pp)
	contRdr.c[contID] = &containercore.Container{Value: testContainer}

	nm.currentEpoch = 100
	nm.netmaps = map[uint64]*netmap.NetMap{}
	var testNetmap netmap.NetMap
	testNetmap.SetEpoch(nm.currentEpoch)
	testNetmap.SetNodes([]netmap.NodeInfo{{}})
	nm.netmaps[nm.currentEpoch] = &testNetmap
	nm.netmaps[nm.currentEpoch-1] = &testNetmap

	_, _, err := router.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(contID.EncodeToString()), &chain.Chain{
		Rules: []chain.Rule{
			{
				Status: chain.AccessDenied,
				Actions: chain.Actions{
					Names: []string{
						nativeschema.MethodGetContainer,
					},
				},
				Resources: chain.Resources{
					Names: []string{
						fmt.Sprintf(nativeschema.ResourceFormatRootContainer, contID.EncodeToString()),
					},
				},
				Condition: []chain.Condition{
					{
						Object: chain.ObjectRequest,
						Key:    nativeschema.PropertyKeyActorRole,
						Value:  nativeschema.PropertyValueContainerRoleOthers,
						Op:     chain.CondStringEquals,
					},
				},
			},
		},
	})
	require.NoError(t, err)

	req := &container.GetRequest{}
	req.SetBody(&container.GetRequestBody{})
	var refContID refs.ContainerID
	contID.WriteToV2(&refContID)
	req.GetBody().SetContainerID(&refContID)

	pk, err := keys.NewPrivateKey()
	require.NoError(t, err)
	require.NoError(t, signature.SignServiceMessage(&pk.PrivateKey, req))

	resp, err := apeSrv.Get(context.Background(), req)
	require.Nil(t, resp)
	var errAccessDenied *apistatus.ObjectAccessDenied
	require.ErrorAs(t, err, &errAccessDenied)
}

func testDenySetContainerEACLForIR(t *testing.T) {
	t.Parallel()
	srv := &srvStub{
		calls: map[string]int{},
	}
	router := inmemory.NewInMemory()
	contRdr := &containerStub{
		c: map[cid.ID]*containercore.Container{},
	}
	ir := &irStub{
		keys: [][]byte{},
	}
	nm := &netmapStub{}
	frostfsIDSubjectReader := &frostfsidStub{
		subjects: map[util.Uint160]*client.Subject{},
	}
	apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv)

	contID := cidtest.ID()
	testContainer := containertest.Container()
	pp := netmap.PlacementPolicy{}
	require.NoError(t, pp.DecodeString("REP 1"))
	testContainer.SetPlacementPolicy(pp)
	contRdr.c[contID] = &containercore.Container{Value: testContainer}

	nm.currentEpoch = 100
	nm.netmaps = map[uint64]*netmap.NetMap{}
	var testNetmap netmap.NetMap
	testNetmap.SetEpoch(nm.currentEpoch)
	testNetmap.SetNodes([]netmap.NodeInfo{{}})
	nm.netmaps[nm.currentEpoch] = &testNetmap
	nm.netmaps[nm.currentEpoch-1] = &testNetmap

	_, _, err := router.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(contID.EncodeToString()), &chain.Chain{
		Rules: []chain.Rule{
			{
				Status: chain.AccessDenied,
				Actions: chain.Actions{
					Names: []string{
						nativeschema.MethodSetContainerEACL,
					},
				},
				Resources: chain.Resources{
					Names: []string{
						fmt.Sprintf(nativeschema.ResourceFormatRootContainer, contID.EncodeToString()),
					},
				},
				Condition: []chain.Condition{
					{
						Object: chain.ObjectRequest,
						Key:    nativeschema.PropertyKeyActorRole,
						Value:  nativeschema.PropertyValueContainerRoleIR,
						Op:     chain.CondStringEquals,
					},
				},
			},
		},
	})
	require.NoError(t, err)

	req := &container.SetExtendedACLRequest{}
	req.SetBody(&container.SetExtendedACLRequestBody{})
	var refContID refs.ContainerID
	contID.WriteToV2(&refContID)
	req.GetBody().SetEACL(&acl.Table{})
	req.GetBody().GetEACL().SetContainerID(&refContID)

	pk, err := keys.NewPrivateKey()
	require.NoError(t, err)
	require.NoError(t, signature.SignServiceMessage(&pk.PrivateKey, req))
	ir.keys = append(ir.keys, pk.PublicKey().Bytes())

	resp, err := apeSrv.SetExtendedACL(context.Background(), req)
	require.Nil(t, resp)
	var errAccessDenied *apistatus.ObjectAccessDenied
	require.ErrorAs(t, err, &errAccessDenied)
}

func testDenyGetContainerEACLForIRSessionToken(t *testing.T) {
	t.Parallel()
	srv := &srvStub{
		calls: map[string]int{},
	}
	router := inmemory.NewInMemory()
	contRdr := &containerStub{
		c: map[cid.ID]*containercore.Container{},
	}
	ir := &irStub{
		keys: [][]byte{},
	}
	nm := &netmapStub{}
	frostfsIDSubjectReader := &frostfsidStub{
		subjects: map[util.Uint160]*client.Subject{},
	}
	apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv)

	contID := cidtest.ID()
	testContainer := containertest.Container()
	pp := netmap.PlacementPolicy{}
	require.NoError(t, pp.DecodeString("REP 1"))
	testContainer.SetPlacementPolicy(pp)
	contRdr.c[contID] = &containercore.Container{Value: testContainer}

	nm.currentEpoch = 100
	nm.netmaps = map[uint64]*netmap.NetMap{}
	var testNetmap netmap.NetMap
	testNetmap.SetEpoch(nm.currentEpoch)
	testNetmap.SetNodes([]netmap.NodeInfo{{}})
	nm.netmaps[nm.currentEpoch] = &testNetmap
	nm.netmaps[nm.currentEpoch-1] = &testNetmap

	_, _, err := router.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(contID.EncodeToString()), &chain.Chain{
		Rules: []chain.Rule{
			{
				Status: chain.AccessDenied,
				Actions: chain.Actions{
					Names: []string{
						nativeschema.MethodGetContainerEACL,
					},
				},
				Resources: chain.Resources{
					Names: []string{
						fmt.Sprintf(nativeschema.ResourceFormatRootContainer, contID.EncodeToString()),
					},
				},
				Condition: []chain.Condition{
					{
						Object: chain.ObjectRequest,
						Key:    nativeschema.PropertyKeyActorRole,
						Value:  nativeschema.PropertyValueContainerRoleIR,
						Op:     chain.CondStringEquals,
					},
				},
			},
		},
	})
	require.NoError(t, err)

	req := &container.GetExtendedACLRequest{}
	req.SetBody(&container.GetExtendedACLRequestBody{})
	var refContID refs.ContainerID
	contID.WriteToV2(&refContID)
	req.GetBody().SetContainerID(&refContID)

	pk, err := keys.NewPrivateKey()
	require.NoError(t, err)
	require.NoError(t, signature.SignServiceMessage(&pk.PrivateKey, req))

	sessionPK, err := keys.NewPrivateKey()
	require.NoError(t, err)
	sToken := sessiontest.ContainerSigned()
	sToken.ApplyOnlyTo(contID)
	require.NoError(t, sToken.Sign(sessionPK.PrivateKey))
	var sTokenV2 session.Token
	sToken.WriteToV2(&sTokenV2)
	metaHeader := new(session.RequestMetaHeader)
	metaHeader.SetSessionToken(&sTokenV2)
	req.SetMetaHeader(metaHeader)

	ir.keys = append(ir.keys, sessionPK.PublicKey().Bytes())

	resp, err := apeSrv.GetExtendedACL(context.Background(), req)
	require.Nil(t, resp)
	var errAccessDenied *apistatus.ObjectAccessDenied
	require.ErrorAs(t, err, &errAccessDenied)
}

func testDenyPutContainerForOthersSessionToken(t *testing.T) {
	t.Parallel()
	srv := &srvStub{
		calls: map[string]int{},
	}
	router := inmemory.NewInMemory()
	contRdr := &containerStub{
		c: map[cid.ID]*containercore.Container{},
	}
	ir := &irStub{
		keys: [][]byte{},
	}
	nm := &netmapStub{}
	frostfsIDSubjectReader := &frostfsidStub{
		subjects: map[util.Uint160]*client.Subject{},
	}
	apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv)

	testContainer := containertest.Container()

	nm.currentEpoch = 100
	nm.netmaps = map[uint64]*netmap.NetMap{}

	_, _, err := router.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(""), &chain.Chain{
		Rules: []chain.Rule{
			{
				Status: chain.AccessDenied,
				Actions: chain.Actions{
					Names: []string{
						nativeschema.MethodPutContainer,
					},
				},
				Resources: chain.Resources{
					Names: []string{
						nativeschema.ResourceFormatRootContainers,
					},
				},
				Condition: []chain.Condition{
					{
						Object: chain.ObjectRequest,
						Key:    nativeschema.PropertyKeyActorRole,
						Value:  nativeschema.PropertyValueContainerRoleOthers,
						Op:     chain.CondStringEquals,
					},
				},
			},
		},
	})
	require.NoError(t, err)

	req := initPutRequest(t, testContainer)

	resp, err := apeSrv.Put(context.Background(), req)
	require.Nil(t, resp)
	var errAccessDenied *apistatus.ObjectAccessDenied
	require.ErrorAs(t, err, &errAccessDenied)
}

func testDenyPutContainerReadNamespaceFromFrostfsID(t *testing.T) {
	t.Parallel()
	srv := &srvStub{
		calls: map[string]int{},
	}
	router := inmemory.NewInMemory()
	contRdr := &containerStub{
		c: map[cid.ID]*containercore.Container{},
	}
	ir := &irStub{
		keys: [][]byte{},
	}
	nm := &netmapStub{}

	cnrID, testContainer := initTestContainer(t, true)
	contRdr.c[cnrID] = &containercore.Container{Value: testContainer}

	nm.currentEpoch = 100
	nm.netmaps = map[uint64]*netmap.NetMap{}

	_, _, err := router.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(testDomainName), &chain.Chain{
		Rules: []chain.Rule{
			{
				Status: chain.AccessDenied,
				Actions: chain.Actions{
					Names: []string{
						nativeschema.MethodPutContainer,
					},
				},
				Resources: chain.Resources{
					Names: []string{
						fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainers, testDomainName),
					},
				},
				Condition: []chain.Condition{
					{
						Object: chain.ObjectRequest,
						Key:    nativeschema.PropertyKeyActorRole,
						Value:  nativeschema.PropertyValueContainerRoleOthers,
						Op:     chain.CondStringEquals,
					},
				},
			},
		},
	})
	require.NoError(t, err)

	req := initPutRequest(t, testContainer)
	ownerScriptHash := initOwnerIDScriptHash(t, testContainer)

	frostfsIDSubjectReader := &frostfsidStub{
		subjects: map[util.Uint160]*client.Subject{
			ownerScriptHash: {
				Namespace: testDomainName,
				Name:      testDomainName,
			},
		},
	}
	apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv)
	resp, err := apeSrv.Put(context.Background(), req)
	require.Nil(t, resp)
	var errAccessDenied *apistatus.ObjectAccessDenied
	require.ErrorAs(t, err, &errAccessDenied)
}

func testDenyPutContainerInvalidNamespace(t *testing.T) {
	t.Parallel()
	srv := &srvStub{
		calls: map[string]int{},
	}
	router := inmemory.NewInMemory()
	contRdr := &containerStub{
		c: map[cid.ID]*containercore.Container{},
	}
	ir := &irStub{
		keys: [][]byte{},
	}
	nm := &netmapStub{}

	cnrID, testContainer := initTestContainer(t, false)
	var domain cnrSDK.Domain
	domain.SetName("incorrect" + testDomainName)
	domain.SetZone("incorrect" + testDomainZone)
	cnrSDK.WriteDomain(&testContainer, domain)
	contRdr.c[cnrID] = &containercore.Container{Value: testContainer}

	nm.currentEpoch = 100
	nm.netmaps = map[uint64]*netmap.NetMap{}

	_, _, err := router.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(testDomainName), &chain.Chain{
		Rules: []chain.Rule{
			{
				Status: chain.AccessDenied,
				Actions: chain.Actions{
					Names: []string{
						nativeschema.MethodPutContainer,
					},
				},
				Resources: chain.Resources{
					Names: []string{
						fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainers, testDomainName),
					},
				},
				Condition: []chain.Condition{
					{
						Object: chain.ObjectRequest,
						Key:    nativeschema.PropertyKeyActorRole,
						Value:  nativeschema.PropertyValueContainerRoleOthers,
						Op:     chain.CondStringEquals,
					},
				},
			},
		},
	})
	require.NoError(t, err)

	req := initPutRequest(t, testContainer)
	ownerScriptHash := initOwnerIDScriptHash(t, testContainer)

	frostfsIDSubjectReader := &frostfsidStub{
		subjects: map[util.Uint160]*client.Subject{
			ownerScriptHash: {
				Namespace: testDomainName,
				Name:      testDomainName,
			},
		},
	}
	apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv)
	resp, err := apeSrv.Put(context.Background(), req)
	require.Nil(t, resp)
	require.ErrorContains(t, err, "invalid domain zone")
}

func testDenyListContainersForPK(t *testing.T) {
	t.Parallel()
	srv := &srvStub{
		calls: map[string]int{},
	}
	router := inmemory.NewInMemory()
	contRdr := &containerStub{
		c: map[cid.ID]*containercore.Container{},
	}
	ir := &irStub{
		keys: [][]byte{},
	}
	nm := &netmapStub{}
	frostfsIDSubjectReader := &frostfsidStub{
		subjects: map[util.Uint160]*client.Subject{},
	}
	apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv)

	nm.currentEpoch = 100
	nm.netmaps = map[uint64]*netmap.NetMap{}

	pk, err := keys.NewPrivateKey()
	require.NoError(t, err)

	_, _, err = router.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(""), &chain.Chain{
		Rules: []chain.Rule{
			{
				Status: chain.AccessDenied,
				Actions: chain.Actions{
					Names: []string{
						nativeschema.MethodListContainers,
					},
				},
				Resources: chain.Resources{
					Names: []string{
						nativeschema.ResourceFormatRootContainers,
					},
				},
				Condition: []chain.Condition{
					{
						Object: chain.ObjectRequest,
						Key:    nativeschema.PropertyKeyActorPublicKey,
						Value:  hex.EncodeToString(pk.PublicKey().Bytes()),
						Op:     chain.CondStringEquals,
					},
				},
			},
		},
	})
	require.NoError(t, err)

	var userID user.ID
	user.IDFromKey(&userID, pk.PrivateKey.PublicKey)

	req := &container.ListRequest{}
	req.SetBody(&container.ListRequestBody{})
	var ownerID refs.OwnerID
	userID.WriteToV2(&ownerID)
	req.GetBody().SetOwnerID(&ownerID)

	require.NoError(t, signature.SignServiceMessage(&pk.PrivateKey, req))

	resp, err := apeSrv.List(context.Background(), req)
	require.Nil(t, resp)
	var errAccessDenied *apistatus.ObjectAccessDenied
	require.ErrorAs(t, err, &errAccessDenied)
}

func testDenyListContainersValidationNamespaceError(t *testing.T) {
	t.Parallel()
	srv := &srvStub{
		calls: map[string]int{},
	}
	router := inmemory.NewInMemory()
	contRdr := &containerStub{
		c: map[cid.ID]*containercore.Container{},
	}
	ir := &irStub{
		keys: [][]byte{},
	}
	nm := &netmapStub{}

	actorPK, err := keys.NewPrivateKey()
	require.NoError(t, err)

	ownerPK, err := keys.NewPrivateKey()
	require.NoError(t, err)

	actorScriptHash, ownerScriptHash := initActorOwnerScriptHashes(t, actorPK, ownerPK)

	const actorDomain = "actor" + testDomainName

	frostfsIDSubjectReader := &frostfsidStub{
		subjects: map[util.Uint160]*client.Subject{
			actorScriptHash: {
				Namespace: actorDomain,
				Name:      actorDomain,
			},
			ownerScriptHash: {
				Namespace: testDomainName,
				Name:      testDomainName,
			},
		},
	}

	apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv)

	nm.currentEpoch = 100
	nm.netmaps = map[uint64]*netmap.NetMap{}

	_, _, err = router.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(""), &chain.Chain{
		Rules: []chain.Rule{
			{
				Status: chain.AccessDenied,
				Actions: chain.Actions{
					Names: []string{
						nativeschema.MethodListContainers,
					},
				},
				Resources: chain.Resources{
					Names: []string{
						nativeschema.ResourceFormatRootContainers,
					},
				},
				Condition: []chain.Condition{
					{
						Object: chain.ObjectRequest,
						Key:    nativeschema.PropertyKeyActorPublicKey,
						Value:  actorPK.PublicKey().String(),
						Op:     chain.CondStringEquals,
					},
				},
			},
		},
	})
	require.NoError(t, err)

	req := initListRequest(t, actorPK, ownerPK)

	resp, err := apeSrv.List(context.Background(), req)
	require.Nil(t, resp)
	require.ErrorContains(t, err, "actor namespace "+actorDomain+" differs")
}

type srvStub struct {
	calls map[string]int
}

func (s *srvStub) AnnounceUsedSpace(context.Context, *container.AnnounceUsedSpaceRequest) (*container.AnnounceUsedSpaceResponse, error) {
	s.calls["AnnounceUsedSpace"]++
	return &container.AnnounceUsedSpaceResponse{}, nil
}

func (s *srvStub) Delete(context.Context, *container.DeleteRequest) (*container.DeleteResponse, error) {
	s.calls["Delete"]++
	return &container.DeleteResponse{}, nil
}

func (s *srvStub) Get(context.Context, *container.GetRequest) (*container.GetResponse, error) {
	s.calls["Get"]++
	return &container.GetResponse{}, nil
}

func (s *srvStub) GetExtendedACL(context.Context, *container.GetExtendedACLRequest) (*container.GetExtendedACLResponse, error) {
	s.calls["GetExtendedACL"]++
	return &container.GetExtendedACLResponse{}, nil
}

func (s *srvStub) List(context.Context, *container.ListRequest) (*container.ListResponse, error) {
	s.calls["List"]++
	return &container.ListResponse{}, nil
}

func (s *srvStub) Put(context.Context, *container.PutRequest) (*container.PutResponse, error) {
	s.calls["Put"]++
	return &container.PutResponse{}, nil
}

func (s *srvStub) SetExtendedACL(context.Context, *container.SetExtendedACLRequest) (*container.SetExtendedACLResponse, error) {
	s.calls["SetExtendedACL"]++
	return &container.SetExtendedACLResponse{}, nil
}

type irStub struct {
	keys [][]byte
}

func (s *irStub) InnerRingKeys() ([][]byte, error) {
	return s.keys, nil
}

type containerStub struct {
	c map[cid.ID]*containercore.Container
}

func (s *containerStub) Get(id cid.ID) (*containercore.Container, error) {
	if v, ok := s.c[id]; ok {
		return v, nil
	}
	return nil, errors.New("container not found")
}

type netmapStub struct {
	netmaps      map[uint64]*netmap.NetMap
	currentEpoch uint64
}

func (s *netmapStub) GetNetMap(diff uint64) (*netmap.NetMap, error) {
	if diff >= s.currentEpoch {
		return nil, errors.New("invalid diff")
	}
	return s.GetNetMapByEpoch(s.currentEpoch - diff)
}

func (s *netmapStub) GetNetMapByEpoch(epoch uint64) (*netmap.NetMap, error) {
	if nm, found := s.netmaps[epoch]; found {
		return nm, nil
	}
	return nil, errors.New("netmap not found")
}

func (s *netmapStub) Epoch() (uint64, error) {
	return s.currentEpoch, nil
}

type frostfsidStub struct {
	subjects map[util.Uint160]*client.Subject
}

func (f *frostfsidStub) GetSubject(owner util.Uint160) (*client.Subject, error) {
	s, ok := f.subjects[owner]
	if !ok {
		return nil, errSubjectNotFound
	}
	return s, nil
}

type testAPEServer struct {
	engine engine.Engine

	containerReader *containerStub

	ir *irStub

	netmap *netmapStub

	frostfsIDSubjectReader *frostfsidStub

	apeChecker *apeChecker
}

func newTestAPEServer() testAPEServer {
	srv := &srvStub{
		calls: map[string]int{},
	}

	engine := inmemory.NewInMemory()

	containerReader := &containerStub{
		c: map[cid.ID]*containercore.Container{},
	}

	ir := &irStub{
		keys: [][]byte{},
	}

	netmap := &netmapStub{}

	frostfsIDSubjectReader := &frostfsidStub{
		subjects: map[util.Uint160]*client.Subject{},
	}

	apeChecker := &apeChecker{
		router:          engine,
		reader:          containerReader,
		ir:              ir,
		nm:              netmap,
		frostFSIDClient: frostfsIDSubjectReader,
		next:            srv,
	}

	return testAPEServer{
		engine:                 engine,
		containerReader:        containerReader,
		ir:                     ir,
		netmap:                 netmap,
		frostfsIDSubjectReader: frostfsIDSubjectReader,
		apeChecker:             apeChecker,
	}
}

func TestValidateContainerBoundedOperation(t *testing.T) {
	t.Parallel()

	t.Run("check root-defined container in root-defined container target rule", func(t *testing.T) {
		t.Parallel()

		components := newTestAPEServer()
		contID, testContainer := initTestContainer(t, false)
		components.containerReader.c[contID] = &containercore.Container{Value: testContainer}
		initTestNetmap(components.netmap)

		_, _, err := components.engine.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(contID.EncodeToString()), &chain.Chain{
			Rules: []chain.Rule{
				{
					Status: chain.AccessDenied,
					Actions: chain.Actions{
						Names: []string{
							nativeschema.MethodGetContainer,
						},
					},
					Resources: chain.Resources{
						Names: []string{
							fmt.Sprintf(nativeschema.ResourceFormatRootContainer, contID.EncodeToString()),
						},
					},
					Condition: []chain.Condition{
						{
							Object: chain.ObjectRequest,
							Key:    nativeschema.PropertyKeyActorRole,
							Value:  nativeschema.PropertyValueContainerRoleOthers,
							Op:     chain.CondStringEquals,
						},
					},
				},
			},
		})
		require.NoError(t, err)

		req := initTestGetContainerRequest(t, contID)

		err = components.apeChecker.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(), nativeschema.MethodGetContainer)
		aErr := apeErr(nativeschema.MethodGetContainer, chain.AccessDenied)
		require.ErrorContains(t, err, aErr.Error())
	})

	t.Run("check root-defined container in testdomain-defined container target rule", func(t *testing.T) {
		t.Parallel()

		components := newTestAPEServer()
		contID, testContainer := initTestContainer(t, false)
		components.containerReader.c[contID] = &containercore.Container{Value: testContainer}
		initTestNetmap(components.netmap)

		_, _, err := components.engine.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(contID.EncodeToString()), &chain.Chain{
			Rules: []chain.Rule{
				{
					Status: chain.AccessDenied,
					Actions: chain.Actions{
						Names: []string{
							nativeschema.MethodGetContainer,
						},
					},
					Resources: chain.Resources{
						Names: []string{
							fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainer, testDomainName, contID.EncodeToString()),
						},
					},
					Condition: []chain.Condition{
						{
							Object: chain.ObjectRequest,
							Key:    nativeschema.PropertyKeyActorRole,
							Value:  nativeschema.PropertyValueContainerRoleOthers,
							Op:     chain.CondStringEquals,
						},
					},
				},
			},
		})
		require.NoError(t, err)

		addDefaultAllowGetPolicy(t, components.engine, contID)

		req := initTestGetContainerRequest(t, contID)

		err = components.apeChecker.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(), nativeschema.MethodGetContainer)
		require.NoError(t, err)
	})

	t.Run("check root-defined container in testdomain namespace target rule", func(t *testing.T) {
		t.Parallel()

		components := newTestAPEServer()
		contID, testContainer := initTestContainer(t, false)
		components.containerReader.c[contID] = &containercore.Container{Value: testContainer}
		initTestNetmap(components.netmap)

		_, _, err := components.engine.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(testDomainName), &chain.Chain{
			Rules: []chain.Rule{
				{
					Status: chain.AccessDenied,
					Actions: chain.Actions{
						Names: []string{
							nativeschema.MethodGetContainer,
						},
					},
					Resources: chain.Resources{
						Names: []string{
							fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainers, testDomainName),
						},
					},
					Condition: []chain.Condition{
						{
							Object: chain.ObjectRequest,
							Key:    nativeschema.PropertyKeyActorRole,
							Value:  nativeschema.PropertyValueContainerRoleOthers,
							Op:     chain.CondStringEquals,
						},
					},
				},
			},
		})
		require.NoError(t, err)

		addDefaultAllowGetPolicy(t, components.engine, contID)

		req := initTestGetContainerRequest(t, contID)

		err = components.apeChecker.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(), nativeschema.MethodGetContainer)
		require.NoError(t, err)
	})

	t.Run("check testdomain-defined container in root-defined container target rule", func(t *testing.T) {
		t.Parallel()

		components := newTestAPEServer()
		contID, testContainer := initTestContainer(t, true)
		components.containerReader.c[contID] = &containercore.Container{Value: testContainer}
		initTestNetmap(components.netmap)

		_, _, err := components.engine.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(contID.EncodeToString()), &chain.Chain{
			Rules: []chain.Rule{
				{
					Status: chain.AccessDenied,
					Actions: chain.Actions{
						Names: []string{
							nativeschema.MethodGetContainer,
						},
					},
					Resources: chain.Resources{
						Names: []string{
							fmt.Sprintf(nativeschema.ResourceFormatRootContainer, contID.EncodeToString()),
						},
					},
					Condition: []chain.Condition{
						{
							Object: chain.ObjectRequest,
							Key:    nativeschema.PropertyKeyActorRole,
							Value:  nativeschema.PropertyValueContainerRoleOthers,
							Op:     chain.CondStringEquals,
						},
					},
				},
			},
		})
		require.NoError(t, err)

		addDefaultAllowGetPolicy(t, components.engine, contID)

		req := initTestGetContainerRequest(t, contID)

		err = components.apeChecker.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(), nativeschema.MethodGetContainer)
		require.NoError(t, err)
	})

	t.Run("check testdomain-defined container in testdomain-defined container target rule", func(t *testing.T) {
		t.Parallel()

		components := newTestAPEServer()
		contID, testContainer := initTestContainer(t, true)
		components.containerReader.c[contID] = &containercore.Container{Value: testContainer}
		initTestNetmap(components.netmap)

		_, _, err := components.engine.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(contID.EncodeToString()), &chain.Chain{
			Rules: []chain.Rule{
				{
					Status: chain.AccessDenied,
					Actions: chain.Actions{
						Names: []string{
							nativeschema.MethodGetContainer,
						},
					},
					Resources: chain.Resources{
						Names: []string{
							fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainer, testDomainName, contID.EncodeToString()),
						},
					},
					Condition: []chain.Condition{
						{
							Object: chain.ObjectRequest,
							Key:    nativeschema.PropertyKeyActorRole,
							Value:  nativeschema.PropertyValueContainerRoleOthers,
							Op:     chain.CondStringEquals,
						},
					},
				},
			},
		})
		require.NoError(t, err)

		addDefaultAllowGetPolicy(t, components.engine, contID)

		req := initTestGetContainerRequest(t, contID)

		err = components.apeChecker.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(), nativeschema.MethodGetContainer)
		aErr := apeErr(nativeschema.MethodGetContainer, chain.AccessDenied)
		require.ErrorContains(t, err, aErr.Error())
	})

	t.Run("check testdomain-defined container in testdomain namespace target rule", func(t *testing.T) {
		t.Parallel()

		components := newTestAPEServer()
		contID, testContainer := initTestContainer(t, true)
		components.containerReader.c[contID] = &containercore.Container{Value: testContainer}
		initTestNetmap(components.netmap)

		_, _, err := components.engine.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(testDomainName), &chain.Chain{
			Rules: []chain.Rule{
				{
					Status: chain.AccessDenied,
					Actions: chain.Actions{
						Names: []string{
							nativeschema.MethodGetContainer,
						},
					},
					Resources: chain.Resources{
						Names: []string{
							fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainers, testDomainName),
						},
					},
					Condition: []chain.Condition{
						{
							Object: chain.ObjectRequest,
							Key:    nativeschema.PropertyKeyActorRole,
							Value:  nativeschema.PropertyValueContainerRoleOthers,
							Op:     chain.CondStringEquals,
						},
					},
				},
			},
		})
		require.NoError(t, err)

		req := initTestGetContainerRequest(t, contID)

		err = components.apeChecker.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(), nativeschema.MethodGetContainer)
		aErr := apeErr(nativeschema.MethodGetContainer, chain.AccessDenied)
		require.ErrorContains(t, err, aErr.Error())
	})
}

func initTestGetContainerRequest(t *testing.T, contID cid.ID) *container.GetRequest {
	req := &container.GetRequest{}
	req.SetBody(&container.GetRequestBody{})
	var refContID refs.ContainerID
	contID.WriteToV2(&refContID)
	req.GetBody().SetContainerID(&refContID)

	pk, err := keys.NewPrivateKey()
	require.NoError(t, err)
	require.NoError(t, signature.SignServiceMessage(&pk.PrivateKey, req))
	return req
}

func initTestNetmap(netmapStub *netmapStub) {
	netmapStub.currentEpoch = 100
	netmapStub.netmaps = map[uint64]*netmap.NetMap{}
	var testNetmap netmap.NetMap
	testNetmap.SetEpoch(netmapStub.currentEpoch)
	testNetmap.SetNodes([]netmap.NodeInfo{{}})
	netmapStub.netmaps[netmapStub.currentEpoch] = &testNetmap
	netmapStub.netmaps[netmapStub.currentEpoch-1] = &testNetmap
}

func initTestContainer(t *testing.T, isDomainSet bool) (cid.ID, cnrSDK.Container) {
	contID := cidtest.ID()
	testContainer := containertest.Container()
	pp := netmap.PlacementPolicy{}
	require.NoError(t, pp.DecodeString("REP 1"))
	testContainer.SetPlacementPolicy(pp)
	if isDomainSet {
		// no domain defined -> container is defined in root namespace
		var domain cnrSDK.Domain
		domain.SetName(testDomainName)
		domain.SetZone(testDomainZone)
		cnrSDK.WriteDomain(&testContainer, domain)
	}
	return contID, testContainer
}

func initPutRequest(t *testing.T, testContainer cnrSDK.Container) *container.PutRequest {
	req := &container.PutRequest{}
	req.SetBody(&container.PutRequestBody{})
	var reqCont container.Container
	testContainer.WriteToV2(&reqCont)
	req.GetBody().SetContainer(&reqCont)

	sessionPK, err := keys.NewPrivateKey()
	require.NoError(t, err)
	sToken := sessiontest.ContainerSigned()
	sToken.ApplyOnlyTo(cid.ID{})
	require.NoError(t, sToken.Sign(sessionPK.PrivateKey))
	var sTokenV2 session.Token
	sToken.WriteToV2(&sTokenV2)
	metaHeader := new(session.RequestMetaHeader)
	metaHeader.SetSessionToken(&sTokenV2)
	req.SetMetaHeader(metaHeader)

	pk, err := keys.NewPrivateKey()
	require.NoError(t, err)
	require.NoError(t, signature.SignServiceMessage(&pk.PrivateKey, req))

	return req
}

func initOwnerIDScriptHash(t *testing.T, testContainer cnrSDK.Container) util.Uint160 {
	var ownerSDK *user.ID
	owner := testContainer.Owner()
	ownerSDK = &owner
	sc, err := ownerSDK.ScriptHash()
	require.NoError(t, err)
	return sc
}

func initActorOwnerScriptHashes(t *testing.T, actorPK *keys.PrivateKey, ownerPK *keys.PrivateKey) (actorScriptHash util.Uint160, ownerScriptHash util.Uint160) {
	var actorUserID user.ID
	user.IDFromKey(&actorUserID, ecdsa.PublicKey(*actorPK.PublicKey()))
	var err error
	actorScriptHash, err = actorUserID.ScriptHash()
	require.NoError(t, err)

	var ownerUserID user.ID
	user.IDFromKey(&ownerUserID, ecdsa.PublicKey(*ownerPK.PublicKey()))
	ownerScriptHash, err = ownerUserID.ScriptHash()
	require.NoError(t, err)
	require.NotEqual(t, ownerScriptHash.String(), actorScriptHash.String())
	return
}

func initListRequest(t *testing.T, actorPK *keys.PrivateKey, ownerPK *keys.PrivateKey) *container.ListRequest {
	var ownerUserID user.ID
	user.IDFromKey(&ownerUserID, ownerPK.PrivateKey.PublicKey)

	req := &container.ListRequest{}
	req.SetBody(&container.ListRequestBody{})
	var ownerID refs.OwnerID
	ownerUserID.WriteToV2(&ownerID)
	req.GetBody().SetOwnerID(&ownerID)

	require.NoError(t, signature.SignServiceMessage(&actorPK.PrivateKey, req))
	return req
}

func addDefaultAllowGetPolicy(t *testing.T, e engine.Engine, contID cid.ID) {
	_, _, err := e.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(contID.EncodeToString()), &chain.Chain{
		Rules: []chain.Rule{
			{
				Status: chain.Allow,
				Actions: chain.Actions{
					Names: []string{
						nativeschema.MethodGetContainer,
					},
				},
				Resources: chain.Resources{
					Names: []string{
						nativeschema.ResourceFormatAllContainers,
					},
				},
			},
		},
	})
	require.NoError(t, err)
}