package webauthn

import (
	"bytes"
	"crypto/rand"
	"errors"
	"io"
	"testing"
)

func testSecret(t *testing.T) []byte {
	t.Helper()
	s := make([]byte, secretLen)
	if _, err := io.ReadFull(rand.Reader, s); err != nil {
		t.Fatalf("rand: %v", err)
	}
	s[0] |= 1 // never all-zero
	return s
}

func testKey(t *testing.T) []byte {
	t.Helper()
	k := make([]byte, keyLen)
	if _, err := io.ReadFull(rand.Reader, k); err != nil {
		t.Fatalf("rand: %v", err)
	}
	return k
}

func TestWrapUnwrapRoundTrip(t *testing.T) {
	key, secret := testKey(t), testSecret(t)

	blob, err := WrapKey(key, secret)
	if err != nil {
		t.Fatalf("WrapKey: %v", err)
	}
	if !bytes.HasPrefix(blob, blobMagicV2) {
		t.Fatalf("blob does not start with the v2 magic: %x", blob[:8])
	}
	// The plaintext key must not be recoverable by reading the file.
	if bytes.Contains(blob, key) {
		t.Fatal("the wrapped blob contains the plaintext key verbatim")
	}

	got, version, err := UnwrapKey(blob, secret)
	if err != nil {
		t.Fatalf("UnwrapKey: %v", err)
	}
	if version != BlobV2 {
		t.Errorf("version = %v, want v2", version)
	}
	if !bytes.Equal(got, key) {
		t.Errorf("unwrapped key differs from the wrapped one")
	}
}

// Fresh salt and nonce per wrap: two blobs of the same key under the same
// secret must not be byte-identical, or the file leaks that nothing changed.
func TestWrapKeyIsNotDeterministic(t *testing.T) {
	key, secret := testKey(t), testSecret(t)
	a, err := WrapKey(key, secret)
	if err != nil {
		t.Fatalf("WrapKey: %v", err)
	}
	b, err := WrapKey(key, secret)
	if err != nil {
		t.Fatalf("WrapKey: %v", err)
	}
	if bytes.Equal(a, b) {
		t.Fatal("two wraps of the same key produced identical blobs")
	}
}

// The failure mode that matters most: a wrong passkey must not unlock.
func TestUnwrapWithWrongSecretFails(t *testing.T) {
	key := testKey(t)
	blob, err := WrapKey(key, testSecret(t))
	if err != nil {
		t.Fatalf("WrapKey: %v", err)
	}
	got, _, err := UnwrapKey(blob, testSecret(t))
	if err == nil {
		t.Fatal("a different secret unwrapped the blob")
	}
	if !errors.Is(err, ErrKeyUnwrap) {
		t.Errorf("err = %v, want ErrKeyUnwrap", err)
	}
	if got != nil {
		t.Error("key material returned alongside an error")
	}
}

// One flipped bit anywhere must fail the GCM tag, including in the salt and
// nonce — those are not authenticated by the tag but they change the
// derivation, so the tag fails anyway.
func TestUnwrapRejectsTamperedBlob(t *testing.T) {
	key, secret := testKey(t), testSecret(t)
	blob, err := WrapKey(key, secret)
	if err != nil {
		t.Fatalf("WrapKey: %v", err)
	}
	for i := range blob {
		bad := bytes.Clone(blob)
		bad[i] ^= 0x01
		if _, _, err := UnwrapKey(bad, secret); err == nil {
			t.Fatalf("byte %d of %d could be flipped and the blob still opened", i, len(blob))
		}
	}
}

func TestUnwrapRejectsTruncatedBlob(t *testing.T) {
	key, secret := testKey(t), testSecret(t)
	blob, err := WrapKey(key, secret)
	if err != nil {
		t.Fatalf("WrapKey: %v", err)
	}
	for _, n := range []int{0, 1, len(blobMagicV2), len(blobMagicV2) + saltLen, len(blob) - 1} {
		if _, _, err := UnwrapKey(blob[:n], secret); err == nil {
			t.Errorf("a %d-byte blob unwrapped", n)
		}
	}
}

// A v2 blob must not be downgradeable to v1 by stripping its header: the magic
// is GCM additional data, so the tag fails once it is gone.
func TestV2BlobCannotBeStrippedToV1(t *testing.T) {
	key, secret := testKey(t), testSecret(t)
	blob, err := WrapKey(key, secret)
	if err != nil {
		t.Fatalf("WrapKey: %v", err)
	}
	if _, _, err := UnwrapKey(blob[len(blobMagicV2):], secret); err == nil {
		t.Fatal("a header-stripped v2 blob was accepted as v1")
	}
}

