package service

import (
	"bytes"
	"context"
	"crypto/rand"
	"encoding/json"
	"errors"
	"io"
	"strings"
	"testing"
	"time"

	webauthnlib "github.com/go-webauthn/webauthn/webauthn"

	"wucher/internal/config"
	"wucher/internal/domain/auth"
	"wucher/internal/shared/pkg/uuidv7"
)

const (
	testCredentialCreationResponseJSON = `{
	"id":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
	"rawId":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
	"type":"public-key",
	"authenticatorAttachment":"platform",
	"clientExtensionResults":{"appid":true},
	"response":{
		"attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjEdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQOsa7QYSUFukFOLTmgeK6x2ktirNMgwy_6vIwwtegxI2flS1X-JAkZL5dsadg-9bEz2J7PnsbB0B08txvsyUSvKlAQIDJiABIVggLKF5xS0_BntttUIrm2Z2tgZ4uQDwllbdIfrrBMABCNciWCDHwin8Zdkr56iSIh0MrB5qZiEzYLQpEOREhMUkY6q4Vw",
		"clientDataJSON":"eyJjaGFsbGVuZ2UiOiJXOEd6RlU4cEdqaG9SYldyTERsYW1BZnFfeTRTMUNaRzFWdW9lUkxBUnJFIiwib3JpZ2luIjoiaHR0cHM6Ly93ZWJhdXRobi5pbyIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ",
		"transports":["usb","nfc","fake"]
	}
}`
	testCredentialAssertionResponseJSON = `{
	"id":"AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
	"rawId":"AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
	"clientExtensionResults":{"appID":"example.com"},
	"type":"public-key",
	"response":{
		"authenticatorData":"dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFXJJiGa3OAAI1vMYKZIsLJfHwVQMANwCOw-atj9C0vhWpfWU-whzNjeQS21Lpxfdk_G-omAtffWztpGoErlNOfuXWRqm9Uj9ANJck1p6lAQIDJiABIVggKAhfsdHcBIc0KPgAcRyAIK_-Vi-nCXHkRHPNaCMBZ-4iWCBxB8fGYQSBONi9uvq0gv95dGWlhJrBwCsj_a4LJQKVHQ",
		"clientDataJSON":"eyJjaGFsbGVuZ2UiOiJFNFBUY0lIX0hmWDFwQzZTaWdrMVNDOU5BbGdlenROMDQzOXZpOHpfYzlrIiwibmV3X2tleXNfbWF5X2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgiLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwidHlwZSI6IndlYmF1dGhuLmdldCJ9",
		"signature":"MEUCIBtIVOQxzFYdyWQyxaLR0tik1TnuPhGVhXVSNgFwLmN5AiEAnxXdCq0UeAVGWxOaFcjBZ_mEZoXqNboY5IkQDdlWZYc",
		"userHandle":"0ToAAAAAAAAAAA"
	}
}`
)

type webAuthnRepoWithErrors struct {
	*mockAuthRepo
	listCredsErr  error
	createCredErr error
	updateCredErr error
}

func newWebAuthnRepoWithErrors() *webAuthnRepoWithErrors {
	return &webAuthnRepoWithErrors{mockAuthRepo: newMockAuthRepo()}
}

func (r *webAuthnRepoWithErrors) ListUserWebAuthnCredentials(ctx context.Context, userID []byte) ([]auth.UserWebAuthnCredential, error) {
	if r.listCredsErr != nil {
		return nil, r.listCredsErr
	}
	return r.mockAuthRepo.ListUserWebAuthnCredentials(ctx, userID)
}

func (r *webAuthnRepoWithErrors) CreateUserWebAuthnCredential(ctx context.Context, credential *auth.UserWebAuthnCredential) error {
	if r.createCredErr != nil {
		return r.createCredErr
	}
	return r.mockAuthRepo.CreateUserWebAuthnCredential(ctx, credential)
}

func (r *webAuthnRepoWithErrors) UpdateUserWebAuthnCredential(ctx context.Context, userID, credentialID, credentialJSON []byte, usedAt time.Time) error {
	if r.updateCredErr != nil {
		return r.updateCredErr
	}
	return r.mockAuthRepo.UpdateUserWebAuthnCredential(ctx, userID, credentialID, credentialJSON, usedAt)
}

func newConfiguredWebAuthnService(repo auth.Repository, tokens TokenStore) *AuthService {
	return NewAuthService(
		repo,
		newMockRoleRepo(),
		tokens,
		nil, AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{
			WebAuthnRPID:          "example.com",
			WebAuthnRPDisplayName: "Wucher Test",
			WebAuthnRPOrigins:     []string{"https://example.com"},
			WebAuthnChallengeTTL:  2 * time.Minute,
		}},
	)
}

func putWebAuthnChallengeToken(t *testing.T, tokens *mockTokenStore, token, ceremony string, userID []byte, expires time.Time) {
	t.Helper()
	envelope := webAuthnChallengeEnvelope{
		Ceremony: ceremony,
		UserID:   append([]byte(nil), userID...),
		Session: webauthnlib.SessionData{
			Challenge:      "challenge",
			RelyingPartyID: "example.com",
			UserID:         append([]byte(nil), userID...),
			Expires:        expires,
		},
	}
	payload, err := json.Marshal(envelope)
	if err != nil {
		t.Fatalf("marshal envelope: %v", err)
	}
	tokens.store[webAuthnChallengeKey(token)] = payload
}

