package inmemory

import (
	"testing"

	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
	"github.com/stretchr/testify/require"
)

const (
	container       = "native:::object/ExYw/*"
	chainID         = "ingress:ExYw"
	nonExistChainId = "ingress:LxGyWyL"
)

var (
	resrc = engine.ContainerTarget(container)
)

func testInmemLocalStorage() *inmemoryLocalStorage {
	return NewInmemoryLocalStorage().(*inmemoryLocalStorage)
}

func TestAddOverride(t *testing.T) {
	inmem := testInmemLocalStorage()

	inmem.AddOverride(chain.Ingress, resrc, &chain.Chain{
		Rules: []chain.Rule{
			{
				Status:    chain.AccessDenied,
				Actions:   chain.Actions{Names: []string{"native::object::delete"}},
				Resources: chain.Resources{Names: []string{"native::object::*"}},
			},
		},
	})

	ingressChains, ok := inmem.nameToResourceChains[chain.Ingress]
	require.True(t, ok)
	resourceChains, ok := ingressChains[resrc]
	require.True(t, ok)
	require.Len(t, resourceChains, 1)
	require.Len(t, resourceChains[0].Rules, 1)

	inmem.AddOverride(chain.Ingress, resrc, &chain.Chain{
		Rules: []chain.Rule{
			{
				Status:    chain.QuotaLimitReached,
				Actions:   chain.Actions{Names: []string{"native::object::put"}},
				Resources: chain.Resources{Names: []string{"native::object::*"}},
			},
			{
				Status:    chain.AccessDenied,
				Actions:   chain.Actions{Names: []string{"native::object::get"}},
				Resources: chain.Resources{Names: []string{"native::object::*"}},
			},
		},
	})

	ingressChains, ok = inmem.nameToResourceChains[chain.Ingress]
	require.True(t, ok)
	resourceChains, ok = ingressChains[resrc]
	require.True(t, ok)
	require.Len(t, resourceChains, 2)
	require.Len(t, resourceChains[1].Rules, 2)
}

func TestRemoveOverride(t *testing.T) {
	t.Run("remove from empty storage", func(t *testing.T) {
		inmem := testInmemLocalStorage()
		err := inmem.RemoveOverride(chain.Ingress, resrc, chain.ID(chainID))
		require.ErrorIs(t, err, engine.ErrChainNameNotFound)
	})

	t.Run("remove not added chain id", func(t *testing.T) {
		inmem := testInmemLocalStorage()
		inmem.AddOverride(chain.Ingress, resrc, &chain.Chain{
			ID: chain.ID(chainID),
			Rules: []chain.Rule{
				{ // Restrict to remove ANY object from the namespace.
					Status:    chain.AccessDenied,
					Actions:   chain.Actions{Names: []string{"native::object::delete"}},
					Resources: chain.Resources{Names: []string{"native::object::*"}},
				},
			},
		})

		err := inmem.RemoveOverride(chain.Ingress, resrc, chain.ID(nonExistChainId))
		require.ErrorIs(t, err, engine.ErrChainNotFound)
	})

	t.Run("remove existing chain id", func(t *testing.T) {
		inmem := testInmemLocalStorage()
		inmem.AddOverride(chain.Ingress, resrc, &chain.Chain{
			ID: chain.ID(chainID),
			Rules: []chain.Rule{
				{ // Restrict to remove ANY object from the namespace.
					Status:    chain.AccessDenied,
					Actions:   chain.Actions{Names: []string{"native::object::delete"}},
					Resources: chain.Resources{Names: []string{"native::object::*"}},
				},
			},
		})

		err := inmem.RemoveOverride(chain.Ingress, resrc, chain.ID(chainID))
		require.NoError(t, err)

		ingressChains, ok := inmem.nameToResourceChains[chain.Ingress]
		require.True(t, ok)
		require.Len(t, ingressChains, 1)
		resourceChains, ok := ingressChains[resrc]
		require.True(t, ok)
		require.Len(t, resourceChains, 0)
	})
}

