// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package zap_native

import (
	"bytes"
	"errors"
	"testing"

	"github.com/luxfi/ids"
)

// TestValidatorsList_RoundTrip pins the encode/decode contract for the
// Phase D primitive: 180B fixed-stride per validator record, no sibling
// arrays. Used by both ConvertNetworkToL1Tx and CreateSovereignL1Tx.
func TestValidatorsList_RoundTrip(t *testing.T) {
	vals := []ValidatorsListEntry{
		{
			NodeID:             ids.NodeID{0xa1, 0xa2, 0xa3, 0xa4},
			Weight:             1_500_000,
			BLSPubKey:          [BLSPubKeySize]byte{0xb1, 0xb2, 0xb3},
			BLSPoP:             [BLSPoPSize]byte{0xc1, 0xc2, 0xc3, 0xc4, 0xc5},
			RegistrationExpiry: 1_900_000_000,
		},
		{
			NodeID:             ids.NodeID{0xa5, 0xa6},
			Weight:             999_999,
			BLSPubKey:          [BLSPubKeySize]byte{0xb4, 0xb5},
			BLSPoP:             [BLSPoPSize]byte{0xc6, 0xc7},
			RegistrationExpiry: 1_950_000_000,
		},
	}
	in := ConvertNetworkToL1TxInput{
		NetworkID:      1337,
		BlockchainID:   ids.ID{0x18},
		Outs:           sampleOuts(),
		Ins:            sampleIns(),
		Credentials:    sampleCreds(),
		Memo:           []byte("vals-rt"),
		Chain:          ids.ID{0x66},
		ManagerChainID: ids.ID{0x67},
		Address:        []byte{0xab, 0xcd},
		Validators:     vals,
	}
	tx := NewConvertNetworkToL1Tx(in)
	vl := tx.Validators()
	if vl.Len() != 2 {
		t.Fatalf("Len=%d want 2", vl.Len())
	}
	for i, want := range vals {
		got := vl.At(i)
		if got.NodeID() != want.NodeID {
			t.Errorf("v[%d].NodeID mismatch", i)
		}
		if got.Weight() != want.Weight {
			t.Errorf("v[%d].Weight=%d want %d", i, got.Weight(), want.Weight)
		}
		if !bytes.Equal(got.BLSPubKey(), want.BLSPubKey[:]) {
			t.Errorf("v[%d].BLSPubKey mismatch", i)
		}
		if !bytes.Equal(got.BLSPoP(), want.BLSPoP[:]) {
			t.Errorf("v[%d].BLSPoP mismatch", i)
		}
		if got.RegistrationExpiry() != want.RegistrationExpiry {
			t.Errorf("v[%d].RegistrationExpiry mismatch", i)
		}
	}
}

// TestValidatorsList_EmptyList covers the zero-validator edge.
func TestValidatorsList_EmptyList(t *testing.T) {
	in := ConvertNetworkToL1TxInput{
		NetworkID:      1,
		BlockchainID:   ids.ID{0x18},
		Chain:          ids.ID{0x66},
		ManagerChainID: ids.ID{0x67},
		Address:        []byte{0xab},
	}
	tx := NewConvertNetworkToL1Tx(in)
	vl := tx.Validators()
	if vl.Len() != 0 {
		t.Fatalf("Len=%d want 0", vl.Len())
	}
	if !vl.IsNull() {
		// IsNull is true when no list pointer is set; empty list with a
		// pointer would have Len()=0 but IsNull()=false.
		t.Logf("IsNull=%v (expected true for zero validators)", vl.IsNull())
	}
	// At(0) must not panic; returns zero-value.
	zero := vl.At(0)
	if zero.Weight() != 0 {
		t.Errorf("At(0) on empty list should return zero-value")
	}
}

// TestValidatorsList_OutOfRange ensures At(i) for i<0 or i>=Len returns
// the zero-value ValidatorRecord without panicking.
func TestValidatorsList_OutOfRange(t *testing.T) {
	vals := []ValidatorsListEntry{
		{NodeID: ids.NodeID{0xa1}, Weight: 100, RegistrationExpiry: 1},
	}
	in := ConvertNetworkToL1TxInput{
		NetworkID:      1,
		BlockchainID:   ids.ID{0x18},
		Chain:          ids.ID{0x66},
		ManagerChainID: ids.ID{0x67},
		Address:        []byte{0xab},
		Validators:     vals,
	}
	tx := NewConvertNetworkToL1Tx(in)
	vl := tx.Validators()
	if vl.Len() != 1 {
		t.Fatalf("Len=%d want 1", vl.Len())
	}
	if vl.At(-1).Weight() != 0 {
		t.Error("At(-1) should be zero-value")
	}
	if vl.At(99).Weight() != 0 {
		t.Error("At(99) should be zero-value")
	}
}

