//go:build !integration

package consensus

import (
	"testing"
	"time"

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

func TestCheckTimestamp_Good(t *testing.T) {
	now := uint64(time.Now().Unix())

	// PoW block within limits.
	err := CheckTimestamp(now, 0, now, nil) // flags=0 -> PoW
	require.NoError(t, err)

	// With sufficient history, timestamp above median.
	timestamps := make([]uint64, config.TimestampCheckWindow)
	for i := range timestamps {
		timestamps[i] = now - 100 + uint64(i)
	}
	err = CheckTimestamp(now, 0, now, timestamps)
	require.NoError(t, err)
}

func TestCheckTimestamp_Bad(t *testing.T) {
	now := uint64(time.Now().Unix())

	// PoW block too far in future.
	future := now + config.BlockFutureTimeLimit + 1
	err := CheckTimestamp(future, 0, now, nil)
	assert.ErrorIs(t, err, ErrTimestampFuture)

	// PoS block too far in future (tighter limit).
	posFlags := uint8(1) // bit 0 = PoS
	posFuture := now + config.PosBlockFutureTimeLimit + 1
	err = CheckTimestamp(posFuture, posFlags, now, nil)
	assert.ErrorIs(t, err, ErrTimestampFuture)

	// Timestamp below median of last 60 blocks.
	timestamps := make([]uint64, config.TimestampCheckWindow)
	for i := range timestamps {
		timestamps[i] = now - 60 + uint64(i) // median ~ now - 30
	}
	oldTimestamp := now - 100 // well below median
	err = CheckTimestamp(oldTimestamp, 0, now, timestamps)
	assert.ErrorIs(t, err, ErrTimestampOld)
}

func TestCheckTimestamp_Ugly(t *testing.T) {
	now := uint64(time.Now().Unix())

	// Fewer than 60 timestamps: skip median check.
	timestamps := make([]uint64, 10)
	for i := range timestamps {
		timestamps[i] = now - 100
	}
	err := CheckTimestamp(now-200, 0, now, timestamps) // old but under 60 entries
	require.NoError(t, err)
}

func validMinerTx(height uint64) *types.Transaction {
	return &types.Transaction{
		Version: types.VersionInitial,
		Vin:     []types.TxInput{types.TxInputGenesis{Height: height}},
		Vout: []types.TxOutput{
			types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
		},
	}
}

func validMinerTxForForks(height uint64, forks []config.HardFork) *types.Transaction {
	tx := validMinerTx(height)
	tx.Version = expectedMinerTxVersion(forks, height)
	if tx.Version >= types.VersionPostHF5 {
		tx.HardforkID = config.VersionAtHeight(forks, height)
	}
	return tx
}

func TestValidateMinerTx_Good(t *testing.T) {
	tx := validMinerTx(100)
	err := ValidateMinerTx(tx, 100, config.MainnetForks)
	require.NoError(t, err)
}

func TestValidateMinerTx_Bad_WrongHeight(t *testing.T) {
	tx := validMinerTx(100)
	err := ValidateMinerTx(tx, 200, config.MainnetForks) // height mismatch
	assert.ErrorIs(t, err, ErrMinerTxHeight)
}

func TestValidateMinerTx_Bad_NoInputs(t *testing.T) {
	tx := &types.Transaction{Version: types.VersionPreHF4}
	err := ValidateMinerTx(tx, 100, config.MainnetForks)
	assert.ErrorIs(t, err, ErrMinerTxInputs)
}

func TestValidateMinerTx_Bad_WrongFirstInput(t *testing.T) {
	tx := &types.Transaction{
		Version: types.VersionPreHF4,
		Vin:     []types.TxInput{types.TxInputToKey{Amount: 1}},
	}
	err := ValidateMinerTx(tx, 100, config.MainnetForks)
	assert.ErrorIs(t, err, ErrMinerTxInputs)
}

func TestValidateMinerTx_Good_PoS(t *testing.T) {
	tx := &types.Transaction{
		Version: types.VersionPreHF4,
		Vin: []types.TxInput{
			types.TxInputGenesis{Height: 100},
			types.TxInputToKey{Amount: 1}, // PoS stake input
		},
		Vout: []types.TxOutput{
			types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
		},
	}
	err := ValidateMinerTx(tx, 100, config.MainnetForks)
	// 2 inputs with genesis + TxInputToKey is valid PoS structure.
	require.NoError(t, err)
}

func TestValidateMinerTx_Good_PoS_ZCAfterHF4(t *testing.T) {
	tx := &types.Transaction{
		Version: types.VersionPostHF4,
		Vin: []types.TxInput{
			types.TxInputGenesis{Height: 101},
			types.TxInputZC{KeyImage: types.KeyImage{1}},
		},
		Vout: []types.TxOutput{
			types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
		},
	}
	err := ValidateMinerTx(tx, 101, config.TestnetForks)
	require.NoError(t, err)
}

func TestValidateMinerTx_Bad_PoS_UnsupportedStakeInput(t *testing.T) {
	tx := &types.Transaction{
		Version: types.VersionPostHF4,
		Vin: []types.TxInput{
			types.TxInputGenesis{Height: 101},
			types.TxInputHTLC{Amount: 1, KeyImage: types.KeyImage{1}},
		},
		Vout: []types.TxOutput{
			types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
		},
	}
	err := ValidateMinerTx(tx, 101, config.TestnetForks)
	assert.ErrorIs(t, err, ErrMinerTxInputs)
}

func TestValidateMinerTx_Version_Good(t *testing.T) {
	tests := []struct {
		name   string
		forks  []config.HardFork
		tx     *types.Transaction
		height uint64
	}{
		{
			name:   "mainnet_pre_hf1_v0",
			forks:  config.MainnetForks,
			height: 100,
			tx:     validMinerTx(100),
		},
		{
			name:   "mainnet_post_hf1_pre_hf4_v1",
			forks:  config.MainnetForks,
			height: 10081,
			tx: &types.Transaction{
				Version: types.VersionPreHF4,
				Vin:     []types.TxInput{types.TxInputGenesis{Height: 10081}},
				Vout:    []types.TxOutput{types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}},
			},
		},
		{
			name:   "testnet_post_hf4_v2",
			forks:  config.TestnetForks,
			height: 101,
			tx: &types.Transaction{
				Version: types.VersionPostHF4,
				Vin:     []types.TxInput{types.TxInputGenesis{Height: 101}},
				Vout:    []types.TxOutput{types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}},
			},
		},
		{
			name:   "testnet_post_hf5_v3",
			forks:  config.TestnetForks,
			height: 201,
			tx: &types.Transaction{
				Version:    types.VersionPostHF5,
				HardforkID: config.HF5,
				Vin:        []types.TxInput{types.TxInputGenesis{Height: 201}},
				Vout:       []types.TxOutput{types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := ValidateMinerTx(tt.tx, tt.height, tt.forks)
			require.NoError(t, err)
		})
	}
}

