// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// SPDX-License-Identifier: EUPL-1.2

package chain

import (
	"testing"

	"dappco.re/go/core/blockchain/config"
	"dappco.re/go/core/blockchain/types"
	store "dappco.re/go/core/store"
	"github.com/stretchr/testify/require"
)

// preHF6Forks is a fork schedule where HF6 never activates,
// so both PoW and PoS targets stay at 120s.
var preHF6Forks = []config.HardFork{
	{Version: config.HF0Initial, Height: 0},
}

// hf6ActiveForks is a fork schedule where HF6 activates at height 100,
// switching both PoW and PoS targets to 240s from block 101 onwards.
var hf6ActiveForks = []config.HardFork{
	{Version: config.HF0Initial, Height: 0},
	{Version: config.HF1, Height: 0},
	{Version: config.HF2, Height: 0},
	{Version: config.HF3, Height: 0},
	{Version: config.HF4Zarcanum, Height: 0},
	{Version: config.HF5, Height: 0},
	{Version: config.HF6, Height: 100},
}

// storeBlocks inserts count blocks with constant intervals and difficulty.
func storeBlocks(t *testing.T, c *Chain, count int, interval uint64, baseDiff uint64) {
	t.Helper()
	for i := uint64(0); i < uint64(count); i++ {
		err := c.PutBlock(&types.Block{}, &BlockMeta{
			Hash:           types.Hash{byte(i + 1)},
			Height:         i,
			Timestamp:      i * interval,
			Difficulty:     baseDiff,
			CumulativeDiff: baseDiff * (i + 1),
		})
		require.NoError(t, err)
	}
}

func TestNextDifficulty_Genesis(t *testing.T) {
	s, err := store.New(":memory:")
	require.NoError(t, err)
	defer s.Close()

	c := New(s)
	diff, err := c.NextDifficulty(0, preHF6Forks)
	require.NoError(t, err)
	require.Equal(t, uint64(1), diff)
}

func TestNextDifficulty_FewBlocks(t *testing.T) {
	s, err := store.New(":memory:")
	require.NoError(t, err)
	defer s.Close()

	c := New(s)

	// Store genesis + 4 blocks with constant 120s intervals and difficulty 1000.
	// Genesis at height 0 is excluded from the LWMA window.
	storeBlocks(t, c, 5, 120, 1000)

	// Next difficulty for height 5 uses blocks 1-4 (n=3 intervals).
	// LWMA formula with constant D and T gives D/n = 1000/3 = 333.
	diff, err := c.NextDifficulty(5, preHF6Forks)
	require.NoError(t, err)
	require.Greater(t, diff, uint64(0))

	expected := uint64(333)
	require.Equal(t, expected, diff)
}

func TestNextDifficulty_EmptyChain(t *testing.T) {
	s, err := store.New(":memory:")
	require.NoError(t, err)
	defer s.Close()

	c := New(s)

	// Height 1 with no blocks stored -- should return starter difficulty.
	diff, err := c.NextDifficulty(1, preHF6Forks)
	require.NoError(t, err)
	require.Equal(t, uint64(1), diff)
}

// --- HF6 boundary tests ---

func TestNextDifficulty_HF6Boundary_Good(t *testing.T) {
	// Verify that blocks at height <= 100 use the 120s target and blocks
	// at height > 100 use the 240s target, given hf6ActiveForks.
	s, err := store.New(":memory:")
	require.NoError(t, err)
	defer s.Close()

	c := New(s)
	storeBlocks(t, c, 105, 120, 1000)

	// Height 100 -- HF6 activates at heights > 100, so this is pre-HF6.
	diffPre, err := c.NextDifficulty(100, hf6ActiveForks)
	require.NoError(t, err)

	// Height 101 -- HF6 is active (height > 100), target becomes 240s.
	diffPost, err := c.NextDifficulty(101, hf6ActiveForks)
	require.NoError(t, err)

	// With 120s actual intervals and a 240s target, LWMA should produce
	// lower difficulty than with a 120s target. The post-HF6 difficulty
	// should differ from the pre-HF6 difficulty because the target doubled.
	require.NotEqual(t, diffPre, diffPost,
		"difficulty should change across HF6 boundary (120s vs 240s target)")
}

func TestNextDifficulty_HF6Boundary_Bad(t *testing.T) {
	// HF6 at height 999,999,999 (mainnet default) -- should never activate
	// for realistic heights, so the target stays at 120s.
	s, err := store.New(":memory:")
	require.NoError(t, err)
	defer s.Close()

	c := New(s)
	storeBlocks(t, c, 105, 120, 1000)

	forks := config.MainnetForks
	diff100, err := c.NextDifficulty(100, forks)
	require.NoError(t, err)

	diff101, err := c.NextDifficulty(101, forks)
	require.NoError(t, err)

	// Both should use the same 120s target -- no HF6 in sight.
	require.Equal(t, diff100, diff101,
		"difficulty should be identical when HF6 is far in the future")
}

func TestNextDifficulty_HF6Boundary_Ugly(t *testing.T) {
	// HF6 at height 0 (active from genesis) -- the 240s target should
	// apply from the very first difficulty calculation.
	s, err := store.New(":memory:")
	require.NoError(t, err)
	defer s.Close()

	c := New(s)
	storeBlocks(t, c, 5, 240, 1000)

	genesisHF6 := []config.HardFork{
		{Version: config.HF0Initial, Height: 0},
		{Version: config.HF6, Height: 0},
	}

	diff, err := c.NextDifficulty(4, genesisHF6)
	require.NoError(t, err)
	require.Greater(t, diff, uint64(0))
}

// --- PoS difficulty tests ---

func TestNextPoSDifficulty_Good(t *testing.T) {
	s, err := store.New(":memory:")
	require.NoError(t, err)
	defer s.Close()

	c := New(s)
	storeBlocks(t, c, 5, 120, 1000)

	// Pre-HF6: PoS target should be 120s (same as PoW).
	diff, err := c.NextPoSDifficulty(5, preHF6Forks)
	require.NoError(t, err)
	require.Equal(t, uint64(333), diff)
}

func TestNextPoSDifficulty_HF6Boundary_Good(t *testing.T) {
	s, err := store.New(":memory:")
	require.NoError(t, err)
	defer s.Close()

	c := New(s)
	storeBlocks(t, c, 105, 120, 1000)

	// Height 100 -- pre-HF6.
	diffPre, err := c.NextPoSDifficulty(100, hf6ActiveForks)
	require.NoError(t, err)

	// Height 101 -- post-HF6, target becomes 240s.
	diffPost, err := c.NextPoSDifficulty(101, hf6ActiveForks)
	require.NoError(t, err)

	require.NotEqual(t, diffPre, diffPost,
		"PoS difficulty should change across HF6 boundary")
}

func TestNextPoSDifficulty_Genesis(t *testing.T) {
	s, err := store.New(":memory:")
	require.NoError(t, err)
	defer s.Close()

	c := New(s)
	diff, err := c.NextPoSDifficulty(0, preHF6Forks)
	require.NoError(t, err)
	require.Equal(t, uint64(1), diff)
}
