package mcpsync

import (
	"encoding/json"
	"os"
	"path/filepath"
	"regexp"
	"testing"

	"github.com/mvanhorn/cli-printing-press/v4/internal/generator"
	"github.com/mvanhorn/cli-printing-press/v4/internal/pipeline"
	"github.com/mvanhorn/cli-printing-press/v4/internal/spec"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gopkg.in/yaml.v3"
)

func TestSyncBackfillsEndpointAnnotations(t *testing.T) {
	t.Parallel()

	apiSpec := &spec.APISpec{
		Name:    "synctest",
		Version: "0.1.0",
		BaseURL: "https://api.example.com",
		Auth: spec.AuthConfig{
			Type:    "api_key",
			Header:  "Authorization",
			Format:  "Bearer {token}",
			EnvVars: []string{"SYNCTEST_TOKEN"},
		},
		Config: spec.ConfigSpec{
			Format: "toml",
			Path:   "~/.config/synctest-pp-cli/config.toml",
		},
		Resources: map[string]spec.Resource{
			"projects": {
				Description: "Manage projects",
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/projects", Description: "List projects"},
					"get":  {Method: "GET", Path: "/projects/{id}", Description: "Get a project"},
				},
			},
		},
	}

	cliDir := filepath.Join(t.TempDir(), "synctest")
	gen := generator.New(apiSpec, cliDir)
	require.NoError(t, gen.Generate())

	specData, err := yaml.Marshal(apiSpec)
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(filepath.Join(cliDir, "spec.yaml"), specData, 0o644))

	endpointPath := filepath.Join(cliDir, "internal", "cli", "projects_list.go")
	endpointData, err := os.ReadFile(endpointPath)
	require.NoError(t, err)
	withoutAnnotation := endpointAnnotationLine.ReplaceAllString(string(endpointData), "")
	require.NotContains(t, withoutAnnotation, `"pp:endpoint"`)
	require.NoError(t, os.WriteFile(endpointPath, []byte(withoutAnnotation), 0o644))

	oldTools := `// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
package mcp

func RegisterNovelFeatureTools() { shellOutToCLI("projects list") }
`
	require.NoError(t, os.WriteFile(filepath.Join(cliDir, "internal", "mcp", "tools.go"), []byte(oldTools), 0o644))

	result, err := Sync(cliDir, Options{})
	require.NoError(t, err)
	assert.True(t, result.Changed)

	updatedEndpoint, err := os.ReadFile(endpointPath)
	require.NoError(t, err)
	assert.Contains(t, string(updatedEndpoint), `Annotations: map[string]string{"pp:endpoint": "projects.list", "pp:method": "GET", "pp:path": "/projects", "mcp:read-only": "true"}`)

	updatedTools, err := os.ReadFile(filepath.Join(cliDir, "internal", "mcp", "tools.go"))
	require.NoError(t, err)
	assert.Contains(t, string(updatedTools), "cobratree.RegisterAll")
}

func TestEnsureEndpointAnnotationUpgradesStaleEndpointAnnotation(t *testing.T) {
	t.Parallel()

	path := filepath.Join(t.TempDir(), "api_keys_delete.go")
	oldAnnotation := `Annotations: map[string]string{"pp:endpoint": "api-keys.delete"}`
	src := `// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
package cli

func newAPIKeysDeleteCmd() {
	cmd := struct {
		Use         string
		Annotations map[string]string
	}{
		Use: "delete",
		` + oldAnnotation + `,
	}
	_ = cmd
}
`
	require.NoError(t, os.WriteFile(path, []byte(src), 0o644))

	newAnnotation := `		Annotations: map[string]string{"pp:endpoint": "api-keys.delete", "pp:method": "DELETE", "pp:path": "/api-keys/{id}"},`
	require.NoError(t, ensureEndpointAnnotation(path, newAnnotation))

	updated, err := os.ReadFile(path)
	require.NoError(t, err)
	assert.Contains(t, string(updated), newAnnotation)
	assert.NotContains(t, string(updated), oldAnnotation)
}

func TestEnsureRootCmdExportPreservesRootFlagPointer(t *testing.T) {
	t.Parallel()

	cliDir := t.TempDir()
	require.NoError(t, os.MkdirAll(filepath.Join(cliDir, "internal", "cli"), 0o755))
	rootPath := filepath.Join(cliDir, "internal", "cli", "root.go")
	require.NoError(t, os.WriteFile(rootPath, []byte(`// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
package cli

import "github.com/spf13/cobra"

type rootFlags struct{}

// Execute runs the CLI in non-interactive mode: never prompts, all values via flags or stdin.
func Execute() error {
	var flags rootFlags

	rootCmd := &cobra.Command{
		Use: "synctest-pp-cli",
	}
	rootCmd.AddCommand(newDoctorCmd(&flags))

	err := rootCmd.Execute()
	return err
}

func newDoctorCmd(flags *rootFlags) *cobra.Command {
	return &cobra.Command{Use: "doctor"}
}

func ExitCode(err error) int { return 0 }
`), 0o644))

	require.NoError(t, ensureRootCmdExport(cliDir))

	updated, err := os.ReadFile(rootPath)
	require.NoError(t, err)
	src := string(updated)
	assert.Contains(t, src, "return newRootCmd(&flags)")
	assert.Contains(t, src, "rootCmd := newRootCmd(&flags)")
	assert.Contains(t, src, "rootCmd.AddCommand(newDoctorCmd(flags))")
	assert.NotContains(t, src, "newRootCmd(flags)")
}

// TestSyncRegeneratesStaleCliutil locks the contract that mcp-sync
// regenerates the cliutil package alongside MCP source. Older library
// CLIs ship cliutil packages that predate helpers the new MCP template
// uses (SanitizeErrorBody, LooksLikeAuthError); without regeneration
// the regenerated tools.go fails to build with "undefined:
// cliutil.SanitizeErrorBody". This test simulates that drift by
// stubbing cliutil/text.go with a minimal file lacking
// SanitizeErrorBody, runs Sync, and asserts the regenerated file
// carries the helper.
func TestSyncRegeneratesStaleCliutil(t *testing.T) {
	t.Parallel()

	apiSpec := &spec.APISpec{
		Name:    "cliutiltest",
		Version: "0.1.0",
		BaseURL: "https://api.example.com",
		Auth: spec.AuthConfig{
			Type:    "api_key",
			Header:  "Authorization",
			Format:  "Bearer {token}",
			EnvVars: []string{"CLIUTILTEST_TOKEN"},
		},
		Config: spec.ConfigSpec{
			Format: "toml",
			Path:   "~/.config/cliutiltest-pp-cli/config.toml",
		},
		Resources: map[string]spec.Resource{
			"items": {
				Description: "Manage items",
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/items", Description: "List items"},
				},
			},
		},
	}

	cliDir := filepath.Join(t.TempDir(), "cliutiltest")
	gen := generator.New(apiSpec, cliDir)
	require.NoError(t, gen.Generate())

	specData, err := yaml.Marshal(apiSpec)
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(filepath.Join(cliDir, "spec.yaml"), specData, 0o644))

	// Stub cliutil/text.go with the marker but without SanitizeErrorBody.
	// Mirrors the actual drift pattern in open-meteo: the file is generator-
	// emitted, just from an older template version that didn't ship the
	// helper yet.
	textPath := filepath.Join(cliDir, "internal", "cliutil", "text.go")
	staleText := `// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
package cliutil

// CleanText is the only helper this older template emitted.
func CleanText(s string) string { return s }
`
	require.NoError(t, os.WriteFile(textPath, []byte(staleText), 0o644))

	// Stub tools.go so Sync detects MCPSurfaceStaticList state and runs
	// the migration path (which calls GenerateMCPSurface).
	oldTools := `// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
package mcp

func RegisterNovelFeatureTools() { shellOutToCLI("items list") }
`
	require.NoError(t, os.WriteFile(filepath.Join(cliDir, "internal", "mcp", "tools.go"), []byte(oldTools), 0o644))

	result, err := Sync(cliDir, Options{})
	require.NoError(t, err)
	assert.True(t, result.Changed)

	regenerated, err := os.ReadFile(textPath)
	require.NoError(t, err)
	src := string(regenerated)
	assert.Contains(t, src, "SanitizeErrorBody", "cliutil/text.go should be regenerated with SanitizeErrorBody")
	assert.NotContains(t, src, "// CleanText is the only helper this older template emitted.", "stale stub content should be replaced")
}

