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

// Adversarial security tests for LP-023 Phase 1 batch 2.
//
// One test per attack vector probed against the native-ZAP tx accessors. Each
// test name follows the convention TestRed_<vector>; failing tests indicate
// either (a) a real defense gap that needs Blue rework, or (b) the test
// captures the attack and demonstrates the defense (passing).
//
// Methodology: feed crafted ZAP buffers directly through Wrap*/Parse and
// observe what the accessors return. If a malformed buffer is rejected at
// Parse, the defense is at the wire layer. If Parse succeeds but accessors
// return zero / nil (silent failure mode of the ZAP Object methods), the
// defense relies on the executor catching invariants — this layered defense
// is documented in each test.
//
// Cross-references to executor-layer defenses are noted inline. The wire
// layer (this package) is conservative: it never panics, never reads OOB,
// and never returns sentinel-corrupt values that look like data. A few
// known ambiguities (nil vs []byte{} Memo, relOffset malleability) are
// captured as INFO-level findings.

package zap_native

import (
	"bytes"
	"encoding/binary"
	"runtime"
	"sync"
	"testing"

	"github.com/luxfi/crypto/bls"
	"github.com/luxfi/ids"
	"github.com/luxfi/zap"
)

// -----------------------------------------------------------------------------
// V1 — Crafted-offset OOB read.
// -----------------------------------------------------------------------------
//
// Threat: a buffer where zap.Parse succeeds (16-byte header validates) but is
// shorter than HeaderSize + 16 + SizeXxxTx. Reading SlashPercentage at offset
// 52 of a 16-byte buffer should not OOB-read.
//
// Observed defense (zap/zap.go::Object.Uint32):
//
//	if pos+4 > len(o.msg.data) { return 0 }
//
// All Uint8/16/32/64 accessors apply this bounds check. So a too-small buffer
// produces silently-zero field reads — no crash, no OOB. The downstream
// executor catches the resulting all-zero field state (e.g. SlashPercentage=0
// !∈ {100_000, 500_000} fails the enum check in slash_validator_tx_executor.go).
//
// Result: PASS — wire-layer defense holds. Documented layered defense.

func TestRed_V1_CraftedOffsetOOBRead(t *testing.T) {
	// Build a minimal valid SlashValidatorTx, then truncate the payload so the
	// buffer is just header + a few bytes — too small for the full 56-byte
	// fixed section.
	tx := NewSlashValidatorTx(ids.NodeID{1}, ids.ID{2}, 100_000)
	full := tx.Bytes()

	// Construct a fraudulent buffer: copy header, claim size = HeaderSize+8,
	// pad to that length. zap.Parse will accept it (magic ok, version ok,
	// size <= len). Accessors should return 0 for OOB reads.
	short := make([]byte, zap.HeaderSize+8)
	copy(short, full[:zap.HeaderSize])
	// Overwrite the embedded size field to claim the truncated length.
	binary.LittleEndian.PutUint32(short[12:16], uint32(len(short)))
	// Set root offset to point into the truncated data segment.
	binary.LittleEndian.PutUint32(short[8:12], uint32(zap.HeaderSize))

	tx2, err := WrapSlashValidatorTx(short)
	if err != nil {
		// zap.Parse may reject. That's fine — wire-layer defense.
		t.Logf("V1: zap.Parse rejected truncated buffer: %v (wire-layer defense)", err)
		return
	}
	// If Parse accepted, accessors must not OOB-read; they should return 0.
	if got := tx2.SlashPercentage(); got != 0 {
		t.Fatalf("V1: SlashPercentage on truncated buffer = %d, want 0 (silent OOB fallback)", got)
	}
	if got := tx2.NodeID(); got != (ids.NodeID{}) {
		t.Fatalf("V1: NodeID on truncated buffer = %v, want zero (silent OOB fallback)", got)
	}
	// Defense-in-depth: executor enum check would reject SlashPercentage=0.
	// See vms/platformvm/txs/executor/slash_validator_tx_executor.go:46.
	t.Logf("V1 defense verified: truncated buffer → silently-zero accessor → executor enum check rejects")
}

// -----------------------------------------------------------------------------
// V2 — Memo pointer escape via negative relative offset.
// -----------------------------------------------------------------------------
//
// Threat: zap/zap.go::Object.Bytes parses relOffset as int32. A negative
// relOffset is mathematically valid and the bounds check only validates the
// FINAL absolute position. An attacker can craft a Memo whose relOffset
// points BACK into the fixed section, causing Memo() to alias bytes of
// NetworkID / BlockchainID — intra-buffer aliasing.
//
// Concrete attack: craft a BaseTx where Memo's relOffset = -8 and length = 4.
// Memo() returns 4 bytes drawn from the NetworkID field's underlying bytes.
//
// Impact analysis: the tx is broadcast publicly; nothing in the buffer is
// secret. The encoded value of NetworkID is wire-public. So this is NOT a
// confidentiality leak. It IS a malleability surface: two distinct buffer
// encodings exist for "logical tx with NetworkID=1337 + Memo=A". After
// TxID integration (later batch), hash(buf1) != hash(buf2). For this
// package (wire-only batch 2), the malleability is dormant.
//
// Result: PASS captures the attack; documents the deferred-impact finding.

func TestRed_V2_MemoPointerEscapeNegativeOffset(t *testing.T) {
	// Build a canonical BaseTx.
	tx := NewBaseTx(0xDEADBEEF, ids.ID{0xCA, 0xFE}, []byte("normal"))
	buf := append([]byte(nil), tx.Bytes()...)

	// Find Memo's (relOffset, length) field at root + OffsetBaseTx_Memo (36).
	root := int(binary.LittleEndian.Uint32(buf[8:12]))
	memoFieldPos := root + OffsetBaseTx_Memo

	// Craft a negative relOffset that points back into NetworkID (offset 0).
	// memoFieldPos is the absolute position of Memo's relOffset cell.
	// NetworkID is at absolute position `root`.
	// relOffset needed = root - memoFieldPos = -OffsetBaseTx_Memo = -36.
	negRel := int32(-OffsetBaseTx_Memo)
	binary.LittleEndian.PutUint32(buf[memoFieldPos:], uint32(negRel))
	binary.LittleEndian.PutUint32(buf[memoFieldPos+4:], 4) // length = 4

	tx2, err := WrapBaseTx(buf)
	if err != nil {
		t.Fatalf("V2: Wrap rejected malleable buffer (would be a wire-layer defense): %v", err)
	}

	memo := tx2.Memo()
	if memo == nil {
		// Wire-layer bounds check rejected the negative offset. Acceptable.
		t.Logf("V2 defense: negative-relOffset Memo bounds-checked to nil. Defense holds at wire layer.")
		return
	}
	// If memo is non-nil, it aliases bytes from inside the fixed section.
	// This proves intra-buffer pointer escape works.
	wantNet := uint32(0xDEADBEEF)
	netBytes := make([]byte, 4)
	binary.LittleEndian.PutUint32(netBytes, wantNet)
	if !bytes.Equal(memo, netBytes) {
		t.Logf("V2 informational: Memo() returned %x; NetworkID bytes %x. Malleability path live but not exact alias.", memo, netBytes)
	} else {
		t.Logf("V2 CONFIRMED: Memo() aliased NetworkID bytes %x. Malleability surface confirmed. Impact: deferred to TxID integration (no executor impact yet — VerifyMemoFieldLength rejects non-empty memos in Durango).", memo)
	}
	// Both outcomes are documented; test does not fail (this is wire-layer
	// behavior; impact lives in the integration layer).
}

