package registry

import (
	"encoding/json"
	"io/fs"
	"os"
	"reflect"
	"sync"
	"testing"
	"testing/fstest"

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

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

// embeddedFixturesFS returns the real-file embedded fixtures (echo, minimal)
// moved out of the production embed into testdata. These are test-only
// placeholder packages (they target api.example.com); the production embed ships
// no packages until a real, reviewed Definition is ready.
func embeddedFixturesFS(t *testing.T) fs.FS {
	t.Helper()
	fsys, err := fs.Sub(os.DirFS("."), "testdata/extensions")
	require.NoError(t, err)
	return fsys
}

// ---- Bootstrap: embedded load + startup-failure paths (acceptance #t2402-3) ----

func TestRegisterEmbeddedExtensions_NoneDropped(t *testing.T) {
	// Build a MapFS with three valid packages and confirm all register.
	fsys := fstest.MapFS{}
	for _, dom := range []string{"alpha", "beta", "gamma"} {
		pkg := makePackage(t, dom, 1, "do-thing", "s-"+dom)
		mergeMapFS(fsys, encodePackageToMapFS(t, dom, pkg))
	}

	r := New()
	require.NoError(t, RegisterEmbeddedExtensions(r, fsys, "."))
	assert.Equal(t, 3, r.Len(), "every package must be registered, none silently dropped")

	got := map[string]int{}
	for _, e := range r.List() {
		got[e.Definition.Domain] = e.Definition.Version
	}
	assert.Equal(t, map[string]int{"alpha": 1, "beta": 1, "gamma": 1}, got)

	// Embedded OpenAPI-derived HTTP packages are External, NOT Builtin. They
	// carry no code executor.
	for _, e := range r.List() {
		assert.Equal(t, External, e.Kind, "embedded packages register as External")
		_, _, ok := r.BuiltinExecutorFor(e.Definition.Digest)
		assert.False(t, ok, "embedded External packages carry no code executor")
	}
}

func TestRegisterEmbeddedExtensions_StartupFailure_CorruptedDigest(t *testing.T) {
	// Encode a valid package, then replace the recorded digest in meta.yaml with
	// a wrong one. LoadDefinition succeeds (it does not verify), VerifyDigest
	// fails. Bootstrap must FAIL and register NOTHING.
	pkg := makePackage(t, "demo", 1, "do-thing", "right-summary")
	fsys := encodePackageToMapFS(t, "demo", pkg)

	wrongDigest := "sha256:" + repeat("0", 64)
	meta := fsys["demo/meta.yaml"]
	fsys["demo/meta.yaml"] = &fstest.MapFile{Data: replaceDigestLine(t, meta.Data, wrongDigest)}

	r := New()
	err := RegisterEmbeddedExtensions(r, fsys, ".")
	require.Error(t, err, "a bad digest must fail bootstrap (fail-closed)")
	assert.Equal(t, 0, r.Len(), "startup failure must register nothing")

	require.NoError(t, spec.VerifyDigest(pkg))
}

func TestRegisterEmbeddedExtensions_StartupFailure_MissingDigest(t *testing.T) {
	pkg := makePackage(t, "demo", 1, "do-thing", "s")
	fsys := encodePackageToMapFS(t, "demo", pkg)
	fsys["demo/meta.yaml"] = &fstest.MapFile{Data: removeDigestLine(t, fsys["demo/meta.yaml"].Data)}

	r := New()
	err := RegisterEmbeddedExtensions(r, fsys, ".")
	require.Error(t, err)
	assert.Equal(t, 0, r.Len())
}

func TestRegisterEmbeddedExtensions_StartupFailure_CorruptedAction(t *testing.T) {
	// A package whose action file content was altered AFTER the digest was
	// recorded: VerifyDigest recomputes a different digest and fails.
	pkg := makePackage(t, "demo", 1, "do-thing", "s")
	fsys := encodePackageToMapFS(t, "demo", pkg)
	actionKey := "demo/" + pkg.Actions[0].File
	action := fsys[actionKey]
	fsys[actionKey] = &fstest.MapFile{Data: replaceFirst(t, action.Data, "summary: s", "summary: tampered")}

	r := New()
	err := RegisterEmbeddedExtensions(r, fsys, ".")
	require.Error(t, err)
	assert.Equal(t, 0, r.Len())
}

func TestRegisterEmbeddedExtensions_StartupFailure_AbortsBeforeAnyRegister(t *testing.T) {
	// Two packages, one valid and one corrupt: bootstrap must register NEITHER
	// (validate-all-before-commit).
	good := makePackage(t, "good", 1, "do-thing", "good-summary")
	fsys := encodePackageToMapFS(t, "good", good)

	bad := makePackage(t, "bad", 1, "do-thing", "bad-summary")
	badFS := encodePackageToMapFS(t, "bad", bad)
	badFS["bad/meta.yaml"] = &fstest.MapFile{Data: replaceDigestLine(t, badFS["bad/meta.yaml"].Data, "sha256:"+repeat("1", 64))}
	mergeMapFS(fsys, badFS)

	r := New()
	err := RegisterEmbeddedExtensions(r, fsys, ".")
	require.Error(t, err, "any bad package fails the whole bootstrap")
	assert.Equal(t, 0, r.Len(), "validate-all-before-commit means no partial registry")
}

// ---- Blocker #5 (adversarial): empty embed is a SUCCESS no-op ----

func TestRegisterEmbeddedExtensions_EmptyEmbed_Succeeds(t *testing.T) {
	// V1 explicitly allows a production embed that ships zero bundled external
	// packages. An empty embed must NOT fail-closed; the server starts with zero
	// built-in Extensions (real Definitions are curated later).
	r := New()
	err := RegisterEmbeddedExtensions(r, fstest.MapFS{}, ".")
	require.NoError(t, err, "an empty embed is a success no-op in V1")
	assert.Equal(t, 0, r.Len(), "empty embed leaves zero built-ins")

	// The production embed (README only) also succeeds with zero packages.
	r2 := New()
	err = RegisterEmbeddedExtensions(r2, extensionFSForTest(t), ".")
	require.NoError(t, err)
	assert.Equal(t, 0, r2.Len())
}

// productionEmbeddedFS returns the real production embed (presets/assets/
// extensions), which currently ships a README only (zero packages).
func extensionFSForTest(t *testing.T) fstest.MapFS {
	t.Helper()
	// Simulate the production README-only embed: a non-package file and no
	// package directories. LoadDefinitions skips non-directory entries, so this
	// yields zero packages.
	return fstest.MapFS{
		"README.md": &fstest.MapFile{Data: []byte("# no bundled extensions yet\n")},
	}
}

func TestRegisterEmbeddedExtensions_Idempotent(t *testing.T) {
	pkg := makePackage(t, "demo", 1, "do-thing", "s")
	fsys := encodePackageToMapFS(t, "demo", pkg)

	r := New()
	require.NoError(t, RegisterEmbeddedExtensions(r, fsys, "."))
	require.Equal(t, 1, r.Len())
	require.NoError(t, RegisterEmbeddedExtensions(r, fsys, "."))
	assert.Equal(t, 1, r.Len(), "second bootstrap is idempotent")
}

func TestRegisterEmbeddedExtensions_NilGuards(t *testing.T) {
	assert.Error(t, RegisterEmbeddedExtensions(nil, fstest.MapFS{}, "."))
	assert.Error(t, RegisterEmbeddedExtensions(New(), nil, "."))
}

// ---- Atomic batch (blocker #2): pre-seeded conflict leaves registry unchanged ----

func TestRegisterEmbeddedExtensions_PreseededConflict_LeavesRegistryUnchanged(t *testing.T) {
	echoPkg := makePackage(t, "echo", 1, "do-thing", "echo-summary")
	minimalPkg := makePackage(t, "minimal", 1, "do-thing", "minimal-summary")

	fsys := fstest.MapFS{}
	mergeMapFS(fsys, encodePackageToMapFS(t, "echo", echoPkg))
	mergeMapFS(fsys, encodePackageToMapFS(t, "minimal", minimalPkg))

	conflictingMinimal := makePackage(t, "minimal", 1, "do-thing", "DIFFERENT-summary")
	require.NotEqual(t, conflictingMinimal.Digest, minimalPkg.Digest)
	r := New()
	_, err := r.RegisterExternal(conflictingMinimal)
	require.NoError(t, err)
	require.Equal(t, 1, r.Len())

	err = RegisterEmbeddedExtensions(r, fsys, ".")
	require.Error(t, err, "a pre-seeded conflict must fail the whole batch")
	var ce *ConflictError
	require.ErrorAs(t, err, &ce)
	assert.Equal(t, "minimal", ce.Domain)

	assert.Equal(t, 1, r.Len(), "atomic batch must not grow the registry on failure")

	_, ok := r.LookupByIdentity("echo", 1)
	assert.False(t, ok, "echo must not be registered when the batch failed atomically")
	_, ok = r.LookupByDigest(minimalPkg.Digest)
	assert.False(t, ok, "the rejected batch minimal must not be reachable")
	got, ok := r.LookupByIdentity("minimal", 1)
	require.True(t, ok)
	assert.Equal(t, conflictingMinimal.Digest, got.Definition.Digest)
}

func TestRegisterEmbeddedExtensions_IntraBatchConflict_CommitsNothing(t *testing.T) {
	pkgA := makePackage(t, "shared", 1, "do", "summary-A")
	pkgB := makePackage(t, "shared", 1, "do", "summary-B")
	require.NotEqual(t, pkgA.Digest, pkgB.Digest)

	fsys := fstest.MapFS{}
	mergeMapFS(fsys, encodePackageToMapFS(t, "dir-a", pkgA))
	mergeMapFS(fsys, encodePackageToMapFS(t, "dir-b", pkgB))

	r := New()
	err := RegisterEmbeddedExtensions(r, fsys, ".")
	require.Error(t, err, "intra-batch conflict must fail the whole batch")
	assert.Equal(t, 0, r.Len(), "intra-batch conflict must commit nothing")
}

func TestRegisterEmbeddedExtensions_ConcurrentWithRegister_NoPartialState(t *testing.T) {
	echoPkg := makePackage(t, "echo", 1, "do", "echo")
	minimalPkg := makePackage(t, "minimal", 1, "do", "minimal")
	conflictingMinimal := makePackage(t, "minimal", 1, "do", "DIFFERENT")
	gammaPkg := makePackage(t, "gamma", 1, "do", "gamma")

	fsys := fstest.MapFS{}
	mergeMapFS(fsys, encodePackageToMapFS(t, "echo", echoPkg))
	mergeMapFS(fsys, encodePackageToMapFS(t, "minimal", minimalPkg))

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

	var wg sync.WaitGroup
	const batches, registers = 8, 8
	start := make(chan struct{})
	for i := 0; i < batches; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			<-start
			_ = RegisterEmbeddedExtensions(r, fsys, ".") // always fails on minimal
		}()
	}
	for i := 0; i < registers; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			<-start
			_, _ = r.RegisterExternal(gammaPkg) // idempotent success
		}()
	}
	close(start)
	wg.Wait()

	_, ok := r.LookupByIdentity("echo", 1)
	assert.False(t, ok, "echo must never be registered by a failing batch")
	_, ok = r.LookupByDigest(minimalPkg.Digest)
	assert.False(t, ok)
	gotGamma, ok := r.LookupByIdentity("gamma", 1)
	require.True(t, ok)
	assert.Equal(t, gammaPkg.Digest, gotGamma.Definition.Digest)
	list := r.List()
	for _, e := range list {
		byDigest, okD := r.LookupByDigest(e.Definition.Digest)
		require.True(t, okD)
		assert.Equal(t, e.Definition.Digest, byDigest.Definition.Digest)
	}
	assert.Equal(t, 2, r.Len())
}