func TestValidateMinerTx_Version_Bad(t *testing.T) {
	tests := []struct {
		name   string
		forks  []config.HardFork
		height uint64
		tx     *types.Transaction
	}{
		{
			name:   "mainnet_pre_hf1_v1",
			forks:  config.MainnetForks,
			height: 100,
			tx: &types.Transaction{
				Version: types.VersionPreHF4,
				Vin:     []types.TxInput{types.TxInputGenesis{Height: 100}},
				Vout:    []types.TxOutput{types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}},
			},
		},
		{
			name:   "mainnet_post_hf1_pre_hf4_v0",
			forks:  config.MainnetForks,
			height: 10081,
			tx: &types.Transaction{
				Version: types.VersionInitial,
				Vin:     []types.TxInput{types.TxInputGenesis{Height: 10081}},
				Vout:    []types.TxOutput{types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}},
			},
		},
		{
			name:   "testnet_post_hf4_v1",
			forks:  config.TestnetForks,
			height: 101,
			tx: &types.Transaction{
				Version: types.VersionPreHF4,
				Vin:     []types.TxInput{types.TxInputGenesis{Height: 101}},
				Vout:    []types.TxOutput{types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}},
			},
		},
		{
			name:   "testnet_post_hf5_wrong_hardfork_id",
			forks:  config.TestnetForks,
			height: 201,
			tx: &types.Transaction{
				Version:    types.VersionPostHF5,
				HardforkID: config.HF4Zarcanum,
				Vin:        []types.TxInput{types.TxInputGenesis{Height: 201}},
				Vout:       []types.TxOutput{types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := ValidateMinerTx(tt.tx, tt.height, tt.forks)
			assert.ErrorIs(t, err, ErrMinerTxVersion)
		})
	}
}

func TestValidateBlockReward_Good(t *testing.T) {
	height := uint64(100)
	tx := validMinerTx(height)
	err := ValidateBlockReward(tx, height, 1000, config.BlockGrantedFullRewardZone, 0, config.MainnetForks)
	require.NoError(t, err)
}