// -----------------------------------------------------------------------------
// V3 — SetBytes(nil) vs SetBytes([]byte{}) round-trip semantics.
// -----------------------------------------------------------------------------
//
// Threat: builder.go::SetBytes treats both nil and empty slice identically
// — writes (offset=0, length=0). Reader.Bytes() sees relOffset==0 and returns
// nil for both. Round-trip is lossy but consistent: nil↔nil and []byte{}↔nil.
//
// Wire-format implication: the bytes for "nil memo" and "empty memo" are
// identical. TxID(nil-memo-tx) == TxID(empty-memo-tx) — which is actually
// GOOD for canonicalization (no malleability through nil/empty distinction).
//
// Result: PASS — documented behavior. INFO-level finding.

func TestRed_V3_NilVsEmptyMemoRoundTrip(t *testing.T) {
	a := NewBaseTx(1, ids.ID{1}, nil)
	b := NewBaseTx(1, ids.ID{1}, []byte{})
	if !bytes.Equal(a.Bytes(), b.Bytes()) {
		t.Fatalf("V3: nil-memo and empty-memo produced different bytes: lossy ambiguity")
	}
	if a.Memo() != nil || b.Memo() != nil {
		t.Fatalf("V3: empty-memo round-trip returned non-nil slice")
	}
	t.Logf("V3 verified: SetBytes(nil) and SetBytes([]byte{}) produce identical wire form; both Memo()→nil. Lossy but canonical — safe for TxID stability.")
}

// -----------------------------------------------------------------------------
// V4 — Size-mismatch schema drift (offset arithmetic vs declared Size).
// -----------------------------------------------------------------------------
//
// Threat: SizeXxxTx must equal the sum of field sizes. A drift means
// accessors read past the declared boundary.
//
// Verification: compute the implied size from the highest-offset field + that
// field's width and compare to the declared SizeXxxTx constant.
//
// v3 layout (TxKind@0 shifts every other field by +1):
//   - SizeBaseTx                  = 45 (Memo@37+8). ✓
//   - SizeRegisterL1ValidatorTx   = 217 (RemainingBalanceOwnerID@185+32). ✓
//   - SizeSlashValidatorTx        = 57 (SlashPercentage@53+4). ✓
//   - SizeTransferChainOwnershipTx = 69 (OwnerAddress@49+20, no padding). ✓
//   - SizeRemoveChainValidatorTx  = 53 (Network@21+32). ✓

func TestRed_V4_SchemaSizeMatchesOffsetArithmetic(t *testing.T) {
	cases := []struct {
		name            string
		lastFieldOffset int
		lastFieldWidth  int
		declaredSize    int
	}{
		{"BaseTx", OffsetBaseTx_Memo, 8, SizeBaseTx},
		{
			"RegisterL1ValidatorTx",
			OffsetRegisterL1ValidatorTx_RemainingBalanceOwnerID, 32,
			SizeRegisterL1ValidatorTx,
		},
		{"SlashValidatorTx", OffsetSlashValidatorTx_SlashPercentage, 4, SizeSlashValidatorTx},
		{
			"TransferChainOwnershipTx",
			OffsetTransferChainOwnershipTx_OwnerAddress, ids.ShortIDLen,
			SizeTransferChainOwnershipTx,
		},
		{"RemoveChainValidatorTx", OffsetRemoveChainValidatorTx_Network, 32, SizeRemoveChainValidatorTx},
	}
	for _, c := range cases {
		c := c
		t.Run(c.name, func(t *testing.T) {
			implied := c.lastFieldOffset + c.lastFieldWidth
			if implied != c.declaredSize {
				t.Fatalf("V4: %s declared size %d but last-field arithmetic = %d",
					c.name, c.declaredSize, implied)
			}
		})
	}
}

// -----------------------------------------------------------------------------
// V5 — BLS-key opaque storage (layered defense contract).
// -----------------------------------------------------------------------------
//
// Threat: an attacker submits a RegisterL1ValidatorTx with garbage bytes in
// the BLS PublicKey and ProofOfPossession fields. Wire layer stores opaque
// 48-byte and 96-byte chunks; round-trip succeeds. Executor MUST validate
// the BLS structure before accepting the validator.
//
// Wire defense: NONE — by design. BLS point validity is not a wire-format
// property; checking it would couple the wire layer to BLS internals.
//
// Executor defense (verified in standard_tx_executor.go:847):
//
//	pop := signer.ProofOfPossession{PublicKey: ..., ProofOfPossession: ...}
//	if err := pop.Verify(); err != nil { return err }
//
// Result: PASS — documented layered contract.

func TestRed_V5_BLSGarbageRoundTripsButExecutorMustValidate(t *testing.T) {
	var garbageKey [bls.PublicKeyLen]byte
	for i := range garbageKey {
		garbageKey[i] = 0xff
	}
	var garbagePoP [bls.SignatureLen]byte
	for i := range garbagePoP {
		garbagePoP[i] = 0xff
	}
	tx := NewRegisterL1ValidatorTx(ids.ID{1}, garbageKey, garbagePoP, 1, ids.ID{2})

	tx2, err := WrapRegisterL1ValidatorTx(tx.Bytes())
	if err != nil {
		t.Fatalf("V5: garbage BLS round-trip rejected at wire layer (unexpected): %v", err)
	}
	if tx2.BLSPublicKey() != garbageKey || tx2.ProofOfPossession() != garbagePoP {
		t.Fatalf("V5: garbage BLS bytes not round-tripped — wire layer modified opaque storage")
	}
	t.Logf("V5 verified: wire layer is opaque-bytes-only. Executor layered defense at standard_tx_executor.go:847 (pop.Verify()) MUST reject. Contract documented.")
}

// -----------------------------------------------------------------------------
// V6 — Alignment behavior.
// -----------------------------------------------------------------------------
//
// Threat: strict-alignment architectures (some ARMv7, MIPS, SPARC) SIGBUS on
// unaligned uint64 reads. ZAP encodes little-endian using binary.LittleEndian
// which performs byte-by-byte assembly — alignment-tolerant by construction.
//
// Wire-format defense: zap.Builder.StartObject calls b.align(Alignment) where
// Alignment=8. Object starts at an 8-aligned absolute offset. Uint64 fields
// at offset-multiple-of-8 within the object are then naturally aligned to
// 8 in the buffer.
//
// Verification: read all 32 uint64 candidates from each tx type and verify
// no panic on the supported platforms (linux/amd64, linux/arm64, darwin/arm64).

