// Copyright 2023 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 chain

import (
	"encoding/json"
	"testing"

	"github.com/holiman/uint256"
	"github.com/stretchr/testify/assert"

	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/execution/protocol/params"
)

func TestConfigValueLookup(t *testing.T) {
	foo := map[uint64]string{5: "A", 10: "B", 20: "C"}
	assert.Equal(t, "", ConfigValueLookup(foo, 0))
	assert.Equal(t, "", ConfigValueLookup(foo, 4))
	assert.Equal(t, "A", ConfigValueLookup(foo, 5))
	assert.Equal(t, "A", ConfigValueLookup(foo, 9))
	assert.Equal(t, "B", ConfigValueLookup(foo, 10))
	assert.Equal(t, "B", ConfigValueLookup(foo, 11))
	assert.Equal(t, "B", ConfigValueLookup(foo, 15))
	assert.Equal(t, "B", ConfigValueLookup(foo, 19))
	assert.Equal(t, "C", ConfigValueLookup(foo, 20))
	assert.Equal(t, "C", ConfigValueLookup(foo, 21))
	assert.Equal(t, "C", ConfigValueLookup(foo, 100))
	assert.Equal(t, "C", ConfigValueLookup(foo, 1_000_000_000_000))

	backupMultiplier := map[uint64]uint64{
		0:        2,
		25275000: 5,
		29638656: 2,
	}
	assert.Equal(t, uint64(2), ConfigValueLookup(backupMultiplier, 0))
	assert.Equal(t, uint64(2), ConfigValueLookup(backupMultiplier, 1))
	assert.Equal(t, uint64(2), ConfigValueLookup(backupMultiplier, 25275000-1))
	assert.Equal(t, uint64(5), ConfigValueLookup(backupMultiplier, 25275000))
	assert.Equal(t, uint64(5), ConfigValueLookup(backupMultiplier, 25275000+1))
	assert.Equal(t, uint64(5), ConfigValueLookup(backupMultiplier, 29638656-1))
	assert.Equal(t, uint64(2), ConfigValueLookup(backupMultiplier, 29638656))
	assert.Equal(t, uint64(2), ConfigValueLookup(backupMultiplier, 29638656+1))

	config := map[uint64]uint64{
		0:         1,
		90000000:  2,
		100000000: 3,
	}
	assert.Equal(t, uint64(1), ConfigValueLookup(config, 0))
	assert.Equal(t, uint64(1), ConfigValueLookup(config, 1))
	assert.Equal(t, uint64(1), ConfigValueLookup(config, 90000000-1))
	assert.Equal(t, uint64(2), ConfigValueLookup(config, 90000000))
	assert.Equal(t, uint64(2), ConfigValueLookup(config, 90000000+1))
	assert.Equal(t, uint64(2), ConfigValueLookup(config, 100000000-1))
	assert.Equal(t, uint64(3), ConfigValueLookup(config, 100000000))
	assert.Equal(t, uint64(3), ConfigValueLookup(config, 100000000+1))

	address1 := common.HexToAddress("0x70bcA57F4579f58670aB2d18Ef16e02C17553C38")
	address2 := common.HexToAddress("0x617b94CCCC2511808A3C9478ebb96f455CF167aA")

	burntContract := map[uint64]common.Address{
		22640000: address1,
		41874000: address2,
	}
	assert.Equal(t, common.Address{}, ConfigValueLookup(burntContract, 10000000))
	assert.Equal(t, address1, ConfigValueLookup(burntContract, 22640000))
	assert.Equal(t, address1, ConfigValueLookup(burntContract, 22640000+1))
	assert.Equal(t, address1, ConfigValueLookup(burntContract, 41874000-1))
	assert.Equal(t, address2, ConfigValueLookup(burntContract, 41874000))
	assert.Equal(t, address2, ConfigValueLookup(burntContract, 41874000+1))
}

func TestEmptyConfigValueLookup(t *testing.T) {
	blobSchedule := make(map[uint64]*params.BlobConfig)
	assert.Nil(t, ConfigValueLookup(blobSchedule, 0))
	assert.Nil(t, ConfigValueLookup(blobSchedule, 1))
}