// TestSyncRefreshesProvenanceFromSpec locks the contract that mcp-sync
// updates .printing-press.json's spec-derived fields from the current
// spec.yaml. Pre-fix, mcp-sync wrote manifest.json from stale provenance,
// so spec.yaml updates to auth.key_url, auth.optional, auth.env_vars,
// etc. never reached the MCPB Configure modal until someone hand-injected
// them into .printing-press.json. Bit recipe-goat twice in one session.
func TestSyncRefreshesProvenanceFromSpec(t *testing.T) {
	t.Parallel()

	// Step 1: generate the CLI with an initial spec (no key_url, not optional).
	apiSpec := &spec.APISpec{
		Name:    "provrefresh",
		Version: "0.1.0",
		BaseURL: "https://api.example.com",
		Auth: spec.AuthConfig{
			Type:    "api_key",
			Header:  "Authorization",
			Format:  "Bearer {token}",
			EnvVars: []string{"PROVREFRESH_TOKEN"},
		},
		Config: spec.ConfigSpec{
			Format: "toml",
			Path:   "~/.config/provrefresh-pp-cli/config.toml",
		},
		Resources: map[string]spec.Resource{
			"items": {
				Description: "Manage items",
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/items", Description: "List items"},
				},
			},
		},
	}
	cliDir := filepath.Join(t.TempDir(), "provrefresh")
	gen := generator.New(apiSpec, cliDir)
	require.NoError(t, gen.Generate())

	// Step 2: simulate a generate-time provenance write. WriteManifestForGenerate
	// is what cli/generate.go calls; the test exercises the same path.
	require.NoError(t, pipeline.WriteManifestForGenerate(pipeline.GenerateManifestParams{
		APIName:   apiSpec.Name,
		OutputDir: cliDir,
		Spec:      apiSpec,
	}))

	// Step 3: edit spec.yaml with the new auth.key_url + auth.optional that
	// the original spec lacked. This is the field-update scenario that
	// breaks without provenance refresh.
	apiSpec.Auth.KeyURL = "https://example.com/get-a-token"
	apiSpec.Auth.Optional = true
	specData, err := yaml.Marshal(apiSpec)
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(filepath.Join(cliDir, "spec.yaml"), specData, 0o644))

	// Step 4: run mcp-sync. It should pick up the new spec values and
	// refresh .printing-press.json + manifest.json.
	result, err := Sync(cliDir, Options{})
	require.NoError(t, err)
	assert.True(t, result.Changed)

	// Step 5: verify .printing-press.json now reflects the new spec.
	provData, err := os.ReadFile(filepath.Join(cliDir, pipeline.CLIManifestFilename))
	require.NoError(t, err)
	var prov pipeline.CLIManifest
	require.NoError(t, json.Unmarshal(provData, &prov))
	assert.Equal(t, "https://example.com/get-a-token", prov.AuthKeyURL, "auth_key_url must refresh from spec.yaml on mcp-sync")
	assert.True(t, prov.AuthOptional, "auth_optional must refresh from spec.yaml on mcp-sync")

	// Step 6: verify the MCPB manifest also reflects the new spec — this
	// is the user-visible failure mode (Configure modal wording / Required
	// flag). Confirms the full chain spec.yaml → .printing-press.json →
	// manifest.json works end-to-end.
	manifestData, err := os.ReadFile(filepath.Join(cliDir, pipeline.MCPBManifestFilename))
	require.NoError(t, err)
	manifestStr := string(manifestData)
	assert.Contains(t, manifestStr, "https://example.com/get-a-token", "manifest description should surface key_url after refresh")
	assert.Contains(t, manifestStr, `"description": "Optional. Sets`, "manifest description should reflect auth_optional with the Optional. prefix")
}

// TestValidateSpecNameMatchesDirAccepts ensures matching name and dir
// pass the preflight without error. The dir basename is the source of
// truth for the package identity (it appears in go.mod, in directory
// listings, in the published library URL); spec.yaml.name should track it.
func TestValidateSpecNameMatchesDirAccepts(t *testing.T) {
	t.Parallel()
	cliDir := filepath.Join(t.TempDir(), "weather-goat")
	require.NoError(t, os.MkdirAll(cliDir, 0o755))
	parsed := &spec.APISpec{Name: "weather-goat"}
	if err := validateSpecNameMatchesDir(cliDir, parsed); err != nil {
		t.Errorf("matching name should accept; got %v", err)
	}
}

// TestValidateSpecNameMatchesDirRejectsDrift catches the weather-goat
// class of bug: spec.yaml.name = "weather" but directory is
// "weather-goat" because an old rename never updated the spec.
func TestValidateSpecNameMatchesDirRejectsDrift(t *testing.T) {
	t.Parallel()
	cliDir := filepath.Join(t.TempDir(), "weather-goat")
	require.NoError(t, os.MkdirAll(cliDir, 0o755))
	parsed := &spec.APISpec{Name: "weather"}
	err := validateSpecNameMatchesDir(cliDir, parsed)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "spec.yaml name \"weather\"")
	assert.Contains(t, err.Error(), "directory-derived slug \"weather-goat\"")
	assert.Contains(t, err.Error(), "--force")
}

// TestValidateSpecNameMatchesDirNoOpOnEmptySpec — a nil spec or empty
// name skips validation. The downstream loadArchivedSpec already errors
// on missing spec; this layer doesn't need to redundantly check.
func TestValidateSpecNameMatchesDirNoOpOnEmptySpec(t *testing.T) {
	t.Parallel()
	cliDir := filepath.Join(t.TempDir(), "anything")
	require.NoError(t, os.MkdirAll(cliDir, 0o755))
	if err := validateSpecNameMatchesDir(cliDir, nil); err != nil {
		t.Errorf("nil spec should not error; got %v", err)
	}
	if err := validateSpecNameMatchesDir(cliDir, &spec.APISpec{}); err != nil {
		t.Errorf("empty name should not error; got %v", err)
	}
}

// TestReconcileSpecNameWithDirAutoFixesInternalYAML — the weather-goat
// case. Internal YAML spec with `name: weather` in a `weather-goat/`
// directory gets auto-corrected: spec.yaml is rewritten and the parsed
// struct's Name is updated in-place so downstream generation uses the
// corrected name.
func TestReconcileSpecNameWithDirAutoFixesInternalYAML(t *testing.T) {
	t.Parallel()
	cliDir := filepath.Join(t.TempDir(), "weather-goat")
	require.NoError(t, os.MkdirAll(cliDir, 0o755))
	specPath := filepath.Join(cliDir, "spec.yaml")
	original := "name: weather\ndescription: \"Weather GOAT\"\nversion: \"1.0.0\"\nbase_url: \"https://api.example.com\"\n"
	require.NoError(t, os.WriteFile(specPath, []byte(original), 0o644))

	parsed := &spec.APISpec{Name: "weather"}
	renamedFrom, err := reconcileSpecNameWithDir(cliDir, parsed)
	require.NoError(t, err)
	assert.Equal(t, "weather", renamedFrom, "should report the prior name")
	assert.Equal(t, "weather-goat", parsed.Name, "parsed.Name should be corrected in place")

	// File on disk should have the corrected name; the rest of the
	// content (description, version, base_url) should be untouched.
	updated, err := os.ReadFile(specPath)
	require.NoError(t, err)
	assert.Contains(t, string(updated), "name: weather-goat\n")
	assert.NotContains(t, string(updated), "name: weather\n",
		"the unanchored 'name: weather' line should be gone")
	assert.Contains(t, string(updated), `description: "Weather GOAT"`)
	assert.Contains(t, string(updated), `base_url: "https://api.example.com"`)
}