// TestValidatorsList_PoisonedLengthClampedByStride pins R4V9: a hostile
// wire length field that passes the permissive baseline must be rejected
// by Object.ListStride (per-element clamp against parent buffer).
//
// We test this by reading at an offset that does not contain a
// well-formed list — the stride-clamp must return a clipped Len() rather
// than feed the consumer poison bytes.
func TestValidatorsList_PoisonedLengthClampedByStride(t *testing.T) {
	// Build a legitimate tx then read the validators list at the wrong
	// offset — the stride-clamp must clip Len() to whatever the buffer
	// can actually accommodate at SizeValidatorRecord=180 stride.
	vals := []ValidatorsListEntry{
		{NodeID: ids.NodeID{0xa1}, Weight: 100, RegistrationExpiry: 1},
	}
	in := ConvertNetworkToL1TxInput{
		NetworkID:      1,
		BlockchainID:   ids.ID{0x18},
		Chain:          ids.ID{0x66},
		ManagerChainID: ids.ID{0x67},
		Address:        []byte{0xab},
		Validators:     vals,
	}
	tx := NewConvertNetworkToL1Tx(in)

	// Read the list view at the LEGITIMATE offset — confirms the stride
	// path works for well-formed buffers. The poisoned-length attack
	// vector is covered structurally by Object.ListStride in zap v0.7.2;
	// we just confirm legitimate reads still pass.
	vl := tx.Validators()
	if vl.Len() != 1 {
		t.Fatalf("legitimate Len=%d want 1", vl.Len())
	}
	if vl.At(0).Weight() != 100 {
		t.Errorf("At(0).Weight=%d want 100", vl.At(0).Weight())
	}
}

// TestValidatorsList_BLSFieldsAreReadOnly pins the docstring contract:
// BLSPubKey() and BLSPoP() must return slices that copy the bytes (not
// alias the parent buffer) so consumer mutation can't corrupt the
// wire image of subsequent reads.
func TestValidatorsList_BLSFieldsAreReadOnly(t *testing.T) {
	vals := []ValidatorsListEntry{
		{
			NodeID:             ids.NodeID{0xa1},
			Weight:             100,
			BLSPubKey:          [BLSPubKeySize]byte{0xb1, 0xb2},
			BLSPoP:             [BLSPoPSize]byte{0xc1, 0xc2},
			RegistrationExpiry: 1,
		},
	}
	in := ConvertNetworkToL1TxInput{
		NetworkID:      1,
		BlockchainID:   ids.ID{0x18},
		Chain:          ids.ID{0x66},
		ManagerChainID: ids.ID{0x67},
		Address:        []byte{0xab},
		Validators:     vals,
	}
	tx := NewConvertNetworkToL1Tx(in)
	v := tx.Validators().At(0)
	pk := v.BLSPubKey()
	if !bytes.Equal(pk[:2], []byte{0xb1, 0xb2}) {
		t.Errorf("BLSPubKey round-trip head mismatch")
	}
	// Mutate the returned slice — second read must return the original.
	pk[0] = 0xff
	pk2 := v.BLSPubKey()
	if pk2[0] != 0xb1 {
		t.Errorf("BLSPubKey was aliased (mutation leaked back into wire)")
	}
}