// v1 blobs still open, and report themselves as v1 so the daemon can warn.
// wrapV1 reproduces the legacy writer this file no longer has.
func wrapV1(t *testing.T, key, secret []byte) []byte {
	t.Helper()
	salt := make([]byte, saltLen)
	nonce := make([]byte, nonceLen)
	if _, err := io.ReadFull(rand.Reader, salt); err != nil {
		t.Fatalf("rand: %v", err)
	}
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		t.Fatalf("rand: %v", err)
	}
	gcm, err := gcmFor(secret, salt, wrapInfoV1)
	if err != nil {
		t.Fatalf("gcmFor: %v", err)
	}
	out := append([]byte{}, salt...)
	out = append(out, nonce...)
	return append(out, gcm.Seal(nil, nonce, key, nil)...)
}

func TestUnwrapReadsLegacyV1(t *testing.T) {
	key := testKey(t)
	// v1 was keyed on the credential public key: not 32 bytes, and that is
	// deliberately still accepted on the read path.
	pub := make([]byte, 77)
	if _, err := io.ReadFull(rand.Reader, pub); err != nil {
		t.Fatalf("rand: %v", err)
	}
	blob := wrapV1(t, key, pub)

	got, version, err := UnwrapKey(blob, pub)
	if err != nil {
		t.Fatalf("UnwrapKey(v1): %v", err)
	}
	if version != BlobV1 {
		t.Errorf("version = %v, want v1", version)
	}
	if !bytes.Equal(got, key) {
		t.Error("v1 round-trip lost the key")
	}
	if _, _, err := UnwrapKey(blob, pub[:76]); err == nil {
		t.Error("a truncated public key opened the v1 blob")
	}
}

// The structural guard against the bug this replaces: a COSE public key is not
// 32 bytes, so it can never be used to write a new blob.
func TestWrapKeyRefusesNonPRFSecret(t *testing.T) {
	key := testKey(t)
	cases := map[string][]byte{
		"nil":              nil,
		"empty":            {},
		"short":            make([]byte, 16),
		"cose public key":  make([]byte, 77),
		"all-zero 32 byte": make([]byte, 32),
	}
	for name, secret := range cases {
		t.Run(name, func(t *testing.T) {
			if _, err := WrapKey(key, secret); err == nil {
				t.Fatalf("WrapKey accepted a %s secret", name)
			}
		})
	}
}

func TestWrapKeyRefusesWrongKeyLength(t *testing.T) {
	secret := testSecret(t)
	for _, n := range []int{0, 16, 31, 33, 64} {
		if _, err := WrapKey(make([]byte, n), secret); err == nil {
			t.Errorf("WrapKey accepted a %d-byte plaintext key", n)
		}
	}
}

// A v2 blob demands exactly 32 bytes on the read path too, so a caller cannot
// go back to passing a public key.
func TestUnwrapV2RefusesNonPRFSecret(t *testing.T) {
	blob, err := WrapKey(testKey(t), testSecret(t))
	if err != nil {
		t.Fatalf("WrapKey: %v", err)
	}
	if _, _, err := UnwrapKey(blob, make([]byte, 77)); !errors.Is(err, ErrKeyUnwrap) {
		t.Fatalf("err = %v, want ErrKeyUnwrap for a 77-byte secret", err)
	}
	if _, _, err := UnwrapKey(blob, nil); err == nil {
		t.Fatal("an empty secret unwrapped a v2 blob")
	}
}

func TestUnwrapRejectsOversizeBlob(t *testing.T) {
	if _, _, err := UnwrapKey(make([]byte, maxBlobLen+1), testSecret(t)); !errors.Is(err, ErrBlobTooLong) {
		t.Fatalf("err = %v, want ErrBlobTooLong", err)
	}
}

// WrapKey refuses an all-zero secret because a blob wrapped under one is a
// blob anyone can open. The v2 unwrap side must refuse it for the same reason:
// if the two ends disagree about what a valid secret is, a blob can be opened
// by material that could never have sealed it.
func TestUnwrapV2RefusesAnAllZeroSecret(t *testing.T) {
	key := bytes.Repeat([]byte{1}, 32)
	blob, err := WrapKey(key, bytes.Repeat([]byte{2}, 32))
	if err != nil {
		t.Fatalf("WrapKey: %v", err)
	}
	if _, _, err := UnwrapKey(blob, make([]byte, 32)); !errors.Is(err, ErrKeyUnwrap) {
		t.Fatalf("UnwrapKey with an all-zero secret = %v, want refusal", err)
	}
}