// TestReconcileSpecNameWithDirSkipsOpenAPI — for OpenAPI specs the name
// derives from info.title and rewriting it is invasive. Auto-fix should
// not touch the file; the validator's --force-required error should
// surface unchanged.
func TestReconcileSpecNameWithDirSkipsOpenAPI(t *testing.T) {
	t.Parallel()
	cliDir := filepath.Join(t.TempDir(), "newname")
	require.NoError(t, os.MkdirAll(cliDir, 0o755))
	specPath := filepath.Join(cliDir, "spec.yaml")
	original := "openapi: 3.0.0\ninfo:\n  title: Old Name\n  version: 1.0.0\npaths: {}\n"
	require.NoError(t, os.WriteFile(specPath, []byte(original), 0o644))

	parsed := &spec.APISpec{Name: "old-name"}
	renamedFrom, err := reconcileSpecNameWithDir(cliDir, parsed)
	require.Error(t, err, "OpenAPI spec must surface the validator error rather than be auto-fixed")
	assert.Equal(t, "", renamedFrom)
	assert.Equal(t, "old-name", parsed.Name, "parsed.Name must not change for OpenAPI specs")
	assert.Contains(t, err.Error(), "--force")

	// File on disk must be untouched.
	after, err := os.ReadFile(specPath)
	require.NoError(t, err)
	assert.Equal(t, original, string(after), "OpenAPI spec must not be rewritten")
}

// TestReconcileSpecNameWithDirNoOpWhenAligned — no drift, no work.
func TestReconcileSpecNameWithDirNoOpWhenAligned(t *testing.T) {
	t.Parallel()
	cliDir := filepath.Join(t.TempDir(), "weather-goat")
	require.NoError(t, os.MkdirAll(cliDir, 0o755))
	parsed := &spec.APISpec{Name: "weather-goat"}
	renamedFrom, err := reconcileSpecNameWithDir(cliDir, parsed)
	require.NoError(t, err)
	assert.Equal(t, "", renamedFrom, "no rename should be reported when names already match")
}

// TestReconcileSpecNameWithDirNoYAMLFallsThroughToValidator — when no
// internal YAML spec exists (e.g., spec.json or schema.graphql), the
// auto-fix path is skipped and the validator's error surfaces.
func TestReconcileSpecNameWithDirNoYAMLFallsThroughToValidator(t *testing.T) {
	t.Parallel()
	cliDir := filepath.Join(t.TempDir(), "weather-goat")
	require.NoError(t, os.MkdirAll(cliDir, 0o755))
	// Only a non-YAML spec exists.
	require.NoError(t, os.WriteFile(filepath.Join(cliDir, "spec.json"), []byte(`{"name":"weather"}`), 0o644))

	parsed := &spec.APISpec{Name: "weather"}
	renamedFrom, err := reconcileSpecNameWithDir(cliDir, parsed)
	require.Error(t, err)
	assert.Equal(t, "", renamedFrom)
	assert.Equal(t, "weather", parsed.Name, "parsed.Name unchanged when auto-fix declines")
	assert.Contains(t, err.Error(), "--force")
}

// TestValidateSpecNameMatchesDirAcceptsSuffixedDir — working dirs and
// public-library checkouts both use the suffixed form `<slug>-pp-cli/`,
// while spec.yaml.name holds the bare slug. The validator must reduce
// the basename to its slug form before comparing.
func TestValidateSpecNameMatchesDirAcceptsSuffixedDir(t *testing.T) {
	t.Parallel()
	cliDir := filepath.Join(t.TempDir(), "producthunt-pp-cli")
	require.NoError(t, os.MkdirAll(cliDir, 0o755))
	parsed := &spec.APISpec{Name: "producthunt"}
	if err := validateSpecNameMatchesDir(cliDir, parsed); err != nil {
		t.Errorf("suffixed dir matching its slug should accept; got %v", err)
	}
}

// TestValidateSpecNameMatchesDirAcceptsLegacySuffixedDir — older library
// CLIs predating -pp-cli use the bare -cli suffix. naming.TrimCLISuffix
// handles both forms; this locks in coverage for the legacy shape.
func TestValidateSpecNameMatchesDirAcceptsLegacySuffixedDir(t *testing.T) {
	t.Parallel()
	cliDir := filepath.Join(t.TempDir(), "weather-cli")
	require.NoError(t, os.MkdirAll(cliDir, 0o755))
	parsed := &spec.APISpec{Name: "weather"}
	if err := validateSpecNameMatchesDir(cliDir, parsed); err != nil {
		t.Errorf("legacy -cli suffix should reduce to the slug and accept; got %v", err)
	}
}

// TestReconcileSpecNameWithDirNoOpOnSuffixedDir — exercises the
// reconcile path: spec.yaml must not be rewritten and parsed.Name must
// not be mutated when the directory is the suffixed form of the slug.
func TestReconcileSpecNameWithDirNoOpOnSuffixedDir(t *testing.T) {
	t.Parallel()
	cliDir := filepath.Join(t.TempDir(), "producthunt-pp-cli")
	require.NoError(t, os.MkdirAll(cliDir, 0o755))
	specPath := filepath.Join(cliDir, "spec.yaml")
	original := "name: producthunt\ndescription: \"Product Hunt API\"\nversion: \"1.0.0\"\nbase_url: \"https://api.producthunt.com\"\n"
	require.NoError(t, os.WriteFile(specPath, []byte(original), 0o644))

	parsed := &spec.APISpec{Name: "producthunt"}
	renamedFrom, err := reconcileSpecNameWithDir(cliDir, parsed)
	require.NoError(t, err)
	assert.Equal(t, "", renamedFrom, "no rename should fire when the suffixed basename strips to the slug")
	assert.Equal(t, "producthunt", parsed.Name, "parsed.Name must not be mutated to the suffixed form")

	after, err := os.ReadFile(specPath)
	require.NoError(t, err)
	assert.Equal(t, original, string(after), "spec.yaml must be byte-identical when no drift exists")
}

// TestReconcileSpecNameWithDirRejectsDriftEvenOnSuffixedDir — stripping
// the CLI suffix must not mask legitimate drift. dir = weather-goat-pp-cli
// reduces to slug "weather-goat", which still mismatches spec.Name "weather".
func TestReconcileSpecNameWithDirRejectsDriftEvenOnSuffixedDir(t *testing.T) {
	t.Parallel()
	cliDir := filepath.Join(t.TempDir(), "weather-goat-pp-cli")
	require.NoError(t, os.MkdirAll(cliDir, 0o755))
	specPath := filepath.Join(cliDir, "spec.yaml")
	original := "name: weather\ndescription: \"Weather GOAT\"\nversion: \"1.0.0\"\nbase_url: \"https://api.example.com\"\n"
	require.NoError(t, os.WriteFile(specPath, []byte(original), 0o644))

	parsed := &spec.APISpec{Name: "weather"}
	renamedFrom, err := reconcileSpecNameWithDir(cliDir, parsed)
	require.NoError(t, err)
	assert.Equal(t, "weather", renamedFrom, "drift to the slug form must still be reported")
	assert.Equal(t, "weather-goat", parsed.Name, "parsed.Name must reach the bare slug, not the suffixed binary name")

	after, err := os.ReadFile(specPath)
	require.NoError(t, err)
	assert.Contains(t, string(after), "name: weather-goat\n", "spec.yaml must hold the bare slug, not the -pp-cli form")
	assert.NotContains(t, string(after), "weather-goat-pp-cli", "the suffix must never reach the slug-of-record")
}

// TestEnsureMCPGoMinVersionBumpsOldPin — older library CLIs predating
// the cobratree templates pin a v0.x version that lacks
// WithReadOnlyHintAnnotation/GetArguments. mcp-sync bumps to the floor
// before regenerating MCP source so the generated CLI compiles.
func TestEnsureMCPGoMinVersionBumpsOldPin(t *testing.T) {
	t.Parallel()
	cliDir := t.TempDir()
	gomod := `module example.com/foo

go 1.23.0

require github.com/mark3labs/mcp-go v0.26.0
`
	require.NoError(t, os.WriteFile(filepath.Join(cliDir, "go.mod"), []byte(gomod), 0o644))

	prior, err := ensureMCPGoMinVersion(cliDir)
	require.NoError(t, err)
	assert.Equal(t, "v0.26.0", prior, "must report the prior pin so callers can log it")

	updated, err := os.ReadFile(filepath.Join(cliDir, "go.mod"))
	require.NoError(t, err)
	assert.Contains(t, string(updated), "github.com/mark3labs/mcp-go v0.47.0")
	assert.NotContains(t, string(updated), "github.com/mark3labs/mcp-go v0.26.0")
	// Surrounding directives must be preserved.
	assert.Contains(t, string(updated), "module example.com/foo")
	assert.Contains(t, string(updated), "go 1.23.0")
}