func TestNilBlobSchedule(t *testing.T) {
	var c Config
	c.CancunTime = common.NewUint64(1)
	c.PragueTime = common.NewUint64(2)

	// Everything should be 0 before Cancun
	assert.Equal(t, uint64(0), c.GetTargetBlobsPerBlock(0))
	assert.Equal(t, uint64(0), c.GetMaxBlobsPerBlock(0))
	assert.Equal(t, uint64(0), c.GetBlobGasPriceUpdateFraction(0))

	// Original EIP-4844 values
	assert.Equal(t, uint64(3), c.GetTargetBlobsPerBlock(1))
	assert.Equal(t, uint64(6), c.GetMaxBlobsPerBlock(1))
	assert.Equal(t, uint64(3338477), c.GetBlobGasPriceUpdateFraction(1))

	// EIP-7691: Blob throughput increase
	assert.Equal(t, uint64(6), c.GetTargetBlobsPerBlock(2))
	assert.Equal(t, uint64(9), c.GetMaxBlobsPerBlock(2))
	assert.Equal(t, uint64(5007716), c.GetBlobGasPriceUpdateFraction(2))
}

// EIP-7892
func TestBlobParameterOnlyHardforks(t *testing.T) {
	cancunTime := uint64(1710338135)
	pragueTime := uint64(1746612311)
	bpo1time := uint64(1775065900)
	bpo2time := uint64(1785952240)

	var c Config
	c.CancunTime = common.NewUint64(cancunTime)
	c.PragueTime = common.NewUint64(pragueTime)
	c.Bpo1Time = common.NewUint64(bpo1time)
	c.Bpo2Time = common.NewUint64(bpo2time)

	c.BlobSchedule = map[string]*params.BlobConfig{
		"cancun": {
			Target:                3,
			Max:                   6,
			BaseFeeUpdateFraction: 3338477,
		},
		"prague": {
			Target:                6,
			Max:                   9,
			BaseFeeUpdateFraction: 5007716,
		},
		"bpo1": {
			Target:                24,
			Max:                   48,
			BaseFeeUpdateFraction: 5007716,
		},
		"bpo2": {
			Target:                36,
			Max:                   56,
			BaseFeeUpdateFraction: 5007716,
		},
	}

	time := uint64(0)
	assert.Equal(t, uint64(0), c.GetTargetBlobsPerBlock(time))
	assert.Equal(t, uint64(0), c.GetMaxBlobsPerBlock(time))
	assert.Equal(t, uint64(0), c.GetBlobGasPriceUpdateFraction(time))

	time = cancunTime
	assert.Equal(t, uint64(3), c.GetTargetBlobsPerBlock(time))
	assert.Equal(t, uint64(6), c.GetMaxBlobsPerBlock(time))
	assert.Equal(t, uint64(3338477), c.GetBlobGasPriceUpdateFraction(time))

	time = (cancunTime + pragueTime) / 2
	assert.Equal(t, uint64(3), c.GetTargetBlobsPerBlock(time))
	assert.Equal(t, uint64(6), c.GetMaxBlobsPerBlock(time))
	assert.Equal(t, uint64(3338477), c.GetBlobGasPriceUpdateFraction(time))

	time = pragueTime
	assert.Equal(t, uint64(6), c.GetTargetBlobsPerBlock(time))
	assert.Equal(t, uint64(9), c.GetMaxBlobsPerBlock(time))
	assert.Equal(t, uint64(5007716), c.GetBlobGasPriceUpdateFraction(time))

	time = (pragueTime + bpo1time) / 2
	assert.Equal(t, uint64(6), c.GetTargetBlobsPerBlock(time))
	assert.Equal(t, uint64(9), c.GetMaxBlobsPerBlock(time))
	assert.Equal(t, uint64(5007716), c.GetBlobGasPriceUpdateFraction(time))

	time = bpo1time
	assert.Equal(t, uint64(24), c.GetTargetBlobsPerBlock(time))
	assert.Equal(t, uint64(48), c.GetMaxBlobsPerBlock(time))
	assert.Equal(t, uint64(5007716), c.GetBlobGasPriceUpdateFraction(time))

	time = (bpo1time + bpo2time) / 2
	assert.Equal(t, uint64(24), c.GetTargetBlobsPerBlock(time))
	assert.Equal(t, uint64(48), c.GetMaxBlobsPerBlock(time))
	assert.Equal(t, uint64(5007716), c.GetBlobGasPriceUpdateFraction(time))

	time = bpo2time
	assert.Equal(t, uint64(36), c.GetTargetBlobsPerBlock(time))
	assert.Equal(t, uint64(56), c.GetMaxBlobsPerBlock(time))
	assert.Equal(t, uint64(5007716), c.GetBlobGasPriceUpdateFraction(time))

	time = bpo2time * 2
	assert.Equal(t, uint64(36), c.GetTargetBlobsPerBlock(time))
	assert.Equal(t, uint64(56), c.GetMaxBlobsPerBlock(time))
	assert.Equal(t, uint64(5007716), c.GetBlobGasPriceUpdateFraction(time))
}