func TestWebAuthnUserMethods(t *testing.T) {
	user := webAuthnUser{
		id:          []byte{1, 2, 3},
		name:        "user@example.com",
		displayName: "User Example",
		credentials: []webauthnlib.Credential{{ID: []byte{9, 8, 7}}},
	}

	id := user.WebAuthnID()
	if !bytes.Equal(id, []byte{1, 2, 3}) {
		t.Fatalf("unexpected id: %v", id)
	}
	id[0] = 99
	if user.id[0] == 99 {
		t.Fatalf("expected returned id to be copied")
	}

	if user.WebAuthnName() != "user@example.com" {
		t.Fatalf("unexpected name")
	}
	if user.WebAuthnDisplayName() != "User Example" {
		t.Fatalf("unexpected display name")
	}

	creds := user.WebAuthnCredentials()
	if len(creds) != 1 {
		t.Fatalf("unexpected credential length")
	}
	creds = append(creds, webauthnlib.Credential{})
	if len(user.credentials) != 1 {
		t.Fatalf("expected credential slice header to be copied")
	}
}

func TestBuildWebAuthnUser(t *testing.T) {
	uid := uuidv7.MustBytes()
	credential := webauthnlib.Credential{ID: []byte{1, 2, 3}}
	user := &auth.User{
		ID:        uid,
		Email:     "  alice@example.com ",
		FirstName: "  Alice ",
		LastName:  " Smith  ",
		IsActive:  true,
	}
	waUser := buildWebAuthnUser(user, []webauthnlib.Credential{credential})
	if waUser.WebAuthnName() != "alice@example.com" {
		t.Fatalf("unexpected webauthn name")
	}
	if waUser.WebAuthnDisplayName() != "Alice Smith" {
		t.Fatalf("unexpected webauthn display name")
	}
	if !bytes.Equal(waUser.WebAuthnID(), uid) {
		t.Fatalf("unexpected webauthn id")
	}

	fallback := buildWebAuthnUser(&auth.User{
		ID:        uid,
		Email:     "bob@example.com",
		FirstName: " ",
		LastName:  " ",
		IsActive:  true,
	}, nil)
	if fallback.WebAuthnDisplayName() != "bob@example.com" {
		t.Fatalf("expected display name fallback to email")
	}
}

func TestAuthServiceIsWebAuthnConfigured(t *testing.T) {
	t.Run("no credentials", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		svc := newConfiguredWebAuthnService(repo, newMockTokenStore())
		ok, err := svc.IsWebAuthnConfigured(context.Background(), uuidv7.MustBytes())
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if ok {
			t.Fatalf("expected not configured")
		}
	})

	t.Run("has credentials", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		userID := uuidv7.MustBytes()
		repo.webauthnCredentials[string(userID)] = []auth.UserWebAuthnCredential{{
			ID:           uuidv7.MustBytes(),
			UserID:       userID,
			CredentialID: []byte("cred-1"),
		}}
		svc := newConfiguredWebAuthnService(repo, newMockTokenStore())
		ok, err := svc.IsWebAuthnConfigured(context.Background(), userID)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if !ok {
			t.Fatalf("expected configured")
		}
	})

	t.Run("repo error", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		repo.listCredsErr = errors.New("list failed")
		svc := newConfiguredWebAuthnService(repo, newMockTokenStore())
		ok, err := svc.IsWebAuthnConfigured(context.Background(), uuidv7.MustBytes())
		if !errors.Is(err, repo.listCredsErr) {
			t.Fatalf("expected repo error, got %v", err)
		}
		if ok {
			t.Fatalf("expected configured false on error")
		}
	})
}

func TestAuthServiceWebAuthnChallengeTTL(t *testing.T) {
	svcDefault := NewAuthService(newMockAuthRepo(), newMockRoleRepo(), nil, nil, AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{
		WebAuthnChallengeTTL: 0,
	}})
	if got := svcDefault.WebAuthnChallengeTTL(); got != 5*time.Minute {
		t.Fatalf("expected default ttl 5m, got %v", got)
	}

	svcCustom := NewAuthService(newMockAuthRepo(), newMockRoleRepo(), nil, nil, AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{
		WebAuthnChallengeTTL: 42 * time.Second,
	}})
	if got := svcCustom.WebAuthnChallengeTTL(); got != 42*time.Second {
		t.Fatalf("expected custom ttl, got %v", got)
	}
}

func TestAuthServiceWebAuthnClient(t *testing.T) {
	t.Run("missing rpid", func(t *testing.T) {
		svc := NewAuthService(newMockAuthRepo(), newMockRoleRepo(), nil, nil, AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{
			WebAuthnRPOrigins: []string{"https://example.com"},
		}})
		if _, err := svc.webAuthnClient(); !errors.Is(err, ErrWebAuthnNotConfigured) {
			t.Fatalf("expected not configured error, got %v", err)
		}
	})

	t.Run("missing origins", func(t *testing.T) {
		svc := NewAuthService(newMockAuthRepo(), newMockRoleRepo(), nil, nil, AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{
			WebAuthnRPID: "example.com",
		}})
		if _, err := svc.webAuthnClient(); !errors.Is(err, ErrWebAuthnNotConfigured) {
			t.Fatalf("expected not configured error, got %v", err)
		}
	})

	t.Run("success trims and defaults", func(t *testing.T) {
		svc := NewAuthService(newMockAuthRepo(), newMockRoleRepo(), nil, nil, AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{
			WebAuthnRPID:          " example.com ",
			WebAuthnRPDisplayName: " ",
			WebAuthnRPOrigins:     []string{" ", " https://example.com "},
			WebAuthnChallengeTTL:  30 * time.Second,
		}})
		client, err := svc.webAuthnClient()
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if client == nil || client.Config == nil {
			t.Fatalf("expected webauthn client")
		}
		if client.Config.RPID != "example.com" {
			t.Fatalf("unexpected rpid: %q", client.Config.RPID)
		}
		if client.Config.RPDisplayName != "Wucher" {
			t.Fatalf("expected default rp display name, got %q", client.Config.RPDisplayName)
		}
		if len(client.Config.RPOrigins) != 1 || client.Config.RPOrigins[0] != "https://example.com" {
			t.Fatalf("unexpected rp origins: %#v", client.Config.RPOrigins)
		}
		if client.Config.Timeouts.Login.Timeout != 30*time.Second {
			t.Fatalf("unexpected login timeout: %v", client.Config.Timeouts.Login.Timeout)
		}
	})
}