// TestEnsureMCPGoMinVersionNoOpAtOrAboveFloor — at or above the floor,
// no-op even if exactly equal. Avoids spurious go.mod rewrites on
// already-current CLIs.
func TestEnsureMCPGoMinVersionNoOpAtOrAboveFloor(t *testing.T) {
	t.Parallel()
	for _, version := range []string{"v0.47.0", "v0.48.0", "v1.0.0"} {
		t.Run(version, func(t *testing.T) {
			t.Parallel()
			cliDir := t.TempDir()
			gomod := "module example.com/foo\nrequire github.com/mark3labs/mcp-go " + version + "\n"
			require.NoError(t, os.WriteFile(filepath.Join(cliDir, "go.mod"), []byte(gomod), 0o644))

			prior, err := ensureMCPGoMinVersion(cliDir)
			require.NoError(t, err)
			assert.Equal(t, "", prior, "must report no-rename when already at or above floor")

			after, err := os.ReadFile(filepath.Join(cliDir, "go.mod"))
			require.NoError(t, err)
			assert.Equal(t, gomod, string(after), "go.mod must be byte-identical when no bump needed")
		})
	}
}

// TestEnsureMCPGoMinVersionNoOpWhenDepAbsent — a CLI without mcp-go
// in go.mod (e.g., a CLI generated before MCP support was added)
// must not have the dep injected. Other migration steps (or a future
// `go mod tidy`) handle that.
func TestEnsureMCPGoMinVersionNoOpWhenDepAbsent(t *testing.T) {
	t.Parallel()
	cliDir := t.TempDir()
	gomod := "module example.com/foo\n\ngo 1.23.0\n"
	require.NoError(t, os.WriteFile(filepath.Join(cliDir, "go.mod"), []byte(gomod), 0o644))

	prior, err := ensureMCPGoMinVersion(cliDir)
	require.NoError(t, err)
	assert.Equal(t, "", prior)

	after, err := os.ReadFile(filepath.Join(cliDir, "go.mod"))
	require.NoError(t, err)
	assert.Equal(t, gomod, string(after))
}

// TestEnsureMCPGoMinVersionLeavesReplaceDirectiveAlone — a `replace`
// directive that pins mcp-go to a fork or local checkout must survive
// the bump. Only the require entry moves; the replace target is
// independent and may legitimately point at any version.
func TestEnsureMCPGoMinVersionLeavesReplaceDirectiveAlone(t *testing.T) {
	t.Parallel()
	cliDir := t.TempDir()
	gomod := `module example.com/foo

go 1.23.0

require github.com/mark3labs/mcp-go v0.26.0

replace github.com/mark3labs/mcp-go => github.com/myfork/mcp-go v0.30.0
`
	require.NoError(t, os.WriteFile(filepath.Join(cliDir, "go.mod"), []byte(gomod), 0o644))

	prior, err := ensureMCPGoMinVersion(cliDir)
	require.NoError(t, err)
	assert.Equal(t, "v0.26.0", prior)

	updated, err := os.ReadFile(filepath.Join(cliDir, "go.mod"))
	require.NoError(t, err)
	assert.Contains(t, string(updated), "github.com/mark3labs/mcp-go v0.47.0",
		"require entry must be bumped to the floor")
	assert.Contains(t, string(updated), "github.com/myfork/mcp-go v0.30.0",
		"replace directive's target must be untouched")
}

// TestMinMCPGoVersionMatchesGoModTemplate is the drift-detection
// gate: minMCPGoVersionForCobratree (used by ensureMCPGoMinVersion to
// decide whether an existing pin is too old) must match the version
// the generator's go.mod.tmpl actually emits. If they drift, freshly
// generated CLIs and freshly migrated CLIs end up on different mcp-go
// versions — silent inconsistency.
func TestMinMCPGoVersionMatchesGoModTemplate(t *testing.T) {
	t.Parallel()
	// The template lives at internal/generator/templates/go.mod.tmpl;
	// this test runs from internal/pipeline/mcpsync/.
	tmplPath := filepath.Join("..", "..", "..", "internal", "generator", "templates", "go.mod.tmpl")
	data, err := os.ReadFile(tmplPath)
	require.NoError(t, err, "go.mod.tmpl must be readable from the test cwd")

	// Match the bare require directive: `require github.com/mark3labs/mcp-go vX.Y.Z`.
	// The template emits the directive literally (no Go template
	// interpolation in the version), so a regex over the raw source
	// is sufficient.
	re := regexp.MustCompile(`(?m)^require[ \t]+github\.com/mark3labs/mcp-go[ \t]+(v[0-9][^\s]+)`)
	m := re.FindSubmatch(data)
	require.NotNil(t, m, "go.mod.tmpl must contain a `require github.com/mark3labs/mcp-go vX.Y.Z` line")
	assert.Equal(t, minMCPGoVersionForCobratree, string(m[1]),
		"minMCPGoVersionForCobratree drifted from the version pinned in go.mod.tmpl — bump both together")
}

// TestRemoveStaleMCPHandlersFile locks behavior for the food52 case:
// older generator templates emitted MCP handlers in a separate
// internal/mcp/handlers.go; the current template emits everything in
// tools.go. mcp-sync needs to delete the stale handlers.go before
// regenerating to avoid "handleContext redeclared in this block"
// build errors.
func TestRemoveStaleMCPHandlersFile(t *testing.T) {
	t.Parallel()

	cliDir := t.TempDir()
	mcpDir := filepath.Join(cliDir, "internal", "mcp")
	require.NoError(t, os.MkdirAll(mcpDir, 0o755))
	handlersPath := filepath.Join(mcpDir, "handlers.go")
	require.NoError(t, os.WriteFile(handlersPath, []byte(`// Copyright 2026 trevin-chow. Licensed under Apache-2.0. See LICENSE.
// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
package mcp
func handleContext() {}
`), 0o644))

	require.NoError(t, removeStaleMCPHandlersFile(cliDir, false))

	if _, err := os.Stat(handlersPath); !os.IsNotExist(err) {
		t.Errorf("expected handlers.go to be removed; stat err: %v", err)
	}
}

func TestRemoveStaleMCPHandlersFileNoOpWhenAbsent(t *testing.T) {
	t.Parallel()

	cliDir := t.TempDir()
	require.NoError(t, os.MkdirAll(filepath.Join(cliDir, "internal", "mcp"), 0o755))

	if err := removeStaleMCPHandlersFile(cliDir, false); err != nil {
		t.Errorf("expected no error when handlers.go is absent, got: %v", err)
	}
}

func TestRemoveStaleMCPHandlersFileRefusesHandEdits(t *testing.T) {
	t.Parallel()

	cliDir := t.TempDir()
	mcpDir := filepath.Join(cliDir, "internal", "mcp")
	require.NoError(t, os.MkdirAll(mcpDir, 0o755))
	handlersPath := filepath.Join(mcpDir, "handlers.go")
	// Hand-edited: marker absent. Removing this would destroy custom logic.
	require.NoError(t, os.WriteFile(handlersPath, []byte(`package mcp
// Hand-written by maintainer. Marker stripped on purpose.
func handleContext() {}
`), 0o644))

	err := removeStaleMCPHandlersFile(cliDir, false)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "appears hand-edited")
	if _, err := os.Stat(handlersPath); err != nil {
		t.Errorf("handlers.go should still exist after refusal; stat err: %v", err)
	}
}

func TestRemoveStaleMCPHandlersFileForceOverride(t *testing.T) {
	t.Parallel()

	cliDir := t.TempDir()
	mcpDir := filepath.Join(cliDir, "internal", "mcp")
	require.NoError(t, os.MkdirAll(mcpDir, 0o755))
	handlersPath := filepath.Join(mcpDir, "handlers.go")
	require.NoError(t, os.WriteFile(handlersPath, []byte(`package mcp
// Hand-written. --force will delete this anyway.
func handleContext() {}
`), 0o644))

	require.NoError(t, removeStaleMCPHandlersFile(cliDir, true))

	if _, err := os.Stat(handlersPath); !os.IsNotExist(err) {
		t.Errorf("expected --force to remove hand-edited handlers.go; stat err: %v", err)
	}
}