func TestRed_V6_AlignmentTolerance(t *testing.T) {
	t.Logf("V6 platform: %s/%s", runtime.GOOS, runtime.GOARCH)

	atx := NewAdvanceTimeTx(0xDEADBEEFCAFEBABE)
	stx := NewSetL1ValidatorWeightTx(ids.ID{1}, 0xAA55_AA55_AA55_AA55, 0x5AA5_5AA5_5AA5_5AA5)
	itx := NewIncreaseL1ValidatorBalanceTx(ids.ID{1}, 0x1234_5678_9ABC_DEF0)
	regTx := NewRegisterL1ValidatorTx(ids.ID{1}, [bls.PublicKeyLen]byte{}, [bls.SignatureLen]byte{},
		0xFFFF_FFFF_FFFF_FFFF, ids.ID{2})
	tcoTx := NewTransferChainOwnershipTx(ids.ID{1}, 1, 0xABCD_EF12_3456_789A, ids.ShortID{1})

	// Read all uint64-bearing fields; alignment-intolerant code would crash.
	_ = atx.Time()
	_ = stx.Nonce()
	_ = stx.Weight()
	_ = itx.Balance()
	_ = regTx.Expiry()
	_ = tcoTx.OwnerLocktime()
	t.Logf("V6 verified: all uint64 fields readable on %s/%s — Builder.align(8) on StartObject keeps offsets natural-aligned",
		runtime.GOOS, runtime.GOARCH)
}

// -----------------------------------------------------------------------------
// V7 — Buffer mutation via aliased Memo() slice (Blue's LOW flag).
// -----------------------------------------------------------------------------
//
// Threat: Memo() returns o.obj.Bytes(...) which is a slice that aliases the
// underlying ZAP buffer. A caller writing to it mutates the canonical bytes.
// This breaks "TxID = hash(buffer)" if computed AFTER a mutation.
//
// Wire defense: documented in base_tx.go:51 — "Callers MUST NOT mutate it".
// No defensive copy.
//
// Practical defense: mutation is detectable. We test that mutation IS possible
// (demonstrates the footgun) and document the contract. Callers must respect
// the read-only contract.

func TestRed_V7_MemoMutationAliasesBuffer(t *testing.T) {
	memo := []byte("canary")
	tx := NewBaseTx(1, ids.ID{1}, memo)

	m1 := tx.Memo()
	if len(m1) == 0 {
		t.Fatal("V7: Memo returned empty slice for non-empty source")
	}
	// Mutate the returned slice.
	originalFirstByte := m1[0]
	m1[0] = 0xFF

	m2 := tx.Memo()
	if m2[0] != 0xFF {
		// If Memo were a defensive copy, m2 would still be 0x63 ('c'). The
		// fact that m2 reflects m1's mutation proves aliasing.
		t.Logf("V7 NOTE: Memo() appears to defensively copy. First byte after mutation: %x (expected 0xff if aliasing).", m2[0])
		return
	}

	// Aliasing confirmed. The buffer itself is also mutated.
	bufBytes := tx.Bytes()
	foundCanary := false
	for i := 0; i < len(bufBytes)-1; i++ {
		if bufBytes[i] == 0xFF && i > 0 && bufBytes[i-1] == 0 {
			// Found the mutated 0xff byte in the buffer.
			foundCanary = true
			break
		}
	}
	if !foundCanary {
		t.Logf("V7: Memo mutation reflected in m2 but not visible at buffer scan position (still proves aliasing via m2)")
	}
	t.Logf("V7 CONFIRMED footgun: Memo() aliases buffer. originalFirstByte=%x mutated→%x. Contract requires callers to not mutate. Defensive-copy would allocate (defeats zero-alloc). RECOMMEND: comment escalation + linter rule.",
		originalFirstByte, m1[0])
}

// -----------------------------------------------------------------------------
// V8 — Variable-memo build with multi-MB inputs (memory exhaustion).
// -----------------------------------------------------------------------------
//
// Threat: zap.Builder.grow doubles capacity unbounded. NewBaseTx with a
// 16MB memo pre-sizes the buffer correctly (no growth), but a builder
// receiving a 16MB SetBytes after initial allocation would double from
// 32MB to 64MB on first write, then to 128MB on next overflow. No upper
// bound enforced.
//
// However, the platformvm executor enforces tx size limits via the codec
// MaxMessageLen (validated in tx_mempool_verifier and codec.Manager).
// Additionally, VerifyMemoFieldLength in current activation REQUIRES
// memo to be EMPTY post-Durango — non-empty memos fail execution.
//
// Wire defense: none — by design (wire is dumb).
// Executor defense: ErrMemoTooLarge (memo must be empty in current activation).
//
// Result: PASS — documents the layered contract and the executor caps.

func TestRed_V8_LargeMemoBuildDoesNotPanic(t *testing.T) {
	// 1MB memo (representative — full 16MB would dominate test runtime).
	memo := make([]byte, 1<<20)
	for i := range memo {
		memo[i] = byte(i)
	}
	tx := NewBaseTx(1, ids.ID{1}, memo)
	got := tx.Memo()
	if len(got) != len(memo) {
		t.Fatalf("V8: large-memo round-trip got len=%d, want %d", len(got), len(memo))
	}
	// Confirm wire layer accepts. Executor will reject via VerifyMemoFieldLength.
	t.Logf("V8 verified: wire layer accepts %d-byte memo without panic. Executor enforces 0-byte memo via VerifyMemoFieldLength in lux/base_tx.go:72 (current activation).", len(memo))
}

// -----------------------------------------------------------------------------
// V9 — SlashPercentage wire-vs-executor cap.
// -----------------------------------------------------------------------------
//
// Threat: wire admits SlashPercentage = 0xFFFFFFFF; executor must cap.
//
// Executor defense (verified in slash_validator_tx_executor.go:46):
//
//	if tx.SlashPercentage != expectedPercent {
//	    return errSlashPercentMismatch
//	}
//
// Where expectedPercent is the enum constant 100_000 (10%) or 500_000 (50%)
// from evidence type. The check is STRICT EQUALITY — any value not exactly
// matching one of two whitelisted percentages is rejected.
//
// Defense-in-depth on overflow: slash_overflow_test.go::TestHIGH2_SlashAmountNoOverflow
// proves the arithmetic (weight/denom)*pct + (weight%denom)*pct/denom is
// overflow-safe even for weight = MaxUint64.
//
// Result: PASS — full layered defense.

func TestRed_V9_SlashPercentageWireAdmits0xFFFFFFFF(t *testing.T) {
	tx := NewSlashValidatorTx(ids.NodeID{1}, ids.ID{2}, 0xFFFFFFFF)
	if got := tx.SlashPercentage(); got != 0xFFFFFFFF {
		t.Fatalf("V9: wire layer rejected 0xFFFFFFFF; expected admission with executor catching it. got=%d", got)
	}
	t.Logf("V9 verified: wire admits 0xFFFFFFFF SlashPercentage. Executor (slash_validator_tx_executor.go:46) requires strict equality with whitelisted enum (100_000 | 500_000) — rejects anything else. Overflow safe arithmetic also verified in slash_overflow_test.go.")
}

// -----------------------------------------------------------------------------
// V10 — TransferChainOwnership multi-address Owner.
// -----------------------------------------------------------------------------
//
// Threat: real fx.OutputOwners can carry N addresses + threshold. Blue's v1
// schema pins single-address (threshold=1, locktime, one Address). If a real
// 2-of-3 Owner is being represented, the wire schema CANNOT encode it.
//
// Constructor signature: NewTransferChainOwnershipTx(chain, threshold, locktime, address ids.ShortID).
// Only a single address can be passed. There is NO silent fallback path —
// the caller is forced to pick one address. Multi-address callers MUST go
// through LUXD_ENABLE_LEGACY_CODEC (per the file header comment).
//
// Wire defense: type system. ids.ShortID is a single address, not a slice.
// Compile-time guarantee — no silent fallback possible.
//
// Result: PASS — defense-by-typesystem confirmed.