func TestValidateBlockReward_Bad_TooMuch(t *testing.T) {
	height := uint64(100)
	tx := &types.Transaction{
		Version: types.VersionPreHF4,
		Vin:     []types.TxInput{types.TxInputGenesis{Height: height}},
		Vout: []types.TxOutput{
			types.TxOutputBare{Amount: config.BlockReward + 1, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
		},
	}
	err := ValidateBlockReward(tx, height, 1000, config.BlockGrantedFullRewardZone, 0, config.MainnetForks)
	assert.ErrorIs(t, err, ErrRewardMismatch)
}

func TestValidateBlockReward_Good_WithFees(t *testing.T) {
	height := uint64(100)
	fees := uint64(50_000_000_000)
	tx := &types.Transaction{
		Version: types.VersionPreHF4,
		Vin:     []types.TxInput{types.TxInputGenesis{Height: height}},
		Vout: []types.TxOutput{
			types.TxOutputBare{Amount: config.BlockReward + fees, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
		},
	}
	err := ValidateBlockReward(tx, height, 1000, config.BlockGrantedFullRewardZone, fees, config.MainnetForks)
	require.NoError(t, err)
}

func TestValidateBlockReward_Good_ZarcanumOutputs(t *testing.T) {
	height := uint64(100)
	tx := &types.Transaction{
		Version: types.VersionPostHF4,
		Vin:     []types.TxInput{types.TxInputGenesis{Height: height}},
		Vout: []types.TxOutput{
			types.TxOutputZarcanum{
				StealthAddress:   types.PublicKey{1},
				ConcealingPoint:  types.PublicKey{2},
				AmountCommitment: types.PublicKey{3},
				BlindedAssetID:   types.PublicKey{4},
				EncryptedAmount:  config.BlockReward / 2,
			},
			types.TxOutputZarcanum{
				StealthAddress:   types.PublicKey{5},
				ConcealingPoint:  types.PublicKey{6},
				AmountCommitment: types.PublicKey{7},
				BlindedAssetID:   types.PublicKey{8},
				EncryptedAmount:  config.BlockReward / 2,
			},
		},
	}

	err := ValidateBlockReward(tx, height, 1000, config.BlockGrantedFullRewardZone, 0, config.MainnetForks)
	require.NoError(t, err)
}

func TestValidateBlockReward_Bad_ZarcanumOutputs(t *testing.T) {
	height := uint64(100)
	tx := &types.Transaction{
		Version: types.VersionPostHF4,
		Vin:     []types.TxInput{types.TxInputGenesis{Height: height}},
		Vout: []types.TxOutput{
			types.TxOutputZarcanum{
				StealthAddress:   types.PublicKey{1},
				ConcealingPoint:  types.PublicKey{2},
				AmountCommitment: types.PublicKey{3},
				BlindedAssetID:   types.PublicKey{4},
				EncryptedAmount:  config.BlockReward + 1,
			},
		},
	}

	err := ValidateBlockReward(tx, height, 1000, config.BlockGrantedFullRewardZone, 0, config.MainnetForks)
	assert.ErrorIs(t, err, ErrRewardMismatch)
}

func TestValidateBlock_Good(t *testing.T) {
	now := uint64(time.Now().Unix())
	height := uint64(100)
	blk := &types.Block{
		BlockHeader: types.BlockHeader{
			MajorVersion: 0, // pre-HF1 on mainnet
			Timestamp:    now,
			Flags:        0, // PoW
		},
		MinerTx: *validMinerTx(height),
	}

	err := ValidateBlock(blk, height, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, config.MainnetForks)
	require.NoError(t, err)
}

func TestValidateBlock_Bad_Timestamp(t *testing.T) {
	now := uint64(time.Now().Unix())
	height := uint64(100)
	blk := &types.Block{
		BlockHeader: types.BlockHeader{
			MajorVersion: 0, // pre-HF1 on mainnet
			Timestamp:    now + config.BlockFutureTimeLimit + 100,
			Flags:        0,
		},
		MinerTx: *validMinerTx(height),
	}

	err := ValidateBlock(blk, height, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, config.MainnetForks)
	assert.ErrorIs(t, err, ErrTimestampFuture)
}

func TestValidateBlock_Bad_MinerTx(t *testing.T) {
	now := uint64(time.Now().Unix())
	height := uint64(100)
	blk := &types.Block{
		BlockHeader: types.BlockHeader{
			MajorVersion: 0, // pre-HF1 on mainnet
			Timestamp:    now,
			Flags:        0,
		},
		MinerTx: *validMinerTx(200), // wrong height
	}

	err := ValidateBlock(blk, height, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, config.MainnetForks)
	assert.ErrorIs(t, err, ErrMinerTxHeight)
}

// --- Block major version tests (Task 10) ---

func TestExpectedBlockMajorVersion_Good(t *testing.T) {
	tests := []struct {
		name   string
		forks  []config.HardFork
		height uint64
		want   uint8
	}{
		// --- Mainnet ---
		{
			name:   "mainnet/genesis",
			forks:  config.MainnetForks,
			height: 0,
			want:   config.BlockMajorVersionInitial, // 0
		},
		{
			name:   "mainnet/pre_HF1",
			forks:  config.MainnetForks,
			height: 5000,
			want:   config.BlockMajorVersionInitial, // 0
		},
		{
			name:   "mainnet/at_HF1_boundary",
			forks:  config.MainnetForks,
			height: 10080,
			want:   config.BlockMajorVersionInitial, // 0 (fork at height > 10080)
		},
		{
			name:   "mainnet/post_HF1",
			forks:  config.MainnetForks,
			height: 10081,
			want:   config.HF1BlockMajorVersion, // 1
		},
		{
			name:   "mainnet/well_past_HF1",
			forks:  config.MainnetForks,
			height: 100000,
			want:   config.HF1BlockMajorVersion, // 1 (HF3 not yet active)
		},

		// --- Testnet (HF3 active from genesis) ---
		{
			name:   "testnet/genesis",
			forks:  config.TestnetForks,
			height: 0,
			want:   config.HF3BlockMajorVersion, // 2 (HF3 at 0)
		},
		{
			name:   "testnet/pre_HF4",
			forks:  config.TestnetForks,
			height: 50,
			want:   config.HF3BlockMajorVersion, // 2 (HF4 at >100)
		},
		{
			name:   "testnet/post_HF4",
			forks:  config.TestnetForks,
			height: 101,
			want:   config.CurrentBlockMajorVersion, // 3
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := expectedBlockMajorVersion(tt.forks, tt.height)
			if got != tt.want {
				t.Errorf("expectedBlockMajorVersion(%d) = %d, want %d", tt.height, got, tt.want)
			}
		})
	}
}

func TestCheckBlockVersion_Good(t *testing.T) {
	now := uint64(time.Now().Unix())
	// Correct version at each mainnet/testnet era.
	tests := []struct {
		name    string
		version uint8
		height  uint64
		forks   []config.HardFork
	}{
		{"mainnet/v0_pre_HF1", config.BlockMajorVersionInitial, 5000, config.MainnetForks},
		{"mainnet/v1_post_HF1", config.HF1BlockMajorVersion, 10081, config.MainnetForks},
		{"testnet/v2_genesis", config.HF3BlockMajorVersion, 0, config.TestnetForks},
		{"testnet/v3_post_HF4", config.CurrentBlockMajorVersion, 101, config.TestnetForks},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			blk := &types.Block{
				BlockHeader: types.BlockHeader{
					MajorVersion: tt.version,
					Timestamp:    now,
					Flags:        0,
				},
			}
			err := checkBlockVersion(blk.MajorVersion, tt.forks, tt.height)
			require.NoError(t, err)
		})
	}
}