// TestEnsureRootCmdExportRewritesMultiArgFlagsCallsites locks the
// regex behavior that distinguishes "&flags" (whole struct address —
// must become "flags" after refactor) from "&flags.someField" (field
// address — must stay). The earlier (&flags)→(flags) ReplaceAll
// missed callsites where &flags appeared in the middle of an arg
// list, breaking compilation on real library CLIs (hackernews has
// `autoRefreshIfStale(cmd.Context(), &flags, resources)`).
func TestEnsureRootCmdExportRewritesMultiArgFlagsCallsites(t *testing.T) {
	t.Parallel()

	cliDir := t.TempDir()
	require.NoError(t, os.MkdirAll(filepath.Join(cliDir, "internal", "cli"), 0o755))
	rootPath := filepath.Join(cliDir, "internal", "cli", "root.go")
	require.NoError(t, os.WriteFile(rootPath, []byte(`// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
package cli

import "github.com/spf13/cobra"

type rootFlags struct{ asJSON bool }

func Execute() error {
	var flags rootFlags

	rootCmd := &cobra.Command{
		Use: "synctest-pp-cli",
	}
	rootCmd.PersistentFlags().BoolVar(&flags.asJSON, "json", false, "")

	// Multi-arg call passing &flags — must become flags.
	autoRefreshIfStale(rootCmd.Context(), &flags, []string{"x"})
	// First-of-many call — must also become flags.
	doSomething(&flags, "x", "y")
	// Last-of-many call — must become flags.
	finalize("a", &flags)
	// Field-access — must NOT become flags.asJSON (would be a different bug).
	useFlag(&flags.asJSON)

	err := rootCmd.Execute()
	return err
}

func ExitCode(err error) int { return 0 }
`), 0o644))

	require.NoError(t, ensureRootCmdExport(cliDir))

	updated, err := os.ReadFile(rootPath)
	require.NoError(t, err)
	src := string(updated)

	// All three bare-&flags callsite shapes should rewrite cleanly.
	assert.Contains(t, src, "autoRefreshIfStale(rootCmd.Context(), flags, []string{")
	assert.Contains(t, src, "doSomething(flags, \"x\", \"y\")")
	assert.Contains(t, src, "finalize(\"a\", flags)")

	// Field-access must remain untouched — &flags.asJSON is still valid
	// when flags is *rootFlags (Go auto-dereferences for field access).
	assert.Contains(t, src, "BoolVar(&flags.asJSON,")
	assert.Contains(t, src, "useFlag(&flags.asJSON)")

	// And the prologue should still produce the canonical RootCmd export.
	assert.Contains(t, src, "func RootCmd() *cobra.Command")
	assert.Contains(t, src, "return newRootCmd(&flags)")
}

// TestEnsureRootCmdExportConditionalBlocks asserts the prolog includes
// suggestFlag and Deliver blocks only when their supporting definitions
// exist in the source. Without conditional inclusion, older library
// CLIs that predate both features fail to build with "undefined" errors.
func TestEnsureRootCmdExportConditionalBlocks(t *testing.T) {
	t.Parallel()

	const oldTemplate = `// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
package cli

import "github.com/spf13/cobra"

type rootFlags struct{ asJSON bool }

func Execute() error {
	var flags rootFlags

	rootCmd := &cobra.Command{
		Use: "oldtemplate-pp-cli",
	}
	rootCmd.PersistentFlags().BoolVar(&flags.asJSON, "json", false, "")
	err := rootCmd.Execute()
	return err
}

func ExitCode(err error) int { return 0 }
`

	const modernTemplate = `// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
package cli

import (
	"bytes"
	"github.com/spf13/cobra"
)

type rootFlags struct {
	asJSON      bool
	compact     bool
	deliverBuf  *bytes.Buffer
	deliverSink DeliverSink
}

type DeliverSink struct{ Scheme, Target string }

func Deliver(s DeliverSink, b []byte, c bool) error { return nil }

func suggestFlag(name string, cmd *cobra.Command) string { return "" }

func Execute() error {
	var flags rootFlags

	rootCmd := &cobra.Command{
		Use: "modern-pp-cli",
	}
	rootCmd.PersistentFlags().BoolVar(&flags.asJSON, "json", false, "")
	err := rootCmd.Execute()
	return err
}

func ExitCode(err error) int { return 0 }
`

	cases := []struct {
		name        string
		fixture     string
		wantSuggest bool
		wantDeliver bool
	}{
		{"old template lacks both features", oldTemplate, false, false},
		{"modern template has both features", modernTemplate, true, true},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			cliDir := t.TempDir()
			require.NoError(t, os.MkdirAll(filepath.Join(cliDir, "internal", "cli"), 0o755))
			rootPath := filepath.Join(cliDir, "internal", "cli", "root.go")
			require.NoError(t, os.WriteFile(rootPath, []byte(tc.fixture), 0o644))

			require.NoError(t, ensureRootCmdExport(cliDir))

			updated, err := os.ReadFile(rootPath)
			require.NoError(t, err)
			src := string(updated)

			// Canonical RootCmd export always emitted.
			assert.Contains(t, src, "func RootCmd() *cobra.Command")
			assert.Contains(t, src, "return newRootCmd(&flags)")

			if tc.wantSuggest {
				assert.Contains(t, src, "suggestFlag(")
			} else {
				assert.NotContains(t, src, "suggestFlag(")
			}
			if tc.wantDeliver {
				assert.Contains(t, src, "Deliver(flags.deliverSink")
			} else {
				assert.NotContains(t, src, "Deliver(flags.deliverSink")
				assert.NotContains(t, src, "flags.deliverBuf")
			}
		})
	}
}

// TestEnsureRootCmdExportDetectsFeaturesAcrossPackage asserts that
// suggestFlag and Deliver are detected even when their definitions
// live in sibling files (helpers.go, deliver.go) rather than root.go.
func TestEnsureRootCmdExportDetectsFeaturesAcrossPackage(t *testing.T) {
	t.Parallel()

	cliDir := t.TempDir()
	pkgDir := filepath.Join(cliDir, "internal", "cli")
	require.NoError(t, os.MkdirAll(pkgDir, 0o755))

	// root.go has the deliverBuf field (so the deliver block applies)
	// but neither Deliver nor suggestFlag are defined here.
	require.NoError(t, os.WriteFile(filepath.Join(pkgDir, "root.go"), []byte(`// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
package cli

import (
	"bytes"
	"github.com/spf13/cobra"
)

type rootFlags struct {
	asJSON      bool
	compact     bool
	deliverBuf  *bytes.Buffer
	deliverSink DeliverSink
}

func Execute() error {
	var flags rootFlags

	rootCmd := &cobra.Command{
		Use: "splittest-pp-cli",
	}
	rootCmd.PersistentFlags().BoolVar(&flags.asJSON, "json", false, "")
	err := rootCmd.Execute()
	return err
}

func ExitCode(err error) int { return 0 }
`), 0o644))

	// deliver.go provides the Deliver function (real generator layout).
	require.NoError(t, os.WriteFile(filepath.Join(pkgDir, "deliver.go"), []byte(`package cli

type DeliverSink struct{ Scheme, Target string }

func Deliver(s DeliverSink, b []byte, c bool) error { return nil }
`), 0o644))

	// helpers.go provides suggestFlag (real generator layout).
	require.NoError(t, os.WriteFile(filepath.Join(pkgDir, "helpers.go"), []byte(`package cli

import "github.com/spf13/cobra"

func suggestFlag(name string, cmd *cobra.Command) string { return "" }
`), 0o644))

	require.NoError(t, ensureRootCmdExport(cliDir))

	updated, err := os.ReadFile(filepath.Join(pkgDir, "root.go"))
	require.NoError(t, err)
	src := string(updated)

	assert.Contains(t, src, "suggestFlag(", "suggestFlag detected in helpers.go must enable the suggestFlag block")
	assert.Contains(t, src, "Deliver(flags.deliverSink", "Deliver detected in deliver.go must enable the deliver block")
}

