package registry

import (
	"context"
	"encoding/json"
	"errors"
	"sync"
	"sync/atomic"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/extension/spec"
)

// fakeExecutor is a test-only Executor used to exercise Builtin registration,
// which requires a real (non-nil) code executor. It is NOT shipped: production
// stage 2 has no Executor implementation (no StubExecutor reaches the public
// API).
type fakeExecutor struct {
	label string
}

func (f fakeExecutor) Strategy() string { return "fake" }

func (f fakeExecutor) Invoke(context.Context, BuiltinInvocation) (BuiltinResult, error) {
	return BuiltinResult{Data: json.RawMessage(`{"ok":true}`)}, nil
}

// ---- Idempotent registration ----

func TestRegisterExternal_Idempotent(t *testing.T) {
	r := New()
	pkg := makePackage(t, "demo", 1, "do-thing", "summary one")

	first, err := r.RegisterExternal(pkg)
	require.NoError(t, err)
	assert.Equal(t, External, first.Kind)

	before := r.Len()

	second, err := r.RegisterExternal(pkg)
	require.NoError(t, err)

	assert.Equal(t, before, r.Len(), "idempotent re-register must not grow the registry")
	assert.Equal(t, first.Definition.Digest, second.Definition.Digest)
	assert.Equal(t, first.Definition.Domain, second.Definition.Domain)
}

func TestRegisterBuiltin_Idempotent(t *testing.T) {
	r := New()
	def := makeBuiltinDefinition(t, "demo", 1, "do-thing", "summary one")

	first, err := r.RegisterBuiltin(def, fakeExecutor{label: "a"}, "rev-1")
	require.NoError(t, err)
	assert.Equal(t, Builtin, first.Kind)
	before := r.Len()

	// Same catalog, same revision, same kind -> idempotent.
	second, err := r.RegisterBuiltin(def, fakeExecutor{label: "a"}, "rev-1")
	require.NoError(t, err)
	assert.Equal(t, before, r.Len(), "idempotent re-register must not grow the registry")
	assert.Equal(t, first.Definition.Digest, second.Definition.Digest)
}

func TestRegister_KindChangeIsRejected(t *testing.T) {
	// Kind is immutable identity: re-registering the same (domain, version) with
	// a DIFFERENT Kind must FAIL with *KindMismatchError. There is no silent
	// External<->Builtin switch. Kind is checked before the digest, so this
	// fires even though External and Builtin digests are computed differently.
	t.Run("external then builtin", func(t *testing.T) {
		r := New()
		pkg := makePackage(t, "demo", 1, "do-thing", "summary one")
		_, err := r.RegisterExternal(pkg)
		require.NoError(t, err)

		def := makeBuiltinDefinition(t, "demo", 1, "do-thing", "summary one")
		_, err = r.RegisterBuiltin(def, fakeExecutor{}, "rev-1")
		require.Error(t, err, "same (domain, version) re-registered with a different kind must fail")

		var kme *KindMismatchError
		require.ErrorAs(t, err, &kme, "must return *KindMismatchError (got %T: %v)", err, err)
		assert.Equal(t, External, kme.Existing)
		assert.Equal(t, Builtin, kme.Incoming)
		assert.Equal(t, 1, r.Len(), "kind-mismatch must not grow the registry")
	})

	t.Run("builtin then external", func(t *testing.T) {
		r := New()
		def := makeBuiltinDefinition(t, "demo", 1, "do-thing", "summary one")
		_, err := r.RegisterBuiltin(def, fakeExecutor{}, "rev-1")
		require.NoError(t, err)

		pkg := makePackage(t, "demo", 1, "do-thing", "summary one")
		_, err = r.RegisterExternal(pkg)
		require.Error(t, err)
		var kme *KindMismatchError
		require.ErrorAs(t, err, &kme)
		assert.Equal(t, Builtin, kme.Existing)
		assert.Equal(t, External, kme.Incoming)
		assert.Equal(t, 1, r.Len())
	})
}

// ---- Conflict rejection (acceptance #t2402-2) ----

