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

package predicate

import (
	"slices"
	"testing"

	"github.com/ava-labs/libevm/common"
	"github.com/stretchr/testify/require"

	"github.com/ava-labs/avalanchego/codec"
	"github.com/ava-labs/avalanchego/utils/set"
)

// Valid result parsing is tested by [TestBlockResultsBytes]
func TestParseBlockResultsInvalid(t *testing.T) {
	tests := []struct {
		name    string
		b       []byte
		wantErr error
	}{
		{
			name:    "nil",
			b:       nil,
			wantErr: codec.ErrCantUnpackVersion,
		},
		{
			name: "too_big",
			b: slices.Concat(
				[]byte{
					// codecID
					0x00, 0x00,
					// BlockResults length
					0x00, 0x00, 0x00, 0x01,
					// txHash
					0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
					0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
					0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
					0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
					// PrecompileResults length
					0x00, 0x00, 0x00, 0x01,
					// precompile address
					0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
					0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
					0x00, 0x00, 0x00, 0x00,
					// Length of bitset
					0x01, 0x00, 0x00, 0x00, // 2^24
				},
				make([]byte, 1<<24), // Append the bitset
			),
			wantErr: codec.ErrUnmarshalTooBig,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			_, err := ParseBlockResults(test.b)
			require.ErrorIs(t, err, test.wantErr)
		})
	}
}

func TestBlockResultsGet(t *testing.T) {
	txHash := common.Hash{1}
	address := common.Address{2}
	tests := []struct {
		name    string
		results BlockResults
		want    set.Bits
	}{
		{
			name:    "nil",
			results: nil,
			want:    set.NewBits(),
		},
		{
			name:    "empty",
			results: make(BlockResults),
			want:    set.NewBits(),
		},
		{
			name: "missing_tx",
			results: BlockResults{
				{3}: {
					address: set.NewBits(1, 2, 3),
				},
			},
			want: set.NewBits(),
		},
		{
			name: "missing_address",
			results: BlockResults{
				txHash: {
					{3}: set.NewBits(1, 2, 3),
				},
			},
			want: set.NewBits(),
		},
		{
			name: "found",
			results: BlockResults{
				txHash: {
					address: set.NewBits(1, 2, 3),
				},
			},
			want: set.NewBits(1, 2, 3),
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			got := test.results.Get(txHash, address)
			require.Equal(t, test.want, got)
		})
	}
}

func TestBlockResultsSet(t *testing.T) {
	txHash := common.Hash{1}
	tests := []struct {
		name  string
		start BlockResults
		toSet PrecompileResults
		want  BlockResults
	}{
		{
			name:  "nil_delete",
			start: nil,
			toSet: nil,
			want:  nil,
		},
		{
			name:  "nil_allocate",
			start: nil,
			toSet: PrecompileResults{
				{3}: set.NewBits(1, 2, 3),
			},
			want: BlockResults{
				txHash: PrecompileResults{
					{3}: set.NewBits(1, 2, 3),
				},
			},
		},
		{
			name:  "empty_allocate",
			start: make(BlockResults),
			toSet: PrecompileResults{
				{3}: set.NewBits(1, 2, 3),
			},
			want: BlockResults{
				txHash: PrecompileResults{
					{3}: set.NewBits(1, 2, 3),
				},
			},
		},
		{
			name: "overwrite_tx",
			start: BlockResults{
				txHash: PrecompileResults{
					{3}: set.NewBits(1, 2, 3),
				},
			},
			toSet: PrecompileResults{
				{3}: set.NewBits(1),
			},
			want: BlockResults{
				txHash: PrecompileResults{
					{3}: set.NewBits(1),
				},
			},
		},
		{
			name: "delete_tx",
			start: BlockResults{
				txHash: PrecompileResults{
					{3}: set.NewBits(1, 2, 3),
				},
			},
			toSet: PrecompileResults{},
			want:  BlockResults{},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			test.start.Set(txHash, test.toSet)
			require.Equal(t, test.want, test.start)
		})
	}
}