func TestAuthServiceStoreWebAuthnChallenge(t *testing.T) {
	t.Run("nil token store", func(t *testing.T) {
		svc := newConfiguredWebAuthnService(newMockAuthRepo(), nil)
		session := &webauthnlib.SessionData{Challenge: "a"}
		if _, err := svc.storeWebAuthnChallenge(context.Background(), webAuthnCeremonyLogin, []byte{1}, session); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
			t.Fatalf("expected invalid challenge error, got %v", err)
		}
	})

	t.Run("nil session", func(t *testing.T) {
		svc := newConfiguredWebAuthnService(newMockAuthRepo(), newMockTokenStore())
		if _, err := svc.storeWebAuthnChallenge(context.Background(), webAuthnCeremonyLogin, []byte{1}, nil); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
			t.Fatalf("expected invalid challenge error, got %v", err)
		}
	})

	t.Run("random token failure", func(t *testing.T) {
		orig := rand.Reader
		rand.Reader = errReader{}
		defer func() { rand.Reader = orig }()

		svc := newConfiguredWebAuthnService(newMockAuthRepo(), newMockTokenStore())
		session := &webauthnlib.SessionData{Challenge: "abc"}
		if _, err := svc.storeWebAuthnChallenge(context.Background(), webAuthnCeremonyRegistration, []byte{1}, session); !errors.Is(err, io.ErrUnexpectedEOF) {
			t.Fatalf("expected random token error, got %v", err)
		}
	})

	t.Run("token store set error", func(t *testing.T) {
		tokens := newMockTokenStore()
		tokens.setErr = errors.New("set failed")
		svc := newConfiguredWebAuthnService(newMockAuthRepo(), tokens)
		session := &webauthnlib.SessionData{Challenge: "abc"}
		if _, err := svc.storeWebAuthnChallenge(context.Background(), webAuthnCeremonyRegistration, []byte{1, 2}, session); !errors.Is(err, tokens.setErr) {
			t.Fatalf("expected set error, got %v", err)
		}
	})

	t.Run("success", func(t *testing.T) {
		tokens := newMockTokenStore()
		svc := newConfiguredWebAuthnService(newMockAuthRepo(), tokens)
		userID := uuidv7.MustBytes()
		session := &webauthnlib.SessionData{
			Challenge:      "abc",
			RelyingPartyID: "example.com",
			UserID:         userID,
			Expires:        time.Now().UTC().Add(time.Minute),
		}
		token, err := svc.storeWebAuthnChallenge(context.Background(), webAuthnCeremonyLogin, userID, session)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if strings.TrimSpace(token) == "" {
			t.Fatalf("expected token")
		}
		if !strings.HasPrefix(tokens.lastKey, "auth:webauthn:challenge:") {
			t.Fatalf("unexpected key: %q", tokens.lastKey)
		}

		raw := tokens.store[tokens.lastKey]
		var envelope webAuthnChallengeEnvelope
		if err := json.Unmarshal(raw, &envelope); err != nil {
			t.Fatalf("unmarshal envelope: %v", err)
		}
		if envelope.Ceremony != webAuthnCeremonyLogin || !bytes.Equal(envelope.UserID, userID) {
			t.Fatalf("unexpected envelope")
		}
	})
}

