package main

import (
	"encoding/hex"
	"os"
	"path/filepath"
	"testing"

	cs "fn-registry/functions/cybersecurity"

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

// openTestStore opens a fresh SQLite membership store in a temp dir.
func openTestStore(t *testing.T) membership.Store {
	t.Helper()
	store, err := membership.Open(filepath.Join(t.TempDir(), "unibus.db"))
	if err != nil {
		t.Fatalf("open store: %v", err)
	}
	t.Cleanup(func() { store.Close() })
	return store
}

// TestProvisionBotGolden is the happy path: provisioning a bot registers it in the
// allowlist with the right handle and role, AND writes a 0600 credentials file
// that LoadIdentity reconstructs into the same identity — so a worker/clientcheck
// binary pointed at the file connects as exactly this user with no extra step.
func TestProvisionBotGolden(t *testing.T) {
	store := openTestStore(t)
	out := filepath.Join(t.TempDir(), "notifier.id")

	signPubHex, endpoint, err := provisionBot(store, "notifier", membership.RoleMember, out)
	if err != nil {
		t.Fatalf("provisionBot: %v", err)
	}

	// Registered in the allowlist with the right handle/role/status.
	u, err := store.GetUser(signPubHex)
	if err != nil {
		t.Fatalf("get provisioned user: %v", err)
	}
	if u.Handle != "notifier" || u.Role != membership.RoleMember || u.Status != membership.StatusActive {
		t.Fatalf("provisioned user row wrong: %+v", u)
	}

	// And it shows up in user list (the `user list` surface).
	users, err := store.ListUsers()
	if err != nil {
		t.Fatalf("list users: %v", err)
	}
	found := false
	for _, x := range users {
		if x.SignPub == signPubHex {
			found = true
		}
	}
	if !found {
		t.Fatalf("provisioned bot missing from user list: %+v", users)
	}

	// Credentials file exists, is 0600, and round-trips through LoadIdentity to the
	// same signing key + endpoint (no-friction contract).
	info, err := os.Stat(out)
	if err != nil {
		t.Fatalf("stat out file: %v", err)
	}
	if perm := info.Mode().Perm(); perm != 0o600 {
		t.Fatalf("out file perms = %o, want 600", perm)
	}
	id, err := client.LoadIdentity(out)
	if err != nil {
		t.Fatalf("LoadIdentity(out): %v", err)
	}
	if got := hex.EncodeToString(id.SignPub); got != signPubHex {
		t.Fatalf("loaded sign_pub %q != provisioned %q", got, signPubHex)
	}
	if got := frame.EndpointID(id.SignPub); got != endpoint {
		t.Fatalf("loaded endpoint %q != reported %q", got, endpoint)
	}
}

// TestProvisionBotDefaultRole: an empty role defaults to member.
func TestProvisionBotDefaultRole(t *testing.T) {
	store := openTestStore(t)
	out := filepath.Join(t.TempDir(), "bot.id")
	signPubHex, _, err := provisionBot(store, "defrole", "", out)
	if err != nil {
		t.Fatalf("provisionBot: %v", err)
	}
	u, err := store.GetUser(signPubHex)
	if err != nil {
		t.Fatalf("get user: %v", err)
	}
	if u.Role != membership.RoleMember {
		t.Fatalf("empty role should default to member, got %q", u.Role)
	}
}

// TestProvisionBotSignPubAlreadyRegistered is the error path: provisioning an
// identity whose signing key is already in the allowlist fails with a clear error
// (not a panic) AND does not write a credentials file (no half-provisioned bot).
func TestProvisionBotSignPubAlreadyRegistered(t *testing.T) {
	store := openTestStore(t)

	// Pre-register a key, then try to provision a bot with that SAME identity.
	id, err := cs.GenerateIdentity()
	if err != nil {
		t.Fatalf("generate identity: %v", err)
	}
	signPubHex := hex.EncodeToString(id.SignPub)
	if err := store.AddUser(signPubHex, "preexisting", membership.RoleMember); err != nil {
		t.Fatalf("pre-register: %v", err)
	}

	out := filepath.Join(t.TempDir(), "dup.id")
	_, _, err = provisionBotWithIdentity(store, id, "dupbot", membership.RoleMember, out)
	if err == nil {
		t.Fatalf("provisioning an already-registered key should error")
	}
	if _, statErr := os.Stat(out); !os.IsNotExist(statErr) {
		t.Fatalf("credentials file must NOT be written on a duplicate-key failure (stat err = %v)", statErr)
	}
}

// TestProvisionBotOutExists is the other error path: an existing --out file is
// refused BEFORE the store is mutated, so the run leaves no orphan user behind.
func TestProvisionBotOutExists(t *testing.T) {
	store := openTestStore(t)
	out := filepath.Join(t.TempDir(), "taken.id")
	if err := os.WriteFile(out, []byte("preexisting credentials"), 0o600); err != nil {
		t.Fatalf("seed out file: %v", err)
	}

	_, _, err := provisionBot(store, "clobber", membership.RoleMember, out)
	if err == nil {
		t.Fatalf("provisioning over an existing out file should error")
	}
	// The store must be untouched: no user was registered.
	users, err := store.ListUsers()
	if err != nil {
		t.Fatalf("list users: %v", err)
	}
	if len(users) != 0 {
		t.Fatalf("no user should be registered when out exists, got %+v", users)
	}
}