func TestRegisterExternal_ConflictReject(t *testing.T) {
	r := New()
	// Same domain+version, different summary -> different digest.
	pkgA := makePackage(t, "demo", 1, "do-thing", "summary A")
	pkgB := makePackage(t, "demo", 1, "do-thing", "summary B")
	require.NotEqual(t, pkgA.Digest, pkgB.Digest, "test prelude: summaries must yield different digests")

	_, err := r.RegisterExternal(pkgA)
	require.NoError(t, err)
	require.Equal(t, 1, r.Len())

	_, err = r.RegisterExternal(pkgB)
	require.Error(t, err, "different digest at same (domain, version) must fail")

	var ce *ConflictError
	require.True(t, errors.As(err, &ce), "must return *ConflictError (got %T: %v)", err, err)
	assert.Equal(t, "demo", ce.Domain)
	assert.Equal(t, 1, ce.Version)
	assert.Equal(t, pkgA.Digest, ce.Existing)
	assert.Equal(t, pkgB.Digest, ce.Incoming)

	// Fail-closed: the registry keeps the FIRST payload, not the conflicting one.
	assert.Equal(t, 1, r.Len(), "conflict must not grow the registry")
	got, ok := r.LookupByIdentity("demo", 1)
	require.True(t, ok)
	assert.Equal(t, pkgA.Digest, got.Definition.Digest, "conflict must not overwrite the existing payload")
}

func TestRegisterExternal_ConflictReject_DoesNotMutateRegistry(t *testing.T) {
	// A failed conflict registration must leave no trace: neither the digest
	// index nor the identity index may reference the rejected payload.
	r := New()
	pkgA := makePackage(t, "demo", 1, "do-thing", "summary A")
	pkgB := makePackage(t, "demo", 1, "do-thing", "summary B")

	_, err := r.RegisterExternal(pkgA)
	require.NoError(t, err)

	_, err = r.RegisterExternal(pkgB)
	require.Error(t, err)

	_, ok := r.LookupByDigest(pkgB.Digest)
	assert.False(t, ok, "rejected payload digest must not be reachable")
	got, ok := r.LookupByIdentity("demo", 1)
	require.True(t, ok)
	assert.Equal(t, pkgA.Digest, got.Definition.Digest)
}

// ---- Concurrent conflict: exactly one wins, others fail, no corruption ----

func TestRegisterExternal_ConcurrentConflict(t *testing.T) {
	const n = 16
	// All share domain+version; each has a unique summary -> unique digest.
	pkgs := make([]spec.Package, n)
	for i := range pkgs {
		pkgs[i] = makePackage(t, "race", 1, "do", uniqueSummary(i))
		for j := 0; j < i; j++ {
			require.NotEqual(t, pkgs[j].Digest, pkgs[i].Digest)
		}
	}

	r := New()
	var wg sync.WaitGroup
	var wins, fails int64
	wg.Add(n)
	start := make(chan struct{})
	for i := range pkgs {
		go func(p spec.Package) {
			defer wg.Done()
			<-start
			_, err := r.RegisterExternal(p)
			if err == nil {
				atomic.AddInt64(&wins, 1)
			} else {
				atomic.AddInt64(&fails, 1)
			}
		}(pkgs[i])
	}
	close(start)
	wg.Wait()

	assert.Equal(t, int64(1), wins, "exactly one goroutine must win the conflict")
	assert.Equal(t, int64(n-1), fails, "all other goroutines must fail")
	assert.Equal(t, 1, r.Len(), "registry must hold exactly one record after the race")

	list := r.List()
	require.Len(t, list, 1)
	digest := list[0].Definition.Digest
	_, ok := r.LookupByDigest(digest)
	assert.True(t, ok)
	got, ok := r.LookupByIdentity("race", 1)
	require.True(t, ok)
	assert.Equal(t, digest, got.Definition.Digest)
}

// ---- Clone-on-read: mutating a returned copy must not affect cached state ----