func TestAuthServiceConsumeWebAuthnChallenge(t *testing.T) {
	t.Run("empty token", func(t *testing.T) {
		svc := newConfiguredWebAuthnService(newMockAuthRepo(), newMockTokenStore())
		if _, err := svc.consumeWebAuthnChallenge(context.Background(), " ", webAuthnCeremonyLogin); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
			t.Fatalf("expected invalid challenge error, got %v", err)
		}
	})

	t.Run("nil token store", func(t *testing.T) {
		svc := newConfiguredWebAuthnService(newMockAuthRepo(), nil)
		if _, err := svc.consumeWebAuthnChallenge(context.Background(), "token", webAuthnCeremonyLogin); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
			t.Fatalf("expected invalid challenge error, got %v", err)
		}
	})

	t.Run("get error", func(t *testing.T) {
		tokens := newMockTokenStore()
		tokens.getErr = errors.New("token store down")
		svc := newConfiguredWebAuthnService(newMockAuthRepo(), tokens)
		if _, err := svc.consumeWebAuthnChallenge(context.Background(), "token", webAuthnCeremonyLogin); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
			t.Fatalf("expected invalid challenge error, got %v", err)
		}
	})

	t.Run("missing token", func(t *testing.T) {
		svc := newConfiguredWebAuthnService(newMockAuthRepo(), newMockTokenStore())
		if _, err := svc.consumeWebAuthnChallenge(context.Background(), "missing", webAuthnCeremonyLogin); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
			t.Fatalf("expected invalid challenge error, got %v", err)
		}
	})

	t.Run("invalid json", func(t *testing.T) {
		tokens := newMockTokenStore()
		tokens.store[webAuthnChallengeKey("badjson")] = []byte("{bad")
		svc := newConfiguredWebAuthnService(newMockAuthRepo(), tokens)
		if _, err := svc.consumeWebAuthnChallenge(context.Background(), "badjson", webAuthnCeremonyLogin); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
			t.Fatalf("expected invalid challenge error, got %v", err)
		}
	})

	t.Run("ceremony mismatch", func(t *testing.T) {
		tokens := newMockTokenStore()
		putWebAuthnChallengeToken(t, tokens, "mismatch", webAuthnCeremonyRegistration, uuidv7.MustBytes(), time.Now().UTC().Add(time.Minute))
		svc := newConfiguredWebAuthnService(newMockAuthRepo(), tokens)
		if _, err := svc.consumeWebAuthnChallenge(context.Background(), "mismatch", webAuthnCeremonyLogin); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
			t.Fatalf("expected invalid challenge error, got %v", err)
		}
	})

	t.Run("expired", func(t *testing.T) {
		tokens := newMockTokenStore()
		putWebAuthnChallengeToken(t, tokens, "expired", webAuthnCeremonyLogin, uuidv7.MustBytes(), time.Now().UTC().Add(-time.Second))
		svc := newConfiguredWebAuthnService(newMockAuthRepo(), tokens)
		if _, err := svc.consumeWebAuthnChallenge(context.Background(), "expired", webAuthnCeremonyLogin); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
			t.Fatalf("expected invalid challenge error, got %v", err)
		}
	})

	t.Run("success deletes token", func(t *testing.T) {
		tokens := newMockTokenStore()
		userID := uuidv7.MustBytes()
		putWebAuthnChallengeToken(t, tokens, "ok", webAuthnCeremonyLogin, userID, time.Now().UTC().Add(time.Minute))
		svc := newConfiguredWebAuthnService(newMockAuthRepo(), tokens)
		envelope, err := svc.consumeWebAuthnChallenge(context.Background(), "ok", webAuthnCeremonyLogin)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if envelope == nil || !bytes.Equal(envelope.UserID, userID) {
			t.Fatalf("unexpected envelope")
		}
		if _, found := tokens.store[webAuthnChallengeKey("ok")]; found {
			t.Fatalf("expected challenge token to be deleted")
		}
	})
}

func TestWebAuthnChallengeKey(t *testing.T) {
	if got := webAuthnChallengeKey("abc"); got != "auth:webauthn:challenge:abc" {
		t.Fatalf("unexpected key: %q", got)
	}
}

func TestAuthServiceLoadWebAuthnCredentials(t *testing.T) {
	t.Run("repo list error", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		repo.listCredsErr = errors.New("list creds failed")
		svc := newConfiguredWebAuthnService(repo, nil)
		if _, err := svc.loadWebAuthnCredentials(context.Background(), uuidv7.MustBytes()); !errors.Is(err, repo.listCredsErr) {
			t.Fatalf("expected list error, got %v", err)
		}
	})

	t.Run("invalid credential json", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		userID := uuidv7.MustBytes()
		repo.webauthnCredentials[string(userID)] = []auth.UserWebAuthnCredential{{
			UserID:         userID,
			CredentialID:   []byte{1},
			CredentialJSON: []byte("{bad"),
		}}
		svc := newConfiguredWebAuthnService(repo, nil)
		if _, err := svc.loadWebAuthnCredentials(context.Background(), userID); err == nil {
			t.Fatalf("expected unmarshal error")
		}
	})

	t.Run("success", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		userID := uuidv7.MustBytes()
		serialized, err := json.Marshal(webauthnlib.Credential{
			ID:              []byte{1, 2, 3},
			PublicKey:       []byte{4, 5, 6},
			AttestationType: "none",
		})
		if err != nil {
			t.Fatalf("marshal credential: %v", err)
		}
		repo.webauthnCredentials[string(userID)] = []auth.UserWebAuthnCredential{{
			UserID:         userID,
			CredentialID:   []byte{1, 2, 3},
			CredentialJSON: serialized,
		}}
		svc := newConfiguredWebAuthnService(repo, nil)
		creds, err := svc.loadWebAuthnCredentials(context.Background(), userID)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if len(creds) != 1 || !bytes.Equal(creds[0].ID, []byte{1, 2, 3}) {
			t.Fatalf("unexpected credentials")
		}
	})
}

