// Copyright 2024 The Erigon Authors
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Erigon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.

package trie

// Experimental code for separating data and structural information

import (
	"encoding/binary"
	"slices"
	"testing"

	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/common/crypto"
	"github.com/erigontech/erigon/execution/types/accounts"
)

func TestHashWithModificationsEmpty(t *testing.T) {
	tr := newEmpty()
	// Populate the trie
	// Build the root
	var stream Stream
	hb := NewHashBuilder(false)
	rootHash, err := HashWithModifications(
		tr,
		common.Hashes{}, []*accounts.Account{}, [][]byte{},
		common.StorageKeys{}, [][]byte{},
		32,
		&stream, // Streams that will be reused for old and new stream
		hb,      // HashBuilder will be reused
		false,
	)
	if err != nil {
		t.Errorf("Could not compute hash with modification: %v", err)
	}
	if rootHash != EmptyRoot {
		t.Errorf("Expected empty root, got: %x", rootHash)
	}
}

// buildAccountStorageTrie populates a trie with 10 hashed-key accounts, every other one
// carrying storage items.
func buildAccountStorageTrie() *Trie {
	tr := newEmpty()
	var preimage [4]byte
	var keys []string
	for b := range uint32(10) {
		binary.BigEndian.PutUint32(preimage[:], b)
		keys = append(keys, string(crypto.Keccak256(preimage[:])))
	}
	slices.Sort(keys)
	var a0, a1 accounts.Account
	a0.Balance.SetUint64(100000)
	a0.Root = EmptyRoot
	a0.CodeHash = accounts.InternCodeHash(emptyState)
	a1.Balance.SetUint64(200000)
	a1.Root = EmptyRoot
	a1.CodeHash = accounts.InternCodeHash(emptyState)
	v := []byte("VALUE")
	for i, key := range keys {
		if i%2 == 0 {
			tr.UpdateAccount([]byte(key), &a0)
		} else {
			tr.UpdateAccount([]byte(key), &a1)
			for _, storageKey := range keys {
				tr.Update([]byte(key+storageKey), v)
			}
		}
	}
	return tr
}

func TestHashWithModificationsNoChanges(t *testing.T) {
	tr := buildAccountStorageTrie()
	expectedHash := tr.Hash()
	// Build the root
	var stream Stream
	hb := NewHashBuilder(false)
	rootHash, err := HashWithModifications(
		tr,
		common.Hashes{}, []*accounts.Account{}, [][]byte{},
		common.StorageKeys{}, [][]byte{},
		40,
		&stream, // Streams that will be reused for old and new stream
		hb,      // HashBuilder will be reused
		false,
	)
	if err != nil {
		t.Errorf("Could not compute hash with modification: %v", err)
	}
	if rootHash != expectedHash {
		t.Errorf("Expected %x, got: %x", expectedHash, rootHash)
	}
}

func TestHashWithModificationsChanges(t *testing.T) {
	tr := buildAccountStorageTrie()
	tr.Hash()
	// Generate account change
	var preimage [4]byte
	binary.BigEndian.PutUint32(preimage[:], 5000000)
	insertKey := crypto.Keccak256Hash(preimage[:])
	var insertA accounts.Account
	insertA.Balance.SetUint64(300000)
	insertA.Root = EmptyRoot
	insertA.CodeHash = accounts.InternCodeHash(emptyState)

	// Build the root
	var stream Stream
	hb := NewHashBuilder(false)
	rootHash, err := HashWithModifications(
		tr,
		common.Hashes{insertKey}, []*accounts.Account{&insertA}, [][]byte{nil},
		common.StorageKeys{}, [][]byte{},
		40,
		&stream, // Streams that will be reused for old and new stream
		hb,      // HashBuilder will be reused
		false,
	)
	if err != nil {
		t.Errorf("Could not compute hash with modification: %v", err)
	}
	tr.UpdateAccount(insertKey[:], &insertA)
	expectedHash := tr.Hash()
	if rootHash != expectedHash {
		t.Errorf("Expected %x, got: %x", expectedHash, rootHash)
	}
}