func TestRegistry_CloneOnReadMutation_DefinitionAndBinding(t *testing.T) {
	r := New()
	pkg := makePackage(t, "demo", 1, "do-thing", "summary one")
	_, err := r.RegisterExternal(pkg)
	require.NoError(t, err)

	got, ok := r.LookupByDigest(pkg.Digest)
	require.True(t, ok)

	// Mutate EVERY nested field of the returned projection deeply: the common
	// Definition catalog and the kind-specific HTTP binding.
	got.Definition.Domain = "mutated"
	got.Definition.Version = 99
	got.Definition.Description = "mutated desc"
	got.Definition.Actions = append(got.Definition.Actions, DefinitionAction{Name: "injected"})
	got.Definition.Actions[0].Name = "mutated-action"
	got.Definition.Actions[0].Summary = "mutated summary"
	got.Definition.Actions[0].Safety = spec.Safety{Class: "destructive"}
	got.Definition.Actions[0].InputSchema["type"] = "mutated"
	got.Definition.Actions[0].InputSchema["properties"].(map[string]any)["id"].(map[string]any)["type"] = "integer"
	got.Definition.Actions[0].OutputSchema["type"] = "mutated"
	got.Definition.Actions[0].Example = map[string]any{"injected": true}
	// HTTP binding (External only).
	got.Binding.Endpoint = spec.Endpoint{BaseURL: "https://evil.example.com"}
	got.Binding.Auth.Profiles = append(got.Binding.Auth.Profiles, spec.AuthProfile{ID: "x"})
	got.Binding.Actions[0].Binding.Method = "DELETE"
	got.Binding.Actions[0].Binding.StaticHeaders["X-Injected"] = "yes"
	got.Binding.Actions[0].Binding.Parameters = append(got.Binding.Actions[0].Binding.Parameters, spec.Parameter{From: "/x"})
	got.Binding.Actions[0].Binding.Response.SuccessStatuses = append(got.Binding.Actions[0].Binding.Response.SuccessStatuses, 500)
	got.Binding.Actions[0].Binding.Response.ContentTypes = append(got.Binding.Actions[0].Binding.Response.ContentTypes, "text/plain")
	got.Binding.Actions[0].Auth.RequiredScopes = append(got.Binding.Actions[0].Auth.RequiredScopes, "extra")

	// A reference obtained after mutation, plus a fresh re-read, must both show
	// pristine cached state.
	again, ok := r.LookupByDigest(pkg.Digest)
	require.True(t, ok)

	assert.Equal(t, "demo", again.Definition.Domain)
	assert.Equal(t, 1, again.Definition.Version)
	assert.Equal(t, "Test demo domain description", again.Definition.Description)
	require.Len(t, again.Definition.Actions, 1)
	assert.Equal(t, "do-thing", again.Definition.Actions[0].Name)
	assert.Equal(t, "summary one", again.Definition.Actions[0].Summary)
	assert.Equal(t, "read", again.Definition.Actions[0].Safety.Class)
	assert.Equal(t, "object", again.Definition.Actions[0].InputSchema["type"])
	idProp := again.Definition.Actions[0].InputSchema["properties"].(map[string]any)["id"].(map[string]any)
	assert.Equal(t, "string", idProp["type"])
	assert.Equal(t, "object", again.Definition.Actions[0].OutputSchema["type"])
	assert.Equal(t, map[string]any{"id": "abc", "tags": []any{"x", "y"}}, again.Definition.Actions[0].Example)
	// Binding is pristine and External-shaped.
	assert.Equal(t, "https://api.test.example.com", again.Binding.Endpoint.BaseURL)
	require.Len(t, again.Binding.Auth.Profiles, 1)
	assert.Equal(t, "GET", again.Binding.Actions[0].Binding.Method)
	assert.NotContains(t, again.Binding.Actions[0].Binding.StaticHeaders, "X-Injected")
	assert.Equal(t, []int{200}, again.Binding.Actions[0].Binding.Response.SuccessStatuses)
	assert.Equal(t, []string{"application/json"}, again.Binding.Actions[0].Binding.Response.ContentTypes)
	assert.Equal(t, []string{"scope-a", "scope-b"}, again.Binding.Actions[0].Auth.RequiredScopes)
}

func TestRegistry_CloneOnReadMutation_List(t *testing.T) {
	// List returns clones too: mutating a list element must not leak.
	r := New()
	pkg := makePackage(t, "demo", 1, "do-thing", "summary one")
	_, err := r.RegisterExternal(pkg)
	require.NoError(t, err)

	list := r.List()
	require.Len(t, list, 1)
	list[0].Definition.Actions[0].Summary = "mutated"
	list[0].Definition.Actions[0].OutputSchema["type"] = "mutated"
	list[0].Binding.Actions[0].Binding.Method = "DELETE"

	again := r.List()
	require.Len(t, again, 1)
	assert.Equal(t, "summary one", again[0].Definition.Actions[0].Summary)
	assert.Equal(t, "object", again[0].Definition.Actions[0].OutputSchema["type"])
	assert.Equal(t, "GET", again[0].Binding.Actions[0].Binding.Method)
}

// ---- Blocker #2 (adversarial): typed-slice clone-on-read ----

