package fair_test

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"testing"

	"github.com/drjones/quantum-arcade/pkg/fair"
)

func TestCommitmentHidesSeed(t *testing.T) {
	s := fair.NewServerSeed()
	c := s.Commitment()
	raw := s.Bytes()
	if bytes.Contains(c[:], raw[:8]) {
		t.Fatal("commitment leaks seed bytes")
	}
}

func TestCommitmentVerifies(t *testing.T) {
	s := fair.NewServerSeed()
	c := s.Commitment()
	if !fair.VerifyCommitment(c, s) {
		t.Fatal("valid seed failed its own commitment")
	}
	other := fair.NewServerSeed()
	if fair.VerifyCommitment(c, other) {
		t.Fatal("a different seed satisfied the commitment")
	}
}

func TestClientSeedDependsOnEveryParticipant(t *testing.T) {
	a := []byte("player-a-pubkey")
	b := []byte("player-b-pubkey")
	c := []byte("player-c-pubkey")

	withAll := fair.ClientSeed([][]byte{a, b, c})
	withoutC := fair.ClientSeed([][]byte{a, b})
	if withAll == withoutC {
		t.Fatal("removing a participant did not change the client seed")
	}
}

// Join order must matter in a defined way, but the same set in the same order
// must always produce the same seed.
func TestClientSeedIsStable(t *testing.T) {
	keys := [][]byte{[]byte("k1"), []byte("k2")}
	if fair.ClientSeed(keys) != fair.ClientSeed(keys) {
		t.Fatal("client seed is not stable for identical input")
	}
}

func TestRoundSeedIsDeterministic(t *testing.T) {
	s := fair.NewServerSeed()
	cs := fair.ClientSeed([][]byte{[]byte("p1")})
	first := fair.RoundSeed(s, cs, 7)
	for i := 0; i < 50; i++ {
		if fair.RoundSeed(s, cs, 7) != first {
			t.Fatal("round seed is not deterministic")
		}
	}
}

func TestNonceSeparatesOutcomes(t *testing.T) {
	s := fair.NewServerSeed()
	cs := fair.ClientSeed([][]byte{[]byte("p1")})
	seen := map[[32]byte]bool{}
	for n := uint64(0); n < 1000; n++ {
		seed := fair.RoundSeed(s, cs, n)
		if seen[seed] {
			t.Fatalf("nonce %d collided with an earlier round seed", n)
		}
		seen[seed] = true
	}
}

// The full protocol as a player would check it: the commitment published before
// the round must match the seed revealed after, and the seed must reproduce the
// outcome.
func TestEndToEndVerification(t *testing.T) {
	server := fair.NewServerSeed()
	published := server.Commitment()

	participants := [][]byte{[]byte("alice"), []byte("bob")}
	cs := fair.ClientSeed(participants)
	const nonce = 42

	seed := fair.RoundSeed(server, cs, nonce)

	// After the round the server reveals the seed. A player recomputes:
	if !fair.VerifyCommitment(published, server) {
		t.Fatal("revealed seed does not match published commitment")
	}
	recomputed := fair.RoundSeed(server, fair.ClientSeed(participants), nonce)
	if recomputed != seed {
		t.Fatal("independent recomputation produced a different seed")
	}
}

func TestServerSeedsAreUnique(t *testing.T) {
	seen := map[[32]byte]bool{}
	for i := 0; i < 1000; i++ {
		s := fair.NewServerSeed()
		if seen[s.Bytes()] {
			t.Fatal("NewServerSeed returned a duplicate")
		}
		seen[s.Bytes()] = true
	}
}

func TestServerSeedRoundTripsThroughBytes(t *testing.T) {
	original := fair.NewServerSeed()
	restored := fair.ServerSeedFromBytes(original.Bytes())

	if restored.Bytes() != original.Bytes() {
		t.Fatal("seed did not survive a byte round trip")
	}
	if restored.Commitment() != original.Commitment() {
		t.Fatal("restored seed produces a different commitment")
	}
	if restored.Hex() != original.Hex() {
		t.Fatal("restored seed renders differently")
	}
}

func TestHexIsFullLength(t *testing.T) {
	s := fair.NewServerSeed()
	if len(s.Hex()) != 64 {
		t.Fatalf("hex seed is %d characters, want 64", len(s.Hex()))
	}
}

func TestBuildProofIsSelfConsistent(t *testing.T) {
	server := fair.NewServerSeed()
	keys := [][]byte{[]byte("alice"), []byte("bob"), []byte("carol")}
	const nonce = 17

	proof := fair.BuildProof(server, keys, nonce)

	if proof.Nonce != nonce {
		t.Fatalf("proof nonce = %d, want %d", proof.Nonce, nonce)
	}
	if len(proof.Participants) != len(keys) {
		t.Fatalf("proof lists %d participants, want %d", len(proof.Participants), len(keys))
	}
	// Every value in the proof must be reproducible from the others.
	seedBytes, err := hex.DecodeString(proof.ServerSeed)
	if err != nil {
		t.Fatal(err)
	}
	sum := sha256.Sum256(seedBytes)
	if hex.EncodeToString(sum[:]) != proof.Commitment {
		t.Fatal("proof commitment does not match its own seed")
	}
	var restored [32]byte
	copy(restored[:], seedBytes)
	want := fair.RoundSeed(fair.ServerSeedFromBytes(restored), fair.ClientSeed(keys), nonce)
	if hex.EncodeToString(want[:]) != proof.RoundSeed {
		t.Fatal("proof round seed does not follow from its inputs")
	}
}

func TestProofParticipantsPreserveOrder(t *testing.T) {
	server := fair.NewServerSeed()
	keys := [][]byte{[]byte("first"), []byte("second")}
	proof := fair.BuildProof(server, keys, 1)

	if proof.Participants[0] != hex.EncodeToString(keys[0]) {
		t.Fatal("participant order was not preserved")
	}
	if proof.Participants[1] != hex.EncodeToString(keys[1]) {
		t.Fatal("participant order was not preserved")
	}
}

// Reordering the same players must change the seed, since order is part of the
// commitment. Otherwise a player could be swapped in without detection.
func TestParticipantOrderAffectsTheSeed(t *testing.T) {
	a, b := []byte("alice"), []byte("bob")
	if fair.ClientSeed([][]byte{a, b}) == fair.ClientSeed([][]byte{b, a}) {
		t.Fatal("reordering participants did not change the client seed")
	}
}

// Length-prefixing must prevent two different participant lists from colliding
// through simple concatenation.
func TestClientSeedResistsConcatenationCollisions(t *testing.T) {
	// Without length prefixes, {"ab","c"} and {"a","bc"} would hash the same.
	one := fair.ClientSeed([][]byte{[]byte("ab"), []byte("c")})
	two := fair.ClientSeed([][]byte{[]byte("a"), []byte("bc")})
	if one == two {
		t.Fatal("different participant lists collided; length prefixing is broken")
	}
}

func TestEmptyParticipantListIsStable(t *testing.T) {
	if fair.ClientSeed(nil) != fair.ClientSeed([][]byte{}) {
		t.Fatal("nil and empty participant lists disagree")
	}
}

// A round with no players must still produce a valid, verifiable outcome.
func TestRoundWithNoPlayersStillVerifies(t *testing.T) {
	server := fair.NewServerSeed()
	commitment := server.Commitment()
	proof := fair.BuildProof(server, nil, 5)

	if !fair.VerifyCommitment(commitment, server) {
		t.Fatal("empty round does not verify")
	}
	if proof.RoundSeed == "" {
		t.Fatal("empty round produced no seed")
	}
}