// TestEnsureRootCmdExportIsIdempotent locks the no-op behavior on a root.go
// that already has RootCmd. Future template changes that add or remove
// surrounding code must not reintroduce the export and double-define it.
func TestEnsureRootCmdExportIsIdempotent(t *testing.T) {
	t.Parallel()

	cliDir := t.TempDir()
	require.NoError(t, os.MkdirAll(filepath.Join(cliDir, "internal", "cli"), 0o755))
	rootPath := filepath.Join(cliDir, "internal", "cli", "root.go")
	original := []byte(`// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
package cli

import "github.com/spf13/cobra"

type rootFlags struct{}

func RootCmd() *cobra.Command {
	var flags rootFlags
	return newRootCmd(&flags)
}

func Execute() error { return nil }

func newRootCmd(flags *rootFlags) *cobra.Command { return &cobra.Command{} }
`)
	require.NoError(t, os.WriteFile(rootPath, original, 0o644))

	require.NoError(t, ensureRootCmdExport(cliDir))

	updated, err := os.ReadFile(rootPath)
	require.NoError(t, err)
	assert.Equal(t, string(original), string(updated), "ensureRootCmdExport should not modify root.go that already exports RootCmd")
}

// TestEnsureRootCmdExportRefusesHandEdits guards the hand-edit safety net.
// If the generator's don't-edit marker is missing the migration must refuse
// rather than silently overwriting whatever shape the file has now.
func TestEnsureRootCmdExportRefusesHandEdits(t *testing.T) {
	t.Parallel()

	cliDir := t.TempDir()
	require.NoError(t, os.MkdirAll(filepath.Join(cliDir, "internal", "cli"), 0o755))
	rootPath := filepath.Join(cliDir, "internal", "cli", "root.go")
	require.NoError(t, os.WriteFile(rootPath, []byte(`// Hand-edited by maintainer; marker stripped on purpose.
package cli

import "github.com/spf13/cobra"

type rootFlags struct{}

func Execute() error {
	var flags rootFlags
	rootCmd := &cobra.Command{Use: "synctest"}
	_ = flags
	return rootCmd.Execute()
}
`), 0o644))

	err := ensureRootCmdExport(cliDir)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "appears hand-edited")
}

// TestEnsureRootCmdExportFailsClosedOnUnknownShape locks the failure path
// when ensureRootCmdExport encounters a generator-marked file whose Execute
// shape no longer matches the regex. The right behavior is "error out so the
// human investigates," not "patch what you find anyway." This test pins that
// contract and will fail if anyone weakens it.
func TestEnsureRootCmdExportFailsClosedOnUnknownShape(t *testing.T) {
	t.Parallel()

	cliDir := t.TempDir()
	require.NoError(t, os.MkdirAll(filepath.Join(cliDir, "internal", "cli"), 0o755))
	rootPath := filepath.Join(cliDir, "internal", "cli", "root.go")
	require.NoError(t, os.WriteFile(rootPath, []byte(`// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
package cli

// This shape doesn't match the prologue ensureRootCmdExport expects.
func Execute() error {
	cfg := loadConfig()
	root := buildRoot(cfg)
	return root.Execute()
}
`), 0o644))

	err := ensureRootCmdExport(cliDir)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "does not match the generated Execute shape")
}

func TestReadRegistryDisplayName(t *testing.T) {
	writeRegistry := func(t *testing.T, libDir string, entries any) {
		t.Helper()
		body := map[string]any{"schema_version": 1, "entries": entries}
		data, err := json.Marshal(body)
		require.NoError(t, err)
		require.NoError(t, os.WriteFile(filepath.Join(libDir, "registry.json"), data, 0o644))
	}

	t.Run("env unset returns empty", func(t *testing.T) {
		t.Setenv("PRINTING_PRESS_LIBRARY_PUBLIC", "")
		assert.Equal(t, "", readRegistryDisplayName("cal-com"))
	})

	t.Run("env set but file missing returns empty", func(t *testing.T) {
		t.Setenv("PRINTING_PRESS_LIBRARY_PUBLIC", t.TempDir())
		assert.Equal(t, "", readRegistryDisplayName("cal-com"))
	})

	t.Run("malformed JSON returns empty", func(t *testing.T) {
		libDir := t.TempDir()
		require.NoError(t, os.WriteFile(filepath.Join(libDir, "registry.json"), []byte("not json"), 0o644))
		t.Setenv("PRINTING_PRESS_LIBRARY_PUBLIC", libDir)
		assert.Equal(t, "", readRegistryDisplayName("cal-com"))
	})

	t.Run("slug not in registry returns empty", func(t *testing.T) {
		libDir := t.TempDir()
		writeRegistry(t, libDir, []map[string]string{
			{"name": "wikipedia", "api": "Wikipedia"},
		})
		t.Setenv("PRINTING_PRESS_LIBRARY_PUBLIC", libDir)
		assert.Equal(t, "", readRegistryDisplayName("cal-com"))
	})

	t.Run("api equal to slug returns empty", func(t *testing.T) {
		libDir := t.TempDir()
		writeRegistry(t, libDir, []map[string]string{
			{"name": "agent-capture", "api": "agent-capture"},
		})
		t.Setenv("PRINTING_PRESS_LIBRARY_PUBLIC", libDir)
		assert.Equal(t, "", readRegistryDisplayName("agent-capture"))
	})

	t.Run("api too long returns empty", func(t *testing.T) {
		libDir := t.TempDir()
		writeRegistry(t, libDir, []map[string]string{
			{"name": "recipe-goat", "api": "Cross-site recipe aggregator (37 trusted sites: King Arthur, Serious Eats, Smitten Kitchen, AllRecipes, Food52, BBC Food, EatingWell, Food Network, and 29 more) + USDA FoodData Central"},
		})
		t.Setenv("PRINTING_PRESS_LIBRARY_PUBLIC", libDir)
		assert.Equal(t, "", readRegistryDisplayName("recipe-goat"))
	})

	t.Run("usable api returns it", func(t *testing.T) {
		libDir := t.TempDir()
		writeRegistry(t, libDir, []map[string]string{
			{"name": "wikipedia", "api": "Wikipedia"},
			{"name": "cal-com", "api": "Cal.com"},
			{"name": "company-goat", "api": "Company GOAT"},
		})
		t.Setenv("PRINTING_PRESS_LIBRARY_PUBLIC", libDir)
		assert.Equal(t, "Cal.com", readRegistryDisplayName("cal-com"))
		assert.Equal(t, "Wikipedia", readRegistryDisplayName("wikipedia"))
		assert.Equal(t, "Company GOAT", readRegistryDisplayName("company-goat"))
	})

	t.Run("whitespace-only api returns empty", func(t *testing.T) {
		libDir := t.TempDir()
		writeRegistry(t, libDir, []map[string]string{
			{"name": "wikipedia", "api": "  \t\n"},
		})
		t.Setenv("PRINTING_PRESS_LIBRARY_PUBLIC", libDir)
		assert.Equal(t, "", readRegistryDisplayName("wikipedia"))
	})

	t.Run("api whitespace is trimmed", func(t *testing.T) {
		libDir := t.TempDir()
		writeRegistry(t, libDir, []map[string]string{
			{"name": "wikipedia", "api": "  Wikipedia  "},
		})
		t.Setenv("PRINTING_PRESS_LIBRARY_PUBLIC", libDir)
		assert.Equal(t, "Wikipedia", readRegistryDisplayName("wikipedia"))
	})

	t.Run("unknown schema_version returns empty", func(t *testing.T) {
		libDir := t.TempDir()
		body := map[string]any{
			"schema_version": 2, // future schema, this binary doesn't understand
			"entries": []map[string]string{
				{"name": "cal-com", "api": "Cal.com"},
			},
		}
		data, err := json.Marshal(body)
		require.NoError(t, err)
		require.NoError(t, os.WriteFile(filepath.Join(libDir, "registry.json"), data, 0o644))
		t.Setenv("PRINTING_PRESS_LIBRARY_PUBLIC", libDir)
		assert.Equal(t, "", readRegistryDisplayName("cal-com"))
	})

	t.Run("missing schema_version returns empty", func(t *testing.T) {
		libDir := t.TempDir()
		body := map[string]any{
			// schema_version omitted; defaults to 0, which doesn't match.
			"entries": []map[string]string{
				{"name": "cal-com", "api": "Cal.com"},
			},
		}
		data, err := json.Marshal(body)
		require.NoError(t, err)
		require.NoError(t, os.WriteFile(filepath.Join(libDir, "registry.json"), data, 0o644))
		t.Setenv("PRINTING_PRESS_LIBRARY_PUBLIC", libDir)
		assert.Equal(t, "", readRegistryDisplayName("cal-com"))
	})
}