func TestAuthServiceBeginWebAuthnRegistrationBranches(t *testing.T) {
	t.Run("invalid user id length", func(t *testing.T) {
		svc := newConfiguredWebAuthnService(newMockAuthRepo(), newMockTokenStore())
		if _, _, err := svc.BeginWebAuthnRegistration(context.Background(), []byte("short")); !errors.Is(err, ErrInvalidToken) {
			t.Fatalf("expected invalid token, got %v", err)
		}
	})

	t.Run("not configured", func(t *testing.T) {
		svc := NewAuthService(newMockAuthRepo(), newMockRoleRepo(), newMockTokenStore(), nil, AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{}})
		if _, _, err := svc.BeginWebAuthnRegistration(context.Background(), uuidv7.MustBytes()); !errors.Is(err, ErrWebAuthnNotConfigured) {
			t.Fatalf("expected not configured, got %v", err)
		}
	})

	t.Run("user fetch error", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		repo.getByIDErr = errors.New("db failed")
		svc := newConfiguredWebAuthnService(repo, newMockTokenStore())
		if _, _, err := svc.BeginWebAuthnRegistration(context.Background(), uuidv7.MustBytes()); !errors.Is(err, repo.getByIDErr) {
			t.Fatalf("expected repo error, got %v", err)
		}
	})

	t.Run("user not found", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		svc := newConfiguredWebAuthnService(repo, newMockTokenStore())
		if _, _, err := svc.BeginWebAuthnRegistration(context.Background(), uuidv7.MustBytes()); !errors.Is(err, ErrInvalidToken) {
			t.Fatalf("expected invalid token, got %v", err)
		}
	})

	t.Run("load credentials error", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		userID := uuidv7.MustBytes()
		repo.users[string(userID)] = &auth.User{ID: userID, Email: "user@example.com", IsActive: true}
		repo.listCredsErr = errors.New("list creds failed")
		svc := newConfiguredWebAuthnService(repo, newMockTokenStore())
		if _, _, err := svc.BeginWebAuthnRegistration(context.Background(), userID); !errors.Is(err, repo.listCredsErr) {
			t.Fatalf("expected list creds error, got %v", err)
		}
	})

	t.Run("token store unavailable", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		userID := uuidv7.MustBytes()
		repo.users[string(userID)] = &auth.User{ID: userID, Email: "user@example.com", IsActive: true}
		svc := newConfiguredWebAuthnService(repo, nil)
		if _, _, err := svc.BeginWebAuthnRegistration(context.Background(), userID); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
			t.Fatalf("expected invalid challenge, got %v", err)
		}
	})

	t.Run("success with exclusions", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		tokens := newMockTokenStore()
		userID := uuidv7.MustBytes()
		repo.users[string(userID)] = &auth.User{
			ID:        userID,
			Email:     "user@example.com",
			FirstName: "Web",
			LastName:  "Authn",
			IsActive:  true,
		}

		serialized, err := json.Marshal(webauthnlib.Credential{
			ID:              []byte{8, 8, 8},
			PublicKey:       []byte{1, 2, 3},
			AttestationType: "none",
		})
		if err != nil {
			t.Fatalf("marshal credential: %v", err)
		}
		repo.webauthnCredentials[string(userID)] = []auth.UserWebAuthnCredential{{
			UserID:         userID,
			CredentialID:   []byte{8, 8, 8},
			CredentialJSON: serialized,
		}}

		svc := newConfiguredWebAuthnService(repo, tokens)
		creation, challengeToken, err := svc.BeginWebAuthnRegistration(context.Background(), userID)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if creation == nil || strings.TrimSpace(challengeToken) == "" {
			t.Fatalf("expected creation options and challenge token")
		}
		if len(creation.Response.CredentialExcludeList) != 1 {
			t.Fatalf("expected one exclusion descriptor")
		}
	})
}