func TestGetOverride(t *testing.T) {
	addChain := &chain.Chain{
		ID: chain.ID(chainID),
		Rules: []chain.Rule{
			{ // Restrict to remove ANY object from the namespace.
				Status:    chain.AccessDenied,
				Actions:   chain.Actions{Names: []string{"native::object::delete"}},
				Resources: chain.Resources{Names: []string{"native::object::*"}},
			},
		},
	}

	t.Run("get from empty storage", func(t *testing.T) {
		inmem := testInmemLocalStorage()
		_, err := inmem.GetOverride(chain.Ingress, resrc, chain.ID(chainID))
		require.ErrorIs(t, err, engine.ErrChainNameNotFound)
	})

	t.Run("get not added chain id", func(t *testing.T) {
		inmem := testInmemLocalStorage()
		inmem.AddOverride(chain.Ingress, resrc, addChain)

		const nonExistingChainID = "ingress:LxGyWyL"

		_, err := inmem.GetOverride(chain.Ingress, resrc, chain.ID(nonExistingChainID))
		require.ErrorIs(t, err, engine.ErrChainNotFound)
	})

	t.Run("get existing chain id", func(t *testing.T) {
		inmem := testInmemLocalStorage()
		inmem.AddOverride(chain.Ingress, resrc, addChain)

		c, err := inmem.GetOverride(chain.Ingress, resrc, chain.ID(chainID))
		require.NoError(t, err)
		require.EqualValues(t, *addChain, *c)
	})

	t.Run("get removed chain id", func(t *testing.T) {
		inmem := testInmemLocalStorage()
		inmem.AddOverride(chain.Ingress, resrc, addChain)

		err := inmem.RemoveOverride(chain.Ingress, resrc, chain.ID(chainID))
		require.NoError(t, err)

		_, err = inmem.GetOverride(chain.Ingress, resrc, chain.ID(chainID))
		require.ErrorIs(t, err, engine.ErrChainNotFound)
	})
}

func TestListOverrides(t *testing.T) {
	addChain := &chain.Chain{
		ID: chain.ID(chainID),
		Rules: []chain.Rule{
			{ // Restrict to remove ANY object from the namespace.
				Status:    chain.AccessDenied,
				Actions:   chain.Actions{Names: []string{"native::object::delete"}},
				Resources: chain.Resources{Names: []string{"native::object::*"}},
			},
		},
	}

	t.Run("list empty storage", func(t *testing.T) {
		inmem := testInmemLocalStorage()
		l, _ := inmem.ListOverrides(chain.Ingress, resrc)
		require.Len(t, l, 0)
	})

	t.Run("list with one added resource", func(t *testing.T) {
		inmem := testInmemLocalStorage()
		inmem.AddOverride(chain.Ingress, resrc, addChain)
		l, _ := inmem.ListOverrides(chain.Ingress, resrc)
		require.Len(t, l, 1)
	})

	t.Run("list after drop", func(t *testing.T) {
		inmem := testInmemLocalStorage()
		inmem.AddOverride(chain.Ingress, resrc, addChain)
		l, _ := inmem.ListOverrides(chain.Ingress, resrc)
		require.Len(t, l, 1)

		_ = inmem.DropAllOverrides(chain.Ingress)
		l, _ = inmem.ListOverrides(chain.Ingress, resrc)
		require.Len(t, l, 0)
	})
}

func TestGenerateID(t *testing.T) {
	inmem := testInmemLocalStorage()
	ids := make([]chain.ID, 0, 100)
	for i := 0; i < 100; i++ {
		ids = append(ids, inmem.generateChainID(chain.Ingress, resrc))
	}
	require.False(t, hasDuplicates(ids))
}

func hasDuplicates(ids []chain.ID) bool {
	seen := make(map[chain.ID]bool)
	for _, id := range ids {
		if seen[id] {
			return true
		}
		seen[id] = true
	}
	return false
}