func TestRegistry_CloneOnRead_TypedSliceExample_NoAliasing(t *testing.T) {
	// Black-box probe: register with a typed slice Example ([]string), lookup,
	// mutate element [0], re-lookup -> pristine. The registry normalizes the
	// stored example to a deterministic JSON tree and deep-clones on every read, so
	// the caller's slice never aliases cached state.
	r := New()
	def := makeBuiltinDefinition(t, "typed", 1, "do", "s")
	def.Actions[0].Example = []string{"one", "two"} // typed slice, the adversarial input
	def.Actions[0].InputSchema = map[string]any{    // accept a string array so the typed-slice example validates
		"$schema": "https://json-schema.org/draft/2020-12/schema",
		"type":    "array",
		"items":   map[string]any{"type": "string"},
	}

	_, err := r.RegisterBuiltin(def, fakeExecutor{}, "rev-1")
	require.NoError(t, err)

	got, ok := r.LookupByIdentity("typed", 1)
	require.True(t, ok)

	// The returned example is a deep-cloned canonical array; mutating it must
	// not affect the cached entry.
	example, ok := got.Definition.Actions[0].Example.([]any)
	require.True(t, ok, "stored example is normalized to a deterministic JSON array")
	example[0] = "mutated"

	again, ok := r.LookupByIdentity("typed", 1)
	require.True(t, ok)
	assert.Equal(t, []any{"one", "two"}, again.Definition.Actions[0].Example,
		"re-lookup must be pristine (no aliasing of the caller's typed slice)")
}

func TestCloneValue_TypedSlicesAndMaps(t *testing.T) {
	// White-box: the defensive cloneValue must deep-copy every typed container a
	// hand-constructed package could carry, not just map[string]any / []any.
	cases := []struct {
		name string
		in   any
		mut  func(v any)
		want any
	}{
		{"[]string", []string{"a", "b"}, func(v any) { v.([]string)[0] = "z" }, []string{"a", "b"}},
		{"[]int", []int{1, 2, 3}, func(v any) { v.([]int)[0] = 99 }, []int{1, 2, 3}},
		{"[]float64", []float64{1.5, 2.5}, func(v any) { v.([]float64)[0] = 9.9 }, []float64{1.5, 2.5}},
		{"[]bool", []bool{true, false}, func(v any) { v.([]bool)[0] = false }, []bool{true, false}},
		{"map[string]string", map[string]string{"k": "v"}, func(v any) { v.(map[string]string)["k"] = "z" }, map[string]string{"k": "v"}},
		{"[]any nested", []any{map[string]any{"n": float64(1)}}, func(v any) {
			v.([]any)[0].(map[string]any)["n"] = float64(99)
		}, []any{map[string]any{"n": float64(1)}}},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			cp := cloneValue(tc.in)
			tc.mut(cp) // mutate the clone
			assert.Equal(t, tc.want, tc.in, "original must be unaffected by mutating the clone")
		})
	}
}

// ---- Digest lookup (acceptance #t2402-3) ----

func TestRegistry_DigestLookup(t *testing.T) {
	r := New()
	pkg := makePackage(t, "demo", 1, "do-thing", "summary one")
	_, err := r.RegisterExternal(pkg)
	require.NoError(t, err)

	got, ok := r.LookupByDigest(pkg.Digest)
	require.True(t, ok)
	assert.Equal(t, pkg.Digest, got.Definition.Digest)

	for _, bad := range []string{"", "sha256:deadbeef", "not-a-digest", "sha256:" + pkg.Digest[7:] + "ff"} {
		miss, ok := r.LookupByDigest(bad)
		assert.False(t, ok, "lookup of unknown digest %q must miss", bad)
		assert.Equal(t, RegisteredExtension{}, miss, "miss must return the zero value, not a stale pointer")
	}
}

func TestRegistry_LookupByIdentity(t *testing.T) {
	r := New()
	pkg := makePackage(t, "demo", 2, "do-thing", "summary one")
	_, err := r.RegisterExternal(pkg)
	require.NoError(t, err)

	got, ok := r.LookupByIdentity("demo", 2)
	require.True(t, ok)
	assert.Equal(t, pkg.Digest, got.Definition.Digest)

	_, ok = r.LookupByIdentity("demo", 1) // wrong version
	assert.False(t, ok)
	_, ok = r.LookupByIdentity("other", 2) // wrong domain
	assert.False(t, ok)
}

// ---- Register input validation (fail-closed on bad input) ----

func TestRegisterExternal_RejectsBadInput(t *testing.T) {
	t.Run("missing digest", func(t *testing.T) {
		r := New()
		pkg := makePackage(t, "demo", 1, "a", "s")
		pkg.Digest = ""
		pkg.Metadata.Digest = ""
		_, err := r.RegisterExternal(pkg)
		require.Error(t, err)
		assert.Equal(t, 0, r.Len())
	})

	t.Run("missing domain", func(t *testing.T) {
		r := New()
		pkg := makePackage(t, "demo", 1, "a", "s")
		pkg.Metadata.Domain = ""
		pkg.Digest = "sha256:anything"
		pkg.Metadata.Digest = pkg.Digest
		_, err := r.RegisterExternal(pkg)
		require.Error(t, err)
		assert.Equal(t, 0, r.Len())
	})

	t.Run("digest fields disagree", func(t *testing.T) {
		r := New()
		pkg := makePackage(t, "demo", 1, "a", "s")
		pkg.Digest = "sha256:" + repeat("a", 64)
		pkg.Metadata.Digest = "sha256:" + repeat("b", 64)
		_, err := r.RegisterExternal(pkg)
		assert.ErrorIs(t, err, ErrDigestMismatch)
		assert.Equal(t, 0, r.Len())
	})
}

