package container

import (
	"context"
	"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"
	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
	containercore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
	apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
	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/stretchr/testify/require"
)

func TestAPE(t *testing.T) {
	t.Parallel()
	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 list containers for owner with PK", testDenyListContainersForPK)
}

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{}
	apeSrv := NewAPEServer(router, contRdr, ir, nm, 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{}
	apeSrv := NewAPEServer(router, contRdr, ir, nm, 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{}
	apeSrv := NewAPEServer(router, contRdr, ir, nm, 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{}
	apeSrv := NewAPEServer(router, contRdr, ir, nm, 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 := &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))

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

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{}
	apeSrv := NewAPEServer(router, contRdr, ir, nm, 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:  pk.PublicKey().String(),
						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)
}

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
}