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

// quorum_guard_node_test.go — CRITICAL-1(c) fail-closed guard + the wiring proof
// the MEDIUM-1 round lacked. The round-1 tests injected a validators.State into
// the source constructors directly and never exercised the path where
// m.validatorState is nil (the production default until the P-Chain publishes
// its State). These tests pin:
//
//	(1) the guard predicate: a K>1 chain with NO height-indexed state is refused
//	    (would otherwise stall finality forever);
//	(2) the failure mechanism: getValidatorState(nil) → the no-op State, whose
//	    GetValidatorSet is EMPTY at every height → the stake source totals 0 and
//	    the set-root is Empty (exactly the inputs that make VerifyWeighted fail
//	    closed). This is what the guard exists to prevent silently;
//	(3) the fix: once a REAL height-indexed state is published, getValidatorState
//	    returns it and the same sources read a live set (non-zero stake / non-Empty
//	    root) — finality can proceed.
package chains

import (
	"context"
	"testing"

	"github.com/luxfi/ids"
	validators "github.com/luxfi/validators"
	"github.com/luxfi/validators/validatorstest"
)

// TestQuorumGuard_RefusesNoopState pins the guard predicate (CRITICAL-1(c)): a
// nil (unpublished) validator state is NOT live, so a K>1 chain must refuse to
// start; a published state IS live.
func TestQuorumGuard_RefusesNoopState(t *testing.T) {
	if quorumValidatorStateLive(nil) {
		t.Fatal("CRITICAL-1(c): a nil validator state must NOT be considered live (would stall finality)")
	}
	live := validatorstest.NewTestState()
	if !quorumValidatorStateLive(live) {
		t.Fatal("a published height-indexed validator state must be considered live")
	}
}

// TestGetValidatorState_NoopIsEmptyAtEveryHeight proves the failure mechanism the
// guard prevents: with no state published, getValidatorState yields the no-op
// State, and the height-pinned sources read an EMPTY set at every height — zero
// total stake and an Empty set-root, the exact inputs that make the engine's
// VerifyWeighted fail closed (ErrQCStakeBelowSupermajority) so NO block finalizes.
func TestGetValidatorState_NoopIsEmptyAtEveryHeight(t *testing.T) {
	netID := ids.GenerateTestID()

	// Production default: validatorState unset → getValidatorState(nil) = no-op.
	noop := getValidatorState(nil)
	if noop == nil {
		t.Fatal("getValidatorState(nil) must return the no-op State, not nil")
	}

	stake := newValidatorStakeSource(noop, netID)
	root := newValidatorSetRootSource(noop, netID)

	for _, h := range []uint64{0, 1, 7, 1_000, 10_000_000} {
		if total := stake.TotalStake(h); total != 0 {
			t.Fatalf("no-op State must report zero total stake at height %d, got %d", h, total)
		}
		if r := root.ValidatorSetRoot(h); r != ids.Empty {
			t.Fatalf("no-op State must commit to Empty set-root at height %d, got %s", h, r)
		}
	}
}

// TestGetValidatorState_LiveStateReadsSet proves the fix: once a real
// height-indexed state is published (as the P-Chain does), getValidatorState
// returns it and the SAME sources read a live set — non-zero stake and a
// non-Empty set-root — so the ⅔-by-stake finality predicate can be satisfied.
func TestGetValidatorState_LiveStateReadsSet(t *testing.T) {
	netID := ids.GenerateTestID()
	n0, n1, n2 := ids.GenerateTestNodeID(), ids.GenerateTestNodeID(), ids.GenerateTestNodeID()

	const H = uint64(7)
	live := validatorstest.NewTestState()
	live.GetValidatorSetF = func(_ context.Context, height uint64, gotNet ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
		if gotNet != netID || height != H {
			return map[ids.NodeID]*validators.GetValidatorOutput{}, nil
		}
		return map[ids.NodeID]*validators.GetValidatorOutput{
			n0: {NodeID: n0, PublicKey: []byte("pk0"), Light: 30, Weight: 30},
			n1: {NodeID: n1, PublicKey: []byte("pk1"), Light: 30, Weight: 30},
			n2: {NodeID: n2, PublicKey: []byte("pk2"), Light: 40, Weight: 40},
		}, nil
	}

	// getValidatorState passes a non-nil state through unchanged.
	got := getValidatorState(live)
	stake := newValidatorStakeSource(got, netID)
	root := newValidatorSetRootSource(got, netID)

	if total := stake.TotalStake(H); total != 100 {
		t.Fatalf("live State must report total stake 100 at the epoch, got %d", total)
	}
	if w := stake.Weight(n2, H); w != 40 {
		t.Fatalf("live State must report n2 weight 40 at the epoch, got %d", w)
	}
	if r := root.ValidatorSetRoot(H); r == ids.Empty {
		t.Fatal("live State must commit to a NON-Empty set-root at the epoch")
	}
	// A different height (no set) is still Empty/zero — the read is height-pinned.
	if stake.TotalStake(H+1) != 0 || root.ValidatorSetRoot(H+1) != ids.Empty {
		t.Fatal("live State read must be height-pinned (empty at a height with no set)")
	}
}