func TestRegisterBuiltin_RejectsBadInput(t *testing.T) {
	t.Run("missing executor", func(t *testing.T) {
		r := New()
		def := makeBuiltinDefinition(t, "demo", 1, "a", "s")
		_, err := r.RegisterBuiltin(def, nil, "rev-1")
		assert.ErrorIs(t, err, ErrBuiltinMissingExecutor)
		assert.Equal(t, 0, r.Len(), "rejected Builtin must not be registered")
	})

	t.Run("missing revision", func(t *testing.T) {
		r := New()
		def := makeBuiltinDefinition(t, "demo", 1, "a", "s")
		_, err := r.RegisterBuiltin(def, fakeExecutor{}, "")
		assert.ErrorIs(t, err, ErrBuiltinMissingRevision)
		assert.Equal(t, 0, r.Len())
	})

	t.Run("missing domain", func(t *testing.T) {
		r := New()
		def := makeBuiltinDefinition(t, "demo", 1, "a", "s")
		def.Domain = ""
		_, err := r.RegisterBuiltin(def, fakeExecutor{}, "rev-1")
		assert.ErrorIs(t, err, ErrMissingDomain)
		assert.Equal(t, 0, r.Len())
	})

	t.Run("invalid safety class", func(t *testing.T) {
		r := New()
		def := makeBuiltinDefinition(t, "demo", 1, "a", "s")
		def.Actions[0].Safety = spec.Safety{Class: "bogus"}
		_, err := r.RegisterBuiltin(def, fakeExecutor{}, "rev-1")
		assert.ErrorIs(t, err, spec.ErrInvalidSafetyClass)
		assert.Equal(t, 0, r.Len())
	})

	t.Run("duplicate action name", func(t *testing.T) {
		r := New()
		def := makeBuiltinDefinition(t, "demo", 1, "a", "s")
		def.Actions = append(def.Actions, def.Actions[0])
		_, err := r.RegisterBuiltin(def, fakeExecutor{}, "rev-1")
		assert.ErrorIs(t, err, spec.ErrDuplicateAction)
		assert.Equal(t, 0, r.Len())
	})
}

// ---- Blocker #3 (adversarial): External has no executor; Builtin executor
// identity is mismatch-checked; BuiltinExecutorFor scopes to Builtin. ----

func TestRegisterExternal_HasNoExecutor(t *testing.T) {
	// (a) External+executor is structurally rejected: RegisterExternal has NO
	//     executor parameter (compile-time), so a caller cannot attach a code
	//     executor to an HTTP Extension.
	// (c) BuiltinExecutorFor(externalDigest) -> ok=false (External has no code
	//     executor; the old misleading ExecutorFor->(nil,true) is gone).
	r := New()
	pkg := makePackage(t, "ext-noexec", 1, "do", "s")
	_, err := r.RegisterExternal(pkg)
	require.NoError(t, err)

	exec, rev, ok := r.BuiltinExecutorFor(pkg.Digest)
	assert.False(t, ok, "BuiltinExecutorFor must not succeed for an External")
	assert.Nil(t, exec, "External has no code executor")
	assert.Empty(t, rev)

	// Unknown digest also reports ok=false, never a stale executor.
	_, _, ok = r.BuiltinExecutorFor("sha256:not-registered")
	assert.False(t, ok)
}