func TestCheckBlockVersion_Bad(t *testing.T) {
	now := uint64(time.Now().Unix())
	tests := []struct {
		name    string
		version uint8
		height  uint64
		forks   []config.HardFork
	}{
		{"mainnet/v1_pre_HF1", config.HF1BlockMajorVersion, 5000, config.MainnetForks},
		{"mainnet/v0_post_HF1", config.BlockMajorVersionInitial, 10081, config.MainnetForks},
		{"mainnet/v2_post_HF1", config.HF3BlockMajorVersion, 10081, config.MainnetForks},
		{"testnet/v1_genesis", config.HF1BlockMajorVersion, 0, config.TestnetForks},
		{"testnet/v2_post_HF4", config.HF3BlockMajorVersion, 101, config.TestnetForks},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			blk := &types.Block{
				BlockHeader: types.BlockHeader{
					MajorVersion: tt.version,
					Timestamp:    now,
					Flags:        0,
				},
			}
			err := checkBlockVersion(blk.MajorVersion, tt.forks, tt.height)
			assert.ErrorIs(t, err, ErrBlockVersion)
		})
	}
}

func TestCheckBlockVersion_Ugly(t *testing.T) {
	now := uint64(time.Now().Unix())

	// Version 255 should never be valid at any height.
	blk := &types.Block{
		BlockHeader: types.BlockHeader{MajorVersion: 255, Timestamp: now},
	}
	err := checkBlockVersion(blk.MajorVersion, config.MainnetForks, 0)
	assert.ErrorIs(t, err, ErrBlockVersion)

	err = checkBlockVersion(blk.MajorVersion, config.MainnetForks, 10081)
	assert.ErrorIs(t, err, ErrBlockVersion)

	// Version 0 at the exact HF1 boundary (height 10080 -- fork not yet active).
	blk0 := &types.Block{
		BlockHeader: types.BlockHeader{MajorVersion: config.BlockMajorVersionInitial, Timestamp: now},
	}
	err = checkBlockVersion(blk0.MajorVersion, config.MainnetForks, 10080)
	require.NoError(t, err)
}