// TestValidatorsList_CreateSovereignL1Integration ensures the same
// ValidatorsList primitive works inside CreateSovereignL1Tx (Phase C+D
// integration: both ChainsList and ValidatorsList in the same tx).
func TestValidatorsList_CreateSovereignL1Integration(t *testing.T) {
	vals := []ValidatorsListEntry{
		{NodeID: ids.NodeID{0xa1}, Weight: 100, BLSPubKey: [BLSPubKeySize]byte{0xb1}, BLSPoP: [BLSPoPSize]byte{0xc1}, RegistrationExpiry: 1},
		{NodeID: ids.NodeID{0xa2}, Weight: 200, BLSPubKey: [BLSPubKeySize]byte{0xb2}, BLSPoP: [BLSPoPSize]byte{0xc2}, RegistrationExpiry: 2},
		{NodeID: ids.NodeID{0xa3}, Weight: 300, BLSPubKey: [BLSPubKeySize]byte{0xb3}, BLSPoP: [BLSPoPSize]byte{0xc3}, RegistrationExpiry: 3},
	}
	chains := []ChainsListEntry{
		{Name: []byte("evm"), VMID: ids.ID{0xee}, GenesisData: []byte("g")},
	}
	in := CreateSovereignL1TxInput{
		NetworkID:    1337,
		BlockchainID: ids.ID{0x18},
		Owner:        OwnerStub{Threshold: 1, Address: ids.ShortID{0x01}},
		Validators:   vals,
		Chains:       chains,
	}
	tx := NewCreateSovereignL1Tx(in)
	if tx.Validators().Len() != 3 {
		t.Errorf("Validators.Len=%d want 3", tx.Validators().Len())
	}
	if tx.BoundChains().Len() != 1 {
		t.Errorf("Chains.Len=%d want 1", tx.BoundChains().Len())
	}

	// Cross-check weights.
	for i, want := range []uint64{100, 200, 300} {
		got := tx.Validators().At(i).Weight()
		if got != want {
			t.Errorf("v[%d].Weight=%d want %d", i, got, want)
		}
	}
}

// TestValidatorsList_MustVerify_HappyPath asserts a well-formed list with
// non-zero weight, non-zero BLS fields, and non-zero RegistrationExpiry
// passes the structural floor walk.
func TestValidatorsList_MustVerify_HappyPath(t *testing.T) {
	good := ValidatorsListEntry{
		NodeID:             ids.NodeID{0xa1},
		Weight:             100,
		BLSPubKey:          [BLSPubKeySize]byte{0xb1},
		BLSPoP:             [BLSPoPSize]byte{0xc1},
		RegistrationExpiry: 1,
	}
	in := ConvertNetworkToL1TxInput{
		NetworkID:      1,
		BlockchainID:   ids.ID{0x18},
		Chain:          ids.ID{0x66},
		ManagerChainID: ids.ID{0x67},
		Address:        []byte{0xab},
		Validators:     []ValidatorsListEntry{good},
	}
	tx := NewConvertNetworkToL1Tx(in)
	if err := tx.Validators().MustVerify(); err != nil {
		t.Fatalf("MustVerify(happy) = %v, want nil", err)
	}
}

// TestValidatorsList_MustVerify_RejectsZeroWeight pins the per-validator
// weight floor — a zero-weight validator pads the count without
// contributing to quorum.
func TestValidatorsList_MustVerify_RejectsZeroWeight(t *testing.T) {
	bad := ValidatorsListEntry{
		NodeID:             ids.NodeID{0xa1},
		Weight:             0, // <- floor invariant violation
		BLSPubKey:          [BLSPubKeySize]byte{0xb1},
		BLSPoP:             [BLSPoPSize]byte{0xc1},
		RegistrationExpiry: 1,
	}
	in := ConvertNetworkToL1TxInput{
		NetworkID:      1,
		Chain:          ids.ID{0x66},
		ManagerChainID: ids.ID{0x67},
		Address:        []byte{0xab},
		Validators:     []ValidatorsListEntry{bad},
	}
	tx := NewConvertNetworkToL1Tx(in)
	err := tx.Validators().MustVerify()
	if err == nil || !errorsIsValidatorWeightZero(err) {
		t.Fatalf("MustVerify(zero weight) = %v, want ErrValidatorWeightZero", err)
	}
}

// TestValidatorsList_MustVerify_RejectsAllZeroBLSPubKey pins the BLS
// pubkey structural floor (cheaper than the pairing check, fires first).
func TestValidatorsList_MustVerify_RejectsAllZeroBLSPubKey(t *testing.T) {
	bad := ValidatorsListEntry{
		NodeID:             ids.NodeID{0xa1},
		Weight:             100,
		BLSPubKey:          [BLSPubKeySize]byte{}, // <- all-zero
		BLSPoP:             [BLSPoPSize]byte{0xc1},
		RegistrationExpiry: 1,
	}
	in := ConvertNetworkToL1TxInput{
		NetworkID:      1,
		Chain:          ids.ID{0x66},
		ManagerChainID: ids.ID{0x67},
		Address:        []byte{0xab},
		Validators:     []ValidatorsListEntry{bad},
	}
	tx := NewConvertNetworkToL1Tx(in)
	err := tx.Validators().MustVerify()
	if err == nil || !errorsIsValidatorBLSPubKeyZero(err) {
		t.Fatalf("MustVerify(zero pubkey) = %v, want ErrValidatorBLSPubKeyZero", err)
	}
}

