package main

import (
	"bytes"
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/binary"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"strings"
	"testing"

	"github.com/kami/maven/internal/webauthn"
)

const prfTestOrigin = "https://maven.test"
const prfTestRPID = "maven.test"

// fakeKeyIPC stands in for the mavend socket and records exactly what secret
// each call received — the point of the whole test file is that it is the PRF
// output and never the credential public key.
type fakeKeyIPC struct {
	unlockSecret []byte
	wrapSecret   []byte
	unlockCalls  int
	wrapCalls    int
	unlockErr    error
	wrapExplicit bool
	// opensWith, when set, is the only secret Unlock accepts. It stands in
	// for a wrapped blob on disk: everything else gets unlockErr.
	opensWith []byte
}

func (f *fakeKeyIPC) Unlock(_ context.Context, secret []byte) error {
	f.unlockCalls++
	f.unlockSecret = bytes.Clone(secret)
	if f.opensWith != nil {
		if bytes.Equal(secret, f.opensWith) {
			return nil
		}
		return errors.New("unwrap key: decrypt failed (wrong credential?)")
	}
	return f.unlockErr
}

func (f *fakeKeyIPC) StoreEncryptionKey(_ context.Context, secret []byte, explicit bool) error {
	f.wrapCalls++
	f.wrapSecret = bytes.Clone(secret)
	f.wrapExplicit = explicit
	return nil
}

func b64u(b []byte) string { return base64.RawURLEncoding.EncodeToString(b) }

// prfAuthenticator is a minimal software authenticator: a P-256 key plus the
// COSE encoding of its public half.
type prfAuthenticator struct {
	key    *ecdsa.PrivateKey
	credID []byte
	cose   []byte
}

func newPRFAuthenticator(t *testing.T) *prfAuthenticator {
	t.Helper()
	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		t.Fatalf("generate key: %v", err)
	}
	x := key.PublicKey.X.FillBytes(make([]byte, 32))
	y := key.PublicKey.Y.FillBytes(make([]byte, 32))
	// COSE_Key: {1: 2 (EC2), 3: -7 (ES256), -1: 1 (P-256), -2: x, -3: y}
	var c []byte
	c = append(c, 0xa5)             // map(5)
	c = append(c, 0x01, 0x02)       // 1: 2
	c = append(c, 0x03, 0x26)       // 3: -7
	c = append(c, 0x20, 0x01)       // -1: 1
	c = append(c, 0x21, 0x58, 0x20) // -2: bytes(32)
	c = append(c, x...)
	c = append(c, 0x22, 0x58, 0x20) // -3: bytes(32)
	c = append(c, y...)
	return &prfAuthenticator{key: key, credID: []byte("prf-cred"), cose: c}
}

func (a *prfAuthenticator) authData(flags byte, counter uint32, attested bool) []byte {
	h := sha256.Sum256([]byte(prfTestRPID))
	d := append([]byte{}, h[:]...)
	d = append(d, flags)
	cb := make([]byte, 4)
	binary.BigEndian.PutUint32(cb, counter)
	d = append(d, cb...)
	if attested {
		d = append(d, make([]byte, 16)...) // aaguid
		l := make([]byte, 2)
		binary.BigEndian.PutUint16(l, uint16(len(a.credID)))
		d = append(d, l...)
		d = append(d, a.credID...)
		d = append(d, a.cose...)
	}
	return d
}

func clientDataJSON(typ, challenge string) []byte {
	b, _ := json.Marshal(map[string]string{"type": typ, "challenge": challenge, "origin": prfTestOrigin})
	return b
}