func TestRegisterBuiltin_RevisionChange_IsConflict_DifferentDigest(t *testing.T) {
	// Blocker #2 (adversarial): the revision is part of the RUNTIME digest.
	// The same catalog with rev-1 vs rev-2 yields DIFFERENT digests, so
	// re-registering at the same (domain, version) with a different revision is
	// a ConflictError (different digest), not a silent overwrite. The revision
	// change is digest-visible and propagates to authorization/fingerprint/
	// stale-detection as a new identity.
	r := New()
	def := makeBuiltinDefinition(t, "demo", 1, "do", "summary one")

	first, err := r.RegisterBuiltin(def, fakeExecutor{label: "impl-a"}, "rev-1")
	require.NoError(t, err)
	require.Equal(t, 1, r.Len())

	// Same catalog, different revision -> DIFFERENT runtime digest.
	second, err := r.RegisterBuiltin(def, fakeExecutor{label: "impl-b"}, "rev-2")
	// rev-2 produces a different digest, so it is a conflict at (domain, version).
	digest2, derr := computeBuiltinDigest(def, "rev-2")
	require.NoError(t, derr)
	_ = second // may or may not be used below depending on error path
	require.Error(t, err, "a revision change at the same (domain, version) must fail (different digest -> conflict)")
	var ce *ConflictError
	require.ErrorAs(t, err, &ce)
	assert.Equal(t, first.Definition.Digest, ce.Existing, "existing digest is the rev-1 digest")
	assert.Equal(t, digest2, ce.Incoming, "incoming digest is the rev-2 digest")
	assert.NotEqual(t, first.Definition.Digest, digest2, "rev-1 and rev-2 must produce DIFFERENT digests")
	assert.Equal(t, 1, r.Len(), "conflict must not grow the registry")

	// The original executor/revision is preserved unchanged.
	exec, rev, ok := r.BuiltinExecutorFor(first.Definition.Digest)
	require.True(t, ok)
	assert.Equal(t, "rev-1", rev)
	assert.Equal(t, "fake", exec.Strategy())

	// Same catalog + same revision is still idempotent.
	before := r.Len()
	_, err = r.RegisterBuiltin(def, fakeExecutor{label: "impl-a"}, "rev-1")
	require.NoError(t, err)
	assert.Equal(t, before, r.Len())
}

func TestRegisterBuiltin_DifferentCatalog_IsConflict(t *testing.T) {
	// A different catalog (different digest) at the same (domain, version) is a
	// ConflictError, not a revision mismatch.
	r := New()
	defA := makeBuiltinDefinition(t, "demo", 1, "do", "summary A")
	defB := makeBuiltinDefinition(t, "demo", 1, "do", "summary B")

	_, err := r.RegisterBuiltin(defA, fakeExecutor{}, "rev-1")
	require.NoError(t, err)

	_, err = r.RegisterBuiltin(defB, fakeExecutor{}, "rev-1")
	require.Error(t, err)
	var ce *ConflictError
	require.ErrorAs(t, err, &ce)
	assert.Equal(t, 1, r.Len())
}

// ---- Blocker #4 (adversarial): Builtin needs NO HTTP binding; External
// carries it. Model separation: common Definition vs kind-specific union. ----

func TestRegisterBuiltin_NoHTTPBinding(t *testing.T) {
	// A Builtin registers with a Definition that has NO endpoint, NO auth
	// profiles, and NO HTTP binding per action. It must not construct a fake
	// spec.Package to do so.
	r := New()
	def := makeBuiltinDefinition(t, "builtin-nohttp", 1, "do", "s")

	// Sanity: the definition carries the common catalog fields and NO
	// HTTP-shaped fields (Definition has no Endpoint/Auth/Binding at all).
	require.NotEmpty(t, def.Name)
	require.NotEmpty(t, def.Actions)

	got, err := r.RegisterBuiltin(def, fakeExecutor{}, "rev-1")
	require.NoError(t, err, "Builtin must register without any HTTP binding")
	assert.Equal(t, Builtin, got.Kind)
	assert.Equal(t, "builtin-nohttp", got.Definition.Domain)
	// The External binding projection is the zero value for a Builtin.
	assert.Empty(t, got.Binding.Endpoint.BaseURL)
	assert.Empty(t, got.Binding.Auth.Profiles)
	assert.Empty(t, got.Binding.Actions)
}

func TestRegisterExternal_CarriesHTTPBinding(t *testing.T) {
	// An External registers from an OpenAPI-derived Package and carries the HTTP
	// binding in the kind-specific projection.
	r := New()
	pkg := makePackage(t, "ext-http", 1, "do", "s")
	got, err := r.RegisterExternal(pkg)
	require.NoError(t, err)
	assert.Equal(t, External, got.Kind)
	require.Len(t, got.Binding.Actions, 1)
	assert.Equal(t, "GET", got.Binding.Actions[0].Binding.Method)
	assert.Equal(t, "/items/{id}", got.Binding.Actions[0].Binding.Path)
	assert.Equal(t, "https://api.test.example.com", got.Binding.Endpoint.BaseURL)
	require.Len(t, got.Binding.Auth.Profiles, 1)
	assert.Equal(t, "default", got.Binding.Auth.Profiles[0].ID)
}

// ---- Blocker #1 (adversarial): Builtin reuses SHARED Definition/Catalog
// validation. Five classes of invalid common-contract must be rejected. ----

