package webauthn

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"errors"
	"testing"
)

// The salt is the identity of every wrapped key file ever written. If it
// changes, every deployment's blob becomes unopenable, so it is pinned here.
func TestPRFSaltIsStable(t *testing.T) {
	salt := PRFSalt()
	if len(salt) != 32 {
		t.Fatalf("salt is %d bytes, want 32", len(salt))
	}
	if got := base64.RawURLEncoding.EncodeToString(salt); got != base64.RawURLEncoding.EncodeToString(PRFSalt()) {
		t.Fatal("PRFSalt is not deterministic")
	}
	// Mutating the returned slice must not affect the next caller.
	salt[0] ^= 0xff
	if bytes.Equal(salt, PRFSalt()) {
		t.Fatal("PRFSalt returned shared backing state")
	}
}

func TestDecodePRFResult(t *testing.T) {
	raw := make([]byte, 32)
	for i := range raw {
		raw[i] = byte(i + 1)
	}
	for _, enc := range []string{
		base64.RawURLEncoding.EncodeToString(raw),
		base64.URLEncoding.EncodeToString(raw),
	} {
		got, err := DecodePRFResult(enc)
		if err != nil {
			t.Fatalf("DecodePRFResult(%q): %v", enc, err)
		}
		if !bytes.Equal(got, raw) {
			t.Errorf("decoded %x, want %x", got, raw)
		}
	}
}

// No PRF must be a distinguishable, named failure — never a silent fallback to
// some other secret.
func TestDecodePRFResultNoPRF(t *testing.T) {
	if _, err := DecodePRFResult(""); !errors.Is(err, ErrNoPRF) {
		t.Fatalf("err = %v, want ErrNoPRF", err)
	}
}

func TestDecodePRFResultRejectsUnusable(t *testing.T) {
	zeros := base64.RawURLEncoding.EncodeToString(make([]byte, 32))
	short := base64.RawURLEncoding.EncodeToString(make([]byte, 16))
	long := base64.RawURLEncoding.EncodeToString(make([]byte, 64))
	for name, in := range map[string]string{
		"not base64": "!!!!",
		"all zero":   zeros,
		"too short":  short,
		"too long":   long,
	} {
		t.Run(name, func(t *testing.T) {
			if _, err := DecodePRFResult(in); err == nil {
				t.Fatalf("accepted a %s PRF result", name)
			}
		})
	}
}

// Both option builders must ask for PRF, or the browser never produces a
// secret and cold-start unlock silently never works.
func TestOptionsRequestPRF(t *testing.T) {
	rp := NewRP(Config{Origin: "http://localhost:8080", RPID: "localhost", RPName: "maven"})

	create, _, err := rp.CreationOptions([]byte("u"), "u")
	if err != nil {
		t.Fatalf("CreationOptions: %v", err)
	}
	if _, ok := extPRF(t, create)["prf"]; !ok {
		t.Error("creation options do not request the prf extension")
	}

	assert, _, err := rp.AssertionOptions()
	if err != nil {
		t.Fatalf("AssertionOptions: %v", err)
	}
	prf, ok := extPRF(t, assert)["prf"].(map[string]any)
	if !ok {
		t.Fatal("assertion options do not request the prf extension")
	}
	eval, _ := prf["eval"].(map[string]any)
	first, _ := eval["first"].(string)
	if first != base64.RawURLEncoding.EncodeToString(PRFSalt()) {
		t.Errorf("prf.eval.first = %q, want the fixed salt", first)
	}
}

func extPRF(t *testing.T, opts any) map[string]any {
	t.Helper()
	b, err := json.Marshal(opts)
	if err != nil {
		t.Fatalf("marshal options: %v", err)
	}
	var m struct {
		Extensions map[string]any `json:"extensions"`
	}
	if err := json.Unmarshal(b, &m); err != nil {
		t.Fatalf("unmarshal options: %v", err)
	}
	return m.Extensions
}
