package client_test

import (
	"encoding/hex"
	"strings"
	"testing"

	"github.com/enmanuel/unibus/pkg/client"
	"github.com/enmanuel/unibus/pkg/membership"
)

// findUserInfo returns the row with the given signing key (case-insensitive).
func findUserInfo(users []client.UserInfo, signPub string) (client.UserInfo, bool) {
	want := strings.ToLower(signPub)
	for _, u := range users {
		if strings.ToLower(u.SignPub) == want {
			return u, true
		}
	}
	return client.UserInfo{}, false
}

// TestClientUsersAdminAPI drives the admin user-management API through the real
// pkg/client methods against an in-process membershipd under enforce: an admin
// client adds a user, lists it, revokes it, and sees the status flip — and a
// non-admin client is denied. This is the path the admin panel uses, so it locks
// the client/server contract the panel depends on.
func TestClientUsersAdminAPI(t *testing.T) {
	h := newHarnessMode(t, membership.AuthEnforce)
	waitHealth(t, h.ctrlURL)

	admin, err := client.New(h.natsURL, h.ctrlURL, mustIdentity(t))
	if err != nil {
		t.Fatalf("connect admin: %v", err)
	}
	defer admin.Close()
	registerClient(t, h, admin, "admin", membership.RoleAdmin)

	member, err := client.New(h.natsURL, h.ctrlURL, mustIdentity(t))
	if err != nil {
		t.Fatalf("connect member: %v", err)
	}
	defer member.Close()
	registerClient(t, h, member, "member", membership.RoleMember)

	// A brand-new identity the admin will register over HTTP.
	carol := mustIdentity(t)
	carolPub := hex.EncodeToString(carol.SignPub)

	// Admin adds carol as a member.
	if err := admin.AddUser(carolPub, "carol", membership.RoleMember); err != nil {
		t.Fatalf("admin AddUser: %v", err)
	}

	// Admin lists: carol present and active.
	users, err := admin.ListUsers()
	if err != nil {
		t.Fatalf("admin ListUsers: %v", err)
	}
	row, ok := findUserInfo(users, carolPub)
	if !ok {
		t.Fatalf("carol missing from list after add: %+v", users)
	}
	if row.Status != membership.StatusActive || row.Role != membership.RoleMember {
		t.Fatalf("carol row wrong after add: %+v", row)
	}

	// Re-adding the same key is a conflict surfaced as an error (no silent upsert).
	if err := admin.AddUser(carolPub, "carol-again", membership.RoleAdmin); err == nil {
		t.Fatalf("re-adding carol should error (409), got nil")
	}

	// Admin revokes carol; list shows the status flip (no hard delete).
	if err := admin.RevokeUser(carolPub); err != nil {
		t.Fatalf("admin RevokeUser: %v", err)
	}
	users, err = admin.ListUsers()
	if err != nil {
		t.Fatalf("admin ListUsers after revoke: %v", err)
	}
	row, ok = findUserInfo(users, carolPub)
	if !ok {
		t.Fatalf("carol vanished after revoke (should be a status flip): %+v", users)
	}
	if row.Status != membership.StatusRevoked {
		t.Fatalf("carol should be revoked, got status %q", row.Status)
	}

	// A non-admin (member) is denied on every user-management method.
	if _, err := member.ListUsers(); err == nil {
		t.Fatalf("non-admin ListUsers should error (403), got nil")
	}
	if err := member.AddUser(carolPub, "x", membership.RoleMember); err == nil {
		t.Fatalf("non-admin AddUser should error (403), got nil")
	}
	if err := member.RevokeUser(carolPub); err == nil {
		t.Fatalf("non-admin RevokeUser should error (403), got nil")
	}
}