func TestRegisterBuiltin_SharedValidation_RejectsInvalidCatalog(t *testing.T) {
	// Each subtest constructs a Builtin Definition with one class of invalid
	// common-contract content. The shared spec.ValidateCatalog validator
	// must reject every one — the Builtin is held to the SAME catalog contract
	// as an OpenAPI-derived External.
	t.Run("bad domain (not kebab-case)", func(t *testing.T) {
		r := New()
		def := makeBuiltinDefinition(t, "demo", 1, "do", "s")
		def.Domain = "BAD_DOMAIN" // uppercase, underscores — not kebab
		_, err := r.RegisterBuiltin(def, fakeExecutor{}, "rev-1")
		require.Error(t, err, "an invalid domain must be rejected by shared validation")
		assert.Equal(t, 0, r.Len())
	})

	t.Run("bad action name (not kebab-case)", func(t *testing.T) {
		r := New()
		def := makeBuiltinDefinition(t, "demo", 1, "Bad_Action", "s")
		_, err := r.RegisterBuiltin(def, fakeExecutor{}, "rev-1")
		require.Error(t, err, "an invalid action name must be rejected by shared validation")
		assert.Equal(t, 0, r.Len())
	})

	t.Run("bad schema (uncompilable)", func(t *testing.T) {
		r := New()
		def := makeBuiltinDefinition(t, "demo", 1, "do", "s")
		def.Actions[0].InputSchema = map[string]any{
			"$schema": "https://json-schema.org/draft/2020-12/schema",
			"type":    123, // non-string type — uncompilable
		}
		_, err := r.RegisterBuiltin(def, fakeExecutor{}, "rev-1")
		require.Error(t, err, "an uncompilable schema must be rejected by shared validation")
		assert.Equal(t, 0, r.Len())
	})

	t.Run("example violates schema", func(t *testing.T) {
		r := New()
		def := makeBuiltinDefinition(t, "demo", 1, "do", "s")
		// The schema requires a string "q"; the example provides an integer.
		def.Actions[0].InputSchema = map[string]any{
			"$schema": "https://json-schema.org/draft/2020-12/schema",
			"type":    "object",
			"properties": map[string]any{
				"q": map[string]any{"type": "string"},
			},
		}
		def.Actions[0].Example = map[string]any{"q": 42} // integer, not string
		_, err := r.RegisterBuiltin(def, fakeExecutor{}, "rev-1")
		require.Error(t, err, "an example that violates its schema must be rejected by shared validation")
		assert.Equal(t, 0, r.Len())
	})

	t.Run("read + destructive safety contradiction", func(t *testing.T) {
		r := New()
		def := makeBuiltinDefinition(t, "demo", 1, "do", "s")
		def.Actions[0].Safety = spec.Safety{Class: "read", Destructive: true}
		_, err := r.RegisterBuiltin(def, fakeExecutor{}, "rev-1")
		require.Error(t, err, "class=read + destructive=true is a contradictory safety contract")
		assert.ErrorIs(t, err, spec.ErrInvalidSafetyClass)
		assert.Equal(t, 0, r.Len())
	})
}

// ---- Blocker #2 (adversarial): revision-in-digest. Same catalog, different
// revision -> different runtime digest. ----

func TestRegisterBuiltin_RevisionInDigest_DifferentRevisionsDifferentDigest(t *testing.T) {
	def := makeBuiltinDefinition(t, "demo", 1, "do", "s")
	digest1, err := computeBuiltinDigest(def, "rev-1")
	require.NoError(t, err)
	digest2, err := computeBuiltinDigest(def, "rev-2")
	require.NoError(t, err)
	assert.NotEqual(t, digest1, digest2,
		"the same catalog with a different revision must produce a DIFFERENT runtime digest")

	// Same revision is stable.
	digest1Again, err := computeBuiltinDigest(def, "rev-1")
	require.NoError(t, err)
	assert.Equal(t, digest1, digest1Again)
}

// ---- Blocker #3 (adversarial): same digest -> same normalized stored Catalog.
// Register with reordered actions -> lookup returns identical normalized
// Catalog (same order, same bytes). ----