func TestBlobParameterInactiveHardfork(t *testing.T) {
	cancunTime := uint64(1710338135)
	pragueTime := uint64(1746612311)

	var c Config
	c.CancunTime = common.NewUint64(cancunTime)
	c.PragueTime = common.NewUint64(pragueTime)
	// Osaka is not activated yet

	c.BlobSchedule = map[string]*params.BlobConfig{
		"cancun": {
			Target:                3,
			Max:                   6,
			BaseFeeUpdateFraction: 3338477,
		},
		"prague": {
			Target:                6,
			Max:                   9,
			BaseFeeUpdateFraction: 5007716,
		},
		"osaka": {
			Target:                12,
			Max:                   24,
			BaseFeeUpdateFraction: 3338477,
		},
	}

	time := pragueTime * 2
	assert.Equal(t, uint64(6), c.GetTargetBlobsPerBlock(time))
	assert.Equal(t, uint64(9), c.GetMaxBlobsPerBlock(time))
	assert.Equal(t, uint64(5007716), c.GetBlobGasPriceUpdateFraction(time))
}

func TestConfigL2JSONRoundTrip(t *testing.T) {
	var c Config
	a := assert.New(t)
	a.NoError(json.Unmarshal([]byte(`{"chainId":1,"l2":{"name":"testl2","foo":1}}`), &c))
	a.JSONEq(`{"name":"testl2","foo":1}`, string(c.L2JSON))
	a.True(c.IsL2())

	var noL2 Config
	a.NoError(json.Unmarshal([]byte(`{"chainId":1}`), &noL2))
	a.False(noL2.IsL2())

	var nullL2 Config
	a.NoError(json.Unmarshal([]byte(`{"chainId":1,"l2":null}`), &nullL2))
	a.False(nullL2.IsL2())

	resolved := Config{L2: fakeL2{}}
	a.True(resolved.IsL2())

	var nilCfg *Config
	a.False(nilCfg.IsL2())
}

type fakeL2 struct{}

func (fakeL2) Name() string                                                 { return "fake" }
func (fakeL2) ResolveRules(l2Version, blockNum, blockTime uint64, r *Rules) {}

func TestBlobParameterDencunAndPectraAtGenesis(t *testing.T) {
	var c Config
	c.CancunTime = common.NewUint64(0)
	c.PragueTime = common.NewUint64(0)

	c.BlobSchedule = map[string]*params.BlobConfig{
		"cancun": {
			Target:                3,
			Max:                   6,
			BaseFeeUpdateFraction: 3338477,
		},
		"prague": {
			Target:                6,
			Max:                   9,
			BaseFeeUpdateFraction: 5007716,
		},
	}

	// Prague should take priority
	assert.Equal(t, uint64(6), c.GetTargetBlobsPerBlock(0))
	assert.Equal(t, uint64(9), c.GetMaxBlobsPerBlock(0))
	assert.Equal(t, uint64(5007716), c.GetBlobGasPriceUpdateFraction(0))
}

func TestCheckConfigForkOrder(t *testing.T) {
	t.Parallel()
	u64 := func(n uint64) *uint64 { return &n }
	cases := []struct {
		name    string
		cfg     Config
		wantErr bool
	}{
		{
			// EEST TangerineWhistle/SpuriousDragon test genesis: EIP-150 active at
			// block 0 with the DAO fork scheduled past the test's blocks. The DAO
			// fork block sits after eip150 but must not be rejected.
			name:    "dao after eip150",
			cfg:     Config{ChainID: uint256.NewInt(1), HomesteadBlock: u64(0), DAOForkBlock: u64(2000), TangerineWhistleBlock: u64(0), SpuriousDragonBlock: u64(0)},
			wantErr: false,
		},
		{
			name:    "dao disabled",
			cfg:     Config{ChainID: uint256.NewInt(1), HomesteadBlock: u64(0), TangerineWhistleBlock: u64(0), SpuriousDragonBlock: u64(0)},
			wantErr: false,
		},
		{
			name:    "eip150 after eip155 still rejected",
			cfg:     Config{ChainID: uint256.NewInt(1), HomesteadBlock: u64(0), TangerineWhistleBlock: u64(10), SpuriousDragonBlock: u64(5)},
			wantErr: true,
		},
	}
	for i := range cases {
		tc := &cases[i]
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			err := tc.cfg.CheckConfigForkOrder()
			if tc.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
		})
	}
}