// TestSyncFallsThroughToRegistryDisplayName exercises the new branch
// in Sync's preservation block end-to-end: spec.yaml without
// display_name, manifest.json with no usable value, registry.json
// with a brand name. Asserts the registry value lands in
// .printing-press.json so subsequent WriteMCPBManifest carries it
// into the published manifest.
func TestSyncFallsThroughToRegistryDisplayName(t *testing.T) {
	apiSpec := &spec.APISpec{
		Name:    "testbrand",
		Version: "0.1.0",
		BaseURL: "https://api.example.com",
		Auth: spec.AuthConfig{
			Type:    "api_key",
			Header:  "Authorization",
			Format:  "Bearer {token}",
			EnvVars: []string{"TESTBRAND_TOKEN"},
		},
		Config: spec.ConfigSpec{
			Format: "toml",
			Path:   "~/.config/testbrand-pp-cli/config.toml",
		},
		Resources: map[string]spec.Resource{
			"items": {
				Description: "Manage items",
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/items", Description: "List items"},
				},
			},
		},
	}
	cliDir := filepath.Join(t.TempDir(), "testbrand")
	gen := generator.New(apiSpec, cliDir)
	require.NoError(t, gen.Generate())
	require.NoError(t, pipeline.WriteManifestForGenerate(pipeline.GenerateManifestParams{
		APIName:   apiSpec.Name,
		OutputDir: cliDir,
		Spec:      apiSpec,
	}))
	specData, err := yaml.Marshal(apiSpec)
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(filepath.Join(cliDir, "spec.yaml"), specData, 0o644))

	// Force manifest.json's display_name to empty so the manifest-side
	// preservation step rejects it and falls through to the registry.
	// Without this, the generator's HumanName-derived "Testbrand" would
	// be preserved and the registry path never gets exercised.
	manifestPath := filepath.Join(cliDir, pipeline.MCPBManifestFilename)
	mcpbData, err := os.ReadFile(manifestPath)
	require.NoError(t, err)
	var mcpb pipeline.MCPBManifest
	require.NoError(t, json.Unmarshal(mcpbData, &mcpb))
	mcpb.DisplayName = ""
	modifiedData, err := json.Marshal(mcpb)
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(manifestPath, modifiedData, 0o644))

	// Set up the public library with a registry.json the fallback can read.
	libDir := t.TempDir()
	registry := map[string]any{
		"schema_version": 1,
		"entries": []map[string]string{
			{"name": "testbrand", "api": "TestBrand!"},
		},
	}
	registryData, err := json.Marshal(registry)
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(filepath.Join(libDir, "registry.json"), registryData, 0o644))
	t.Setenv("PRINTING_PRESS_LIBRARY_PUBLIC", libDir)

	_, err = Sync(cliDir, Options{})
	require.NoError(t, err)

	// .printing-press.json must reflect the registry-derived value;
	// the broken alternative is silently regressing to "Testbrand".
	provData, err := os.ReadFile(filepath.Join(cliDir, pipeline.CLIManifestFilename))
	require.NoError(t, err)
	var prov pipeline.CLIManifest
	require.NoError(t, json.Unmarshal(provData, &prov))
	assert.Equal(t, "TestBrand!", prov.DisplayName)
}

// TestApplyManifestNameOverrideReplacesParsedName locks the contract
// that the manifest's api_name outranks the spec-derived slug and the
// spec-shaped Config.Path migrates alongside parsed.Name so downstream
// emitters (mcp_binary, tools-manifest api_name, cmd/<slug>-pp-{cli,mcp}/,
// README config path) all follow the manifest.
func TestApplyManifestNameOverrideReplacesParsedName(t *testing.T) {
	t.Parallel()
	cliDir := filepath.Join(t.TempDir(), "telegram-pp-cli")
	require.NoError(t, os.MkdirAll(cliDir, 0o755))
	require.NoError(t, pipeline.WriteCLIManifest(cliDir, pipeline.CLIManifest{
		APIName: "telegram",
		CLIName: "telegram-pp-cli",
	}))

	parsed := &spec.APISpec{
		Name: "telegram-bot",
		Auth: spec.AuthConfig{
			Type:    "api_key",
			Header:  "X-API-Key",
			EnvVars: []string{"TELEGRAM_BOT_API_KEY"},
			EnvVarSpecs: []spec.AuthEnvVar{{
				Name:      "TELEGRAM_BOT_API_KEY",
				Kind:      spec.AuthEnvVarKindPerCall,
				Required:  true,
				Sensitive: true,
				Inferred:  true,
			}},
		},
		Config: spec.ConfigSpec{
			Format: "toml",
			Path:   "~/.config/telegram-bot-pp-cli/config.toml",
		},
	}
	prior := applyManifestNameOverride(cliDir, parsed)
	assert.Equal(t, "telegram-bot", prior, "should report the prior spec-derived slug")
	assert.Equal(t, "telegram", parsed.Name, "parsed.Name should adopt the manifest's api_name")
	assert.Equal(t, "~/.config/telegram-pp-cli/config.toml", parsed.Config.Path,
		"spec-derived Config.Path must follow the override so README's Config file: line tracks the binary")
	assert.Equal(t, []string{"TELEGRAM_API_KEY"}, parsed.Auth.EnvVars,
		"inferred auth env vars should follow the manifest api_name")
	require.Len(t, parsed.Auth.EnvVarSpecs, 1)
	assert.Equal(t, "TELEGRAM_API_KEY", parsed.Auth.EnvVarSpecs[0].Name,
		"inferred auth env var specs should follow the manifest api_name")
}

func TestApplyCatalogMetadataUsesCatalogBaseURL(t *testing.T) {
	t.Parallel()

	parsed := &spec.APISpec{
		Name:                 "elevenlabs",
		BaseURL:              spec.PlaceholderBaseURL,
		BaseURLIsPlaceholder: true,
		Auth: spec.AuthConfig{
			Type:   "api_key",
			Header: "xi-api-key",
		},
	}

	applyCatalogMetadata(parsed)

	assert.Equal(t, "https://api.elevenlabs.io", parsed.BaseURL)
	assert.False(t, parsed.BaseURLIsPlaceholder)
	assert.Equal(t, "ElevenLabs", parsed.DisplayName)
	assert.Equal(t, "https://elevenlabs.io/app/settings/api-keys", parsed.Auth.KeyURL)
	assert.Equal(t, "standard", parsed.HTTPTransport)
	assert.Equal(t, "official", parsed.SpecSource)
}

// TestApplyManifestNameOverrideNoOpWhenManifestAgrees — most CLIs are
// already aligned (manifest api_name matches spec-derived slug). The
// override must be a no-op so it doesn't churn unrelated state.
func TestApplyManifestNameOverrideNoOpWhenManifestAgrees(t *testing.T) {
	t.Parallel()
	cliDir := filepath.Join(t.TempDir(), "producthunt-pp-cli")
	require.NoError(t, os.MkdirAll(cliDir, 0o755))
	require.NoError(t, pipeline.WriteCLIManifest(cliDir, pipeline.CLIManifest{
		APIName: "producthunt",
		CLIName: "producthunt-pp-cli",
	}))

	parsed := &spec.APISpec{
		Name: "producthunt",
		Config: spec.ConfigSpec{
			Format: "toml",
			Path:   "~/.config/producthunt-pp-cli/config.toml",
		},
	}
	prior := applyManifestNameOverride(cliDir, parsed)
	assert.Equal(t, "", prior, "agreement should report no override")
	assert.Equal(t, "producthunt", parsed.Name)
	assert.Equal(t, "~/.config/producthunt-pp-cli/config.toml", parsed.Config.Path)
}

// TestApplyManifestNameOverrideMissingManifest — older library CLIs
// predate .printing-press.json. The override must fall through silently
// so the legacy reconcileSpecNameWithDir path stays in charge.
func TestApplyManifestNameOverrideMissingManifest(t *testing.T) {
	t.Parallel()
	cliDir := filepath.Join(t.TempDir(), "legacy-pp-cli")
	require.NoError(t, os.MkdirAll(cliDir, 0o755))

	parsed := &spec.APISpec{
		Name: "legacy-api",
		Config: spec.ConfigSpec{
			Format: "toml",
			Path:   "~/.config/legacy-api-pp-cli/config.toml",
		},
	}
	prior := applyManifestNameOverride(cliDir, parsed)
	assert.Equal(t, "", prior, "missing manifest should report no override")
	assert.Equal(t, "legacy-api", parsed.Name, "parsed.Name must be untouched when manifest is absent")
	assert.Equal(t, "~/.config/legacy-api-pp-cli/config.toml", parsed.Config.Path)
}