func TestAuthServiceFinishWebAuthnRegistrationBranches(t *testing.T) {
	t.Run("invalid user id length", func(t *testing.T) {
		svc := newConfiguredWebAuthnService(newMockAuthRepo(), newMockTokenStore())
		err := svc.FinishWebAuthnRegistration(context.Background(), []byte("short"), "token", []byte("{}"), "name")
		if !errors.Is(err, ErrInvalidToken) {
			t.Fatalf("expected invalid token, got %v", err)
		}
	})

	t.Run("not configured", func(t *testing.T) {
		svc := NewAuthService(newMockAuthRepo(), newMockRoleRepo(), newMockTokenStore(), nil, AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{}})
		err := svc.FinishWebAuthnRegistration(context.Background(), uuidv7.MustBytes(), "token", []byte("{}"), "name")
		if !errors.Is(err, ErrWebAuthnNotConfigured) {
			t.Fatalf("expected not configured, got %v", err)
		}
	})

	t.Run("invalid challenge", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		userID := uuidv7.MustBytes()
		repo.users[string(userID)] = &auth.User{ID: userID, Email: "user@example.com", IsActive: true}
		svc := newConfiguredWebAuthnService(repo, newMockTokenStore())
		err := svc.FinishWebAuthnRegistration(context.Background(), userID, "missing", []byte("{}"), "name")
		if !errors.Is(err, ErrWebAuthnChallengeInvalid) {
			t.Fatalf("expected invalid challenge, got %v", err)
		}
	})

	t.Run("challenge user mismatch", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		tokens := newMockTokenStore()
		userID := uuidv7.MustBytes()
		repo.users[string(userID)] = &auth.User{ID: userID, Email: "user@example.com", IsActive: true}
		putWebAuthnChallengeToken(t, tokens, "tok1", webAuthnCeremonyRegistration, uuidv7.MustBytes(), time.Now().UTC().Add(time.Minute))
		svc := newConfiguredWebAuthnService(repo, tokens)
		err := svc.FinishWebAuthnRegistration(context.Background(), userID, "tok1", []byte("{}"), "name")
		if !errors.Is(err, ErrWebAuthnChallengeInvalid) {
			t.Fatalf("expected challenge invalid, got %v", err)
		}
	})

	t.Run("user fetch error", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		tokens := newMockTokenStore()
		userID := uuidv7.MustBytes()
		repo.getByIDErr = errors.New("db failed")
		putWebAuthnChallengeToken(t, tokens, "tokfetch", webAuthnCeremonyRegistration, userID, time.Now().UTC().Add(time.Minute))
		svc := newConfiguredWebAuthnService(repo, tokens)
		err := svc.FinishWebAuthnRegistration(context.Background(), userID, "tokfetch", []byte("{}"), "name")
		if !errors.Is(err, repo.getByIDErr) {
			t.Fatalf("expected user fetch error, got %v", err)
		}
	})

	t.Run("user not found", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		tokens := newMockTokenStore()
		userID := uuidv7.MustBytes()
		putWebAuthnChallengeToken(t, tokens, "tok2", webAuthnCeremonyRegistration, userID, time.Now().UTC().Add(time.Minute))
		svc := newConfiguredWebAuthnService(repo, tokens)
		err := svc.FinishWebAuthnRegistration(context.Background(), userID, "tok2", []byte("{}"), "name")
		if !errors.Is(err, ErrInvalidToken) {
			t.Fatalf("expected invalid token, got %v", err)
		}
	})

	t.Run("load credentials error", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		tokens := newMockTokenStore()
		userID := uuidv7.MustBytes()
		repo.users[string(userID)] = &auth.User{ID: userID, Email: "user@example.com", IsActive: true}
		repo.listCredsErr = errors.New("list creds failed")
		putWebAuthnChallengeToken(t, tokens, "tok3", webAuthnCeremonyRegistration, userID, time.Now().UTC().Add(time.Minute))
		svc := newConfiguredWebAuthnService(repo, tokens)
		err := svc.FinishWebAuthnRegistration(context.Background(), userID, "tok3", []byte("{}"), "name")
		if !errors.Is(err, repo.listCredsErr) {
			t.Fatalf("expected list creds error, got %v", err)
		}
	})

	t.Run("invalid payload", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		tokens := newMockTokenStore()
		userID := uuidv7.MustBytes()
		repo.users[string(userID)] = &auth.User{ID: userID, Email: "user@example.com", IsActive: true}
		putWebAuthnChallengeToken(t, tokens, "tok4", webAuthnCeremonyRegistration, userID, time.Now().UTC().Add(time.Minute))
		svc := newConfiguredWebAuthnService(repo, tokens)
		err := svc.FinishWebAuthnRegistration(context.Background(), userID, "tok4", []byte("{}"), "name")
		if !errors.Is(err, ErrWebAuthnPayloadInvalid) {
			t.Fatalf("expected payload invalid, got %v", err)
		}
	})

	t.Run("verification failed", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		tokens := newMockTokenStore()
		userID := uuidv7.MustBytes()
		repo.users[string(userID)] = &auth.User{
			ID:        userID,
			Email:     "user@example.com",
			FirstName: "A",
			LastName:  "B",
			IsActive:  true,
		}
		svc := newConfiguredWebAuthnService(repo, tokens)
		_, challengeToken, err := svc.BeginWebAuthnRegistration(context.Background(), userID)
		if err != nil {
			t.Fatalf("begin registration: %v", err)
		}
		err = svc.FinishWebAuthnRegistration(context.Background(), userID, challengeToken, []byte(testCredentialCreationResponseJSON), "Device A")
		if !errors.Is(err, ErrWebAuthnVerificationFailed) {
			t.Fatalf("expected verification failed, got %v", err)
		}
	})

	t.Run("success persists credential", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		tokens := newMockTokenStore()
		userID := uuidv7.MustBytes()
		repo.users[string(userID)] = &auth.User{
			ID:        userID,
			Email:     "user@example.com",
			FirstName: "Web",
			LastName:  "Authn",
			IsActive:  true,
		}

		svc := NewAuthService(
			repo,
			newMockRoleRepo(),
			tokens,
			nil, AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{
				WebAuthnRPID:          "webauthn.io",
				WebAuthnRPDisplayName: "Wucher Test",
				WebAuthnRPOrigins:     []string{"https://webauthn.io"},
				WebAuthnChallengeTTL:  2 * time.Minute,
			}},
		)

		envelope := webAuthnChallengeEnvelope{
			Ceremony: webAuthnCeremonyRegistration,
			UserID:   append([]byte(nil), userID...),
			Session: webauthnlib.SessionData{
				Challenge:      "W8GzFU8pGjhoRbWrLDlamAfq_y4S1CZG1VuoeRLARrE",
				RelyingPartyID: "webauthn.io",
				UserID:         append([]byte(nil), userID...),
				Expires:        time.Now().UTC().Add(time.Minute),
				CredParams:     webauthnlib.CredentialParametersDefault(),
			},
		}
		payload, err := json.Marshal(envelope)
		if err != nil {
			t.Fatalf("marshal envelope: %v", err)
		}
		tokens.store[webAuthnChallengeKey("toksuccess")] = payload

		if err := svc.FinishWebAuthnRegistration(context.Background(), userID, "toksuccess", []byte(testCredentialCreationResponseJSON), "  Device A  "); err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		records := repo.webauthnCredentials[string(userID)]
		if len(records) != 1 {
			t.Fatalf("expected one stored webauthn credential, got %d", len(records))
		}
		if records[0].Name != "Device A" {
			t.Fatalf("expected trimmed credential name, got %q", records[0].Name)
		}
		if len(records[0].CredentialID) == 0 || len(records[0].CredentialJSON) == 0 {
			t.Fatalf("expected persisted credential data")
		}
	})
}

