package registry

import (
	"encoding/json"
	"testing"

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

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

// ---- Stage-2 round-6 blocker (adversarial): the detachedJSONValue JSON round-trip
// in BOTH internal/extensions/spec (External DefinitionDigest) AND the registry (Builtin
// computeBuiltinDigest) must use json.Decoder.UseNumber so large integers (>2^53)
// survive losslessly. A float64 coercion collapses 2^53 and 2^53+1 to the same
// value, COLLIDING the digest of two distinct Agent-visible examples. This test
// proves both paths now derive DISTINCT digests for the two values. ----

func TestDefinitionDigest_LargeInteger_NoCollision(t *testing.T) {
	// 2^53 and 2^53+1: distinct integers that a float64 round-trip collapses to
	// the same value. On a 64-bit host these are exact int values.
	const (
		two53   = 9007199254740992 // 2^53, exactly representable as float64
		two53P1 = 9007199254740993 // 2^53 + 1, NOT exactly representable as float64
	)

	// --- External path: presets/spec.DefinitionDigest uses detachedJSONValue for
	// the example in the semantic projection. The two packages are IDENTICAL except
	// for the single example value, so a collision would be a real lossless bug
	// (not masked by any other differing field). ---
	extA := makePackage(t, "large-int-ext", 1, "do-thing", "same summary")
	extA.Actions[0].Catalog.Example = two53
	extB := makePackage(t, "large-int-ext", 1, "do-thing", "same summary")
	extB.Actions[0].Catalog.Example = two53P1
	extDA, err := spec.DefinitionDigest(extA)
	require.NoError(t, err)
	extDB, err := spec.DefinitionDigest(extB)
	require.NoError(t, err)
	assert.NotEqual(t, extDA, extDB,
		"External DefinitionDigest must NOT collide for 2^53 vs 2^53+1 (lossless json.Number round-trip)")

	// --- Builtin path: computeBuiltinDigest uses detachedJSONValue for the example in
	// the runtime projection. Again the two definitions are IDENTICAL except for
	// the single example value. ---
	binA := makeBuiltinDefinition(t, "large-int-bin", 1, "do-thing", "same summary")
	binA.Actions[0].Example = two53
	binB := makeBuiltinDefinition(t, "large-int-bin", 1, "do-thing", "same summary")
	binB.Actions[0].Example = two53P1
	binDA, err := computeBuiltinDigest(binA, "rev-1")
	require.NoError(t, err)
	binDB, err := computeBuiltinDigest(binB, "rev-1")
	require.NoError(t, err)
	assert.NotEqual(t, binDA, binDB,
		"Builtin computeBuiltinDigest must NOT collide for 2^53 vs 2^53+1 (lossless json.Number round-trip)")

	// Sanity: the External digest and Builtin digest are computed over DIFFERENT
	// domains, so they intentionally differ even for the same example value.
	assert.NotEqual(t, extDA, binDA, "External and Builtin digest domains must be distinct")

	// The 2^53 value IS exactly float64-representable, so it is the unchanged
	// baseline: only the 2^53+1 digest moves from the pre-fix (rounded) value.
	// The NotEqual above already proves the two digests are distinct; no separate
	// baseline assertion is needed.
}

// ---- Stage-2 round-6 blocker (adversarial): a valid External schema carrying a
// large-integer constraint (maxLength = 2^53+1) must pass ValidateDefinition AND
// RegisterExternal. RegisterExternal runs ValidateDefinition -> VerifyDigest ->
// NormalizeDefinition -> post-VerifyDigest; the post-VerifyDigest failed before
// the fix because NormalizeDefinition's detachedJSONValue rounded the constraint to
// float64, changing the digest of the normalized package. With UseNumber the
// constraint is preserved losslessly, so the post-normalize digest still matches.
// ----

func TestRegisterExternal_LargeIntegerSchema_PostNormalizeDigestOK(t *testing.T) {
	const two53P1 = 9007199254740993 // 2^53 + 1, NOT exactly representable as float64

	r := New()
	pkg := makePackage(t, "large-int-schema", 1, "do-thing", "schema with large maxLength")

	// Inject a large-integer constraint (>2^53) onto the "id" property. The
	// existing example "abc" (length 3) still satisfies it, so the schema is valid.
	idProp := pkg.Actions[0].Catalog.InputSchema["properties"].(map[string]any)["id"].(map[string]any)
	idProp["maxLength"] = two53P1 // exact int on a 64-bit host

	// Recompute and record the digest over the modified schema. DefinitionDigest
	// marshals the schema's int losslessly (int -> exact bytes).
	digest, err := spec.DefinitionDigest(pkg)
	require.NoError(t, err)
	pkg.Metadata.Digest = digest
	pkg.Digest = digest

	// ValidateDefinition compiles the schema via the jsonschema library and must
	// accept the large-integer constraint.
	require.NoError(t, spec.ValidateDefinition(pkg),
		"valid large-integer schema must pass ValidateDefinition")

	// RegisterExternal runs the full validate -> verify -> normalize -> post-verify
	// pipeline. Before the fix, the post-VerifyDigest FAILED because NormalizeDefinition
	// rounded maxLength to float64 (2^53), changing the normalized package's digest.
	reg, err := r.RegisterExternal(pkg)
	require.NoError(t, err,
		"RegisterExternal must succeed: post-normalize VerifyDigest must pass (no float64 rounding)")
	assert.Equal(t, External, reg.Kind)
	assert.Equal(t, digest, reg.Definition.Digest,
		"registered digest must match the recorded large-integer-schema digest")

	// The stored Definition preserves the large-integer constraint EXACTLY as a
	// json.Number leaf (no float64 rounding through the canonical tree).
	storedID, ok := reg.Definition.Actions[0].InputSchema["properties"].(map[string]any)["id"].(map[string]any)
	require.True(t, ok, "stored schema must retain the id property map")
	assert.Equal(t, json.Number("9007199254740993"), storedID["maxLength"],
		"stored schema maxLength must be preserved losslessly as json.Number, not rounded to float64")
}