// ---- Real fixtures: load, verify, and register through the Package contract ----

func TestRegisterEmbeddedExtensions_RealFixtures(t *testing.T) {
	fsys := embeddedFixturesFS(t)
	r := New()
	require.NoError(t, RegisterEmbeddedExtensions(r, fsys, "."))

	// Load the fixture packages independently so we can compare the registered
	// digests against freshly-loaded ones (non-tautological: not x==x).
	loaded, err := spec.LoadDefinitions(fsys, ".")
	require.NoError(t, err)
	wantDigest := make(map[string]string, len(loaded))
	for _, p := range loaded {
		wantDigest[p.Metadata.Domain] = p.Digest
	}

	list := r.List()
	require.GreaterOrEqual(t, len(list), 2, "expected at least the fixtures (echo, minimal)")

	domains := map[string]bool{}
	for _, e := range list {
		assert.Equal(t, External, e.Kind, "fixtures register as External")
		domains[e.Definition.Domain] = true
		assert.Equal(t, wantDigest[e.Definition.Domain], e.Definition.Digest,
			"registered digest must match the freshly-loaded fixture digest")
		assert.NotEmpty(t, e.Definition.Digest)
		assert.True(t, len(e.Definition.Digest) > len("sha256:"), "digest looks like a sha256")
	}
	assert.True(t, domains["minimal"], "minimal fixture must be registered")
	assert.True(t, domains["echo"], "echo fixture must be registered")
}