// TestApplyManifestNameOverrideMalformedJSON — a corrupted
// .printing-press.json (truncated, partial write, hand-edited and
// broken) must fall through without overriding, and must NOT be
// indistinguishable from the missing-file case. The stderr warning
// is the operator's only signal that the guard was bypassed.
func TestApplyManifestNameOverrideMalformedJSON(t *testing.T) {
	t.Parallel()
	cliDir := filepath.Join(t.TempDir(), "corrupt-pp-cli")
	require.NoError(t, os.MkdirAll(cliDir, 0o755))
	// Truncated JSON — would unmarshal to an error, not "{}".
	require.NoError(t, os.WriteFile(
		filepath.Join(cliDir, pipeline.CLIManifestFilename),
		[]byte(`{"api_name": "corrupt`),
		0o644,
	))

	parsed := &spec.APISpec{Name: "spec-derived"}
	prior := applyManifestNameOverride(cliDir, parsed)
	assert.Equal(t, "", prior, "malformed manifest must not produce an override")
	assert.Equal(t, "spec-derived", parsed.Name, "parsed.Name must be preserved when the manifest cannot be parsed")
}

// TestApplyManifestNameOverrideEmptyAPIName — a malformed manifest with
// no api_name (corrupted, partial write) must not clobber parsed.Name
// with the empty string.
func TestApplyManifestNameOverrideEmptyAPIName(t *testing.T) {
	t.Parallel()
	cliDir := filepath.Join(t.TempDir(), "partial-pp-cli")
	require.NoError(t, os.MkdirAll(cliDir, 0o755))
	require.NoError(t, pipeline.WriteCLIManifest(cliDir, pipeline.CLIManifest{
		// APIName intentionally empty — simulates a partially-written manifest.
		CLIName: "partial-pp-cli",
	}))

	parsed := &spec.APISpec{Name: "partial-api"}
	prior := applyManifestNameOverride(cliDir, parsed)
	assert.Equal(t, "", prior)
	assert.Equal(t, "partial-api", parsed.Name, "empty api_name must not override")
}

// TestApplyManifestNameOverridePreservesCustomConfigPath — when an
// operator hand-customized Config.Path (e.g., per-environment paths,
// XDG_CONFIG_HOME-style overrides), the override must not rewrite it.
// Only the spec-derived shape `~/.config/<old>-pp-cli/config.toml` is
// migrated; everything else is preserved.
func TestApplyManifestNameOverridePreservesCustomConfigPath(t *testing.T) {
	t.Parallel()
	cliDir := filepath.Join(t.TempDir(), "telegram-pp-cli")
	require.NoError(t, os.MkdirAll(cliDir, 0o755))
	require.NoError(t, pipeline.WriteCLIManifest(cliDir, pipeline.CLIManifest{
		APIName: "telegram",
		CLIName: "telegram-pp-cli",
	}))

	parsed := &spec.APISpec{
		Name: "telegram-bot",
		Config: spec.ConfigSpec{
			Format: "toml",
			Path:   "$XDG_CONFIG_HOME/telegram/config.toml",
		},
	}
	prior := applyManifestNameOverride(cliDir, parsed)
	assert.Equal(t, "telegram-bot", prior)
	assert.Equal(t, "telegram", parsed.Name)
	assert.Equal(t, "$XDG_CONFIG_HOME/telegram/config.toml", parsed.Config.Path,
		"hand-customized Config.Path must not be rewritten by the override")
}

// TestSyncManifestAPINameWinsOverSpecDerivedSlug is the end-to-end
// integration: generate a CLI under a "telegram-pp-cli/" directory,
// then rewrite spec.yaml so the parsed slug diverges. Asserts that
// every name-bearing surface (.printing-press.json, tools-manifest.json,
// manifest.json, cmd/ directories) keeps tracking the manifest's
// api_name instead of regenerating against the spec-derived slug.
func TestSyncManifestAPINameWinsOverSpecDerivedSlug(t *testing.T) {
	t.Parallel()

	apiSpec := &spec.APISpec{
		Name:    "telegram",
		Version: "0.1.0",
		BaseURL: "https://api.telegram.org",
		Auth: spec.AuthConfig{
			Type:    "api_key",
			Header:  "Authorization",
			Format:  "Bearer {token}",
			EnvVars: []string{"TELEGRAM_TOKEN"},
		},
		Config: spec.ConfigSpec{
			Format: "toml",
			Path:   "~/.config/telegram-pp-cli/config.toml",
		},
		Resources: map[string]spec.Resource{
			"messages": {
				Description: "Send messages",
				Endpoints: map[string]spec.Endpoint{
					"send": {Method: "POST", Path: "/messages", Description: "Send a message"},
				},
			},
		},
	}
	cliDir := filepath.Join(t.TempDir(), "telegram-pp-cli")
	gen := generator.New(apiSpec, cliDir)
	require.NoError(t, gen.Generate())
	require.NoError(t, pipeline.WriteManifestForGenerate(pipeline.GenerateManifestParams{
		APIName:   apiSpec.Name,
		OutputDir: cliDir,
		Spec:      apiSpec,
	}))

	// Rewrite spec.yaml so the archived spec slug diverges from the
	// manifest api_name — the shape loadArchivedSpec produces when
	// info.title's slug differs from the chosen --name.
	driftedSpec := *apiSpec
	driftedSpec.Name = "telegram-bot"
	driftedSpec.Config.Path = "~/.config/telegram-bot-pp-cli/config.toml"
	specData, err := yaml.Marshal(&driftedSpec)
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(filepath.Join(cliDir, "spec.yaml"), specData, 0o644))

	result, err := Sync(cliDir, Options{Force: true})
	require.NoError(t, err)
	assert.True(t, result.Changed)

	// .printing-press.json's mcp_binary must keep tracking the manifest
	// api_name, not the spec-derived "telegram-bot" slug.
	provData, err := os.ReadFile(filepath.Join(cliDir, pipeline.CLIManifestFilename))
	require.NoError(t, err)
	var prov pipeline.CLIManifest
	require.NoError(t, json.Unmarshal(provData, &prov))
	assert.Equal(t, "telegram", prov.APIName, "api_name must hold the manifest-authoritative slug")
	assert.Equal(t, "telegram-pp-mcp", prov.MCPBinary, "mcp_binary must derive from manifest api_name, not spec title")

	// tools-manifest.json's api_name must follow the same source of truth.
	toolsData, err := os.ReadFile(filepath.Join(cliDir, pipeline.ToolsManifestFilename))
	require.NoError(t, err)
	var tools struct {
		APIName string `json:"api_name"`
	}
	require.NoError(t, json.Unmarshal(toolsData, &tools))
	assert.Equal(t, "telegram", tools.APIName, "tools-manifest api_name must hold the manifest-authoritative slug")

	// The MCPB manifest's name/entry_point must point at the actual
	// built binary "telegram-pp-mcp", not "telegram-bot-pp-mcp".
	manifestData, err := os.ReadFile(filepath.Join(cliDir, pipeline.MCPBManifestFilename))
	require.NoError(t, err)
	var manifest struct {
		Name   string `json:"name"`
		Server struct {
			EntryPoint string `json:"entry_point"`
		} `json:"server"`
	}
	require.NoError(t, json.Unmarshal(manifestData, &manifest))
	assert.Equal(t, "telegram-pp-mcp", manifest.Name, "MCPB manifest name must derive from manifest api_name")
	assert.Equal(t, "bin/telegram-pp-mcp", manifest.Server.EntryPoint, "entry_point must derive from manifest api_name, not spec title")

	// No stray cmd/telegram-bot-pp-{cli,mcp}/ directories should have
	// been created by GenerateMCPSurface.
	_, err = os.Stat(filepath.Join(cliDir, "cmd", "telegram-bot-pp-cli"))
	assert.True(t, os.IsNotExist(err), "no stray cmd/telegram-bot-pp-cli/ directory should be created")
	_, err = os.Stat(filepath.Join(cliDir, "cmd", "telegram-bot-pp-mcp"))
	assert.True(t, os.IsNotExist(err), "no stray cmd/telegram-bot-pp-mcp/ directory should be created")
}