func TestRed_V10_TransferChainOwnershipIsCompileTimeSingleAddress(t *testing.T) {
	// Verify the constructor's address parameter is ids.ShortID (single addr),
	// not []ids.ShortID. We can only construct with one address.
	tx := NewTransferChainOwnershipTx(ids.ID{1}, 1, 0, ids.ShortID{0xAA})
	if tx.OwnerAddress() != (ids.ShortID{0xAA}) {
		t.Fatalf("V10: single-address Owner round-trip mismatch")
	}
	if tx.OwnerThreshold() != 1 {
		t.Fatalf("V10: threshold round-trip mismatch")
	}
	t.Logf("V10 verified: constructor signature enforces single-address Owner at compile time. Multi-address callers must use legacy codec (LUXD_ENABLE_LEGACY_CODEC=1) — no silent fallback path exists.")
}

// -----------------------------------------------------------------------------
// V11 — TxID determinism: Wrap(buf).Bytes() == buf.
// -----------------------------------------------------------------------------
//
// Threat: re-wrapping a buffer should return identical bytes. Otherwise
// TxID = hash(Wrap(buf).Bytes()) would diverge from hash(buf).
//
// Verification: ptr identity (Bytes() returns the same backing array, not a
// copy).

func TestRed_V11_BytesReturnIdenticalAfterRewrap(t *testing.T) {
	cases := []struct {
		name string
		buf  []byte
	}{
		{"AdvanceTimeTx", NewAdvanceTimeTx(42).Bytes()},
		{"BaseTx", NewBaseTx(1337, ids.ID{1}, []byte("memo")).Bytes()},
		{"RewardValidatorTx", NewRewardValidatorTx(ids.ID{1}).Bytes()},
		{"SlashValidatorTx", NewSlashValidatorTx(ids.NodeID{1}, ids.ID{2}, 100_000).Bytes()},
		{"RegisterL1ValidatorTx", NewRegisterL1ValidatorTx(ids.ID{1}, [bls.PublicKeyLen]byte{}, [bls.SignatureLen]byte{}, 1, ids.ID{2}).Bytes()},
		{"TransferChainOwnershipTx", NewTransferChainOwnershipTx(ids.ID{1}, 1, 0, ids.ShortID{1}).Bytes()},
		{"RemoveChainValidatorTx", NewRemoveChainValidatorTx(ids.NodeID{1}, ids.ID{2}).Bytes()},
		{"DisableL1ValidatorTx", NewDisableL1ValidatorTx(ids.ID{1}).Bytes()},
		{"IncreaseL1ValidatorBalanceTx", NewIncreaseL1ValidatorBalanceTx(ids.ID{1}, 1).Bytes()},
		{"SetL1ValidatorWeightTx", NewSetL1ValidatorWeightTx(ids.ID{1}, 1, 1).Bytes()},
	}
	for _, c := range cases {
		c := c
		t.Run(c.name, func(t *testing.T) {
			msg, err := zap.Parse(c.buf)
			if err != nil {
				t.Fatalf("V11: Parse rejected own builder output: %v", err)
			}
			if !bytes.Equal(msg.Bytes(), c.buf) {
				t.Fatalf("V11: Wrap(buf).Bytes() != buf — TxID stability violated")
			}
		})
	}
}

// -----------------------------------------------------------------------------
// V12 — Concurrent reads of the same buffer (race freedom).
// -----------------------------------------------------------------------------
//
// Threat: two goroutines call tx.Time() / tx.SlashPercentage() simultaneously.
// Zero-copy should imply race-free reads — the underlying Object struct is
// read-only, accessors do no mutation.
//
// Verification: spawn N goroutines reading every field; run under -race.

func TestRed_V12_ConcurrentAccessorReadsRaceFree(t *testing.T) {
	stx := NewSlashValidatorTx(ids.NodeID{1, 2, 3}, ids.ID{4, 5, 6}, 100_000)
	btx := NewBaseTx(1337, ids.ID{7, 8, 9}, []byte("concurrent"))
	regTx := NewRegisterL1ValidatorTx(ids.ID{1}, [bls.PublicKeyLen]byte{}, [bls.SignatureLen]byte{},
		1_900_000_000, ids.ID{2})

	const goroutines = 32
	const iterations = 1000
	var wg sync.WaitGroup
	wg.Add(goroutines)
	for g := 0; g < goroutines; g++ {
		go func() {
			defer wg.Done()
			for i := 0; i < iterations; i++ {
				_ = stx.NodeID()
				_ = stx.Network()
				_ = stx.SlashPercentage()
				_ = btx.NetworkID()
				_ = btx.BlockchainID()
				_ = btx.Memo()
				_ = regTx.ValidationID()
				_ = regTx.BLSPublicKey()
				_ = regTx.ProofOfPossession()
				_ = regTx.Expiry()
			}
		}()
	}
	wg.Wait()
	t.Logf("V12 verified: %d goroutines × %d iterations no race. (Run with -race for guarantee.)", goroutines, iterations)
}

// -----------------------------------------------------------------------------
// V13 — Mempool poison: malformed buffer must not panic.
// -----------------------------------------------------------------------------
//
// Threat: a buffer that passes magic + version + size check but contains
// adversarial offset/length combinations. Each accessor must remain panic-free
// regardless of buffer content.
//
// Verification: feed adversarial buffers built from random / patterned bytes
// and observe accessors complete normally.

func TestRed_V13_AdversarialBuffersDoNotPanic(t *testing.T) {
	// Build a real BaseTx, then corrupt random byte positions in the data
	// segment and confirm accessors don't panic.
	base := NewBaseTx(1, ids.ID{1}, []byte("memo")).Bytes()

	corruptions := []struct {
		name string
		fn   func([]byte) []byte
	}{
		{"all-0xff data", func(b []byte) []byte {
			out := append([]byte(nil), b...)
			for i := zap.HeaderSize; i < len(out); i++ {
				out[i] = 0xFF
			}
			return out
		}},
		{"swap header endianness", func(b []byte) []byte {
			out := append([]byte(nil), b...)
			// Swap the root offset and the size field — chaos.
			for i := 8; i < 16; i++ {
				out[i] ^= 0x55
			}
			return out
		}},
		{"zero everything except header", func(b []byte) []byte {
			out := append([]byte(nil), b...)
			for i := zap.HeaderSize; i < len(out); i++ {
				out[i] = 0
			}
			return out
		}},
		{"giant claimed length", func(b []byte) []byte {
			out := append([]byte(nil), b...)
			// Find memo field; set length = 0x7FFFFFFF.
			root := int(binary.LittleEndian.Uint32(out[8:12]))
			memoFieldPos := root + OffsetBaseTx_Memo
			binary.LittleEndian.PutUint32(out[memoFieldPos+4:], 0x7FFFFFFF)
			return out
		}},
	}

	for _, c := range corruptions {
		c := c
		t.Run(c.name, func(t *testing.T) {
			defer func() {
				if r := recover(); r != nil {
					t.Fatalf("V13: panic on adversarial buffer %q: %v", c.name, r)
				}
			}()
			b := c.fn(base)
			tx, err := WrapBaseTx(b)
			if err != nil {
				// Wire-layer rejection is acceptable.
				return
			}
			_ = tx.NetworkID()
			_ = tx.BlockchainID()
			_ = tx.Memo()
		})
	}
}