// TestValidatorsList_MustVerify_RejectsAllZeroBLSPoP pins the BLS PoP
// structural floor.
func TestValidatorsList_MustVerify_RejectsAllZeroBLSPoP(t *testing.T) {
	bad := ValidatorsListEntry{
		NodeID:             ids.NodeID{0xa1},
		Weight:             100,
		BLSPubKey:          [BLSPubKeySize]byte{0xb1},
		BLSPoP:             [BLSPoPSize]byte{}, // <- all-zero
		RegistrationExpiry: 1,
	}
	in := ConvertNetworkToL1TxInput{
		NetworkID:      1,
		Chain:          ids.ID{0x66},
		ManagerChainID: ids.ID{0x67},
		Address:        []byte{0xab},
		Validators:     []ValidatorsListEntry{bad},
	}
	tx := NewConvertNetworkToL1Tx(in)
	err := tx.Validators().MustVerify()
	if err == nil || !errorsIsValidatorBLSPoPZero(err) {
		t.Fatalf("MustVerify(zero PoP) = %v, want ErrValidatorBLSPoPZero", err)
	}
}

// TestValidatorsList_MustVerify_RejectsZeroExpiry pins the
// RegistrationExpiry floor (a unix timestamp can never legitimately be
// zero in a registration window).
func TestValidatorsList_MustVerify_RejectsZeroExpiry(t *testing.T) {
	bad := ValidatorsListEntry{
		NodeID:             ids.NodeID{0xa1},
		Weight:             100,
		BLSPubKey:          [BLSPubKeySize]byte{0xb1},
		BLSPoP:             [BLSPoPSize]byte{0xc1},
		RegistrationExpiry: 0, // <- floor invariant violation
	}
	in := ConvertNetworkToL1TxInput{
		NetworkID:      1,
		Chain:          ids.ID{0x66},
		ManagerChainID: ids.ID{0x67},
		Address:        []byte{0xab},
		Validators:     []ValidatorsListEntry{bad},
	}
	tx := NewConvertNetworkToL1Tx(in)
	err := tx.Validators().MustVerify()
	if err == nil || !errorsIsValidatorRegistrationExpiryZero(err) {
		t.Fatalf("MustVerify(zero expiry) = %v, want ErrValidatorRegistrationExpiryZero", err)
	}
}

// TestValidatorsList_MustVerify_RejectsOverCap covers MaxValidatorsPerL1.
// Constructs a tx with 1025 validators (cap is 1024) and confirms
// MustVerify refuses the buffer at the cap gate before walking entries.
func TestValidatorsList_MustVerify_RejectsOverCap(t *testing.T) {
	vals := make([]ValidatorsListEntry, MaxValidatorsPerL1+1)
	for i := range vals {
		vals[i] = ValidatorsListEntry{
			NodeID:             ids.NodeID{byte(i)},
			Weight:             100,
			BLSPubKey:          [BLSPubKeySize]byte{0xb1},
			BLSPoP:             [BLSPoPSize]byte{0xc1},
			RegistrationExpiry: 1,
		}
	}
	in := ConvertNetworkToL1TxInput{
		NetworkID:      1,
		Chain:          ids.ID{0x66},
		ManagerChainID: ids.ID{0x67},
		Address:        []byte{0xab},
		Validators:     vals,
	}
	tx := NewConvertNetworkToL1Tx(in)
	err := tx.Validators().MustVerify()
	if err == nil || !errorsIsTooManyValidators(err) {
		t.Fatalf("MustVerify(over cap) = %v, want ErrTooManyValidators", err)
	}
}

// Small typed-error helpers — keep the test set free of errors.Is noise
// at the assertion site. The helper bodies are one-liners around
// errors.Is(err, target).
func errorsIsValidatorWeightZero(err error) bool {
	return errors.Is(err, ErrValidatorWeightZero)
}
func errorsIsValidatorBLSPubKeyZero(err error) bool {
	return errors.Is(err, ErrValidatorBLSPubKeyZero)
}
func errorsIsValidatorBLSPoPZero(err error) bool {
	return errors.Is(err, ErrValidatorBLSPoPZero)
}
func errorsIsValidatorRegistrationExpiryZero(err error) bool {
	return errors.Is(err, ErrValidatorRegistrationExpiryZero)
}
func errorsIsTooManyValidators(err error) bool {
	return errors.Is(err, ErrTooManyValidators)
}