// register drives POST /register/finish with a valid attestation.
func (a *prfAuthenticator) register(t *testing.T, h *PasskeyHandle) {
	t.Helper()
	_, chal, err := h.rp.CreationOptions([]byte("u"), "user")
	if err != nil {
		t.Fatalf("CreationOptions: %v", err)
	}
	// {"fmt":"none","attStmt":{},"authData":<bytes>}
	att := []byte{0xa3}
	att = append(att, 0x63, 'f', 'm', 't', 0x64, 'n', 'o', 'n', 'e')
	att = append(att, 0x67, 'a', 't', 't', 'S', 't', 'm', 't', 0xa0)
	ad := a.authData(1<<6|0x05, 0, true)
	att = append(att, 0x68, 'a', 'u', 't', 'h', 'D', 'a', 't', 'a')
	att = append(att, 0x59, byte(len(ad)>>8), byte(len(ad)))
	att = append(att, ad...)

	body, _ := json.Marshal(map[string]any{
		"challenge": chal,
		"credential": map[string]any{
			"id":   b64u(a.credID),
			"type": "public-key",
			"response": map[string]any{
				"clientDataJSON":    b64u(clientDataJSON("webauthn.create", chal)),
				"attestationObject": b64u(att),
			},
		},
	})
	w := httptest.NewRecorder()
	h.RegisterFinish(w, httptest.NewRequest(http.MethodPost, "/auth/webauthn/register/finish", bytes.NewReader(body)))
	if w.Code != http.StatusOK {
		t.Fatalf("RegisterFinish: %d %s", w.Code, w.Body.String())
	}
}

// assert drives POST /assert/finish with a valid assertion and the given
// base64url PRF result.
func (a *prfAuthenticator) assert(t *testing.T, h *PasskeyHandle, prf string) *httptest.ResponseRecorder {
	t.Helper()
	return a.assertExplicit(t, h, prf, false)
}

// assertExplicit is assert with control over the explicit flag the rewrite
// button sets.
func (a *prfAuthenticator) assertExplicit(t *testing.T, h *PasskeyHandle, prf string, explicit bool) *httptest.ResponseRecorder {
	t.Helper()
	_, chal, err := h.rp.AssertionOptions()
	if err != nil {
		t.Fatalf("AssertionOptions: %v", err)
	}
	ad := a.authData(0x05, 7, false)
	cdj := clientDataJSON("webauthn.get", chal)
	hash := sha256.Sum256(cdj)
	sig, err := ecdsa.SignASN1(rand.Reader, a.key, append(append([]byte{}, ad...), hash[:]...))
	if err != nil {
		t.Fatalf("sign: %v", err)
	}
	body, _ := json.Marshal(map[string]any{
		"challenge": chal,
		"prf":       prf,
		"explicit":  explicit,
		"credential": map[string]any{
			"id":   b64u(a.credID),
			"type": "public-key",
			"response": map[string]any{
				"clientDataJSON":    b64u(cdj),
				"authenticatorData": b64u(ad),
				"signature":         b64u(sig),
			},
		},
	})
	w := httptest.NewRecorder()
	h.AssertFinish(w, httptest.NewRequest(http.MethodPost, "/auth/webauthn/assert/finish", bytes.NewReader(body)))
	return w
}

func newPRFHandle(t *testing.T, key *fakeKeyIPC) *PasskeyHandle {
	t.Helper()
	store, err := newCredentialStore(filepath.Join(t.TempDir(), "passkeys.json"))
	if err != nil {
		t.Fatalf("credential store: %v", err)
	}
	return &PasskeyHandle{
		rp:        webauthn.NewRP(webauthn.Config{Origin: prfTestOrigin, RPID: prfTestRPID, RPName: "maven"}),
		encryptFn: key,
		store:     store,
		session:   webauthn.NewPasskeySession(0),
	}
}