func TestRegisterEmbeddedExtensions_RealFixtures_PassPackageValidation(t *testing.T) {
	// Every Package must pass the same load, validation, normalization, and
	// digest verification contract used by Registry bootstrap.
	fsys := embeddedFixturesFS(t)
	packages, err := spec.LoadDefinitions(fsys, ".")
	require.NoError(t, err)
	require.NotEmpty(t, packages)
	for _, pkg := range packages {
		normalized, err := spec.ValidateAndNormalizeDefinition(pkg)
		require.NoError(t, err, "fixture %q must satisfy the Package Spec", pkg.Metadata.Domain)
		require.NoError(t, spec.VerifyDigest(normalized),
			"fixture %q must retain its Definition digest after normalization", pkg.Metadata.Domain)
	}
}

// ---- Thread-safety: concurrent reads after startup registration ----

func TestRegistry_ConcurrentReadsSafe(t *testing.T) {
	r := New()
	for _, dom := range []string{"a", "b", "c", "d"} {
		pkg := makePackage(t, dom, 1, "act", "summary "+dom)
		_, err := r.RegisterExternal(pkg)
		require.NoError(t, err)
	}

	var wg sync.WaitGroup
	for i := 0; i < 32; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			_ = r.List()
			_ = r.Len()
			for _, dom := range []string{"a", "b", "c", "d"} {
				got, ok := r.LookupByIdentity(dom, 1)
				if !ok {
					t.Errorf("concurrent lookup miss for %s", dom)
					return
				}
				got.Definition.Actions[0].Summary = "concurrent-mutation"
				got.Definition.Actions[0].OutputSchema["type"] = "x"
				got.Binding.Actions[0].Binding.Method = "DELETE"
				_ = got
			}
			_, _ = r.LookupByDigest("sha256:unknown-" + itoa(i))
		}(i)
	}
	wg.Wait()
	// Under -race this test fails if any read/clone shares mutable state.
}

