package ape

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

	cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
	apechain "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
	nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/stretchr/testify/require"
)

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

	tb := eacl.NewTable()
	ch, err := ConvertEACLToAPE(tb)
	require.NoError(t, err)

	vu := &eacl.ValidationUnit{}
	vu.WithEACLTable(tb)
	req := &testRequest{
		res: &testResource{name: nativeschema.ResourceFormatRootObjects},
	}

	compare(t, vu, ch, req)

	cnrID := cidtest.ID()
	tb.SetCID(cnrID)
	vu.WithContainerID(&cnrID)
	req.res.name = fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cnrID.EncodeToString())

	ch, err = ConvertEACLToAPE(tb)
	require.NoError(t, err)

	compare(t, vu, ch, req)
}

func TestNoTargets(t *testing.T) {
	t.Parallel()
	for _, act := range []eacl.Action{eacl.ActionAllow, eacl.ActionDeny} {
		cnrID := cidtest.ID()
		tb := eacl.NewTable()
		tb.SetCID(cnrID)

		vu := &eacl.ValidationUnit{}
		vu.WithEACLTable(tb)
		vu.WithContainerID(&cnrID)
		vu.WithRole(eacl.RoleOthers)

		// deny delete without role or key specified
		record := eacl.NewRecord()
		record.SetAction(act)
		record.SetOperation(eacl.OperationDelete)
		record.AddObjectContainerIDFilter(eacl.MatchStringEqual, cnrID)

		tb.AddRecord(record)

		ch, err := ConvertEACLToAPE(tb)
		require.NoError(t, err)

		req := &testRequest{
			props: map[string]string{
				nativeschema.PropertyKeyActorRole: eacl.RoleOthers.String(),
			},
			res: &testResource{name: fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cnrID.EncodeToString())},
		}
		compare(t, vu, ch, req)
	}
}

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

	t.Run("target match by role only", func(t *testing.T) {
		t.Parallel()

		for _, act := range []eacl.Action{eacl.ActionAllow, eacl.ActionDeny} {
			cnrID := cidtest.ID()
			tb := eacl.NewTable()
			tb.SetCID(cnrID)

			vu := &eacl.ValidationUnit{}
			vu.WithEACLTable(tb)
			vu.WithContainerID(&cnrID)
			vu.WithRole(eacl.RoleOthers)

			// allow/deny for OTHERS
			record := eacl.NewRecord()
			record.SetAction(act)
			record.SetOperation(eacl.OperationDelete)

			target := eacl.NewTarget()
			target.SetRole(eacl.RoleOthers)
			record.SetTargets(*target)

			tb.AddRecord(record)

			ch, err := ConvertEACLToAPE(tb)
			require.NoError(t, err)

			req := &testRequest{
				props: map[string]string{
					nativeschema.PropertyKeyActorRole: nativeschema.PropertyValueContainerRoleOthers,
				},
				res: &testResource{name: fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cnrID.EncodeToString())},
			}
			compare(t, vu, ch, req)
		}
	})

	t.Run("target match by role and public key", func(t *testing.T) {
		t.Parallel()

		for _, act := range []eacl.Action{eacl.ActionAllow, eacl.ActionDeny} {
			cnrID := cidtest.ID()
			tb := eacl.NewTable()
			tb.SetCID(cnrID)

			vu := &eacl.ValidationUnit{}
			vu.WithEACLTable(tb)
			vu.WithContainerID(&cnrID)
			vu.WithRole(eacl.RoleOthers)

			// allow/deny for OTHERS
			record := eacl.NewRecord()
			record.SetAction(act)
			record.SetOperation(eacl.OperationDelete)

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

			vu.WithSenderKey(p2.PublicKey().Bytes())

			target := eacl.NewTarget()
			target.SetRole(eacl.RoleOthers)
			target.SetBinaryKeys([][]byte{p1.PublicKey().Bytes(), p2.PublicKey().Bytes()})
			record.SetTargets(*target)

			tb.AddRecord(record)

			ch, err := ConvertEACLToAPE(tb)
			require.NoError(t, err)

			req := &testRequest{
				props: map[string]string{
					nativeschema.PropertyKeyActorRole:      nativeschema.PropertyValueContainerRoleOthers,
					nativeschema.PropertyKeyActorPublicKey: string(p2.PublicKey().Bytes()),
				},
				res: &testResource{name: fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cnrID.EncodeToString())},
			}
			compare(t, vu, ch, req)
		}
	})

	t.Run("target match by public key only", func(t *testing.T) {
		t.Parallel()

		for _, act := range []eacl.Action{eacl.ActionAllow, eacl.ActionDeny} {
			cnrID := cidtest.ID()
			tb := eacl.NewTable()
			tb.SetCID(cnrID)

			vu := &eacl.ValidationUnit{}
			vu.WithEACLTable(tb)
			vu.WithContainerID(&cnrID)

			// allow/deny for OTHERS
			record := eacl.NewRecord()
			record.SetAction(act)
			record.SetOperation(eacl.OperationDelete)

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

			vu.WithSenderKey(p2.PublicKey().Bytes())

			target := eacl.NewTarget()
			target.SetRole(eacl.RoleOthers)
			target.SetBinaryKeys([][]byte{p1.PublicKey().Bytes(), p2.PublicKey().Bytes()})
			record.SetTargets(*target)

			tb.AddRecord(record)

			ch, err := ConvertEACLToAPE(tb)
			require.NoError(t, err)

			req := &testRequest{
				props: map[string]string{
					nativeschema.PropertyKeyActorPublicKey: hex.EncodeToString(p2.PublicKey().Bytes()),
				},
				res: &testResource{name: fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cnrID.EncodeToString())},
			}
			compare(t, vu, ch, req)
		}
	})

	t.Run("target doesn't match", func(t *testing.T) {
		t.Parallel()

		for _, act := range []eacl.Action{eacl.ActionAllow, eacl.ActionDeny} {
			cnrID := cidtest.ID()
			tb := eacl.NewTable()
			tb.SetCID(cnrID)

			vu := &eacl.ValidationUnit{}
			vu.WithEACLTable(tb)
			vu.WithContainerID(&cnrID)
			vu.WithRole(eacl.RoleSystem)

			// allow/deny for OTHERS
			record := eacl.NewRecord()
			record.SetAction(act)
			record.SetOperation(eacl.OperationDelete)

			target := eacl.NewTarget()
			target.SetRole(eacl.RoleOthers)
			record.SetTargets(*target)

			tb.AddRecord(record)

			ch, err := ConvertEACLToAPE(tb)
			require.NoError(t, err)

			req := &testRequest{
				props: map[string]string{
					nativeschema.PropertyKeyActorRole: eacl.RoleSystem.String(),
				},
				res: &testResource{name: fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cnrID.EncodeToString())},
			}
			compare(t, vu, ch, req)
		}
	})
}

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

	t.Run("object attributes", func(t *testing.T) {
		t.Parallel()

		const attrKey = "attribute_1"
		const attrValue = "attribute_1_value"

		for _, act := range []eacl.Action{eacl.ActionAllow, eacl.ActionDeny} {
			cnrID := cidtest.ID()
			tb := eacl.NewTable()
			tb.SetCID(cnrID)

			vu := &eacl.ValidationUnit{}
			vu.WithEACLTable(tb)
			vu.WithContainerID(&cnrID)
			vu.WithRole(eacl.RoleOthers)
			vu.WithHeaderSource(&testHeaderSource{
				headers: map[eacl.FilterHeaderType][]eacl.Header{
					eacl.HeaderFromObject: {&testHeader{key: attrKey, value: attrValue}},
				},
			})

			// allow/deny for OTHERS
			record := eacl.NewRecord()
			record.SetAction(act)
			record.SetOperation(eacl.OperationDelete)

			target := eacl.NewTarget()
			target.SetRole(eacl.RoleOthers)
			record.SetTargets(*target)

			record.AddObjectAttributeFilter(eacl.MatchStringEqual, attrKey, attrValue)

			tb.AddRecord(record)

			ch, err := ConvertEACLToAPE(tb)
			require.NoError(t, err)

			req := &testRequest{
				props: map[string]string{
					nativeschema.PropertyKeyActorRole: nativeschema.PropertyValueContainerRoleOthers,
				},
				res: &testResource{
					name: fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cnrID.EncodeToString()),
					props: map[string]string{
						attrKey: attrValue,
					},
				},
			}
			compare(t, vu, ch, req)
		}
	})

	t.Run("request attributes", func(t *testing.T) {
		t.Parallel()

		const attrKey = "attribute_1"
		const attrValue = "attribute_1_value"

		for _, act := range []eacl.Action{eacl.ActionAllow, eacl.ActionDeny} {
			cnrID := cidtest.ID()
			tb := eacl.NewTable()
			tb.SetCID(cnrID)

			vu := &eacl.ValidationUnit{}
			vu.WithEACLTable(tb)
			vu.WithContainerID(&cnrID)
			vu.WithRole(eacl.RoleOthers)
			vu.WithHeaderSource(&testHeaderSource{
				headers: map[eacl.FilterHeaderType][]eacl.Header{
					eacl.HeaderFromRequest: {&testHeader{key: attrKey, value: attrValue}},
				},
			})

			// allow/deny for OTHERS
			record := eacl.NewRecord()
			record.SetAction(act)
			record.SetOperation(eacl.OperationDelete)

			target := eacl.NewTarget()
			target.SetRole(eacl.RoleOthers)
			record.SetTargets(*target)

			record.AddFilter(eacl.HeaderFromRequest, eacl.MatchStringEqual, attrKey, attrValue)

			tb.AddRecord(record)

			ch, err := ConvertEACLToAPE(tb)
			require.NoError(t, err)

			req := &testRequest{
				props: map[string]string{
					nativeschema.PropertyKeyActorRole: nativeschema.PropertyValueContainerRoleOthers,
					attrKey:                           attrValue,
				},
				res: &testResource{
					name: fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cnrID.EncodeToString()),
				},
			}
			compare(t, vu, ch, req)
		}
	})
}