func TestAuthServiceBeginWebAuthnLoginBranches(t *testing.T) {
	t.Run("not configured", func(t *testing.T) {
		svc := NewAuthService(newMockAuthRepo(), newMockRoleRepo(), newMockTokenStore(), nil, AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{}})
		if _, _, err := svc.BeginWebAuthnLogin(context.Background(), "user@example.com"); !errors.Is(err, ErrWebAuthnNotConfigured) {
			t.Fatalf("expected not configured, got %v", err)
		}
	})

	t.Run("user fetch error", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		repo.getByMailErr = errors.New("db failed")
		svc := newConfiguredWebAuthnService(repo, newMockTokenStore())
		if _, _, err := svc.BeginWebAuthnLogin(context.Background(), "user@example.com"); !errors.Is(err, repo.getByMailErr) {
			t.Fatalf("expected db error, got %v", err)
		}
	})

	t.Run("user not found", func(t *testing.T) {
		svc := newConfiguredWebAuthnService(newWebAuthnRepoWithErrors(), newMockTokenStore())
		if _, _, err := svc.BeginWebAuthnLogin(context.Background(), "user@example.com"); !errors.Is(err, ErrInvalidCredentials) {
			t.Fatalf("expected invalid credentials, got %v", err)
		}
	})

	t.Run("email not verified", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		userID := uuidv7.MustBytes()
		repo.usersByMail["user@example.com"] = &auth.User{ID: userID, Email: "user@example.com", IsActive: true}
		svc := newConfiguredWebAuthnService(repo, newMockTokenStore())
		if _, _, err := svc.BeginWebAuthnLogin(context.Background(), " user@example.com "); !errors.Is(err, ErrEmailNotVerified) {
			t.Fatalf("expected email not verified, got %v", err)
		}
	})

	t.Run("load credentials error", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		userID := uuidv7.MustBytes()
		now := time.Now().UTC()
		repo.usersByMail["user@example.com"] = &auth.User{ID: userID, Email: "user@example.com", EmailVerifiedAt: &now, IsActive: true}
		repo.listCredsErr = errors.New("list creds failed")
		svc := newConfiguredWebAuthnService(repo, newMockTokenStore())
		if _, _, err := svc.BeginWebAuthnLogin(context.Background(), "user@example.com"); !errors.Is(err, repo.listCredsErr) {
			t.Fatalf("expected list creds error, got %v", err)
		}
	})

	t.Run("no credentials configured", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		userID := uuidv7.MustBytes()
		now := time.Now().UTC()
		repo.usersByMail["user@example.com"] = &auth.User{ID: userID, Email: "user@example.com", EmailVerifiedAt: &now, IsActive: true}
		svc := newConfiguredWebAuthnService(repo, newMockTokenStore())
		if _, _, err := svc.BeginWebAuthnLogin(context.Background(), "user@example.com"); !errors.Is(err, ErrWebAuthnCredentialUnavailable) {
			t.Fatalf("expected credential unavailable, got %v", err)
		}
	})

	t.Run("token store unavailable", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		userID := uuidv7.MustBytes()
		now := time.Now().UTC()
		repo.usersByMail["user@example.com"] = &auth.User{ID: userID, Email: "user@example.com", EmailVerifiedAt: &now, IsActive: true}
		serialized, err := json.Marshal(webauthnlib.Credential{ID: []byte{1, 2, 3}, PublicKey: []byte{4}})
		if err != nil {
			t.Fatalf("marshal credential: %v", err)
		}
		repo.webauthnCredentials[string(userID)] = []auth.UserWebAuthnCredential{{
			UserID:         userID,
			CredentialID:   []byte{1, 2, 3},
			CredentialJSON: serialized,
		}}
		svc := newConfiguredWebAuthnService(repo, nil)
		if _, _, err := svc.BeginWebAuthnLogin(context.Background(), "user@example.com"); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
			t.Fatalf("expected invalid challenge, got %v", err)
		}
	})

	t.Run("success", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		tokens := newMockTokenStore()
		userID := uuidv7.MustBytes()
		now := time.Now().UTC()
		repo.usersByMail["user@example.com"] = &auth.User{
			ID:              userID,
			Email:           "user@example.com",
			EmailVerifiedAt: &now,
			FirstName:       "Web",
			LastName:        "User",
			IsActive:        true,
		}
		serialized, err := json.Marshal(webauthnlib.Credential{
			ID:              []byte{1, 2, 3},
			PublicKey:       []byte{4, 5, 6},
			AttestationType: "none",
		})
		if err != nil {
			t.Fatalf("marshal credential: %v", err)
		}
		repo.webauthnCredentials[string(userID)] = []auth.UserWebAuthnCredential{{
			UserID:         userID,
			CredentialID:   []byte{1, 2, 3},
			CredentialJSON: serialized,
		}}

		svc := newConfiguredWebAuthnService(repo, tokens)
		assertion, token, err := svc.BeginWebAuthnLogin(context.Background(), " user@example.com ")
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if assertion == nil || strings.TrimSpace(token) == "" {
			t.Fatalf("expected assertion and challenge token")
		}
		if len(assertion.Response.AllowedCredentials) != 1 {
			t.Fatalf("expected allowed credential list")
		}
	})
}