// The fix for Vikunja #14: what goes over IPC is the PRF secret from the
// authenticator, not the credential public key sitting in passkeys.json.
func TestAssertSendsPRFSecretNotPublicKey(t *testing.T) {
	key := &fakeKeyIPC{}
	h := newPRFHandle(t, key)
	auth := newPRFAuthenticator(t)
	auth.register(t, h)

	// Enrolment must not wrap anything: create() yields no PRF result.
	if key.wrapCalls != 0 || key.unlockCalls != 0 {
		t.Fatalf("registration touched the key IPC (wrap=%d unlock=%d)", key.wrapCalls, key.unlockCalls)
	}

	secret := make([]byte, 32)
	for i := range secret {
		secret[i] = byte(i + 1)
	}
	if w := auth.assert(t, h, b64u(secret)); w.Code != http.StatusOK {
		t.Fatalf("AssertFinish: %d %s", w.Code, w.Body.String())
	}

	if key.unlockCalls != 1 || key.wrapCalls != 1 {
		t.Fatalf("unlock=%d wrap=%d, want 1 and 1", key.unlockCalls, key.wrapCalls)
	}
	if !bytes.Equal(key.unlockSecret, secret) {
		t.Errorf("Unlock got %x, want the PRF secret %x", key.unlockSecret, secret)
	}
	if !bytes.Equal(key.wrapSecret, secret) {
		t.Errorf("StoreEncryptionKey got %x, want the PRF secret %x", key.wrapSecret, secret)
	}
	// And explicitly: not the credential public key.
	pub, _, err := h.store.Lookup(b64u(auth.credID))
	if err != nil {
		t.Fatalf("lookup: %v", err)
	}
	if bytes.Equal(key.unlockSecret, pub) {
		t.Fatal("the credential public key was sent as the unlock secret")
	}
}

// An authenticator without PRF must produce no unlock attempt at all — the
// assertion still succeeds (step-up works), but cold-start unlock stays off
// rather than falling back to something weaker.
func TestAssertWithoutPRFDoesNotUnlock(t *testing.T) {
	for _, prf := range []string{"", "!!!not-base64!!!", b64u(make([]byte, 32)), b64u(make([]byte, 16))} {
		key := &fakeKeyIPC{}
		h := newPRFHandle(t, key)
		auth := newPRFAuthenticator(t)
		auth.register(t, h)

		w := auth.assert(t, h, prf)
		if w.Code != http.StatusOK {
			t.Fatalf("prf=%q: AssertFinish %d %s", prf, w.Code, w.Body.String())
		}
		if key.unlockCalls != 0 || key.wrapCalls != 0 {
			t.Errorf("prf=%q: unlock=%d wrap=%d, want no key IPC at all", prf, key.unlockCalls, key.wrapCalls)
		}
	}
}

// A failed unlock must not fail the assertion: step-up is independently valid,
// and a locked daemon degrades rather than breaking the login.
func TestAssertSucceedsWhenUnlockFails(t *testing.T) {
	key := &fakeKeyIPC{unlockErr: errors.New("wrong credential")}
	h := newPRFHandle(t, key)
	auth := newPRFAuthenticator(t)
	auth.register(t, h)

	secret := bytes.Repeat([]byte{3}, 32)
	if w := auth.assert(t, h, b64u(secret)); w.Code != http.StatusOK {
		t.Fatalf("AssertFinish: %d %s", w.Code, w.Body.String())
	}
	if key.unlockCalls == 0 {
		t.Error("unlock was never attempted")
	}
}

// A forged assertion must never reach the unlock path.
func TestForgedAssertionNeverUnlocks(t *testing.T) {
	key := &fakeKeyIPC{}
	h := newPRFHandle(t, key)
	auth := newPRFAuthenticator(t)
	auth.register(t, h)

	// A different key signing over the same credential id.
	attacker := newPRFAuthenticator(t)
	attacker.credID = auth.credID
	w := attacker.assert(t, h, b64u(bytes.Repeat([]byte{4}, 32)))
	if w.Code == http.StatusOK {
		t.Fatal("an assertion signed by the wrong key was accepted")
	}
	if key.unlockCalls != 0 || key.wrapCalls != 0 {
		t.Fatalf("a forged assertion reached the key IPC (unlock=%d wrap=%d)", key.unlockCalls, key.wrapCalls)
	}
}

// The browser side is the only place the PRF result exists. If the page stops
// asking for it or stops reading it back, cold-start unlock silently dies with
// nothing failing, so the page source is asserted directly.
func TestPasskeyPageRequestsAndPostsPRF(t *testing.T) {
	for _, want := range []string{
		"getClientExtensionResults",
		"ext.prf.results.first",
		"body:JSON.stringify({challenge,prf,",
	} {
		if !strings.Contains(passkeyPageHTML, want) {
			t.Errorf("the passkey page no longer contains %q", want)
		}
	}
}

