package registry

import (
	"testing"
	"testing/fstest"

	"github.com/stretchr/testify/require"

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

// makePackage builds a self-consistent, digest-verified in-memory External
// Package with rich nested structure (schemas, headers, scopes, statuses,
// example) so the clone-on-read tests can mutate every kind of nested field.
// Two packages that differ only in `summary` produce DIFFERENT digests (summary
// is Agent-visible catalog and part of the canonical projection), which is what
// the conflict tests rely on.
func makePackage(t *testing.T, domain string, version int, actionName, summary string) spec.Package {
	t.Helper()
	pkg := spec.Package{
		Metadata: spec.Metadata{
			SchemaVersion: 1,
			Domain:        domain,
			Version:       version,
			Name:          "Test " + domain,
			Description:   "Test " + domain + " domain description",
			Endpoint:      spec.Endpoint{BaseURL: "https://api.test.example.com"},
			Auth: spec.Auth{
				Default: "default",
				Profiles: []spec.AuthProfile{
					{ID: "default", Type: "oauth2_bearer"},
				},
			},
		},
		Actions: []spec.Action{{
			Name: actionName,
			File: "actions/" + actionName + ".yaml",
			Auth: &spec.ActionAuthRequirement{
				Profile:        "default",
				RequiredScopes: []string{"scope-a", "scope-b"},
			},
			Catalog: spec.Catalog{
				Summary:     summary,
				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)},
						"tags": map[string]any{"type": "array", "items": map[string]any{"type": "string"}},
					},
				},
				OutputSchema: map[string]any{
					"$schema": "https://json-schema.org/draft/2020-12/schema",
					"type":    "object",
					"properties": map[string]any{
						"id": map[string]any{"type": "string"},
					},
				},
				Example: map[string]any{"id": "abc", "tags": []any{"x", "y"}},
			},
			Binding: spec.Binding{
				Method:        "GET",
				Path:          "/items/{id}",
				StaticHeaders: map[string]string{"Accept": "application/json"},
				Parameters: []spec.Parameter{{
					From: "/id", In: "path", Name: "id",
				}},
				Response: spec.Response{
					Mode:            "passthrough",
					SuccessStatuses: []int{200},
					ContentTypes:    []string{"application/json"},
				},
			},
		}},
	}
	digest, err := spec.DefinitionDigest(pkg)
	require.NoError(t, err)
	pkg.Metadata.Digest = digest
	pkg.Digest = digest
	return pkg
}

// makeBuiltinDefinition builds a COMMON Definition (Agent-visible catalog) with
// NO HTTP binding, NO endpoint, and NO auth profiles — exactly the kind-specific
// shape a code-registered Builtin supplies (spec §5.1). Two definitions that
// differ only in `summary` derive distinct catalog digests, which the
// revision-mismatch and conflict tests rely on.
func makeBuiltinDefinition(t *testing.T, domain string, version int, actionName, summary string) Definition {
	t.Helper()
	return Definition{
		Domain:      domain,
		Version:     version,
		Name:        "Test " + domain,
		Description: "Test " + domain + " builtin description",
		Actions: []DefinitionAction{{
			Name:        actionName,
			Summary:     summary,
			Description: "does one thing in-process",
			Safety:      spec.Safety{Class: "read", Idempotent: true},
			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"},
				},
			},
			OutputSchema: map[string]any{
				"$schema": "https://json-schema.org/draft/2020-12/schema",
				"type":    "object",
				"properties": map[string]any{
					"ok": map[string]any{"type": "boolean"},
				},
			},
			Example: map[string]any{"q": "hello"},
		}},
	}
}

// uniqueSummary returns a distinct, lexicographically varied summary for index i
// so concurrent-conflict definitions derive distinct catalog digests.
func uniqueSummary(i int) string {
	letter := string(rune('A' + i%26))
	return "summary " + letter + "-" + itoa(i/26)
}

