package s3api

import "testing"

func TestVerifyKeyCommitment_DefaultAcceptsMissing(t *testing.T) {
	// Default behaviour mirrors AWS: a missing commitment field is treated
	// as a legacy object and accepted. This is the cushion that lets
	// operators upgrade without breaking pre-commitment uploads.
	prev := requireKeyCommitment.Load()
	t.Cleanup(func() { requireKeyCommitment.Store(prev) })
	requireKeyCommitment.Store(false)

	if err := VerifyKeyCommitment([]byte("k"), []byte("iv"), "AES256", nil); err != nil {
		t.Fatalf("default path should accept missing commitment, got: %v", err)
	}
}

func TestVerifyKeyCommitment_StrictRejectsMissing(t *testing.T) {
	// Strict mode: any object whose metadata lacks the commitment field is
	// rejected. Closes the silent-downgrade vector — an attacker who can
	// strip the commitment from metadata can no longer bypass verification.
	prev := requireKeyCommitment.Load()
	t.Cleanup(func() { requireKeyCommitment.Store(prev) })
	requireKeyCommitment.Store(true)

	err := VerifyKeyCommitment([]byte("k"), []byte("iv"), "AES256", nil)
	if err == nil {
		t.Fatal("strict mode should reject missing commitment; got nil error")
	}
}

func TestVerifyKeyCommitment_StrictAcceptsValidCommitment(t *testing.T) {
	// Strict mode does not change the verification outcome for objects that
	// do carry a commitment — the only behavioural delta is the missing
	// case.
	prev := requireKeyCommitment.Load()
	t.Cleanup(func() { requireKeyCommitment.Store(prev) })
	requireKeyCommitment.Store(true)

	key := []byte("strict-mode-test-key")
	// IV here is just an opaque input to the HMAC commitment; the test
	// doesn't pass it into AES-CTR so it doesn't have to be the AES block
	// size. The 16 bytes match the AES block size to keep the literal
	// realistic.
	iv := []byte("strict-mode-iv16")
	commit := ComputeKeyCommitment(key, iv, "AES256")
	if err := VerifyKeyCommitment(key, iv, "AES256", commit); err != nil {
		t.Fatalf("strict mode should accept valid commitment, got: %v", err)
	}
}

func TestVerifyKeyCommitment_RejectsTamperedKey(t *testing.T) {
	// Real attack shape: an attacker who can mutate object metadata cannot
	// craft a valid commitment without the original key. The verify path
	// must catch it whether they tamper with the key, the IV, or the
	// algorithm — all three are bound by the HMAC.
	prev := requireKeyCommitment.Load()
	t.Cleanup(func() { requireKeyCommitment.Store(prev) })
	requireKeyCommitment.Store(false)

	originalKey := []byte("legit-key-for-commitment")
	iv := []byte("commitment-iv-16")
	algo := "AES256"
	commit := ComputeKeyCommitment(originalKey, iv, algo)

	t.Run("tampered key", func(t *testing.T) {
		if err := VerifyKeyCommitment([]byte("attacker-substituted-key"), iv, algo, commit); err == nil {
			t.Fatal("verify must reject when the key changed but commitment did not")
		}
	})
	t.Run("tampered IV", func(t *testing.T) {
		if err := VerifyKeyCommitment(originalKey, []byte("attacker-iv-16!!"), algo, commit); err == nil {
			t.Fatal("verify must reject when the IV changed but commitment did not")
		}
	})
	t.Run("tampered algorithm", func(t *testing.T) {
		if err := VerifyKeyCommitment(originalKey, iv, "AES128", commit); err == nil {
			t.Fatal("verify must reject when the algorithm changed but commitment did not")
		}
	})
	t.Run("tampered commitment", func(t *testing.T) {
		bad := append([]byte{}, commit...)
		bad[0] ^= 0x01
		if err := VerifyKeyCommitment(originalKey, iv, algo, bad); err == nil {
			t.Fatal("verify must reject when the commitment itself was flipped")
		}
	})
}

func TestSetRequireKeyCommitment(t *testing.T) {
	prev := requireKeyCommitment.Load()
	t.Cleanup(func() { requireKeyCommitment.Store(prev) })

	SetRequireKeyCommitment(true)
	if !requireKeyCommitment.Load() {
		t.Fatal("SetRequireKeyCommitment(true) did not propagate to the atomic")
	}
	SetRequireKeyCommitment(false)
	if requireKeyCommitment.Load() {
		t.Fatal("SetRequireKeyCommitment(false) did not propagate to the atomic")
	}
}