func TestValidateBlock_MajorVersion_Good(t *testing.T) {
	now := uint64(time.Now().Unix())
	tests := []struct {
		name    string
		forks   []config.HardFork
		height  uint64
		version uint8
	}{
		// Mainnet: pre-HF1 expects version 0.
		{name: "mainnet_preHF1", forks: config.MainnetForks, height: 5000, version: 0},
		// Mainnet: post-HF1 expects version 1.
		{name: "mainnet_postHF1", forks: config.MainnetForks, height: 20000, version: 1},
		// Testnet: HF1 active from genesis, HF3 active from genesis, expects version 2.
		{name: "testnet_genesis", forks: config.TestnetForks, height: 5, version: 2},
		// Testnet: post-HF4 (height > 100) expects version 3.
		{name: "testnet_postHF4", forks: config.TestnetForks, height: 200, version: 3},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			blk := &types.Block{
				BlockHeader: types.BlockHeader{
					MajorVersion: tt.version,
					Timestamp:    now,
					Flags:        0,
				},
				MinerTx: *validMinerTxForForks(tt.height, tt.forks),
			}
			err := ValidateBlock(blk, tt.height, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, tt.forks)
			require.NoError(t, err)
		})
	}
}

func TestValidateBlock_MajorVersion_Bad(t *testing.T) {
	now := uint64(time.Now().Unix())
	tests := []struct {
		name    string
		forks   []config.HardFork
		height  uint64
		version uint8
	}{
		// Mainnet: pre-HF1 with wrong version 1.
		{name: "mainnet_preHF1_v1", forks: config.MainnetForks, height: 5000, version: 1},
		// Mainnet: post-HF1 with wrong version 0.
		{name: "mainnet_postHF1_v0", forks: config.MainnetForks, height: 20000, version: 0},
		// Mainnet: post-HF1 with wrong version 2.
		{name: "mainnet_postHF1_v2", forks: config.MainnetForks, height: 20000, version: 2},
		// Testnet: post-HF4 with wrong version 2.
		{name: "testnet_postHF4_v2", forks: config.TestnetForks, height: 200, version: 2},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			blk := &types.Block{
				BlockHeader: types.BlockHeader{
					MajorVersion: tt.version,
					Timestamp:    now,
					Flags:        0,
				},
				MinerTx: *validMinerTxForForks(tt.height, tt.forks),
			}
			err := ValidateBlock(blk, tt.height, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, tt.forks)
			assert.ErrorIs(t, err, ErrBlockMajorVersion)
		})
	}
}

func TestValidateBlock_MajorVersion_Ugly(t *testing.T) {
	now := uint64(time.Now().Unix())
	// Boundary test: exactly at HF1 activation height (10080) on mainnet.
	// HF1 activates at heights strictly greater than 10080, so at height
	// 10080 itself HF1 is NOT active; version must be 0.
	blk := &types.Block{
		BlockHeader: types.BlockHeader{
			MajorVersion: 0,
			Timestamp:    now,
			Flags:        0,
		},
		MinerTx: *validMinerTx(10080),
	}
	err := ValidateBlock(blk, 10080, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, config.MainnetForks)
	require.NoError(t, err)

	// At height 10081, HF1 IS active; version must be 1.
	blk2 := &types.Block{
		BlockHeader: types.BlockHeader{
			MajorVersion: 1,
			Timestamp:    now,
			Flags:        0,
		},
		MinerTx: *validMinerTx(10081),
	}
	err = ValidateBlock(blk2, 10081, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, config.MainnetForks)
	require.NoError(t, err)
}
