package daemon

import (
	"context"
	"sort"
	"strings"
	"testing"

	"banger/internal/rpc"
)

// TestRPCHandlersMatchDocumentedMethods pins the surface of the RPC
// table: adding or removing a method should be an explicit, reviewable
// change. If the keyset drifts and this test isn't updated alongside,
// that's a red flag — either the documented list is stale, or a
// method sneaked in without being discussed.
//
// The expected list is the single source of truth for "methods
// banger speaks." Any production code consulting it (CLI completions,
// docs generator) can grep this test.
func TestRPCHandlersMatchDocumentedMethods(t *testing.T) {
	expected := []string{
		"image.cache.prune",
		"image.delete",
		"image.list",
		"image.promote",
		"image.pull",
		"image.register",
		"image.show",

		"kernel.catalog",
		"kernel.delete",
		"kernel.import",
		"kernel.list",
		"kernel.pull",
		"kernel.show",

		"daemon.operations.list",

		"ping",
		"shutdown",

		"vm.create",
		"vm.create.begin",
		"vm.create.cancel",
		"vm.create.status",
		"vm.delete",
		"vm.health",
		"vm.kill",
		"vm.list",
		"vm.logs",
		"vm.ping",
		"vm.ports",
		"vm.restart",
		"vm.set",
		"vm.show",
		"vm.ssh",
		"vm.start",
		"vm.stats",
		"vm.stop",

		"vm.workspace.export",
		"vm.workspace.prepare",
	}

	got := make([]string, 0, len(rpcHandlers))
	for name := range rpcHandlers {
		got = append(got, name)
	}
	sort.Strings(got)
	sort.Strings(expected)

	if len(got) != len(expected) {
		t.Fatalf("method count: got %d, want %d\n  got:  %v\n  want: %v", len(got), len(expected), got, expected)
	}
	for i := range expected {
		if got[i] != expected[i] {
			t.Fatalf("method[%d]: got %q, want %q\n  full got:  %v\n  full want: %v", i, got[i], expected[i], got, expected)
		}
	}
}

// TestRPCHandlersAllNonNil catches a silly-but-possible footgun:
// registering a method with a nil function literal.
func TestRPCHandlersAllNonNil(t *testing.T) {
	for name, h := range rpcHandlers {
		if h == nil {
			t.Errorf("rpcHandlers[%q] = nil", name)
		}
	}
}

// TestDispatchStampsOpIDOnError pins the contract that every error
// response leaving dispatch carries an op_id, even on the
// short-circuit paths (bad_version, unknown_method) that never
// reach a handler. Operators rely on this id to correlate a CLI
// failure to a daemon log line.
func TestDispatchStampsOpIDOnError(t *testing.T) {
	d := &Daemon{}
	t.Run("unknown_method", func(t *testing.T) {
		resp := d.dispatch(context.Background(), rpc.Request{Version: rpc.Version, Method: "no.such.method"})
		if resp.OK {
			t.Fatalf("expected error response, got %+v", resp)
		}
		if resp.Error == nil || resp.Error.Code != "unknown_method" {
			t.Fatalf("error = %+v, want unknown_method", resp.Error)
		}
		if !strings.HasPrefix(resp.Error.OpID, "op-") {
			t.Fatalf("op_id = %q, want op-* prefix", resp.Error.OpID)
		}
	})
	t.Run("bad_version", func(t *testing.T) {
		resp := d.dispatch(context.Background(), rpc.Request{Version: rpc.Version + 99, Method: "ping"})
		if resp.OK {
			t.Fatalf("expected error response, got %+v", resp)
		}
		if resp.Error == nil || resp.Error.Code != "bad_version" {
			t.Fatalf("error = %+v, want bad_version", resp.Error)
		}
		if !strings.HasPrefix(resp.Error.OpID, "op-") {
			t.Fatalf("op_id = %q, want op-* prefix", resp.Error.OpID)
		}
	})
}

// TestDispatchPropagatesOpIDFromContext covers the case where a
// handler returns its own rpc.NewError with an empty op_id (most
// service errors do); the dispatch wrapper must stamp the
// dispatch-generated id on the way out.
func TestDispatchPropagatesOpIDFromContext(t *testing.T) {
	d := &Daemon{
		requestHandler: func(_ context.Context, _ rpc.Request) rpc.Response {
			return rpc.NewError("operation_failed", "deliberate test failure")
		},
	}
	resp := d.dispatch(context.Background(), rpc.Request{Version: rpc.Version, Method: "anything"})
	if resp.OK || resp.Error == nil {
		t.Fatalf("expected error response, got %+v", resp)
	}
	if !strings.HasPrefix(resp.Error.OpID, "op-") {
		t.Fatalf("dispatch did not stamp op_id: %+v", resp.Error)
	}
}