func TestAuthServiceFinishWebAuthnLoginBranches(t *testing.T) {
	t.Run("not configured", func(t *testing.T) {
		svc := NewAuthService(newMockAuthRepo(), newMockRoleRepo(), newMockTokenStore(), nil, AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{}})
		if _, err := svc.FinishWebAuthnLogin(context.Background(), "token", []byte("{}")); !errors.Is(err, ErrWebAuthnNotConfigured) {
			t.Fatalf("expected not configured, got %v", err)
		}
	})

	t.Run("invalid challenge", func(t *testing.T) {
		svc := newConfiguredWebAuthnService(newWebAuthnRepoWithErrors(), newMockTokenStore())
		if _, err := svc.FinishWebAuthnLogin(context.Background(), "missing", []byte("{}")); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
			t.Fatalf("expected invalid challenge, got %v", err)
		}
	})

	t.Run("challenge invalid user id length", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		tokens := newMockTokenStore()
		putWebAuthnChallengeToken(t, tokens, "baduid", webAuthnCeremonyLogin, []byte("short"), time.Now().UTC().Add(time.Minute))
		svc := newConfiguredWebAuthnService(repo, tokens)
		if _, err := svc.FinishWebAuthnLogin(context.Background(), "baduid", []byte("{}")); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
			t.Fatalf("expected invalid challenge, got %v", err)
		}
	})

	t.Run("user fetch error", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		repo.getByIDErr = errors.New("db failed")
		tokens := newMockTokenStore()
		userID := uuidv7.MustBytes()
		putWebAuthnChallengeToken(t, tokens, "tok1", webAuthnCeremonyLogin, userID, time.Now().UTC().Add(time.Minute))
		svc := newConfiguredWebAuthnService(repo, tokens)
		if _, err := svc.FinishWebAuthnLogin(context.Background(), "tok1", []byte("{}")); !errors.Is(err, repo.getByIDErr) {
			t.Fatalf("expected db error, got %v", err)
		}
	})

	t.Run("user not found", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		tokens := newMockTokenStore()
		userID := uuidv7.MustBytes()
		putWebAuthnChallengeToken(t, tokens, "tok2", webAuthnCeremonyLogin, userID, time.Now().UTC().Add(time.Minute))
		svc := newConfiguredWebAuthnService(repo, tokens)
		if _, err := svc.FinishWebAuthnLogin(context.Background(), "tok2", []byte("{}")); !errors.Is(err, ErrInvalidCredentials) {
			t.Fatalf("expected invalid credentials, got %v", err)
		}
	})

	t.Run("load credentials error", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		repo.listCredsErr = errors.New("list creds failed")
		tokens := newMockTokenStore()
		userID := uuidv7.MustBytes()
		repo.users[string(userID)] = &auth.User{ID: userID, Email: "user@example.com", IsActive: true}
		putWebAuthnChallengeToken(t, tokens, "tok3", webAuthnCeremonyLogin, userID, time.Now().UTC().Add(time.Minute))
		svc := newConfiguredWebAuthnService(repo, tokens)
		if _, err := svc.FinishWebAuthnLogin(context.Background(), "tok3", []byte("{}")); !errors.Is(err, repo.listCredsErr) {
			t.Fatalf("expected list creds error, got %v", err)
		}
	})

	t.Run("no credentials", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		tokens := newMockTokenStore()
		userID := uuidv7.MustBytes()
		repo.users[string(userID)] = &auth.User{ID: userID, Email: "user@example.com", IsActive: true}
		putWebAuthnChallengeToken(t, tokens, "tok4", webAuthnCeremonyLogin, userID, time.Now().UTC().Add(time.Minute))
		svc := newConfiguredWebAuthnService(repo, tokens)
		if _, err := svc.FinishWebAuthnLogin(context.Background(), "tok4", []byte("{}")); !errors.Is(err, ErrWebAuthnCredentialUnavailable) {
			t.Fatalf("expected credential unavailable, got %v", err)
		}
	})

	t.Run("invalid payload", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		tokens := newMockTokenStore()
		userID := uuidv7.MustBytes()
		repo.users[string(userID)] = &auth.User{ID: userID, Email: "user@example.com", IsActive: true}
		serialized, err := json.Marshal(webauthnlib.Credential{ID: []byte{1}, PublicKey: []byte{2}})
		if err != nil {
			t.Fatalf("marshal credential: %v", err)
		}
		repo.webauthnCredentials[string(userID)] = []auth.UserWebAuthnCredential{{
			UserID:         userID,
			CredentialID:   []byte{1},
			CredentialJSON: serialized,
		}}
		putWebAuthnChallengeToken(t, tokens, "tok5", webAuthnCeremonyLogin, userID, time.Now().UTC().Add(time.Minute))
		svc := newConfiguredWebAuthnService(repo, tokens)
		if _, err := svc.FinishWebAuthnLogin(context.Background(), "tok5", []byte("{}")); !errors.Is(err, ErrWebAuthnPayloadInvalid) {
			t.Fatalf("expected payload invalid, got %v", err)
		}
	})

	t.Run("verification failed", func(t *testing.T) {
		repo := newWebAuthnRepoWithErrors()
		tokens := newMockTokenStore()
		userID := uuidv7.MustBytes()
		now := time.Now().UTC()
		user := &auth.User{
			ID:              userID,
			Email:           "user@example.com",
			EmailVerifiedAt: &now,
			FirstName:       "Web",
			LastName:        "User",
			IsActive:        true,
		}
		repo.usersByMail["user@example.com"] = user
		repo.users[string(userID)] = user
		serialized, err := json.Marshal(webauthnlib.Credential{
			ID:              []byte{1, 2, 3},
			PublicKey:       []byte{4, 5, 6},
			AttestationType: "none",
		})
		if err != nil {
			t.Fatalf("marshal credential: %v", err)
		}
		repo.webauthnCredentials[string(userID)] = []auth.UserWebAuthnCredential{{
			UserID:         userID,
			CredentialID:   []byte{1, 2, 3},
			CredentialJSON: serialized,
		}}

		svc := newConfiguredWebAuthnService(repo, tokens)
		_, challengeToken, err := svc.BeginWebAuthnLogin(context.Background(), "user@example.com")
		if err != nil {
			t.Fatalf("begin login: %v", err)
		}
		if _, err := svc.FinishWebAuthnLogin(context.Background(), challengeToken, []byte(testCredentialAssertionResponseJSON)); !errors.Is(err, ErrWebAuthnVerificationFailed) {
			t.Fatalf("expected verification failed, got %v", err)
		}
	})
}