// -----------------------------------------------------------------------------
// V14 — Cross-tx-type confusion (CLOSED by schema v3 TxKind discriminator).
// -----------------------------------------------------------------------------
//
// Threat: an attacker submits an AdvanceTimeTx buffer to WrapBaseTx. Prior
// to schema v3 this returned a typed BaseTx whose accessors yielded garbage
// (the AdvanceTimeTx's 8-byte Time value reinterpreted as NetworkID + first
// 4 bytes of BlockchainID).
//
// Wire defense (v3): TxKind discriminator at offset 0 of every tx. Wrap*Tx
// reads it and returns ErrWrongTxKind on mismatch. Every pairing of
// {valid-tx-buf, wrong-Wrap-function} now rejects at the wire layer.

func TestRed_V14_CrossTypeConfusion(t *testing.T) {
	// Build canonical buffers for every tx type, then assert that every
	// non-matching Wrap*Tx rejects with ErrWrongTxKind.
	advance := NewAdvanceTimeTx(0xDEAD_BEEF_CAFE_BABE).Bytes()
	base := NewBaseTx(1, ids.ID{1}, []byte("memo")).Bytes()
	reward := NewRewardValidatorTx(ids.ID{2}).Bytes()
	slash := NewSlashValidatorTx(ids.NodeID{3}, ids.ID{4}, 100_000).Bytes()
	disable := NewDisableL1ValidatorTx(ids.ID{5}).Bytes()
	register := NewRegisterL1ValidatorTx(ids.ID{6}, [bls.PublicKeyLen]byte{},
		[bls.SignatureLen]byte{}, 1, ids.ID{7}).Bytes()
	incBal := NewIncreaseL1ValidatorBalanceTx(ids.ID{9}, 1).Bytes()
	transfer := NewTransferChainOwnershipTx(ids.ID{10}, 1, 0, ids.ShortID{1}).Bytes()
	remove := NewRemoveChainValidatorTx(ids.NodeID{11}, ids.ID{12}).Bytes()

	// Cross-confusion attempts. Each WrapXxx(bufY) where Y != X MUST reject.
	cases := []struct {
		name string
		fn   func() error
	}{
		{"WrapBaseTx(AdvanceTimeTx)", func() error { _, err := WrapBaseTx(advance); return err }},
		{"WrapBaseTx(RewardValidatorTx)", func() error { _, err := WrapBaseTx(reward); return err }},
		{"WrapSlashValidatorTx(BaseTx)", func() error { _, err := WrapSlashValidatorTx(base); return err }},
		{"WrapRegisterL1ValidatorTx(SlashValidatorTx)", func() error { _, err := WrapRegisterL1ValidatorTx(slash); return err }},
		{"WrapAdvanceTimeTx(RewardValidatorTx)", func() error { _, err := WrapAdvanceTimeTx(reward); return err }},
		{"WrapDisableL1ValidatorTx(RewardValidatorTx)", func() error { _, err := WrapDisableL1ValidatorTx(reward); return err }},
		{"WrapSetL1ValidatorWeightTx(IncreaseL1ValidatorBalanceTx)", func() error { _, err := WrapSetL1ValidatorWeightTx(incBal); return err }},
		{"WrapTransferChainOwnershipTx(RemoveChainValidatorTx)", func() error { _, err := WrapTransferChainOwnershipTx(remove); return err }},
		{"WrapRemoveChainValidatorTx(TransferChainOwnershipTx)", func() error { _, err := WrapRemoveChainValidatorTx(transfer); return err }},
		{"WrapRegisterL1ValidatorTx(DisableL1ValidatorTx)", func() error { _, err := WrapRegisterL1ValidatorTx(disable); return err }},
		{"WrapRewardValidatorTx(AdvanceTimeTx)", func() error { _, err := WrapRewardValidatorTx(register); return err }},
	}
	for _, c := range cases {
		c := c
		t.Run(c.name, func(t *testing.T) {
			err := c.fn()
			if err == nil {
				t.Fatalf("V14: %s succeeded; expected ErrWrongTxKind (v3 discriminator gap)", c.name)
			}
			if err != ErrWrongTxKind {
				t.Fatalf("V14: %s returned %v; expected ErrWrongTxKind", c.name, err)
			}
		})
	}

	// Sanity: a buffer with TxKind=0 (reserved) is also rejected.
	t.Run("reservedKindRejected", func(t *testing.T) {
		buf := append([]byte(nil), base...)
		root := int(binary.LittleEndian.Uint32(buf[8:12]))
		buf[root+OffsetTxKind] = 0
		_, err := WrapBaseTx(buf)
		if err != ErrWrongTxKind {
			t.Fatalf("V14: reserved TxKind=0 not rejected; got err=%v", err)
		}
	})
}

// -----------------------------------------------------------------------------
// V15 — Activation timestamp gaming.
// -----------------------------------------------------------------------------
//
// Threat: a block builder emits a block at timestamp ZAPActivationUnix-1
// containing ZAP-encoded txs. Validators receiving the block must decide:
// reject the wrong-codec tx, or accept it because the block is pre-activation?
//
// Defense (codec_select.go::ShouldUseZAPForWrite): under default (legacy
// disabled), ZAP is ALWAYS the write codec regardless of timestamp. Under
// legacy-enabled, ZAP write is gated on timestamp >= activation. Read path
// uses IsZAPBytes — a 4-byte magic check independent of timestamp.
//
// Risk surface: in legacy-enabled mode, a tx encoded in ZAP at pre-activation
// timestamp will be PARSEABLE (IsZAPBytes=true) but the block-builder rule
// (ShouldUseZAPForWrite=false at that timestamp) says it shouldn't have been
// written. Validators applying ShouldUseZAPForWrite as an ACCEPTANCE rule
// would reject; validators applying it as a WRITE-ONLY rule would accept.
//
// THE RULE MUST BE: validators reject ZAP-encoded txs at pre-activation
// timestamps when in legacy-compatible mode. Otherwise a pre-activation
// block accepting ZAP txs creates a fork between legacy-only and
// legacy-aware nodes.
//
// This package does not own the consensus rule — only the wire encoding.
// The owning layer is block/executor. Document the requirement.

func TestRed_V15_PreActivationZAPTxRejection(t *testing.T) {
	// LP-023 cutover (2026-06-02): ZAPActivationUnix is now 0 — there is no
	// pre-activation window. V15's threat model only applies in the legacy
	// cutover-window scenario, which is closed. We pin the always-on
	// invariant: regardless of LegacyEnabled, the write rule says ZAP for
	// every timestamp.
	atx := NewAdvanceTimeTx(0) // any timestamp; activation gate is dead
	if !IsZAPBytes(atx.Bytes()) {
		t.Fatal("V15: AdvanceTimeTx must be ZAP-encoded (wire layer is timestamp-agnostic post-LP-023)")
	}

	defer func(prev bool) { LegacyEnabled = prev }(LegacyEnabled)

	LegacyEnabled = false
	for _, ts := range []uint64{0, 1, 1782604800, 1782604800 + 86400} {
		if !ShouldUseZAPForWrite(ts) {
			t.Fatalf("V15: legacy-disabled should write ZAP for ts=%d", ts)
		}
	}
	LegacyEnabled = true
	for _, ts := range []uint64{0, 1, 1782604800, 1782604800 + 86400} {
		if !ShouldUseZAPForWrite(ts) {
			t.Fatalf("V15: legacy-enabled with activation=0 should still write ZAP for ts=%d (no pre-activation window exists)", ts)
		}
	}
	t.Logf("V15 closed: LP-023 cutover (2026-06-02) makes ZAP unconditional on the write path. The legacy cutover-window threat model is no longer applicable; LegacyEnabled is now read-only-relevant.")
}