// ---- Blocker #1 (adversarial): embedded path and direct path share ONE
// preparation route, so reordered sets → same digest → byte-identical stored
// Binding. Previously registerEmbeddedBatch built straight from LoadDefinitions
// output and BYPASSED NormalizeDefinition, so two packages differing only in
// auth-profile/scope/status/content-type order shared a digest but produced
// different serialized stored Bindings (and differed from a direct
// RegisterExternal of the same content). ----

// parityPackage builds a valid, digest-verified External Package with rich
// unordered-set content (two auth profiles, two scopes, two statuses, two
// content types). When reversed=false the sets are in sorted order; when
// reversed=true every order-independent set is reversed. Both forms have the
// SAME semantic content and therefore the SAME DefinitionDigest.
func parityPackage(t *testing.T, reversed bool) spec.Package {
	t.Helper()
	profiles := []spec.AuthProfile{
		{ID: "alpha", Type: "oauth2_bearer"},
		{ID: "beta", Type: "api_key", In: "header", Name: "X-Beta"},
	}
	scopes := []string{"scope-a", "scope-b"}
	statuses := []int{200, 201}
	contentTypes := []string{"application/json", "text/plain"}
	if reversed {
		profiles[0], profiles[1] = profiles[1], profiles[0]
		scopes[0], scopes[1] = scopes[1], scopes[0]
		statuses[0], statuses[1] = statuses[1], statuses[0]
		contentTypes[0], contentTypes[1] = contentTypes[1], contentTypes[0]
	}
	pkg := spec.Package{
		Metadata: spec.Metadata{
			SchemaVersion: 1,
			Domain:        "parity",
			Version:       1,
			Name:          "Parity",
			Description:   "parity description",
			Endpoint:      spec.Endpoint{BaseURL: "https://api.parity.example.com"},
			Auth: spec.Auth{
				Default:  "alpha",
				Profiles: profiles,
			},
		},
		Actions: []spec.Action{{
			Name: "do-thing",
			File: "actions/do-thing.yaml",
			Auth: &spec.ActionAuthRequirement{Profile: "alpha", RequiredScopes: scopes},
			Catalog: spec.Catalog{
				Summary:     "do",
				Description: "does one thing",
				Safety:      spec.Safety{Class: "read", Idempotent: true},
				InputSchema: map[string]any{
					"$schema":              "https://json-schema.org/draft/2020-12/schema",
					"type":                 "object",
					"additionalProperties": false,
					"required":             []any{"id"},
					"properties": map[string]any{
						"id": map[string]any{"type": "string", "minLength": float64(1)},
					},
				},
				Example: map[string]any{"id": "abc"},
			},
			Binding: spec.Binding{
				Method: "GET",
				Path:   "/items/{id}",
				Parameters: []spec.Parameter{{
					From: "/id", In: "path", Name: "id",
				}},
				Response: spec.Response{
					Mode:            "passthrough",
					SuccessStatuses: statuses,
					ContentTypes:    contentTypes,
				},
			},
		}},
	}
	digest, err := spec.DefinitionDigest(pkg)
	require.NoError(t, err)
	pkg.Metadata.Digest = digest
	pkg.Digest = digest
	return pkg
}