func TestRegisterBuiltin_ReorderedActions_NormalizedStorage(t *testing.T) {
	// Two definitions with the SAME actions in DIFFERENT order. The digest must
	// be identical (actions are sorted in the projection), and the STORED
	// Catalog must be in canonical (sorted) order.
	def1 := Definition{
		Domain: "reorder", Version: 1, Name: "Reorder Test",
		Actions: []DefinitionAction{
			{Name: "zebra", Summary: "z", Safety: spec.Safety{Class: "read"},
				InputSchema: minimalSchema()},
			{Name: "alpha", Summary: "a", Safety: spec.Safety{Class: "read"},
				InputSchema: minimalSchema()},
		},
	}
	def2 := Definition{
		Domain: "reorder", Version: 1, Name: "Reorder Test",
		Actions: []DefinitionAction{
			{Name: "alpha", Summary: "a", Safety: spec.Safety{Class: "read"},
				InputSchema: minimalSchema()},
			{Name: "zebra", Summary: "z", Safety: spec.Safety{Class: "read"},
				InputSchema: minimalSchema()},
		},
	}

	// Both compute the SAME digest (actions sorted by name in the projection).
	d1, err := computeBuiltinDigest(def1, "rev-1")
	require.NoError(t, err)
	d2, err := computeBuiltinDigest(def2, "rev-1")
	require.NoError(t, err)
	assert.Equal(t, d1, d2, "reordered actions must produce the same digest")

	// Register def1 (zebra first); the STORED Catalog must be in sorted
	// (alpha, zebra) order.
	r := New()
	got, err := r.RegisterBuiltin(def1, fakeExecutor{}, "rev-1")
	require.NoError(t, err)
	require.Len(t, got.Definition.Actions, 2)
	assert.Equal(t, "alpha", got.Definition.Actions[0].Name, "stored actions must be sorted by name")
	assert.Equal(t, "zebra", got.Definition.Actions[1].Name)

	// A fresh lookup returns the SAME normalized order.
	again, ok := r.LookupByIdentity("reorder", 1)
	require.True(t, ok)
	require.Len(t, again.Definition.Actions, 2)
	assert.Equal(t, "alpha", again.Definition.Actions[0].Name)
	assert.Equal(t, "zebra", again.Definition.Actions[1].Name)
}

func TestRegisterExternal_ReorderedActions_NormalizedStorage(t *testing.T) {
	// Build a package with two actions in non-sorted order.
	pkgA := makePackage(t, "reorder-ext-a", 1, "zebra-action", "z summary")
	// Add a second action "alpha-action" to the same package.
	pkgA.Actions = append(pkgA.Actions, spec.Action{
		Name: "alpha-action",
		File: "actions/alpha-action.yaml",
		Catalog: spec.Catalog{
			Summary:     "a summary",
			Description: "alpha action",
			Safety:      spec.Safety{Class: "read"},
			InputSchema: map[string]any{
				"$schema": "https://json-schema.org/draft/2020-12/schema",
				"type":    "object",
				"properties": map[string]any{
					"id": map[string]any{"type": "string"},
				},
			},
		},
		Binding: spec.Binding{
			Method: "GET",
			Path:   "/alpha",
			Response: spec.Response{
				Mode:            "passthrough",
				SuccessStatuses: []int{200},
				ContentTypes:    []string{"application/json"},
			},
		},
	})
	// Fix auth reference for both actions.
	pkgA.Actions[0].Auth = &spec.ActionAuthRequirement{Profile: "default"}
	pkgA.Actions[1].Auth = &spec.ActionAuthRequirement{Profile: "default"}
	digest, err := spec.DefinitionDigest(pkgA)
	require.NoError(t, err)
	pkgA.Digest = digest
	pkgA.Metadata.Digest = digest

	r := New()
	got, err := r.RegisterExternal(pkgA)
	require.NoError(t, err)

	// The STORED Definition actions are in sorted order: alpha-action, zebra-action.
	require.Len(t, got.Definition.Actions, 2)
	assert.Equal(t, "alpha-action", got.Definition.Actions[0].Name,
		"stored External actions must be sorted by name (normalized storage)")
	assert.Equal(t, "zebra-action", got.Definition.Actions[1].Name)

	// The binding actions are also in sorted order.
	require.Len(t, got.Binding.Actions, 2)
	assert.Equal(t, "alpha-action", got.Binding.Actions[0].Name)
	assert.Equal(t, "zebra-action", got.Binding.Actions[1].Name)

	// Build the same package with reversed action order -> same digest, same
	// stored order.
	pkgB := pkgA
	pkgB.Actions = []spec.Action{pkgA.Actions[1], pkgA.Actions[0]}
	// Re-verify the digest is the same (canonical projection sorts).
	assert.Equal(t, pkgA.Digest, got.Definition.Digest)

	again, ok := r.LookupByDigest(pkgA.Digest)
	require.True(t, ok)
	assert.Equal(t, "alpha-action", again.Definition.Actions[0].Name)
	assert.Equal(t, "zebra-action", again.Definition.Actions[1].Name)
}

// minimalSchema returns a minimal valid Draft 2020-12 input schema for Builtin
// action test definitions.
func minimalSchema() map[string]any {
	return map[string]any{
		"$schema": "https://json-schema.org/draft/2020-12/schema",
		"type":    "object",
	}
}