// -----------------------------------------------------------------------------
// V16 (Red-added) — Header malleability: flags field is unconstrained.
// -----------------------------------------------------------------------------
//
// Threat: the 2-byte Flags field in the ZAP header (offset 6) is not validated
// by zap.Parse. An attacker can flip FlagCompressed/FlagEncrypted/FlagSigned
// bits in a buffer that is otherwise plaintext. The Parse function accepts
// the flags as-is; the readers in this package don't consult them.
//
// Impact: two distinct buffers (same data, different Flags) decode to the
// same logical tx. TxID = hash(buf1) != hash(buf2) — transaction malleability
// after TxID integration lands.
//
// Severity: MEDIUM (deferred — depends on TxID integration). Mitigation
// recommendation: zap.Parse should reject any FlagCompressed/FlagEncrypted
// not paired with a decompression/decryption capability, or zap_native should
// require Flags=0 on entry.

func TestRed_V16_HeaderFlagsAreMalleable(t *testing.T) {
	tx := NewBaseTx(1, ids.ID{1}, []byte("memo"))
	buf := append([]byte(nil), tx.Bytes()...)

	// Toggle the FlagSigned bit (LSB+2) in the Flags field at offset 6.
	binary.LittleEndian.PutUint16(buf[6:8], uint16(zap.FlagSigned))

	// Re-parse. Should succeed; the wire layer doesn't validate flags.
	tx2, err := WrapBaseTx(buf)
	if err != nil {
		t.Logf("V16 wire defense (unexpected but acceptable): Parse rejected flag-mutated buffer: %v", err)
		return
	}
	if tx2.NetworkID() != 1 || tx2.BlockchainID() != (ids.ID{1}) {
		t.Fatal("V16: flag-mutated buffer should still decode the same logical tx")
	}
	if bytes.Equal(buf, tx.Bytes()) {
		t.Fatal("V16: buffers are identical; we didn't actually mutate")
	}
	t.Logf("V16 CONFIRMED: header Flags field is unconstrained. Two distinct buffers decode same logical tx. Malleability surface — dormant until TxID integration. RECOMMENDATION: zap.Parse should reject unknown flag bits OR zap_native readers should require Flags=0.")
}

// -----------------------------------------------------------------------------
// V17 (Red-added) — Trailing-bytes malleability.
// -----------------------------------------------------------------------------
//
// Threat: zap.Parse honors the embedded size field and slices data to
// data[:size]. Bytes BEYOND the declared size are discarded silently. An
// attacker can append arbitrary bytes to a valid tx; Parse returns the
// truncated message; the original buffer (with trailing junk) hashes
// differently than the parsed message bytes.
//
// Wire defense: data[:size] truncation. Parsed buffer is canonical.
//
// Open question: does any caller compute TxID over the OUTER (with-trailing)
// buffer rather than the parsed-inner buffer? In this package, Bytes() returns
// the parsed/truncated buffer — so TxID computed via tx.Bytes() is stable.
//
// Result: PASS — wire defense in place, but caller MUST use tx.Bytes() not
// the original input.

func TestRed_V17_TrailingBytesTruncatedAtParse(t *testing.T) {
	tx := NewBaseTx(1, ids.ID{1}, []byte("memo"))
	orig := tx.Bytes()

	// Append 100 trailing junk bytes.
	junk := append([]byte(nil), orig...)
	for i := 0; i < 100; i++ {
		junk = append(junk, byte(i))
	}

	tx2, err := WrapBaseTx(junk)
	if err != nil {
		t.Fatalf("V17: Parse rejected trailing-byte buffer: %v", err)
	}
	if len(tx2.Bytes()) != len(orig) {
		t.Fatalf("V17: tx2.Bytes() len=%d, want %d (Parse must truncate at declared size)", len(tx2.Bytes()), len(orig))
	}
	if !bytes.Equal(tx2.Bytes(), orig) {
		t.Fatalf("V17: tx2.Bytes() differs from canonical buffer")
	}
	t.Logf("V17 verified: zap.Parse truncates at declared size; tx.Bytes() returns canonical form. TxID = hash(tx.Bytes()) is stable even when input has trailing junk. CONTRACT: TxID-computing callers MUST hash tx.Bytes(), NOT the original input buffer.")
}

// -----------------------------------------------------------------------------
// V18 (Red-added) — Size field underflow / overflow at Parse boundary.
// -----------------------------------------------------------------------------
//
// Threat: zap.Parse validates `size <= len(data)` but does NOT validate
// `size >= HeaderSize`. A buffer with size = 0..15 passes the upper bound
// check but is internally inconsistent. Object accessors would Uint32-read
// at offset 8..11 within HeaderSize bytes (valid), then operate on data[:size]
// where size < HeaderSize means the slice excludes the header bytes the
// accessor was supposed to use.
//
// Verification: try size=0 and size=15.

func TestRed_V18_SizeFieldUnderflowAtParse(t *testing.T) {
	hdr := make([]byte, zap.HeaderSize)
	copy(hdr, zap.Magic)
	binary.LittleEndian.PutUint16(hdr[4:6], zap.Version)
	binary.LittleEndian.PutUint32(hdr[8:12], 0) // root offset = 0
	binary.LittleEndian.PutUint32(hdr[12:16], 0) // claimed size = 0

	// Parse the buffer.
	defer func() {
		if r := recover(); r != nil {
			t.Fatalf("V18 PANIC on size-underflow buffer: %v — wire layer fails open", r)
		}
	}()
	msg, err := zap.Parse(hdr)
	if err != nil {
		t.Logf("V18 wire defense: Parse rejected size=0 buffer: %v", err)
		return
	}
	// If accepted, msg.data = hdr[:0] — empty slice. Any further access via
	// Root() reads root offset from msg.data[8:12] which would PANIC on empty.
	defer func() {
		if r := recover(); r != nil {
			t.Logf("V18 INTERNAL PANIC after Parse accepted size=0: %v. This is a WIRE-LAYER GAP: Parse should require size >= HeaderSize.", r)
		}
	}()
	_ = msg.Root()
	t.Logf("V18: size=0 Parse accepted, Root() returned without panic — implementation tolerates degenerate case.")
}

// -----------------------------------------------------------------------------
// V19 (Red-added) — Root offset out-of-bounds.
// -----------------------------------------------------------------------------
//
// Threat: Parse validates the message size but not the root offset. The root
// offset (header bytes 8:12) can point past end-of-message. msg.Root() then
// returns Object{offset: bogus}, and accessors call Uint32(bogus+fieldOffset)
// which the bounds-check catches → returns 0.
//
// Result: bounds-check catches it. Silent zero again. Same layered-defense
// argument as V1.