func TestRegisterEmbeddedExtensions_NormalizedStorage_ByteIdenticalBinding(t *testing.T) {
	pkgSorted := parityPackage(t, false)
	pkgReordered := parityPackage(t, true)

	// Precondition: the two forms differ ONLY in set ordering, so they share a
	// digest (the semantic projection sorts every order-independent
	// set).
	require.Equal(t, pkgSorted.Digest, pkgReordered.Digest,
		"precondition: reordered sets must share a digest")

	// Two embedded packages (reordered sets) registered via the embedded batch
	// path in separate registries must produce byte-identical stored Bindings.
	// registerEmbeddedBatch is the storage path RegisterEmbeddedExtensions
	// delegates to; it previously bypassed NormalizeDefinition.
	rA := New()
	require.NoError(t, rA.registerEmbeddedBatch([]spec.Package{pkgSorted}))
	rB := New()
	require.NoError(t, rB.registerEmbeddedBatch([]spec.Package{pkgReordered}))

	gotA, ok := rA.LookupByDigest(pkgSorted.Digest)
	require.True(t, ok)
	gotB, ok := rB.LookupByDigest(pkgReordered.Digest)
	require.True(t, ok)

	bytesA, err := json.Marshal(gotA.Binding)
	require.NoError(t, err)
	bytesB, err := json.Marshal(gotB.Binding)
	require.NoError(t, err)
	assert.Equal(t, string(bytesA), string(bytesB),
		"two embedded packages with reordered sets must serialize to a byte-identical stored Binding")
	assert.True(t, reflect.DeepEqual(gotA.Binding, gotB.Binding),
		"two embedded packages with reordered sets must store structurally identical Bindings")

	// Parity: the embedded path and a direct RegisterExternal of the same
	// (reordered) content must also converge to a byte-identical stored Binding.
	// This is the core fix — both paths route through ValidateAndNormalizeDefinition.
	rC := New()
	_, err = rC.RegisterExternal(pkgReordered)
	require.NoError(t, err)
	gotC, ok := rC.LookupByDigest(pkgReordered.Digest)
	require.True(t, ok)
	bytesC, err := json.Marshal(gotC.Binding)
	require.NoError(t, err)
	assert.Equal(t, string(bytesA), string(bytesC),
		"embedded path and direct RegisterExternal must produce byte-identical stored Bindings for the same semantic content")
}