func TestNoHeader(t *testing.T) {
	t.Skip("Should pass after https://git.frostfs.info/TrueCloudLab/policy-engine/issues/8#issuecomment-26126")

	t.Parallel()

	const attrKey = "attribute_1"
	cnrID := cidtest.ID()
	tb := eacl.NewTable()
	tb.SetCID(cnrID)

	vu := &eacl.ValidationUnit{}
	vu.WithEACLTable(tb)
	vu.WithContainerID(&cnrID)
	vu.WithRole(eacl.RoleOthers)
	vu.WithHeaderSource(&testHeaderSource{
		headers: map[eacl.FilterHeaderType][]eacl.Header{
			eacl.HeaderFromRequest: {},
		},
	})

	// allow/deny for OTHERS
	record := eacl.NewRecord()
	record.SetAction(eacl.ActionDeny)
	record.SetOperation(eacl.OperationDelete)

	target := eacl.NewTarget()
	target.SetRole(eacl.RoleOthers)
	record.SetTargets(*target)

	record.AddFilter(eacl.HeaderFromRequest, eacl.MatchStringEqual, attrKey, "")

	tb.AddRecord(record)

	ch, err := ConvertEACLToAPE(tb)
	require.NoError(t, err)

	req := &testRequest{
		props: map[string]string{
			nativeschema.PropertyKeyActorRole: eacl.RoleOthers.String(),
		},
		res: &testResource{
			name: fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cnrID.EncodeToString()),
		},
	}
	compare(t, vu, ch, req)
}