func TestRed_V19_RootOffsetPastEnd(t *testing.T) {
	tx := NewSlashValidatorTx(ids.NodeID{1}, ids.ID{2}, 100_000)
	buf := append([]byte(nil), tx.Bytes()...)

	// Set root offset to a value way past EOF.
	binary.LittleEndian.PutUint32(buf[8:12], 0x7FFFFFFF)

	tx2, err := WrapSlashValidatorTx(buf)
	if err != nil {
		t.Logf("V19 wire defense: Parse rejected out-of-range root offset: %v", err)
		return
	}
	defer func() {
		if r := recover(); r != nil {
			t.Fatalf("V19 PANIC on out-of-range root: %v", r)
		}
	}()
	// All fields should return zero (Uint8/16/32/64 bounds check).
	got := tx2.SlashPercentage()
	if got != 0 {
		t.Fatalf("V19: SlashPercentage on bogus-root buffer = %d, want 0 (bounds check)", got)
	}
	t.Logf("V19 verified: out-of-range root offset → accessors silently return 0 → executor enum check rejects (defense in depth).")
}

// -----------------------------------------------------------------------------
// V20 (Red-added) — Memo length field overflow into negative int.
// -----------------------------------------------------------------------------
//
// Threat: length is read as uint32; cast to int. On 32-bit platforms (Go
// theoretical), int(uint32(0x80000000)) is negative — could underflow
// arithmetic. On 64-bit platforms, int(uint32) is positive, no overflow.
//
// Lux runs 64-bit only — this is a non-issue. Documented for completeness.

func TestRed_V20_MemoLengthCastSafety(t *testing.T) {
	if (^uint(0) >> 32) == 0 {
		t.Skip("V20: skipped on 32-bit platforms (Lux is 64-bit only)")
	}
	// On 64-bit, int(uint32(MaxUint32)) = positive.
	// Build buffer with claimed length = MaxUint32; bounds check rejects.
	tx := NewBaseTx(1, ids.ID{1}, []byte("memo"))
	buf := append([]byte(nil), tx.Bytes()...)
	root := int(binary.LittleEndian.Uint32(buf[8:12]))
	memoFieldPos := root + OffsetBaseTx_Memo

	binary.LittleEndian.PutUint32(buf[memoFieldPos+4:], 0xFFFFFFFF)

	tx2, err := WrapBaseTx(buf)
	if err != nil {
		t.Logf("V20: Parse rejected overflow-length buffer: %v", err)
		return
	}
	memo := tx2.Memo()
	if memo != nil {
		t.Fatalf("V20: oversized length not bounds-checked; Memo() returned len=%d", len(memo))
	}
	t.Logf("V20 verified: uint32(MaxUint32) length passes through uint32→int positive cast on 64-bit; bounds check in zap.Object.Bytes catches absPos+length > len(data) → returns nil.")
}

// -----------------------------------------------------------------------------
// LP-023 v3.1 Red round 2 — regression tests pinned in-tree.
// -----------------------------------------------------------------------------
//
// RED-HIGH-3: poisoned EvidenceEntry (Rel,Len) cursors panic the consumer
// when used to slice the parent message/sig blobs. The safe MessageA/B +
// SignatureA/B accessors on a Bind'd list clamp against parent blob
// length and return empty slices.

func TestRedRound2_HIGH3_EvidenceListSafeAccessorsClamp(t *testing.T) {
	// Forge an EvidenceList with one entry whose MessageARel=0 and
	// MessageALen=0xFFFFFFFF — the wire-format poison Red demonstrated in
	// /tmp/red_zap_attacks/batch3_attacks_test.go.
	b := zap.NewBuilder(512)
	mb := []byte("msgA-1")
	sb := []byte("sigA-1")

	var entry [SizeEvidenceEntry]byte
	putU64(entry[OffsetEvidenceEntry_Height:], 1)
	entry[OffsetEvidenceEntry_EvidenceType] = 1
	putU32(entry[OffsetEvidenceEntry_MessageARel:], 0)
	putU32(entry[OffsetEvidenceEntry_MessageALen:], 0xFFFFFFFF) // POISONED
	putU32(entry[OffsetEvidenceEntry_SignatureARel:], 0)
	putU32(entry[OffsetEvidenceEntry_SignatureALen:], 0)
	putU32(entry[OffsetEvidenceEntry_MessageBRel:], 0xFFFFFFFF) // also poisoned (rel oob)
	putU32(entry[OffsetEvidenceEntry_MessageBLen:], 1)
	putU32(entry[OffsetEvidenceEntry_SignatureBRel:], 0)
	putU32(entry[OffsetEvidenceEntry_SignatureBLen:], 0xFFFFFFFE) // poisoned (length oob)

	lb := b.StartList(SizeEvidenceEntry)
	lb.AddBytes(entry[:])
	listOff, _ := lb.Finish()
	// SetList's count is entry count (1), not byte count (48). The list
	// element-stride is SizeEvidenceEntry; consumers index by entry index.
	ob := b.StartObject(24)
	ob.SetList(0, listOff, 1)
	ob.SetBytes(8, mb)
	ob.SetBytes(16, sb)
	ob.FinishAsRoot()
	buf := b.Finish()

	msg, err := zap.Parse(buf)
	if err != nil {
		t.Fatalf("Parse failed: %v", err)
	}
	mb2 := msg.Root().Bytes(8)
	sb2 := msg.Root().Bytes(16)

	defer func() {
		if r := recover(); r != nil {
			t.Fatalf("RED-HIGH-3 regression: panic on poisoned cursors: %v", r)
		}
	}()

	// Bound list — safe accessors clamp.
	list := NewEvidenceListView(msg.Root(), 0).Bind(mb2, sb2)
	if list.Len() != 1 {
		t.Fatalf("expected 1 entry, got %d", list.Len())
	}
	e := list.At(0)

	if got := e.MessageA(); got != nil {
		t.Fatalf("RED-HIGH-3 regression (MessageA): got len=%d, want nil (poisoned len=0xFFFFFFFF)", len(got))
	}
	if got := e.MessageB(); got != nil {
		t.Fatalf("RED-HIGH-3 regression (MessageB): got len=%d, want nil (poisoned rel=0xFFFFFFFF)", len(got))
	}
	if got := e.SignatureB(); got != nil {
		t.Fatalf("RED-HIGH-3 regression (SignatureB): got len=%d, want nil (poisoned len=0xFFFFFFFE)", len(got))
	}
	// Compile-time enforcement (NEW-V2): unbound EvidenceListView.At(0)
	// returns an EvidenceEntry (no safe accessors). Calling .MessageA() on
	// it is a build error — the type system refuses, forcing consumers
	// through Bind() first. This is the "EvidenceEntry has no method
	// MessageA" compile-time gate.
	unbound := NewEvidenceListView(msg.Root(), 0).At(0)
	_ = unbound.Height()             // raw accessor: OK
	rel, length := unbound.MessageARange() // raw cursor: OK
	_, _ = rel, length
	// unbound.MessageA()           // BUILD ERROR — EvidenceEntry lacks MessageA
}