func TestBlockResultsBytes(t *testing.T) {
	tests := []struct {
		name  string
		input BlockResults
		want  []byte
	}{
		{
			name:  "nil",
			input: nil,
			want: []byte{
				// codecID
				0x00, 0x00,
				// results length
				0x00, 0x00, 0x00, 0x00,
			},
		},
		{
			name:  "empty",
			input: make(BlockResults),
			want: []byte{
				// codecID
				0x00, 0x00,
				// results length
				0x00, 0x00, 0x00, 0x00,
			},
		},
		{
			name: "single_tx_single_result",
			input: BlockResults{
				{1}: {
					{2}: set.NewBits(1, 2, 3),
				},
			},
			want: []byte{
				// codecID
				0x00, 0x00,
				// BlockResults length
				0x00, 0x00, 0x00, 0x01,
				// txHash
				0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				// PrecompileResults length
				0x00, 0x00, 0x00, 0x01,
				// precompile address
				0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00,
				// Length of bitset
				0x00, 0x00, 0x00, 0x01,
				// bitset
				0b00001110,
			},
		},
		{
			name: "single_tx_multiple_results",
			input: BlockResults{
				{1}: {
					{2}: set.NewBits(1, 2, 3),
					{3}: set.NewBits(0, 1, 2, 3),
				},
			},
			want: []byte{
				// codecID
				0x00, 0x00,
				// BlockResults length
				0x00, 0x00, 0x00, 0x01,
				// txHash
				0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				// PrecompileResults length
				0x00, 0x00, 0x00, 0x02,
				// precompile address
				0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00,
				// Length of bitset
				0x00, 0x00, 0x00, 0x01,
				// bitset
				0b00001110,
				// precompile address
				0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00,
				// Length of bitset
				0x00, 0x00, 0x00, 0x01,
				// bitset
				0b00001111,
			},
		},
		{
			name: "multiple_txs_single_result",
			input: BlockResults{
				{1}: {
					{2}: set.NewBits(1, 2, 3),
				},
				{2}: {
					{3}: set.NewBits(3, 2, 1),
				},
			},
			want: []byte{
				// codecID
				0x00, 0x00,
				// BlockResults length
				0x00, 0x00, 0x00, 0x02,
				// txHash
				0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				// PrecompileResults length
				0x00, 0x00, 0x00, 0x01,
				// precompile address
				0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00,
				// Length of bitset
				0x00, 0x00, 0x00, 0x01,
				// bitset
				0b00001110,
				// txHash
				0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				// PrecompileResults length
				0x00, 0x00, 0x00, 0x01,
				// precompile address
				0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00,
				// Length of bitset
				0x00, 0x00, 0x00, 0x01,
				// bitset
				0b00001110,
			},
		},
		{
			name: "multiple_txs_multiple_results",
			input: BlockResults{
				{1}: {
					{2}: set.NewBits(1, 2, 3),
					{3}: set.NewBits(3, 2, 1),
				},
				{2}: {
					{2}: set.NewBits(1, 2, 3),
					{3}: set.NewBits(3, 2, 1),
				},
			},
			want: []byte{
				// codecID
				0x00, 0x00,
				// BlockResults length
				0x00, 0x00, 0x00, 0x02,
				// txHash
				0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				// PrecompileResults length
				0x00, 0x00, 0x00, 0x02,
				// precompile address
				0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00,
				// Length of bitset
				0x00, 0x00, 0x00, 0x01,
				// bitset
				0b00001110,
				// precompile address
				0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00,
				// Length of bitset
				0x00, 0x00, 0x00, 0x01,
				// bitset
				0b00001110,
				// txHash
				0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				// PrecompileResults length
				0x00, 0x00, 0x00, 0x02,
				// precompile address
				0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00,
				// Length of bitset
				0x00, 0x00, 0x00, 0x01,
				// bitset
				0b00001110,
				// precompile address
				0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00,
				// Length of bitset
				0x00, 0x00, 0x00, 0x01,
				// bitset
				0b00001110,
			},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			require := require.New(t)

			got, err := test.input.Bytes()
			require.NoError(err)
			require.Equal(test.want, got)

			input := test.input
			if input == nil {
				// nil and empty should be considered equivalent
				input = make(BlockResults)
			}

			parsed, err := ParseBlockResults(got)
			require.NoError(err)
			require.Equal(input, parsed)
		})
	}
}