func compare(t *testing.T, vu *eacl.ValidationUnit, ch *apechain.Chain, req *testRequest) {
	validator := eacl.NewValidator()
	for eaclOp, apeOp := range eaclOperationToEngineAction {
		vu.WithOperation(eaclOp)
		req.op = apeOp.Names[0]

		eaclAct, recordFound := validator.CalculateAction(vu)
		apeSt, ruleFound := ch.Match(req)

		require.Equal(t, recordFound, ruleFound)
		require.NotEqual(t, eacl.ActionUnknown, eaclAct)
		if eaclAct == eacl.ActionAllow {
			if recordFound {
				require.Equal(t, apechain.Allow, apeSt)
			} else {
				require.Equal(t, apechain.NoRuleFound, apeSt)
			}
		} else {
			require.Equal(t, apechain.AccessDenied, apeSt)
		}
	}
}

type testRequest struct {
	op    string
	props map[string]string
	res   *testResource
}

func (r *testRequest) Operation() string {
	return r.op
}

func (r *testRequest) Property(key string) string {
	if v, ok := r.props[key]; ok {
		return v
	}
	return ""
}

func (r *testRequest) Resource() resource.Resource {
	return r.res
}

type testResource struct {
	name  string
	props map[string]string
}

func (r *testResource) Name() string {
	return r.name
}

func (r *testResource) Property(key string) string {
	if v, ok := r.props[key]; ok {
		return v
	}
	return ""
}

type testHeaderSource struct {
	headers map[eacl.FilterHeaderType][]eacl.Header
}

func (s *testHeaderSource) HeadersOfType(t eacl.FilterHeaderType) ([]eacl.Header, bool) {
	v, ok := s.headers[t]
	return v, ok
}

type testHeader struct {
	key, value string
}

func (h *testHeader) Key() string   { return h.key }
func (h *testHeader) Value() string { return h.value }