// TestRedRound2_HIGH3_SigIndicesArraySliceClamp pins the existence + clamp
// semantics of SigIndicesArray.Slice. Prior to LP-023 v3.1 r2 this accessor
// was referenced in comments but did not exist; consumers built the slice
// inline via raw At() loops with attacker-controlled start/count.
func TestRedRound2_HIGH3_SigIndicesArraySliceClamp(t *testing.T) {
	b := zap.NewBuilder(256)
	lb := b.StartList(4)
	lb.AddUint32(10)
	lb.AddUint32(20)
	lb.AddUint32(30)
	off, count := lb.Finish()
	ob := b.StartObject(8)
	ob.SetList(0, off, count)
	ob.FinishAsRoot()
	buf := b.Finish()

	msg, err := zap.Parse(buf)
	if err != nil {
		t.Fatalf("Parse: %v", err)
	}
	arr := SigIndicesArrayView(msg.Root(), 0)

	// Honest slice.
	if got := arr.Slice(0, 3); len(got) != 3 || got[0] != 10 || got[2] != 30 {
		t.Fatalf("honest Slice(0,3) = %v", got)
	}
	if got := arr.Slice(1, 2); len(got) != 2 || got[0] != 20 || got[1] != 30 {
		t.Fatalf("honest Slice(1,2) = %v", got)
	}
	// Poisoned start.
	if got := arr.Slice(0xFFFFFFFF, 1); got != nil {
		t.Fatalf("poisoned start: got %v, want nil", got)
	}
	// Poisoned count.
	if got := arr.Slice(0, 0xFFFFFFFF); got != nil {
		t.Fatalf("poisoned count: got len=%d, want nil", len(got))
	}
	// start + count overflows.
	if got := arr.Slice(2, 0xFFFFFFFE); got != nil {
		t.Fatalf("overflowing (start, count): got %v, want nil", got)
	}
}

// TestRedRound2_HIGH3_SignatureArraySliceClamp pins SignatureArray.Slice.
func TestRedRound2_HIGH3_SignatureArraySliceClamp(t *testing.T) {
	var s1, s2 [SigBlobSize]byte
	for i := range s1 {
		s1[i] = byte(i)
		s2[i] = byte(0xFF - i)
	}
	sigs := [][SigBlobSize]byte{s1, s2}

	b := zap.NewBuilder(512)
	off, count := WriteSignatureArray(b, sigs)
	ob := b.StartObject(8)
	ob.SetList(0, off, count)
	ob.FinishAsRoot()
	buf := b.Finish()

	msg, err := zap.Parse(buf)
	if err != nil {
		t.Fatalf("Parse: %v", err)
	}
	arr := SignatureArrayView(msg.Root(), 0)

	if got := arr.Slice(0, 2); len(got) != 2 || got[0] != s1 || got[1] != s2 {
		t.Fatalf("honest Slice(0,2) = %v", got)
	}
	if got := arr.Slice(1, 1); len(got) != 1 || got[0] != s2 {
		t.Fatalf("honest Slice(1,1) = %v", got)
	}
	if got := arr.Slice(0xFFFFFFFF, 1); got != nil {
		t.Fatalf("poisoned start: got %v, want nil", got)
	}
	if got := arr.Slice(0, 0xFFFFFFFF); got != nil {
		t.Fatalf("poisoned count: got len=%d, want nil", len(got))
	}
	if got := arr.Slice(2, 0xFFFFFFFE); got != nil {
		t.Fatalf("overflowing (start, count): got %v, want nil", got)
	}
}

// TestRedRound2_MEDIUM1_V2BufferRejectedAtSchemaGate pins v1-vs-v2 schema
// confusion: a Version1-header buffer that happens to carry NetworkID=11
// (whose byte 0 == 0x0B == TxKindBaseFull) must be rejected before TxKind
// is interpreted. Prior to LP-023 v3.1 r2 such a buffer would slip past
// the discriminator gate.
func TestRedRound2_MEDIUM1_V2BufferRejectedAtSchemaGate(t *testing.T) {
	// Forge a Version1 buffer where byte 0 of the root happens to be
	// TxKindBaseFull (0x0B).
	b := zap.NewBuilderV1(128)
	ob := b.StartObject(SizeBaseTxFull)
	// In v2 schema, byte 0 of the root would be a uint32 NetworkID. We set
	// byte 0 to 11 (0x0B) directly — the discriminator-byte-0 collision
	// from Red's v9 attack.
	ob.SetUint8(0, 0x0B)
	ob.FinishAsRoot()
	buf := b.Finish()

	// Confirm Version1 was emitted.
	if v := binary.LittleEndian.Uint16(buf[4:6]); v != zap.Version1 {
		t.Fatalf("test setup: expected Version1, got %d", v)
	}

	// Wrap*Tx MUST reject at the schema-version gate.
	if _, err := WrapBaseTxFull(buf); err != ErrWrongSchemaVersion {
		t.Fatalf("RED-MEDIUM-1 regression: WrapBaseTxFull on Version1 buffer returned %v, want ErrWrongSchemaVersion", err)
	}
	if _, err := WrapBaseTx(buf); err != ErrWrongSchemaVersion {
		t.Fatalf("RED-MEDIUM-1 regression: WrapBaseTx on Version1 buffer returned %v, want ErrWrongSchemaVersion", err)
	}
	// Every Wrap*Tx must reject (uniform gate).
	for name, fn := range map[string]func([]byte) error{
		"AdvanceTimeTx":                func(b []byte) error { _, e := WrapAdvanceTimeTx(b); return e },
		"RewardValidatorTx":            func(b []byte) error { _, e := WrapRewardValidatorTx(b); return e },
		"SetL1ValidatorWeightTx":       func(b []byte) error { _, e := WrapSetL1ValidatorWeightTx(b); return e },
		"IncreaseL1ValidatorBalanceTx": func(b []byte) error { _, e := WrapIncreaseL1ValidatorBalanceTx(b); return e },
		"DisableL1ValidatorTx":         func(b []byte) error { _, e := WrapDisableL1ValidatorTx(b); return e },
		"RegisterL1ValidatorTx":        func(b []byte) error { _, e := WrapRegisterL1ValidatorTx(b); return e },
		"SlashValidatorTx":             func(b []byte) error { _, e := WrapSlashValidatorTx(b); return e },
		"TransferChainOwnershipTx":     func(b []byte) error { _, e := WrapTransferChainOwnershipTx(b); return e },
		"RemoveChainValidatorTx":       func(b []byte) error { _, e := WrapRemoveChainValidatorTx(b); return e },
		"ImportTx":                     func(b []byte) error { _, e := WrapImportTx(b); return e },
		"ExportTx":                     func(b []byte) error { _, e := WrapExportTx(b); return e },
		"CreateChainTx":                func(b []byte) error { _, e := WrapCreateChainTx(b); return e },
		"AddPermissionlessValidatorTx": func(b []byte) error { _, e := WrapAddPermissionlessValidatorTx(b); return e },
	} {
		if err := fn(buf); err != ErrWrongSchemaVersion {
			t.Errorf("RED-MEDIUM-1 regression: %s on Version1 buffer returned %v, want ErrWrongSchemaVersion", name, err)
		}
	}
}

// TestRedRound2_MEDIUM1_HonestV2BuffersStillWork confirms that current
// builder output (Version2 by default) continues to round-trip through
// Wrap*Tx. The schema gate only rejects Version1.
func TestRedRound2_MEDIUM1_HonestV2BuffersStillWork(t *testing.T) {
	atx := NewAdvanceTimeTx(42)
	if _, err := WrapAdvanceTimeTx(atx.Bytes()); err != nil {
		t.Fatalf("Honest v2 AdvanceTimeTx rejected: %v", err)
	}
	btx := NewBaseTx(1, ids.ID{1}, []byte("memo"))
	if _, err := WrapBaseTx(btx.Bytes()); err != nil {
		t.Fatalf("Honest v2 BaseTx rejected: %v", err)
	}
}