func itoa(n int) string {
	if n == 0 {
		return "0"
	}
	var b []byte
	for n > 0 {
		b = append([]byte{byte('0' + n%10)}, b...)
		n /= 10
	}
	return string(b)
}

// encodePackageToMapFS renders pkg into the deterministic on-disk file set via
// spec.EncodeDefinition and roots it under root in a MapFS. Using the real
// encoder guarantees the on-disk bytes and the recorded digest are always
// consistent, so corruption tests can mutate precisely one field and observe a
// clean verify failure.
func encodePackageToMapFS(t *testing.T, root string, pkg spec.Package) fstest.MapFS {
	t.Helper()
	files, err := spec.EncodeDefinition(pkg)
	require.NoError(t, err)
	fsys := fstest.MapFS{}
	for rel, data := range files {
		fsys[root+"/"+rel] = &fstest.MapFile{Data: data}
	}
	return fsys
}

// mergeMapFS merges several rooted MapFS packages into one.
func mergeMapFS(into fstest.MapFS, from fstest.MapFS) {
	for k, v := range from {
		into[k] = v
	}
}

func repeat(s string, n int) string {
	b := make([]byte, 0, len(s)*n)
	for i := 0; i < n; i++ {
		b = append(b, s...)
	}
	return string(b)
}

// replaceDigestLine swaps the "digest:" line in a meta.yaml byte slice. It is
// only used on EncodeDefinition output, which always emits the digest line.
func replaceDigestLine(t *testing.T, data []byte, newDigest string) []byte {
	t.Helper()
	lines := splitLines(data)
	replaced := false
	for i, ln := range lines {
		if isDigestLine(ln) {
			lines[i] = "digest: " + newDigest
			replaced = true
			break
		}
	}
	require.True(t, replaced, "fixture must contain a digest line to replace")
	return joinLines(lines)
}

func removeDigestLine(t *testing.T, data []byte) []byte {
	t.Helper()
	lines := splitLines(data)
	out := lines[:0]
	removed := false
	for _, ln := range lines {
		if !removed && isDigestLine(ln) {
			removed = true
			continue
		}
		out = append(out, ln)
	}
	require.True(t, removed, "fixture must contain a digest line to remove")
	return joinLines(out)
}

// replaceFirst replaces the first occurrence of oldS with newS in data,
// requiring at least one match.
func replaceFirst(t *testing.T, data []byte, oldS, newS string) []byte {
	t.Helper()
	idx := indexOf(data, oldS)
	require.GreaterOrEqual(t, idx, 0, "fixture must contain %q to replace", oldS)
	out := make([]byte, 0, len(data)-len(oldS)+len(newS))
	out = append(out, data[:idx]...)
	out = append(out, newS...)
	out = append(out, data[idx+len(oldS):]...)
	return out
}

func isDigestLine(ln string) bool {
	return len(ln) >= len("digest:") && ln[:len("digest:")] == "digest:"
}

func splitLines(data []byte) []string {
	if len(data) == 0 {
		return nil
	}
	out := []string{}
	start := 0
	for i, b := range data {
		if b == '\n' {
			out = append(out, string(data[start:i]))
			start = i + 1
		}
	}
	if start < len(data) {
		out = append(out, string(data[start:]))
	}
	return out
}

func joinLines(lines []string) []byte {
	if len(lines) == 0 {
		return nil
	}
	total := 0
	for _, ln := range lines {
		total += len(ln) + 1
	}
	out := make([]byte, 0, total)
	for _, ln := range lines {
		out = append(out, ln...)
		out = append(out, '\n')
	}
	return out
}

func indexOf(data []byte, s string) int {
	if len(s) == 0 {
		return 0
	}
	if len(data) < len(s) {
		return -1
	}
	for i := 0; i <= len(data)-len(s); i++ {
		if string(data[i:i+len(s)]) == s {
			return i
		}
	}
	return -1
}
