// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package proposervm

import (
	"context"
	"testing"

	"github.com/stretchr/testify/require"

	validators "github.com/luxfi/validators"
	"github.com/luxfi/ids"
	"github.com/luxfi/node/cache/lru"
)

// mockValidatorState implements validators.State for testing
type mockValidatorState struct {
	getNetworkIDCallCount int
	netIDMap              map[ids.ID]ids.ID
}

func (m *mockValidatorState) GetNetworkID(chainID ids.ID) (ids.ID, error) {
	m.getNetworkIDCallCount++
	if netID, ok := m.netIDMap[chainID]; ok {
		return netID, nil
	}
	return ids.Empty, nil
}

func (m *mockValidatorState) GetChainID(ids.ID) (ids.ID, error) {
	return ids.Empty, nil
}

func (m *mockValidatorState) GetValidatorSet(context.Context, uint64, ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
	return nil, nil
}

func (m *mockValidatorState) GetCurrentValidators(context.Context, uint64, ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
	return nil, nil
}

func (m *mockValidatorState) GetCurrentHeight(context.Context) (uint64, error) {
	return 0, nil
}

func (m *mockValidatorState) GetMinimumHeight(context.Context) (uint64, error) {
	return 0, nil
}

func (m *mockValidatorState) GetWarpValidatorSets(context.Context, []uint64, []ids.ID) (map[ids.ID]map[uint64]*validators.WarpSet, error) {
	return nil, nil
}

func (m *mockValidatorState) GetWarpValidatorSet(context.Context, uint64, ids.ID) (*validators.WarpSet, error) {
	return nil, nil
}

// TestValidatorStateWrapperCache verifies ChainID caching in validatorStateWrapper
func TestValidatorStateWrapperCache(t *testing.T) {
	require := require.New(t)

	chainID1 := ids.GenerateTestID()
	netID1 := ids.GenerateTestID()

	chainID2 := ids.GenerateTestID()
	netID2 := ids.GenerateTestID()

	mock := &mockValidatorState{
		netIDMap: map[ids.ID]ids.ID{
			chainID1: netID1,
			chainID2: netID2,
		},
	}

	// Create wrapper with cache
	wrapper := &validatorStateWrapper{
		ctx:         context.Background(),
		vs:          mock,
		netIDsCache: lru.NewCache[ids.ID, ids.ID](4096),
	}

	ctx := context.Background()

	// First call - cache miss
	result1, err := wrapper.GetNetworkID(ctx, chainID1)
	require.NoError(err)
	require.Equal(netID1, result1)
	require.Equal(1, mock.getNetworkIDCallCount, "First call should hit underlying state")

	// Second call - cache hit
	result2, err := wrapper.GetNetworkID(ctx, chainID1)
	require.NoError(err)
	require.Equal(netID1, result2)
	require.Equal(1, mock.getNetworkIDCallCount, "Second call should use cache")

	// Different chainID - cache miss
	result3, err := wrapper.GetNetworkID(ctx, chainID2)
	require.NoError(err)
	require.Equal(netID2, result3)
	require.Equal(2, mock.getNetworkIDCallCount, "Different chainID should miss cache")

	// Same chainID again - cache hit
	result4, err := wrapper.GetNetworkID(ctx, chainID2)
	require.NoError(err)
	require.Equal(netID2, result4)
	require.Equal(2, mock.getNetworkIDCallCount, "Cached value should be used")
}

// TestInterfacesToConsensusValidatorStateAdapterCache verifies ChainID caching in adapter
func TestInterfacesToConsensusValidatorStateAdapterCache(t *testing.T) {
	require := require.New(t)

	chainID1 := ids.GenerateTestID()
	netID1 := ids.GenerateTestID()

	mock := &mockValidatorState{
		netIDMap: map[ids.ID]ids.ID{
			chainID1: netID1,
		},
	}

	// Create adapter with cache
	adapter := &interfacesToConsensusValidatorStateAdapter{
		ctx:         context.Background(),
		vs:          mock,
		netIDsCache: lru.NewCache[ids.ID, ids.ID](4096),
	}

	// First call - cache miss
	result1, err := adapter.GetNetworkID(chainID1)
	require.NoError(err)
	require.Equal(netID1, result1)
	require.Equal(1, mock.getNetworkIDCallCount, "First call should hit underlying state")

	// Second call - cache hit
	result2, err := adapter.GetNetworkID(chainID1)
	require.NoError(err)
	require.Equal(netID1, result2)
	require.Equal(1, mock.getNetworkIDCallCount, "Second call should use cache")
}

// TestChainIDCacheSize verifies cache eviction works correctly
func TestChainIDCacheSize(t *testing.T) {
	require := require.New(t)

	mock := &mockValidatorState{
		netIDMap: make(map[ids.ID]ids.ID),
	}

	// Create wrapper with small cache size for testing eviction
	wrapper := &validatorStateWrapper{
		ctx:         context.Background(),
		vs:          mock,
		netIDsCache: lru.NewCache[ids.ID, ids.ID](2), // Only cache 2 entries
	}

	ctx := context.Background()

	// Generate test IDs
	chainIDs := make([]ids.ID, 3)
	netIDs := make([]ids.ID, 3)
	for i := range chainIDs {
		chainIDs[i] = ids.GenerateTestID()
		netIDs[i] = ids.GenerateTestID()
		mock.netIDMap[chainIDs[i]] = netIDs[i]
	}

	// Fill cache with 2 entries
	_, err := wrapper.GetNetworkID(ctx, chainIDs[0])
	require.NoError(err)
	require.Equal(1, mock.getNetworkIDCallCount)

	_, err = wrapper.GetNetworkID(ctx, chainIDs[1])
	require.NoError(err)
	require.Equal(2, mock.getNetworkIDCallCount)

	// Add third entry - should evict oldest
	_, err = wrapper.GetNetworkID(ctx, chainIDs[2])
	require.NoError(err)
	require.Equal(3, mock.getNetworkIDCallCount)

	// Access first entry again - should be cache miss (evicted)
	_, err = wrapper.GetNetworkID(ctx, chainIDs[0])
	require.NoError(err)
	require.Equal(4, mock.getNetworkIDCallCount, "First entry should have been evicted")

	// Access second entry - should also be cache miss (was evicted when we added chainIDs[0])
	// Cache state after adding third: [1, 2]
	// Cache state after re-adding first: [2, 0] (1 was evicted as oldest)
	_, err = wrapper.GetNetworkID(ctx, chainIDs[1])
	require.NoError(err)
	require.Equal(5, mock.getNetworkIDCallCount, "Second entry should have been evicted when first was re-added")
}