// A box enrolled before Vikunja #14 has a v1 blob wrapped under the credential
// PUBLIC key. The PRF secret cannot open it, and this handler is the only
// caller of Unlock, so without the legacy retry that box stays locked forever
// while a perfectly good passkey is asserted at it.
func TestLegacyV1BlobStillColdStarts(t *testing.T) {
	key := &fakeKeyIPC{}
	h := newPRFHandle(t, key)
	auth := newPRFAuthenticator(t)
	auth.register(t, h)

	pub, _, err := h.store.Lookup(b64u(auth.credID))
	if err != nil {
		t.Fatalf("lookup: %v", err)
	}
	// The daemon only opens under the public key — a v1 blob.
	key.opensWith = pub

	secret := bytes.Repeat([]byte{9}, 32)
	if w := auth.assert(t, h, b64u(secret)); w.Code != http.StatusOK {
		t.Fatalf("AssertFinish: %d %s", w.Code, w.Body.String())
	}
	if key.unlockCalls != 2 {
		t.Fatalf("unlock attempted %d times, want 2 (PRF, then the legacy public key)", key.unlockCalls)
	}
	if !bytes.Equal(key.unlockSecret, pub) {
		t.Fatal("the legacy retry did not send the credential public key, so a v1 box can never cold-start again")
	}
}

// The PRF secret is tried first and, when it works, the public key is never
// sent. The legacy retry is a one-way door out of v1, not a fallback offered
// to every assertion.
func TestPRFUnlockNeverFallsBackWhenItWorks(t *testing.T) {
	secret := bytes.Repeat([]byte{7}, 32)
	key := &fakeKeyIPC{opensWith: secret}
	h := newPRFHandle(t, key)
	auth := newPRFAuthenticator(t)
	auth.register(t, h)

	if w := auth.assert(t, h, b64u(secret)); w.Code != http.StatusOK {
		t.Fatalf("AssertFinish: %d %s", w.Code, w.Body.String())
	}
	if key.unlockCalls != 1 {
		t.Fatalf("unlock attempted %d times, want 1", key.unlockCalls)
	}
}

// Wrapping the at-rest key is an explicit act, never a side effect of a
// step-up. A page POSTing a substituted prf on a routine assertion must not
// make the daemon re-wrap the database key under it.
func TestPlainAssertionAsksForNoRewrite(t *testing.T) {
	key := &fakeKeyIPC{}
	h := newPRFHandle(t, key)
	auth := newPRFAuthenticator(t)
	auth.register(t, h)

	if w := auth.assert(t, h, b64u(bytes.Repeat([]byte{5}, 32))); w.Code != http.StatusOK {
		t.Fatalf("AssertFinish: %d %s", w.Code, w.Body.String())
	}
	if key.wrapCalls != 1 {
		t.Fatalf("wrapCalls = %d, want 1", key.wrapCalls)
	}
	if key.wrapExplicit {
		t.Fatal("a plain step-up asked the daemon to rewrite the cold-start key")
	}
}

// The rewrite button, and only the rewrite button, sets explicit.
func TestRewriteButtonAsksForAnExplicitWrap(t *testing.T) {
	key := &fakeKeyIPC{}
	h := newPRFHandle(t, key)
	auth := newPRFAuthenticator(t)
	auth.register(t, h)

	if w := auth.assertExplicit(t, h, b64u(bytes.Repeat([]byte{6}, 32)), true); w.Code != http.StatusOK {
		t.Fatalf("AssertFinish: %d %s", w.Code, w.Body.String())
	}
	if !key.wrapExplicit {
		t.Fatal("the explicit flag did not reach the daemon, so the rewrite button cannot work")
	}
}

// The page is the only place the explicit flag originates. If the button or
// the field goes away, rewriting a cold-start key becomes impossible with
// nothing failing.
func TestPasskeyPageHasTheRewriteButton(t *testing.T) {
	for _, want := range []string{
		"rewrite cold-start key",
		"explicit:!!explicit",
		"async function rewrapKey()",
	} {
		if !strings.Contains(passkeyPageHTML, want) {
			t.Errorf("the passkey page no longer contains %q", want)
		}
	}
}
