package cli

import (
	"bytes"
	"encoding/json"
	"fmt"
	"go/parser"
	"go/token"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/mvanhorn/cli-printing-press/v4/internal/browsersniff"
	"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/mvanhorn/cli-printing-press/v4/internal/specmeta"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "spec.yaml")
	outputDir := filepath.Join(dir, "regenapp")
	require.NoError(t, os.WriteFile(specPath, []byte(`name: regenapp
description: Regen app API
version: 0.1.0
base_url: https://api.example.com
auth:
  type: none
config:
  format: toml
  path: ~/.config/regenapp-pp-cli/config.toml
resources:
  items:
    description: Manage items
    endpoints:
      list:
        method: GET
        path: /items
        description: List items
`), 0o644))

	runGenerate := func() {
		cmd := newGenerateCmd()
		cmd.SetArgs([]string{
			"--spec", specPath,
			"--output", outputDir,
			"--validate=false",
			"--force",
		})
		require.NoError(t, cmd.Execute())
	}

	runGenerate()

	novelPath := filepath.Join(outputDir, "internal", "cli", "novel_keep.go")
	novelSource := []byte(`package cli

func novelPreservedSentinel() string { return "preserved" }
`)
	require.NoError(t, os.WriteFile(novelPath, novelSource, 0o644))

	siblingPath := filepath.Join(outputDir, "internal", "source", "custom", "client.go")
	siblingSource := []byte(`package custom

func KeepSiblingPackage() string { return "preserved" }
`)
	require.NoError(t, os.MkdirAll(filepath.Dir(siblingPath), 0o755))
	require.NoError(t, os.WriteFile(siblingPath, siblingSource, 0o644))

	staleGeneratedPath := filepath.Join(outputDir, "internal", "cli", "old_generated.go")
	require.NoError(t, os.WriteFile(staleGeneratedPath, []byte(`// Copyright 2026 trevin-chow.
// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
package cli

func staleGeneratedCommand() {}
`), 0o644))

	runGenerate()

	gotNovel, err := os.ReadFile(novelPath)
	require.NoError(t, err)
	assert.Equal(t, string(novelSource), string(gotNovel))

	gotSibling, err := os.ReadFile(siblingPath)
	require.NoError(t, err)
	assert.Equal(t, string(siblingSource), string(gotSibling))

	// Stale generator-emitted files (PUBLISHED-ONLY-TEMPLATED in regenmerge
	// terms — present in published, absent from fresh) are dropped on regen.
	assert.NoFileExists(t, staleGeneratedPath)

	runGoCommandForCLITest(t, outputDir, "mod", "tidy")
	runGoCommandForCLITest(t, outputDir, "build", "./cmd/regenapp-pp-cli")
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "spec.yaml")
	outputDir := filepath.Join(dir, "legacyversion")
	require.NoError(t, os.WriteFile(specPath, []byte(`name: legacyversion
description: Legacy version API
version: 0.1.0
base_url: https://api.example.com
auth:
  type: none
config:
  format: toml
  path: ~/.config/legacyversion-pp-cli/config.toml
resources:
  items:
    description: Manage items
    endpoints:
      list:
        method: GET
        path: /items
        description: List items
`), 0o644))

	runGenerate := func() {
		cmd := newGenerateCmd()
		cmd.SetArgs([]string{
			"--spec", specPath,
			"--output", outputDir,
			"--validate=false",
			"--force",
		})
		require.NoError(t, cmd.Execute())
	}

	runGenerate()

	rootPath := filepath.Join(outputDir, "internal", "cli", "root.go")
	versionPath := filepath.Join(outputDir, "internal", "cli", "version.go")
	rootSrc, err := os.ReadFile(rootPath)
	require.NoError(t, err)
	legacyVersionDecls := `

// version is the printed CLI's version, overridable at build time via ldflags.
var version = "2026.6.1"

// newVersionCmd prints the CLI name and version.
func newVersionCmd() *cobra.Command {
	return &cobra.Command{
		Use:   "version",
		Short: "Print version",
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Printf("%s %s\n", cmd.Root().Name(), version)
		},
	}
}
`
	require.NoError(t, os.WriteFile(rootPath, append(rootSrc, []byte(legacyVersionDecls)...), 0o644))
	require.NoError(t, os.Remove(versionPath))

	mcpMainPath := filepath.Join(outputDir, "cmd", "legacyversion-pp-mcp", "main.go")
	mcpMain, err := os.ReadFile(mcpMainPath)
	require.NoError(t, err)
	mcpMain = bytes.Replace(mcpMain, []byte(`// version is the printed MCP server's version, overridable at build time via ldflags.
var version = "0.0.0-dev"

`), nil, 1)
	mcpMain = bytes.Replace(mcpMain, []byte("\t\tversion,\n"), []byte("\t\t\"2026.6.1\",\n"), 1)
	require.NoError(t, os.WriteFile(mcpMainPath, mcpMain, 0o644))
	require.NotContains(t, string(mcpMain), `var version =`,
		"legacy MCP fixture must strip the generated version var before the second generate")

	runGenerate()

	assert.NoFileExists(t, versionPath, "legacy root.go version layout must not gain internal/cli/version.go")
	rootSrc, err = os.ReadFile(rootPath)
	require.NoError(t, err)
	assert.Contains(t, string(rootSrc), `var version = "2026.6.1"`)
	assert.Contains(t, string(rootSrc), "func newVersionCmd() *cobra.Command")
	mcpMain, err = os.ReadFile(mcpMainPath)
	require.NoError(t, err)
	assert.NotContains(t, string(mcpMain), `var version =`)
	assert.Contains(t, string(mcpMain), `"2026.6.1"`)

	runGoCommandForCLITest(t, outputDir, "mod", "tidy")
	runGoCommandForCLITest(t, outputDir, "build", "./...")
}

func TestApplyLibraryAttributionForGeneratePreservesCreatorAndPrependsReprinter(t *testing.T) {
	tmp := t.TempDir()
	t.Setenv("PRINTING_PRESS_HOME", tmp)

	libDir := filepath.Join(pipeline.PublishedLibraryRoot(), "test")
	require.NoError(t, os.MkdirAll(libDir, 0o755))
	require.NoError(t, pipeline.WriteCLIManifest(libDir, pipeline.CLIManifest{
		SchemaVersion: pipeline.CurrentCLIManifestSchemaVersion,
		APIName:       "test",
		CLIName:       "test-pp-cli",
		Creator:       &spec.Person{Handle: "mvanhorn", Name: "Matt Van Horn"},
		Contributors:  []spec.Person{{Handle: "jane-doe", Name: "Jane Doe"}},
		Owner:         "mvanhorn",
		Printer:       "mvanhorn",
		PrinterName:   "Matt Van Horn",
	}))

	apiSpec := &spec.APISpec{
		Name:    "test",
		Creator: spec.Person{Handle: "tmchow", Name: "Trevin Chow"},
		Owner:   "tmchow",
	}
	applyLibraryAttributionForGenerate(apiSpec, spec.Person{Handle: "tmchow", Name: "Trevin Chow"})

	assert.Equal(t, "mvanhorn", apiSpec.Creator.Handle)
	assert.Equal(t, "Matt Van Horn", apiSpec.Creator.Name)
	assert.Equal(t, "mvanhorn", apiSpec.Owner)
	require.Len(t, apiSpec.Contributors, 2)
	assert.Equal(t, "tmchow", apiSpec.Contributors[0].Handle, "reprinter is front-listed")
	assert.Equal(t, "jane-doe", apiSpec.Contributors[1].Handle)
}

func TestApplyLibraryAttributionForGenerateLeavesBrandNewAPIAlone(t *testing.T) {
	tmp := t.TempDir()
	t.Setenv("PRINTING_PRESS_HOME", tmp)

	apiSpec := &spec.APISpec{
		Name:    "new-api",
		Creator: spec.Person{Handle: "tmchow", Name: "Trevin Chow"},
	}
	applyLibraryAttributionForGenerate(apiSpec, spec.Person{Handle: "tmchow", Name: "Trevin Chow"})

	assert.Equal(t, "tmchow", apiSpec.Creator.Handle)
	assert.Empty(t, apiSpec.Contributors)
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "spec.yaml")
	outputDir := filepath.Join(dir, "tenderned")
	require.NoError(t, os.WriteFile(specPath, []byte(`name: tenderned
description: Tenderned API
version: 0.1.0
base_url: https://api.example.com
auth:
  type: none
config:
  format: toml
  path: ~/.config/tenderned-pp-cli/config.toml
resources:
  tenders:
    description: Manage tenders
    endpoints:
      list:
        method: GET
        path: /tenders
        description: List tenders
`), 0o644))

	const modulePath = "github.com/mvanhorn/printing-press-library/library/sales-and-crm/tenderned"
	require.NoError(t, os.MkdirAll(outputDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(outputDir, "go.mod"), []byte("module "+modulePath+"\n\ngo 1.26.4\n"), 0o644))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", specPath,
		"--output", outputDir,
		"--validate=false",
		"--force",
	})
	require.NoError(t, cmd.Execute())

	mainSrc, err := os.ReadFile(filepath.Join(outputDir, "cmd", "tenderned-pp-cli", "main.go"))
	require.NoError(t, err)
	assert.Contains(t, string(mainSrc), modulePath+"/internal/cli")
	assert.NotContains(t, string(mainSrc), `"tenderned-pp-cli/internal/cli"`)

	clientSrc, err := os.ReadFile(filepath.Join(outputDir, "internal", "client", "client.go"))
	require.NoError(t, err)
	assert.Contains(t, string(clientSrc), modulePath+"/internal/config")
	assert.Contains(t, string(clientSrc), `"tenderned-pp-cli/0.1.0"`)

	runGoCommandForCLITest(t, outputDir, "mod", "tidy")
	runGoCommandForCLITest(t, outputDir, "build", "./cmd/tenderned-pp-cli")
}

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

	dir := t.TempDir()
	snapshotDir := filepath.Join(dir, "brokenapp.preserve-1")
	freshDir := filepath.Join(dir, "brokenapp")
	for _, root := range []string{snapshotDir, freshDir} {
		require.NoError(t, os.MkdirAll(filepath.Join(root, "internal", "cli"), 0o755))
		require.NoError(t, os.WriteFile(filepath.Join(root, "go.mod"), []byte("module brokenapp\n\ngo 1.22\n"), 0o644))
	}
	require.NoError(t, os.WriteFile(filepath.Join(snapshotDir, "internal", "cli", "root.go"), []byte(`// Generated by CLI Printing Press. DO NOT EDIT.
package cli

func Execute() {
}

var _ = missingSymbol
`), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(freshDir, "internal", "cli", "root.go"), []byte(`// Generated by CLI Printing Press. DO NOT EDIT.
package cli

func Execute() {}
`), 0o644))

	err := finalizeForceMerge(snapshotDir, freshDir, nil, true)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "validating post-merge generated project")
	assert.Contains(t, err.Error(), "go build ./...")
	assert.DirExists(t, snapshotDir, "failed post-merge validation must leave the recovery snapshot in place")
}

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

	assert.Equal(t,
		"github.com/mvanhorn/cli-printing-press/cmd/cli-printing-press",
		forceRegenCommandModulePath("v1.9.0"))
	assert.Equal(t,
		"github.com/mvanhorn/cli-printing-press/v3/cmd/cli-printing-press",
		forceRegenCommandModulePath("3.7.0"))
	assert.Equal(t,
		"github.com/mvanhorn/cli-printing-press/v4/cmd/cli-printing-press",
		forceRegenCommandModulePath("v4.19.0"))
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "device.yaml")
	outputDir := filepath.Join(dir, "renamed-device")
	require.NoError(t, os.WriteFile(specPath, []byte(`version: 1
name: original-device
display_name: Original Device
protocol: ble
ble:
  services:
    - uuid: "ff00"
      characteristics:
        - uuid: "ff01"
          properties: [read]
capabilities:
  telemetry:
    - name: battery
      source_characteristic_uuid: "ff01"
session:
  mode: one-shot
`), 0o644))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", specPath,
		"--name", "renamed-device",
		"--output", outputDir,
		"--validate=false",
	})
	require.NoError(t, cmd.Execute())

	archived, err := os.ReadFile(filepath.Join(outputDir, "device-spec.yaml"))
	require.NoError(t, err)
	assert.Contains(t, string(archived), "name: renamed-device")
	assert.NotContains(t, string(archived), "name: original-device")
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "spec.yaml")
	outputDir := filepath.Join(dir, "regenextras")
	require.NoError(t, os.WriteFile(specPath, []byte(`name: regenextras
description: Regen extras API
version: 0.1.0
base_url: https://api.example.com
auth:
  type: none
config:
  format: toml
  path: ~/.config/regenextras-pp-cli/config.toml
resources:
  markets:
    description: Manage markets
    endpoints:
      list:
        method: GET
        path: /markets
        description: List markets
`), 0o644))

	runGenerate := func() {
		cmd := newGenerateCmd()
		cmd.SetArgs([]string{
			"--spec", specPath,
			"--output", outputDir,
			"--validate=false",
			"--force",
		})
		require.NoError(t, cmd.Execute())
	}

	runGenerate()

	manuscriptPath := filepath.Join(outputDir, ".manuscripts", "20260523-171100", "research.json")
	require.NoError(t, os.MkdirAll(filepath.Dir(manuscriptPath), 0o755))
	require.NoError(t, os.WriteFile(manuscriptPath, []byte(`{"accepted":true}`), 0o640))
	wantModTime := time.Date(2026, 5, 23, 17, 11, 0, 0, time.UTC)
	require.NoError(t, os.Chtimes(manuscriptPath, wantModTime, wantModTime))

	manifestPath := filepath.Join(outputDir, pipeline.CLIManifestFilename)
	require.NoError(t, os.WriteFile(manifestPath, []byte(`{
  "schema_version": 1,
  "api_name": "regenextras",
  "cli_name": "regenextras-pp-cli",
  "category": "other",
  "novel_features": [
    {
      "name": "Market scanner",
      "command": "markets scan",
      "description": "Finds active markets"
    }
  ],
  "operator_note": "published-library override"
}`), 0o644))

	runGenerate()

	gotManuscript, err := os.ReadFile(manuscriptPath)
	require.NoError(t, err)
	assert.JSONEq(t, `{"accepted":true}`, string(gotManuscript))
	info, err := os.Stat(manuscriptPath)
	require.NoError(t, err)
	assert.Equal(t, os.FileMode(0o640), info.Mode().Perm())
	assert.True(t, info.ModTime().Equal(wantModTime), "manuscript mtime should survive force regen")

	data, err := os.ReadFile(manifestPath)
	require.NoError(t, err)
	var manifest pipeline.CLIManifest
	require.NoError(t, json.Unmarshal(data, &manifest))
	assert.Equal(t, "other", manifest.Category)
	require.Len(t, manifest.NovelFeatures, 1)
	assert.Equal(t, "Market scanner", manifest.NovelFeatures[0].Name)

	var raw map[string]json.RawMessage
	require.NoError(t, json.Unmarshal(data, &raw))
	assert.JSONEq(t, `"published-library override"`, string(raw["operator_note"]))
}

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

	researchDir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(researchDir, "research.json"), []byte(`{
  "api_name": "regenextras",
  "novel_features": [
    {
      "name": "Planned scanner",
      "command": "planned scan",
      "description": "Planned feature"
    }
  ],
  "novel_features_built": []
}`), 0o644))

	gen := generator.New(&spec.APISpec{
		Name: "regenextras",
		Auth: spec.AuthConfig{Type: "none"},
	}, t.TempDir())

	got := loadResearchSources(gen, researchDir)
	require.NotNil(t, got, "empty built list is an explicit fresh result, not unavailable metadata")
	assert.Empty(t, got)
	assert.Empty(t, gen.NovelFeatures)
}

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

	cmd := newGenerateCmd()
	var out bytes.Buffer
	cmd.SetOut(&out)
	cmd.SetErr(&out)
	cmd.SetArgs([]string{"--help"})

	require.NoError(t, cmd.Execute())
	assert.Contains(t, out.String(), "Recreate the base output directory while preserving hand-edits to generated files via AST-based merge")
	assert.Contains(t, out.String(), "--strict-refs")
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "missing-ref.yaml")
	outputDir := filepath.Join(dir, "missing-ref")
	require.NoError(t, os.WriteFile(specPath, []byte(`
openapi: 3.0.3
info:
  title: Missing Ref API
  version: 1.0.0
servers:
  - url: https://api.example.com
paths:
  /widgets:
    get:
      operationId: listWidgets
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema:
                type: object
                properties:
                  widget:
                    $ref: "#/components/schemas/MissingWidget"
components:
  schemas:
    Present:
      type: object
      properties:
        id:
          type: string
`), 0o644))

	runGenerate := func(args ...string) error {
		cmd := newGenerateCmd()
		baseArgs := []string{
			"--spec", specPath,
			"--output", outputDir,
			"--dry-run",
		}
		cmd.SetArgs(append(baseArgs, args...))
		return cmd.Execute()
	}

	require.Error(t, runGenerate())
	require.NoError(t, runGenerate("--lenient"))
	require.Error(t, runGenerate("--lenient", "--strict-refs"))
}

// TestGenerateCmdForcePreservesIssue907HandEdits exercises the canonical
// hand-edit shapes that --force must preserve across regen:
//  1. decl-set additions to a templated client file (added function)
//  2. literal-value drift in templated config.go (changed const literal)
//  3. cmd.AddCommand(...) line added to a parent-command file
func TestGenerateCmdForcePreservesIssue907HandEdits(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	specPath := filepath.Join(dir, "spec.yaml")
	outputDir := filepath.Join(dir, "issue907app")
	require.NoError(t, os.WriteFile(specPath, []byte(`name: issue907app
description: Issue 907 fixture API
version: 0.1.0
base_url: https://api.example.com
auth:
  type: none
config:
  format: toml
  path: ~/.config/issue907app-pp-cli/config.toml
resources:
  transactions:
    description: Manage transactions
    endpoints:
      list:
        method: GET
        path: /transactions
        description: List transactions
      get:
        method: GET
        path: /transactions/{id}
        description: Get a transaction
        params:
          - name: id
            type: string
            required: true
            in: path
`), 0o644))

	runGenerate := func() {
		cmd := newGenerateCmd()
		cmd.SetArgs([]string{
			"--spec", specPath,
			"--output", outputDir,
			"--validate=false",
			"--force",
		})
		require.NoError(t, cmd.Execute())
	}

	runGenerate()

	// Case 1: hand-author a function inside the templated client.go so the
	// classifier flags TEMPLATED-WITH-ADDITIONS (decl-set superset).
	clientPath := filepath.Join(outputDir, "internal", "client", "client.go")
	clientBefore, err := os.ReadFile(clientPath)
	require.NoError(t, err)
	clientEdited := append(clientBefore, []byte("\nfunc HandAuthoredHelper() string { return \"hand-edit\" }\n")...)
	require.NoError(t, os.WriteFile(clientPath, clientEdited, 0o644))

	// Case 2: hand-edit the templated config.go to change a const literal.
	configPath := filepath.Join(outputDir, "internal", "config", "config.go")
	configBefore, err := os.ReadFile(configPath)
	require.NoError(t, err)
	configEdited := bytes.Replace(configBefore, []byte(`"Bearer "`), []byte(`"Token "`), 1)
	require.NoError(t, os.WriteFile(configPath, configEdited, 0o644))

	// Case 4: hand-add an AddCommand to the parent-command transactions.go.
	transactionsPath := filepath.Join(outputDir, "internal", "cli", "transactions.go")
	transactionsBefore, err := os.ReadFile(transactionsPath)
	require.NoError(t, err)
	transactionsEdited := bytes.Replace(
		transactionsBefore,
		[]byte("return cmd"),
		[]byte("cmd.AddCommand(newTransactionsHandAddedCmd(flags))\n\treturn cmd"),
		1,
	)
	require.NoError(t, os.WriteFile(transactionsPath, transactionsEdited, 0o644))

	// Hand-add the constructor file so re-injection's referent-existence
	// check passes.
	handAddedPath := filepath.Join(outputDir, "internal", "cli", "transactions_hand_added.go")
	require.NoError(t, os.WriteFile(handAddedPath, []byte(`package cli

import "github.com/spf13/cobra"

func newTransactionsHandAddedCmd(flags *rootFlags) *cobra.Command {
	return &cobra.Command{Use: "hand-added"}
}
`), 0o644))

	runGenerate()

	// Case 1 assertion: hand-authored function survives.
	gotClient, err := os.ReadFile(clientPath)
	require.NoError(t, err)
	assert.Contains(t, string(gotClient), "HandAuthoredHelper",
		"hand-authored function in templated client.go must survive --force regen (TEMPLATED-WITH-ADDITIONS)")

	// Case 2 assertion: literal change survives.
	gotConfig, err := os.ReadFile(configPath)
	require.NoError(t, err)
	if bytes.Contains(configBefore, []byte(`"Bearer "`)) {
		assert.Contains(t, string(gotConfig), `"Token "`,
			"literal value drift in templated config.go must survive --force regen (TEMPLATED-VALUE-DRIFT)")
	}

	// Case 4 assertion: AddCommand line is re-injected into the freshly
	// emitted parent-command file.
	gotTransactions, err := os.ReadFile(transactionsPath)
	require.NoError(t, err)
	assert.Contains(t, string(gotTransactions), "newTransactionsHandAddedCmd",
		"hand-added AddCommand call must be re-injected into fresh transactions.go")
	// And the hand-added constructor file (NOVEL) survives.
	gotHandAdded, err := os.ReadFile(handAddedPath)
	require.NoError(t, err)
	assert.Contains(t, string(gotHandAdded), "newTransactionsHandAddedCmd",
		"novel hand-written constructor file must survive --force regen")

	// Tree must still build after merge.
	runGoCommandForCLITest(t, outputDir, "mod", "tidy")
	runGoCommandForCLITest(t, outputDir, "build", "./cmd/issue907app-pp-cli")
}

// TestGenerateCmdForceCrossSpecFallsBackToNovelOnly exercises R7: when the
// user runs --force against a different spec than the snapshot recorded,
// templated heuristic preservation is skipped (avoiding silent old+new
// endpoint fusion). Novel hand-written files still survive because they are
// spec-orthogonal.
func TestGenerateCmdForceCrossSpecFallsBackToNovelOnly(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	outputDir := filepath.Join(dir, "crossspecapp")
	specA := filepath.Join(dir, "spec_a.yaml")
	specB := filepath.Join(dir, "spec_b.yaml")

	specBody := func(name string) []byte {
		return []byte(`name: ` + name + `
description: Cross-spec fixture API ` + name + `
version: 0.1.0
base_url: https://api.example.com
auth:
  type: none
config:
  format: toml
  path: ~/.config/` + name + `-pp-cli/config.toml
resources:
  items:
    description: Manage items
    endpoints:
      list:
        method: GET
        path: /items
        description: List items
`)
	}
	require.NoError(t, os.WriteFile(specA, specBody("crossspecapp"), 0o644))
	require.NoError(t, os.WriteFile(specB, specBody("crossspecapprenamed"), 0o644))

	runGenerateWith := func(specFile string) {
		cmd := newGenerateCmd()
		cmd.SetArgs([]string{
			"--spec", specFile,
			"--output", outputDir,
			"--validate=false",
			"--force",
		})
		require.NoError(t, cmd.Execute())
	}

	runGenerateWith(specA)

	// Hand-edit a templated config.go (literal drift).
	configPath := filepath.Join(outputDir, "internal", "config", "config.go")
	configBefore, err := os.ReadFile(configPath)
	require.NoError(t, err)
	configEdited := bytes.Replace(configBefore, []byte(`"Bearer "`), []byte(`"Token "`), 1)
	require.NoError(t, os.WriteFile(configPath, configEdited, 0o644))

	// Add a novel hand-written file (no marker, will be NOVEL).
	novelPath := filepath.Join(outputDir, "internal", "cli", "novel_helper.go")
	require.NoError(t, os.WriteFile(novelPath, []byte(`package cli

func novelHelperFn() string { return "kept" }
`), 0o644))

	runGenerateWith(specB)

	// Cross-spec: literal drift NOT preserved (NovelOnly skips
	// TEMPLATED-VALUE-DRIFT).
	gotConfig, err := os.ReadFile(configPath)
	require.NoError(t, err)
	if bytes.Contains(configBefore, []byte(`"Bearer "`)) {
		assert.NotContains(t, string(gotConfig), `"Token "`,
			"cross-spec --force must NOT preserve TEMPLATED-VALUE-DRIFT (silent fusion guard)")
	}

	// Novel file still preserved.
	gotNovel, err := os.ReadFile(novelPath)
	require.NoError(t, err)
	assert.Contains(t, string(gotNovel), "novelHelperFn",
		"novel hand-written file is spec-orthogonal and must survive cross-spec regen")
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "spec.yaml")
	outputDir := filepath.Join(dir, "symlinkapp")
	require.NoError(t, os.WriteFile(specPath, []byte(`name: symlinkapp
description: Symlink app API
version: 0.1.0
base_url: https://api.example.com
auth:
  type: none
config:
  format: toml
  path: ~/.config/symlinkapp-pp-cli/config.toml
resources:
  items:
    description: Manage items
    endpoints:
      list:
        method: GET
        path: /items
        description: List items
`), 0o644))

	runGenerate := func() error {
		cmd := newGenerateCmd()
		cmd.SetArgs([]string{
			"--spec", specPath,
			"--output", outputDir,
			"--validate=false",
			"--force",
		})
		return cmd.Execute()
	}

	require.NoError(t, runGenerate())

	externalTarget := filepath.Join(dir, "outside.go")
	require.NoError(t, os.WriteFile(externalTarget, []byte("package cli\n"), 0o644))
	linkPath := filepath.Join(outputDir, "internal", "cli", "novel_link.go")
	if err := os.Symlink(externalTarget, linkPath); err != nil {
		t.Skipf("symlink unavailable: %v", err)
	}

	err := runGenerate()
	require.Error(t, err)
	assert.Contains(t, err.Error(), "refusing to snapshot symlinked internal/cli file")
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "spec.yaml")
	outputDir := filepath.Join(dir, "symlinksiblingapp")
	require.NoError(t, os.WriteFile(specPath, []byte(`name: symlinksiblingapp
description: Symlink sibling app API
version: 0.1.0
base_url: https://api.example.com
auth:
  type: none
config:
  format: toml
  path: ~/.config/symlinksiblingapp-pp-cli/config.toml
resources:
  items:
    description: Manage items
    endpoints:
      list:
        method: GET
        path: /items
        description: List items
`), 0o644))

	runGenerate := func() error {
		cmd := newGenerateCmd()
		cmd.SetArgs([]string{
			"--spec", specPath,
			"--output", outputDir,
			"--validate=false",
			"--force",
		})
		return cmd.Execute()
	}

	require.NoError(t, runGenerate())

	externalSibling := filepath.Join(dir, "external-source")
	require.NoError(t, os.MkdirAll(externalSibling, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(externalSibling, "client.go"), []byte("package source\n"), 0o644))
	if err := os.Symlink(externalSibling, filepath.Join(outputDir, "internal", "source")); err != nil {
		t.Skipf("symlink unavailable: %v", err)
	}

	err := runGenerate()
	require.Error(t, err)
	assert.Contains(t, err.Error(), "refusing to snapshot symlinked internal sibling package")
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "spec.yaml")
	outputDir := filepath.Join(dir, "symlinkparentapp")
	require.NoError(t, os.WriteFile(specPath, []byte(`name: symlinkparentapp
description: Symlink parent app API
version: 0.1.0
base_url: https://api.example.com
auth:
  type: none
config:
  format: toml
  path: ~/.config/symlinkparentapp-pp-cli/config.toml
resources:
  items:
    description: Manage items
    endpoints:
      list:
        method: GET
        path: /items
        description: List items
`), 0o644))

	runGenerate := func() error {
		cmd := newGenerateCmd()
		cmd.SetArgs([]string{
			"--spec", specPath,
			"--output", outputDir,
			"--validate=false",
			"--force",
		})
		return cmd.Execute()
	}

	require.NoError(t, runGenerate())

	externalInternal := filepath.Join(dir, "external-internal")
	require.NoError(t, os.MkdirAll(filepath.Join(externalInternal, "cli"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(externalInternal, "cli", "novel.go"), []byte("package cli\n"), 0o644))
	require.NoError(t, os.RemoveAll(filepath.Join(outputDir, "internal")))
	if err := os.Symlink(externalInternal, filepath.Join(outputDir, "internal")); err != nil {
		t.Skipf("symlink unavailable: %v", err)
	}

	err := runGenerate()
	require.Error(t, err)
	assert.Contains(t, err.Error(), "refusing to snapshot symlinked internal")
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "spec.yaml")
	analysisPath := filepath.Join(dir, "traffic-analysis.json")
	outputDir := filepath.Join(dir, "analysisapp")

	require.NoError(t, os.WriteFile(specPath, []byte(`name: analysisapp
description: Analysis app API
version: 0.1.0
base_url: https://api.example.com
auth:
  type: none
config:
  format: toml
  path: ~/.config/analysisapp-pp-cli/config.toml
resources:
  items:
    description: Manage items
    endpoints:
      list:
        method: GET
        path: /items
        description: List items
`), 0o644))
	require.NoError(t, os.WriteFile(analysisPath, []byte(`{
  "version": "1",
  "summary": {
    "target_url": "https://example.com/app?token=secret#frag",
    "entry_count": 4,
    "api_entry_count": 2
  },
  "protocols": [
    {"label": "rest_json", "confidence": 0.9}
  ],
  "generation_hints": ["requires_browser_auth"],
  "warnings": [
    {"type": "weak_schema_evidence", "message": "Only one response shape was captured.", "confidence": 0.7}
  ],
  "candidate_commands": [
    {"name": "items-list", "rationale": "Observed successful list traffic.", "confidence": 0.8}
  ]
}`), 0o644))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", specPath,
		"--output", outputDir,
		"--validate=false",
		"--force",
		"--spec-source", "browser-sniffed",
		"--traffic-analysis", analysisPath,
	})

	require.NoError(t, cmd.Execute())

	readme, err := os.ReadFile(filepath.Join(outputDir, "README.md"))
	require.NoError(t, err)
	assert.Contains(t, string(readme), "## Discovery Signals")
	assert.Contains(t, string(readme), "Target observed: https://example.com/app")
	assert.NotContains(t, string(readme), "token=secret")
	assert.Contains(t, string(readme), "requires_browser_auth")
	assert.Contains(t, string(readme), "weak_schema_evidence")

	skill, err := os.ReadFile(filepath.Join(outputDir, "SKILL.md"))
	require.NoError(t, err)
	assert.Contains(t, string(skill), "## Discovery Signals")
	assert.Contains(t, string(skill), "requires_browser_auth")

	agentContext, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "agent_context.go"))
	require.NoError(t, err)
	assert.Contains(t, string(agentContext), `Source:        "traffic-analysis"`)
	assert.Contains(t, string(agentContext), `TargetURL:     "https://example.com/app"`)
	assert.NotContains(t, string(agentContext), "token=secret")
	assert.Contains(t, string(agentContext), `requires_browser_auth`)
	_, err = parser.ParseFile(token.NewFileSet(), "agent_context.go", agentContext, parser.ParseComments)
	require.NoError(t, err)
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "spec.yaml")
	researchDir := filepath.Join(dir, "research")
	outputDir := filepath.Join(dir, "novelapp")
	require.NoError(t, os.MkdirAll(researchDir, 0o755))

	require.NoError(t, os.WriteFile(specPath, []byte(`name: novelapp
description: Novel app API
version: 0.1.0
base_url: https://api.example.com
auth:
  type: none
config:
  format: toml
  path: ~/.config/novelapp-pp-cli/config.toml
resources:
  items:
    description: Manage items
    endpoints:
      list:
        method: GET
        path: /items
        description: List items
`), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(researchDir, "research.json"), []byte(`{
  "api_name": "novelapp",
  "novelty_score": 8,
  "alternatives": [],
  "gaps": [],
  "patterns": [],
  "recommendation": "proceed",
  "researched_at": "2026-04-25T00:00:00Z",
  "novel_features": [
    {
      "name": "Item insight",
      "command": "items insight",
      "description": "Summarize item state",
      "rationale": "Requires local correlation"
    }
  ]
}`), 0o644))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", specPath,
		"--output", outputDir,
		"--validate=false",
		"--force",
		"--research-dir", researchDir,
	})

	require.NoError(t, cmd.Execute())

	data, err := os.ReadFile(filepath.Join(outputDir, pipeline.CLIManifestFilename))
	require.NoError(t, err)
	var manifest pipeline.CLIManifest
	require.NoError(t, json.Unmarshal(data, &manifest))
	assert.Empty(t, manifest.NovelFeatures)

	root, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "root.go"))
	require.NoError(t, err)
	assert.NotContains(t, string(root), "newNovelItemsCmd")

	parent, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "promoted_items.go"))
	require.NoError(t, err)
	assert.Contains(t, string(parent), "cmd.AddCommand(newNovelItemsInsightCmd(flags))")
	stub, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "items_insight.go"))
	require.NoError(t, err)
	assert.Contains(t, string(stub), `Use:         "insight"`)
	assert.Contains(t, string(stub), `TODO: implement novel feature %q", "items insight"`)
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "spec.yaml")
	researchDir := filepath.Join(dir, "research")
	outputDir := filepath.Join(dir, "verifiedapp")
	require.NoError(t, os.MkdirAll(researchDir, 0o755))

	require.NoError(t, os.WriteFile(specPath, []byte(`name: verifiedapp
description: Verified app API
version: 0.1.0
base_url: https://api.example.com
auth:
  type: none
config:
  format: toml
  path: ~/.config/verifiedapp-pp-cli/config.toml
resources:
  items:
    description: Manage items
    endpoints:
      list:
        method: GET
        path: /items
        description: List items
`), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(researchDir, "research.json"), []byte(`{
  "api_name": "verifiedapp",
  "novelty_score": 8,
  "alternatives": [],
  "gaps": [],
  "patterns": [],
  "recommendation": "proceed",
  "researched_at": "2026-04-25T00:00:00Z",
  "novel_features": [
    {
      "name": "Planned insight",
      "command": "items planned",
      "description": "Planned but not verified",
      "rationale": "Requires local correlation"
    }
  ],
  "novel_features_built": [
    {
      "name": "Built insight",
      "command": "items insight",
      "description": "Summarize item state",
      "rationale": "Requires local correlation"
    }
  ]
}`), 0o644))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", specPath,
		"--output", outputDir,
		"--validate=false",
		"--force",
		"--research-dir", researchDir,
	})

	require.NoError(t, cmd.Execute())

	data, err := os.ReadFile(filepath.Join(outputDir, pipeline.CLIManifestFilename))
	require.NoError(t, err)
	var manifest pipeline.CLIManifest
	require.NoError(t, json.Unmarshal(data, &manifest))
	require.Len(t, manifest.NovelFeatures, 1)
	assert.Equal(t, "Built insight", manifest.NovelFeatures[0].Name)
	assert.Equal(t, "items insight", manifest.NovelFeatures[0].Command)

	which, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "which.go"))
	require.NoError(t, err)
	assert.Contains(t, string(which), `Command: "items insight"`)
	assert.NotContains(t, string(which), "items planned")
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "spec.yaml")
	analysisPath := filepath.Join(dir, "traffic-analysis.json")
	outputDir := filepath.Join(dir, "clearanceapp")

	require.NoError(t, os.WriteFile(specPath, []byte(`name: clearanceapp
description: Clearance app API
version: 0.1.0
base_url: https://www.producthunt.com
auth:
  type: none
config:
  format: toml
  path: ~/.config/clearanceapp-pp-cli/config.toml
resources:
  posts:
    description: Manage posts
    endpoints:
      list:
        method: GET
        path: /frontend/graphql
        description: List posts
`), 0o644))
	require.NoError(t, os.WriteFile(analysisPath, []byte(`{
  "version": "1",
  "summary": {
    "target_url": "https://www.producthunt.com",
    "entry_count": 1,
    "api_entry_count": 1,
    "http_version_distribution": {"h2": 9}
  },
  "reachability": {
    "mode": "browser_clearance_http",
    "confidence": 0.9,
    "reasons": ["managed bot challenge observed"]
  },
  "generation_hints": ["browser_clearance_required", "graphql_persisted_query"]
}`), 0o644))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", specPath,
		"--output", outputDir,
		"--validate=false",
		"--force",
		"--traffic-analysis", analysisPath,
	})

	require.NoError(t, cmd.Execute())

	gomod, err := os.ReadFile(filepath.Join(outputDir, "go.mod"))
	require.NoError(t, err)
	assert.Contains(t, string(gomod), "github.com/enetx/surf")

	authGo, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "auth.go"))
	require.NoError(t, err)
	assert.Contains(t, string(authGo), "auth login --chrome")
	assert.Contains(t, string(authGo), "auth login --browser")
	assert.Contains(t, string(authGo), "auth refresh")
	assert.Contains(t, string(authGo), "auth refresh-queries")
	assert.Contains(t, string(authGo), ".producthunt.com")
	assert.Contains(t, string(authGo), "Could not close temporary browser capture session")
	assert.NotContains(t, string(authGo), "newAuthCloseBrowserCmd")

	clientGo, err := os.ReadFile(filepath.Join(outputDir, "internal", "client", "client.go"))
	require.NoError(t, err)
	assert.NotContains(t, string(clientGo), `req.Header.Set("User-Agent"`)
	assert.Contains(t, string(clientGo), `"github.com/enetx/surf"`)
	// HAR distribution declares H/2 majority -> ForceHTTP2 is emitted and
	// ForceHTTP3 is not. With an empty distribution the bare browser-chrome
	// enum would emit neither; the fixture above pins the HAR-driven path.
	assert.Contains(t, string(clientGo), "ForceHTTP2()")
	assert.NotContains(t, string(clientGo), "ForceHTTP3()")
	assert.NotContains(t, string(clientGo), "runBrowserUseFetch")
	assert.NotContains(t, string(clientGo), "runAgentBrowserFetch")
	assert.NotContains(t, string(clientGo), "browser runtime required")

	doctorGo, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "doctor.go"))
	require.NoError(t, err)
	assert.NotContains(t, string(doctorGo), "doctorBrowserRuntimeStatus")

	configGo, err := os.ReadFile(filepath.Join(outputDir, "internal", "config", "config.go"))
	require.NoError(t, err)
	assert.NotContains(t, string(configGo), "BrowserRuntime")

	agentContext, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "agent_context.go"))
	require.NoError(t, err)
	assert.Regexp(t, `Reachability:\s+"browser_clearance_http \(90% confidence\)"`, string(agentContext))
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "spec.yaml")
	analysisPath := filepath.Join(dir, "traffic-analysis.json")
	outputDir := filepath.Join(dir, "clearancepostapp")

	require.NoError(t, os.WriteFile(specPath, []byte(`name: clearancepostapp
description: Clearance POST app API
version: 0.1.0
base_url: https://www.producthunt.com
auth:
  type: none
config:
  format: toml
  path: ~/.config/clearancepostapp-pp-cli/config.toml
resources:
  graphql:
    description: GraphQL endpoint
    endpoints:
      query:
        method: POST
        path: /frontend/graphql
        description: Run GraphQL query
        body:
          - name: body
            type: object
            required: true
`), 0o644))
	require.NoError(t, os.WriteFile(analysisPath, []byte(`{
  "version": "1",
  "summary": {
    "target_url": "https://www.producthunt.com",
    "entry_count": 1,
    "api_entry_count": 1,
    "http_version_distribution": {"h2": 9}
  },
  "reachability": {
    "mode": "browser_clearance_http",
    "confidence": 0.9,
    "reasons": ["managed bot challenge observed"]
  },
  "generation_hints": ["browser_clearance_required", "graphql_persisted_query"]
}`), 0o644))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", specPath,
		"--output", outputDir,
		"--validate=false",
		"--force",
		"--traffic-analysis", analysisPath,
	})

	require.NoError(t, cmd.Execute())

	clientGo, err := os.ReadFile(filepath.Join(outputDir, "internal", "client", "client.go"))
	require.NoError(t, err)
	assert.Contains(t, string(clientGo), `"github.com/enetx/surf"`)
	// HAR distribution declares H/2 majority -> ForceHTTP2 emits.
	assert.Contains(t, string(clientGo), "ForceHTTP2()")
	assert.NotContains(t, string(clientGo), "ForceHTTP3()")
	assert.NotContains(t, string(clientGo), "runBrowserUseFetch")
	assert.NotContains(t, string(clientGo), "runAgentBrowserFetch")

	authGo, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "auth.go"))
	require.NoError(t, err)
	assert.NotContains(t, string(authGo), "no browser-session validation endpoint configured")
	assert.NotContains(t, string(authGo), "browser-session-proof.json")
	assert.NotContains(t, string(authGo), "newAuthCloseBrowserCmd")
	assert.NotContains(t, string(authGo), "auth close-browser")
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "spec.yaml")
	analysisPath := filepath.Join(dir, "traffic-analysis.json")
	outputDir := filepath.Join(dir, "browserrequiredapp")

	require.NoError(t, os.WriteFile(specPath, []byte(`name: browserrequiredapp
description: Browser required app API
version: 0.1.0
base_url: https://www.example.com
auth:
  type: none
config:
  format: toml
  path: ~/.config/browserrequiredapp-pp-cli/config.toml
resources:
  pages:
    description: Manage pages
    endpoints:
      list:
        method: GET
        path: /app
        description: List pages
`), 0o644))
	require.NoError(t, os.WriteFile(analysisPath, []byte(`{
  "version": "1",
  "summary": {
    "target_url": "https://www.example.com/app",
    "entry_count": 1,
    "api_entry_count": 1
  },
  "reachability": {
    "mode": "browser_required",
    "confidence": 0.85,
    "reasons": ["page-context execution required"]
  },
  "generation_hints": ["requires_page_context"]
}`), 0o644))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", specPath,
		"--output", outputDir,
		"--validate=false",
		"--force",
		"--traffic-analysis", analysisPath,
	})

	err := cmd.Execute()
	require.Error(t, err)
	assert.Contains(t, err.Error(), "requires live browser page-context execution")
	assert.Contains(t, err.Error(), "not a shippable printed CLI runtime")
}

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

	rootSpec := &spec.APISpec{
		Name:    "core",
		Version: "0.1.0",
		BaseURL: "https://tenant.example.com",
		Resources: map[string]spec.Resource{
			"projects": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/projects"},
				},
			},
		},
		Types: map[string]spec.TypeDef{},
	}
	wikiSpec := &spec.APISpec{
		Name:    "wiki",
		Version: "0.1.0",
		BaseURL: "https://tenant.example.com/wiki/api/v2",
		Resources: map[string]spec.Resource{
			"pages": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/pages"},
				},
				SubResources: map[string]spec.Resource{
					"comments": {
						Endpoints: map[string]spec.Endpoint{
							"list": {Method: "GET", Path: "/pages/{page_id}/comments"},
						},
					},
				},
			},
		},
		Types: map[string]spec.TypeDef{},
	}

	merged := mergeSpecs([]*spec.APISpec{rootSpec, wikiSpec}, "combo")

	assert.Equal(t, "https://tenant.example.com", merged.BaseURL)
	assert.Empty(t, merged.Resources["projects"].BaseURL)
	assert.Empty(t, merged.Resources["pages"].BaseURL)
	assert.Empty(t, merged.Resources["pages"].SubResources["comments"].BaseURL)
	assert.Equal(t, "/wiki/api/v2/pages", merged.Resources["pages"].Endpoints["list"].Path)
	assert.Equal(
		t,
		"/wiki/api/v2/pages/{page_id}/comments",
		merged.Resources["pages"].SubResources["comments"].Endpoints["list"].Path,
	)
}

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

	specA := &spec.APISpec{
		Name:    "admin",
		Version: "0.1.0",
		BaseURL: "https://analytics.example.com",
		Resources: map[string]spec.Resource{
			"accounts": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/v1beta/accounts"},
				},
			},
			"reports": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/v1beta/reports"},
				},
			},
		},
		Types: map[string]spec.TypeDef{},
	}
	specB := &spec.APISpec{
		Name:    "analytics-admin",
		Version: "0.1.0",
		BaseURL: "https://analytics.example.com",
		Resources: map[string]spec.Resource{
			"accounts": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/v1beta/accounts"},
				},
			},
			"metadata": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/v1beta/metadata"},
				},
			},
		},
		Types: map[string]spec.TypeDef{},
	}

	merged := mergeSpecs([]*spec.APISpec{specA, specB}, "ga4")

	assert.Contains(t, merged.Resources, "accounts")
	assert.NotContains(t, merged.Resources, "analytics-admin-accounts")
	assert.Equal(t, "/v1beta/accounts", merged.Resources["accounts"].Endpoints["list"].Path)
}

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

	specA := &spec.APISpec{
		Name:    "admin",
		Version: "0.1.0",
		BaseURL: "https://analytics.example.com",
		Resources: map[string]spec.Resource{
			"accounts": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/v1beta/accounts"},
				},
			},
			"reports": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/v1beta/reports"},
				},
			},
		},
		Types: map[string]spec.TypeDef{},
	}
	specB := &spec.APISpec{
		Name:    "analytics-admin",
		Version: "0.1.0",
		BaseURL: "https://analytics.example.com",
		Resources: map[string]spec.Resource{
			"accounts": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/v1alpha/accounts"},
				},
			},
			"metadata": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/v1alpha/metadata"},
				},
			},
		},
		Types: map[string]spec.TypeDef{},
	}

	merged := mergeSpecs([]*spec.APISpec{specA, specB}, "ga4")

	assert.Contains(t, merged.Resources, "accounts")
	assert.Contains(t, merged.Resources, "analytics-admin-accounts")
	assert.Equal(t, "/v1beta/accounts", merged.Resources["accounts"].Endpoints["list"].Path)
	assert.Equal(t, "/v1alpha/accounts", merged.Resources["analytics-admin-accounts"].Endpoints["list"].Path)
}

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

	specA := &spec.APISpec{
		Name:    "admin",
		Version: "0.1.0",
		BaseURL: "https://analytics.example.com",
		Resources: map[string]spec.Resource{
			"accounts": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/v1beta/accounts"},
				},
			},
			"reports": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/v1beta/reports"},
				},
			},
		},
		Types: map[string]spec.TypeDef{},
	}
	specB := &spec.APISpec{
		Name:    "analytics-admin",
		Version: "0.1.0",
		BaseURL: "https://analytics.example.com",
		Resources: map[string]spec.Resource{
			"accounts": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/v1beta/accounts/"},
				},
			},
			"metadata": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/v1beta/metadata"},
				},
			},
		},
		Types: map[string]spec.TypeDef{},
	}

	merged := mergeSpecs([]*spec.APISpec{specA, specB}, "ga4")

	assert.Contains(t, merged.Resources, "accounts")
	assert.NotContains(t, merged.Resources, "analytics-admin-accounts")
	assert.Equal(t, "/v1beta/accounts", merged.Resources["accounts"].Endpoints["list"].Path)
}

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

	specA := &spec.APISpec{
		Name:    "admin",
		Version: "0.1.0",
		BaseURL: "https://analytics.example.com",
		Resources: map[string]spec.Resource{
			"accounts": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/v1beta/accounts"},
				},
			},
			"reports": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/v1beta/reports"},
				},
			},
		},
		Types: map[string]spec.TypeDef{},
	}
	specB := &spec.APISpec{
		Name:    "analytics-admin",
		Version: "0.1.0",
		BaseURL: "https://analytics.example.com",
		Resources: map[string]spec.Resource{
			"accounts": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/v1beta/accounts"},
				},
			},
			"metadata": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/v1beta/metadata"},
				},
			},
		},
		Types: map[string]spec.TypeDef{},
	}

	merged := mergeSpecsWithOptions([]*spec.APISpec{specA, specB}, "ga4", mergeSpecOptions{NamePrefix: true})

	assert.NotContains(t, merged.Resources, "accounts")
	assert.Contains(t, merged.Resources, "admin-accounts")
	assert.Contains(t, merged.Resources, "analytics-admin-accounts")
	assert.Equal(t, "/v1beta/accounts", merged.Resources["admin-accounts"].Endpoints["list"].Path)
	assert.Equal(t, "/v1beta/accounts", merged.Resources["analytics-admin-accounts"].Endpoints["list"].Path)
}

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

	specA := &spec.APISpec{
		Name:    "admin",
		Version: "0.1.0",
		BaseURL: "https://analytics.example.com",
		Resources: map[string]spec.Resource{
			"accounts": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/v1beta/accounts"},
				},
			},
		},
		Types: map[string]spec.TypeDef{},
	}
	specB := &spec.APISpec{
		Name:    "admin",
		Version: "0.1.0",
		BaseURL: "https://analytics.example.com",
		Resources: map[string]spec.Resource{
			"accounts": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/v1alpha/accounts"},
				},
			},
		},
		Types: map[string]spec.TypeDef{},
	}

	merged := mergeSpecsWithOptions([]*spec.APISpec{specA, specB}, "ga4", mergeSpecOptions{NamePrefix: true})

	assert.Contains(t, merged.Resources, "admin-accounts")
	assert.Contains(t, merged.Resources, "admin-accounts-2")
	assert.Equal(t, "/v1beta/accounts", merged.Resources["admin-accounts"].Endpoints["list"].Path)
	assert.Equal(t, "/v1alpha/accounts", merged.Resources["admin-accounts-2"].Endpoints["list"].Path)
}

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

	dir := t.TempDir()
	specAPath := filepath.Join(dir, "admin.yaml")
	specBPath := filepath.Join(dir, "analytics-admin.yaml")
	outputDir := filepath.Join(dir, "ga4")
	specA := `name: admin
description: Admin API
version: 0.1.0
base_url: https://analytics.example.com
auth:
  type: none
config:
  format: toml
  path: ~/.config/admin-pp-cli/config.toml
resources:
  accounts:
    description: Accounts
    endpoints:
      list:
        method: GET
        path: /v1beta/accounts
        description: List accounts
  reports:
    description: Reports
    endpoints:
      list:
        method: GET
        path: /v1beta/reports
        description: List reports
`
	specB := `name: analytics-admin
description: Analytics Admin API
version: 0.1.0
base_url: https://analytics.example.com
auth:
  type: none
config:
  format: toml
  path: ~/.config/analytics-admin-pp-cli/config.toml
resources:
  accounts:
    description: Accounts
    endpoints:
      list:
        method: GET
        path: /v1beta/accounts
        description: List accounts
  metadata:
    description: Metadata
    endpoints:
      list:
        method: GET
        path: /v1beta/metadata
        description: List metadata
`
	require.NoError(t, os.WriteFile(specAPath, []byte(specA), 0o644))
	require.NoError(t, os.WriteFile(specBPath, []byte(specB), 0o644))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", specAPath,
		"--spec", specBPath,
		"--name", "ga4",
		"--output", outputDir,
		"--validate=false",
		"--force",
	})

	require.NoError(t, cmd.Execute())
	root, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "root.go"))
	require.NoError(t, err)
	assert.Contains(t, string(root), "newAccountsPromotedCmd(flags)")
	assert.NotContains(t, string(root), "newAnalyticsAdminAccountsPromotedCmd(flags)")
}

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

	primary := &spec.APISpec{
		Name:    "primary",
		Version: "0.1.0",
		BaseURL: "https://api.example.com",
		Auth: spec.AuthConfig{
			Type:             "bearer_token",
			Header:           "Authorization",
			AuthorizationURL: "https://accounts.example.com/auth",
			TokenURL:         "https://accounts.example.com/token",
			Scopes:           []string{"read.primary"},
			AdditionalHeaders: []spec.AdditionalAuthHeader{
				{Header: "X-Primary-Key", EnvVar: spec.AuthEnvVar{Name: "PRIMARY_KEY", Kind: spec.AuthEnvVarKindPerCall, Required: true, Sensitive: true}},
			},
		},
		Resources: map[string]spec.Resource{
			"primary": {Endpoints: map[string]spec.Endpoint{"list": {Method: "GET", Path: "/primary"}}},
		},
		Types: map[string]spec.TypeDef{},
	}
	secondary := &spec.APISpec{
		Name:    "secondary",
		Version: "0.1.0",
		BaseURL: "https://api.example.com",
		Auth: spec.AuthConfig{
			Type:             "bearer_token",
			Header:           "Authorization",
			AuthorizationURL: "https://accounts.example.com/auth",
			TokenURL:         "https://accounts.example.com/token",
			Scopes:           []string{"read.secondary", "read.primary"},
			AdditionalHeaders: []spec.AdditionalAuthHeader{
				{Header: "X-Secondary-Key", EnvVar: spec.AuthEnvVar{Name: "SECONDARY_KEY", Kind: spec.AuthEnvVarKindPerCall, Required: true, Sensitive: true}},
				{Header: "X-Primary-Key", EnvVar: spec.AuthEnvVar{Name: "PRIMARY_KEY", Kind: spec.AuthEnvVarKindPerCall, Required: true, Sensitive: true}},
			},
		},
		Resources: map[string]spec.Resource{
			"secondary": {Endpoints: map[string]spec.Endpoint{"list": {Method: "GET", Path: "/secondary"}}},
		},
		Types: map[string]spec.TypeDef{},
	}
	secondaryQuerySibling := &spec.APISpec{
		Name:    "secondaryquery",
		Version: "0.1.0",
		BaseURL: "https://api.example.com",
		Auth: spec.AuthConfig{
			Type:   "bearer_token",
			Header: "Authorization",
			AdditionalHeaders: []spec.AdditionalAuthHeader{
				{Header: "secondary_token", In: "query", EnvVar: spec.AuthEnvVar{Name: "SECONDARY_QUERY_TOKEN", Kind: spec.AuthEnvVarKindPerCall, Required: true, Sensitive: true}},
			},
		},
		Resources: map[string]spec.Resource{
			"secondaryquery": {Endpoints: map[string]spec.Endpoint{"list": {Method: "GET", Path: "/secondaryquery"}}},
		},
		Types: map[string]spec.TypeDef{},
	}
	standaloneKey := &spec.APISpec{
		Name:    "standalone",
		Version: "0.1.0",
		BaseURL: "https://api.example.com",
		Auth: spec.AuthConfig{
			Type:   "api_key",
			Header: "X-Standalone-Key",
			In:     "header",
			Scheme: "StandaloneKey",
			EnvVarSpecs: []spec.AuthEnvVar{
				{Name: " STANDALONE_KEY ", Required: true, Sensitive: true},
			},
		},
		Resources: map[string]spec.Resource{
			"standalone": {Endpoints: map[string]spec.Endpoint{"list": {Method: "GET", Path: "/standalone"}}},
		},
		Types: map[string]spec.TypeDef{},
	}
	queryKey := &spec.APISpec{
		Name:    "querykey",
		Version: "0.1.0",
		BaseURL: "https://api.example.com",
		Auth: spec.AuthConfig{
			Type:   "api_key",
			Header: "api_key",
			In:     "query",
			EnvVarSpecs: []spec.AuthEnvVar{
				{Name: "QUERY_KEY", Kind: spec.AuthEnvVarKindPerCall, Required: true, Sensitive: true},
			},
		},
		Resources: map[string]spec.Resource{
			"querykey": {Endpoints: map[string]spec.Endpoint{"list": {Method: "GET", Path: "/querykey"}}},
		},
		Types: map[string]spec.TypeDef{},
	}
	ambiguousKey := &spec.APISpec{
		Name:    "ambiguous",
		Version: "0.1.0",
		BaseURL: "https://api.example.com",
		Auth: spec.AuthConfig{
			Type:   "api_key",
			Header: "X-Ambiguous-Key",
			In:     "header",
			EnvVarSpecs: []spec.AuthEnvVar{
				{Name: "AMBIGUOUS_KEY", Kind: spec.AuthEnvVarKindPerCall, Required: true, Sensitive: true},
				{Name: "AMBIGUOUS_SECRET", Kind: spec.AuthEnvVarKindPerCall, Required: true, Sensitive: true},
			},
		},
		Resources: map[string]spec.Resource{
			"ambiguous": {Endpoints: map[string]spec.Endpoint{"list": {Method: "GET", Path: "/ambiguous"}}},
		},
		Types: map[string]spec.TypeDef{},
	}
	crossHostKey := &spec.APISpec{
		Name:    "crosshost",
		Version: "0.1.0",
		BaseURL: "https://other.example.net",
		Auth: spec.AuthConfig{
			Type:   "api_key",
			Header: "X-Cross-Host-Key",
			In:     "header",
			EnvVarSpecs: []spec.AuthEnvVar{
				{Name: "CROSS_HOST_KEY", Kind: spec.AuthEnvVarKindPerCall, Required: true, Sensitive: true},
			},
		},
		Resources: map[string]spec.Resource{
			"crosshost": {Endpoints: map[string]spec.Endpoint{"list": {Method: "GET", Path: "/crosshost"}}},
		},
		Types: map[string]spec.TypeDef{},
	}
	foreignOAuth := &spec.APISpec{
		Name:    "foreignoauth",
		Version: "0.1.0",
		BaseURL: "https://api.example.com",
		Auth: spec.AuthConfig{
			Type:             "bearer_token",
			Header:           "Authorization",
			AuthorizationURL: "https://login.other.example.net/auth",
			TokenURL:         "https://login.other.example.net/token",
			Scopes:           []string{"read.foreign"},
		},
		Resources: map[string]spec.Resource{
			"foreignoauth": {Endpoints: map[string]spec.Endpoint{"list": {Method: "GET", Path: "/foreignoauth"}}},
		},
		Types: map[string]spec.TypeDef{},
	}
	noFlowOAuth := &spec.APISpec{
		Name:    "noflowoauth",
		Version: "0.1.0",
		BaseURL: "https://api.example.com",
		Auth: spec.AuthConfig{
			Type:   "bearer_token",
			Header: "Authorization",
			Scopes: []string{"read.noflow"},
		},
		Resources: map[string]spec.Resource{
			"noflowoauth": {Endpoints: map[string]spec.Endpoint{"list": {Method: "GET", Path: "/noflowoauth"}}},
		},
		Types: map[string]spec.TypeDef{},
	}

	merged := mergeSpecs([]*spec.APISpec{primary, secondary, secondaryQuerySibling, standaloneKey, queryKey, ambiguousKey, crossHostKey, foreignOAuth, noFlowOAuth}, "combo")

	assert.Equal(t, []string{"read.primary", "read.secondary"}, merged.Auth.Scopes)
	require.Len(t, merged.Auth.AdditionalHeaders, 3)
	assertAdditionalAuthHeader(t, merged.Auth.AdditionalHeaders, "X-Primary-Key", "PRIMARY_KEY")
	assertAdditionalAuthHeader(t, merged.Auth.AdditionalHeaders, "X-Secondary-Key", "SECONDARY_KEY")
	assertAdditionalAuthHeader(t, merged.Auth.AdditionalHeaders, "X-Standalone-Key", "STANDALONE_KEY")
	assertNoAdditionalAuthHeader(t, merged.Auth.AdditionalHeaders, "secondary_token")
	assert.Equal(t, " STANDALONE_KEY ", standaloneKey.Auth.EnvVarSpecs[0].Name)
	assert.Empty(t, standaloneKey.Auth.EnvVarSpecs[0].Kind)
}

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

	trelloShaped := &spec.APISpec{
		Name:    "trello",
		Version: "0.1.0",
		BaseURL: "https://api.trello.com/1",
		Auth: spec.AuthConfig{
			Type:   "api_key",
			Header: "key",
			In:     "query",
			EnvVarSpecs: []spec.AuthEnvVar{
				{Name: "TRELLO_API_KEY", Kind: spec.AuthEnvVarKindPerCall, Required: true, Sensitive: true},
			},
			AdditionalHeaders: []spec.AdditionalAuthHeader{
				{Header: "token", In: "query", EnvVar: spec.AuthEnvVar{Name: "TRELLO_TOKEN", Kind: spec.AuthEnvVarKindPerCall, Required: true, Sensitive: true}},
				{Header: "X-Mergeable-Key", In: "header", EnvVar: spec.AuthEnvVar{Name: "MERGEABLE_KEY", Kind: spec.AuthEnvVarKindPerCall, Required: true, Sensitive: true}},
			},
		},
		Resources: map[string]spec.Resource{
			"cards": {Endpoints: map[string]spec.Endpoint{"list": {Method: "GET", Path: "/cards"}}},
		},
		Types: map[string]spec.TypeDef{},
	}
	otherOrigin := &spec.APISpec{
		Name:    "other",
		Version: "0.1.0",
		BaseURL: "https://other.example.net",
		Auth:    spec.AuthConfig{Type: "none"},
		Resources: map[string]spec.Resource{
			"items": {Endpoints: map[string]spec.Endpoint{"list": {Method: "GET", Path: "/items"}}},
		},
		Types: map[string]spec.TypeDef{},
	}

	merged := mergeSpecs([]*spec.APISpec{trelloShaped, otherOrigin}, "combo")

	assertNoAdditionalAuthHeader(t, merged.Auth.AdditionalHeaders, "token")
	assertAdditionalAuthHeader(t, merged.Auth.AdditionalHeaders, "X-Mergeable-Key", "MERGEABLE_KEY")

	noAuthFirst := &spec.APISpec{
		Name:    "none",
		Version: "0.1.0",
		BaseURL: "https://none.example.com",
		Auth:    spec.AuthConfig{Type: "none"},
		Resources: map[string]spec.Resource{
			"none": {Endpoints: map[string]spec.Endpoint{"list": {Method: "GET", Path: "/none"}}},
		},
		Types: map[string]spec.TypeDef{},
	}
	laterSelected := &spec.APISpec{
		Name:    "oauthquery",
		Version: "0.1.0",
		BaseURL: "https://api.example.com",
		Auth: spec.AuthConfig{
			Type:             "bearer_token",
			Header:           "Authorization",
			AuthorizationURL: "https://accounts.example.com/auth",
			TokenURL:         "https://accounts.example.com/token",
			AdditionalHeaders: []spec.AdditionalAuthHeader{
				{Header: "oauth_token_query", In: "query", EnvVar: spec.AuthEnvVar{Name: "OAUTH_QUERY_TOKEN", Kind: spec.AuthEnvVarKindPerCall, Required: true, Sensitive: true}},
				{Header: "X-OAuth-Key", In: "header", EnvVar: spec.AuthEnvVar{Name: "OAUTH_HEADER_KEY", Kind: spec.AuthEnvVarKindPerCall, Required: true, Sensitive: true}},
			},
		},
		Resources: map[string]spec.Resource{
			"oauthquery": {Endpoints: map[string]spec.Endpoint{"list": {Method: "GET", Path: "/oauthquery"}}},
		},
		Types: map[string]spec.TypeDef{},
	}

	mergedLater := mergeSpecs([]*spec.APISpec{noAuthFirst, laterSelected, otherOrigin}, "combo")

	assertNoAdditionalAuthHeader(t, mergedLater.Auth.AdditionalHeaders, "oauth_token_query")
	assertAdditionalAuthHeader(t, mergedLater.Auth.AdditionalHeaders, "X-OAuth-Key", "OAUTH_HEADER_KEY")
}

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

	primary := &spec.APISpec{
		Name:    "primary",
		Version: "0.1.0",
		BaseURL: "https://api.example.com",
		Auth:    spec.AuthConfig{Type: "none"},
		Resources: map[string]spec.Resource{
			"primary": {Endpoints: map[string]spec.Endpoint{"list": {Method: "GET", Path: "/primary"}}},
		},
		Types: map[string]spec.TypeDef{},
	}
	secondary := &spec.APISpec{
		Name:    "secondary",
		Version: "0.1.0",
		BaseURL: "https://api.example.com",
		Auth: spec.AuthConfig{
			Type:             "bearer_token",
			Header:           "Authorization",
			AuthorizationURL: "https://accounts.example.com/auth",
			TokenURL:         "https://accounts.example.com/token",
			Scopes:           []string{"read.secondary"},
		},
		Resources: map[string]spec.Resource{
			"secondary": {Endpoints: map[string]spec.Endpoint{"list": {Method: "GET", Path: "/secondary"}}},
		},
		Types: map[string]spec.TypeDef{},
	}

	merged := mergeSpecs([]*spec.APISpec{primary, secondary}, "combo")

	assert.Equal(t, "bearer_token", merged.Auth.Type)
	assert.Equal(t, "https://accounts.example.com/auth", merged.Auth.AuthorizationURL)
	assert.Equal(t, "https://accounts.example.com/token", merged.Auth.TokenURL)
	assert.Equal(t, []string{"read.secondary"}, merged.Auth.Scopes)
}

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

	single := &spec.APISpec{
		Name:    "single",
		Version: "0.1.0",
		BaseURL: "https://api.example.com",
		Auth: spec.AuthConfig{
			Type:             "bearer_token",
			Header:           "Authorization",
			AuthorizationURL: "https://accounts.example.com/auth",
			TokenURL:         "https://accounts.example.com/token",
			Scopes:           []string{"scope.z", "scope.a"},
		},
		Resources: map[string]spec.Resource{
			"single": {Endpoints: map[string]spec.Endpoint{"list": {Method: "GET", Path: "/single"}}},
		},
		Types: map[string]spec.TypeDef{},
	}

	merged := mergeSpecs([]*spec.APISpec{single}, "single")

	assert.Equal(t, []string{"scope.z", "scope.a"}, merged.Auth.Scopes)
}

func assertAdditionalAuthHeader(t *testing.T, headers []spec.AdditionalAuthHeader, wantHeader, wantEnvVar string) {
	t.Helper()
	for _, header := range headers {
		if header.Header == wantHeader && header.EnvVar.Name == wantEnvVar {
			return
		}
	}
	assert.Failf(t, "missing additional auth header", "header %q with env var %q not found in %#v", wantHeader, wantEnvVar, headers)
}

func assertNoAdditionalAuthHeader(t *testing.T, headers []spec.AdditionalAuthHeader, wantHeader string) {
	t.Helper()
	for _, header := range headers {
		if header.Header == wantHeader {
			assert.Failf(t, "unexpected additional auth header", "header %q found in %#v", wantHeader, headers)
			return
		}
	}
}

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

	dir := t.TempDir()
	primarySpecPath := filepath.Join(dir, "youtube.yaml")
	analyticsSpecPath := filepath.Join(dir, "analytics.yaml")
	outputDir := filepath.Join(dir, "youtube")
	require.NoError(t, os.WriteFile(primarySpecPath, []byte(`openapi: 3.0.3
info:
  title: YouTube
  version: 1.0.0
servers:
  - url: https://www.googleapis.com/youtube/v3
components:
  securitySchemes:
    OAuth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://accounts.google.com/o/oauth2/v2/auth
          tokenUrl: https://oauth2.googleapis.com/token
          scopes:
            https://www.googleapis.com/auth/youtube: Manage YouTube account
            https://www.googleapis.com/auth/youtube.readonly: View YouTube account
security:
  - OAuth2: []
paths:
  /channels:
    get:
      operationId: listChannels
      responses:
        "200":
          description: OK
`), 0o644))
	require.NoError(t, os.WriteFile(analyticsSpecPath, []byte(`openapi: 3.0.3
info:
  title: YouTube Analytics
  version: 1.0.0
servers:
  - url: https://youtubeanalytics.googleapis.com/v2
components:
  securitySchemes:
    OAuth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://accounts.google.com/o/oauth2/v2/auth
          tokenUrl: https://oauth2.googleapis.com/token
          scopes:
            https://www.googleapis.com/auth/yt-analytics.readonly: View YouTube analytics reports
security:
  - OAuth2: []
paths:
  /reports:
    get:
      operationId: queryReports
      responses:
        "200":
          description: OK
`), 0o644))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", primarySpecPath,
		"--spec", analyticsSpecPath,
		"--name", "youtube",
		"--output", outputDir,
		"--validate=false",
		"--force",
	})
	require.NoError(t, cmd.Execute())

	authFile, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "auth.go"))
	require.NoError(t, err)
	authSource := string(authFile)
	assert.Contains(t, authSource, `"https://www.googleapis.com/auth/youtube"`)
	assert.Contains(t, authSource, `"https://www.googleapis.com/auth/youtube.readonly"`)
	assert.Contains(t, authSource, `"https://www.googleapis.com/auth/yt-analytics.readonly"`)

	singleOutputDir := filepath.Join(dir, "youtube-single")
	cmd = newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", primarySpecPath,
		"--name", "youtube",
		"--output", singleOutputDir,
		"--validate=false",
		"--force",
	})
	require.NoError(t, cmd.Execute())

	singleAuthFile, err := os.ReadFile(filepath.Join(singleOutputDir, "internal", "cli", "auth.go"))
	require.NoError(t, err)
	assert.NotContains(t, string(singleAuthFile), "yt-analytics.readonly")
}

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

	dir := t.TempDir()
	rootSpecPath := filepath.Join(dir, "core.yaml")
	wikiSpecPath := filepath.Join(dir, "wiki.yaml")
	outputDir := filepath.Join(dir, "combo")
	require.NoError(t, os.WriteFile(rootSpecPath, []byte(`name: core
description: Core API
version: 0.1.0
base_url: https://tenant.example.com
auth:
  type: none
resources:
  projects:
    description: Projects
    endpoints:
      list:
        method: GET
        path: /projects
        description: List projects
`), 0o644))
	require.NoError(t, os.WriteFile(wikiSpecPath, []byte(`name: wiki
description: Wiki API
version: 0.1.0
base_url: https://tenant.example.com/wiki/api/v2
auth:
  type: none
resources:
  pages:
    description: Pages
    endpoints:
      list:
        method: GET
        path: /pages
        description: List pages
    sub_resources:
      comments:
        description: Page comments
        endpoints:
          list:
            method: GET
            path: /pages/{page_id}/comments
            description: List page comments
`), 0o644))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", rootSpecPath,
		"--spec", wikiSpecPath,
		"--name", "combo",
		"--output", outputDir,
		"--validate=false",
		"--force",
	})

	require.NoError(t, cmd.Execute())

	commentsHandler, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "pages_comments_list.go"))
	require.NoError(t, err)
	assert.Contains(
		t,
		string(commentsHandler),
		`path := "/wiki/api/v2/pages/{page_id}/comments"`,
	)

}

// TestGenerateArchivesMergedSpecForMultiSpec asserts that a generate run
// with multiple --spec inputs archives the merged in-memory APISpec — with
// the merged title and the union of resources — rather than just the first
// input's raw bytes.
func TestGenerateArchivesMergedSpecForMultiSpec(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	coreSpecPath := filepath.Join(dir, "core.yaml")
	wikiSpecPath := filepath.Join(dir, "wiki.yaml")
	outputDir := filepath.Join(dir, "combo")
	require.NoError(t, os.WriteFile(coreSpecPath, []byte(`name: core
description: Core API
version: 0.1.0
base_url: https://tenant.example.com
auth:
  type: none
resources:
  projects:
    description: Projects
    endpoints:
      list:
        method: GET
        path: /projects
        description: List projects
`), 0o644))
	require.NoError(t, os.WriteFile(wikiSpecPath, []byte(`name: wiki
description: Wiki API
version: 0.1.0
base_url: https://tenant.example.com/wiki/api/v2
auth:
  type: none
resources:
  pages:
    description: Pages
    endpoints:
      list:
        method: GET
        path: /pages
        description: List pages
`), 0o644))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", coreSpecPath,
		"--spec", wikiSpecPath,
		"--name", "combo",
		"--output", outputDir,
		"--validate=false",
		"--force",
	})
	require.NoError(t, cmd.Execute())

	// Multi-spec must archive as spec.json (canonical JSON of merged APISpec),
	// not spec.yaml (which would be the first input verbatim).
	assert.NoFileExists(t, filepath.Join(outputDir, "spec.yaml"))
	archived, err := os.ReadFile(filepath.Join(outputDir, "spec.json"))
	require.NoError(t, err)

	parsed, err := spec.ParseBytes(archived)
	require.NoError(t, err, "archived merged spec must round-trip through spec.ParseBytes")

	// The merged title is the --name, not "core" (the first input).
	assert.Equal(t, "combo", parsed.Name)

	// The merged BaseURL pins the archive to the merged struct rather than to
	// either input alone — both inputs share https://tenant.example.com as the
	// host, and that is what mergeSpecs resolves to.
	assert.Equal(t, "https://tenant.example.com", parsed.BaseURL, "archived merged spec carries the merged BaseURL")

	// Both inputs' resources are present in the archived snapshot.
	assert.Contains(t, parsed.Resources, "projects", "first-input resource preserved")
	assert.Contains(t, parsed.Resources, "pages", "second-input resource present in merged archive")
}

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

	dir := t.TempDir()
	contactsSpecPath := filepath.Join(dir, "contacts.yaml")
	companiesSpecPath := filepath.Join(dir, "companies.yaml")
	outputDir := filepath.Join(dir, "crm")
	require.NoError(t, os.WriteFile(contactsSpecPath, []byte(`openapi: 3.0.3
info:
  title: Contacts
  version: 1.0.0
servers:
  - url: https://api.example.com
paths:
  /api/v1/crm/v3/objects/contacts:
    get:
      operationId: listContacts
      responses:
        "200":
          description: OK
  /api/v1/crm/v3/objects/contacts/{contactId}:
    get:
      operationId: getContact
      parameters:
        - name: contactId
          in: path
          required: true
          schema: {type: string}
      responses:
        "200":
          description: OK
`), 0o644))
	require.NoError(t, os.WriteFile(companiesSpecPath, []byte(`openapi: 3.0.3
info:
  title: Companies
  version: 1.0.0
servers:
  - url: https://api.example.com
paths:
  /api/v1/crm/v3/objects/companies:
    get:
      operationId: listCompanies
      responses:
        "200":
          description: OK
  /api/v1/crm/v3/objects/companies/{companyId}:
    get:
      operationId: getCompany
      parameters:
        - name: companyId
          in: path
          required: true
          schema: {type: string}
      responses:
        "200":
          description: OK
`), 0o644))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", contactsSpecPath,
		"--spec", companiesSpecPath,
		"--name", "crm",
		"--output", outputDir,
		"--validate=false",
		"--force",
	})
	require.NoError(t, cmd.Execute())

	archived, err := os.ReadFile(filepath.Join(outputDir, "spec.json"))
	require.NoError(t, err)
	parsed, err := spec.ParseBytes(archived)
	require.NoError(t, err)

	assert.Contains(t, parsed.Resources, "contacts")
	assert.Contains(t, parsed.Resources, "companies")
	assert.Equal(t, "Manage contacts", parsed.Resources["contacts"].Description)
	assert.Equal(t, "Manage companies", parsed.Resources["companies"].Description)
	assert.NotContains(t, parsed.Resources, "crm")
	assert.NotContains(t, parsed.Resources, "companies-crm")
}

// TestArchiveSpecBytesBranches covers each branch of archiveSpecBytes
// directly, including the json-input single-spec arm that the integration
// tests above don't exercise (their fixtures are YAML).
func TestArchiveSpecBytesBranches(t *testing.T) {
	t.Parallel()

	t.Run("multi-spec marshals merged APISpec as JSON", func(t *testing.T) {
		merged := &spec.APISpec{
			Name:      "combo",
			Version:   "0.1.0",
			BaseURL:   "https://example.com",
			Resources: map[string]spec.Resource{"things": {Description: "Things"}},
		}
		specs := []*spec.APISpec{
			{Name: "a", Resources: map[string]spec.Resource{}},
			{Name: "b", Resources: map[string]spec.Resource{}},
		}
		data, name, ok := archiveSpecBytes(merged, specs, [][]byte{[]byte("a"), []byte("b")})
		require.True(t, ok)
		assert.Equal(t, "spec.json", name)
		assert.True(t, json.Valid(data), "multi-spec archive must be valid JSON")
		assert.Contains(t, string(data), `"name": "combo"`)
	})

	t.Run("single-spec JSON input returns raw bytes as spec.json", func(t *testing.T) {
		jsonInput := []byte(`{"name":"solo","resources":{}}`)
		data, name, ok := archiveSpecBytes(nil, []*spec.APISpec{{Name: "solo"}}, [][]byte{jsonInput})
		require.True(t, ok)
		assert.Equal(t, "spec.json", name)
		assert.Equal(t, jsonInput, data, "single-spec JSON archive is byte-identical to input")
	})

	t.Run("single-spec YAML input returns raw bytes as spec.yaml", func(t *testing.T) {
		yamlInput := []byte("name: solo\nresources: {}\n")
		data, name, ok := archiveSpecBytes(nil, []*spec.APISpec{{Name: "solo"}}, [][]byte{yamlInput})
		require.True(t, ok)
		assert.Equal(t, "spec.yaml", name)
		assert.Equal(t, yamlInput, data, "single-spec YAML archive is byte-identical to input")
	})

	t.Run("no inputs returns ok=false", func(t *testing.T) {
		_, _, ok := archiveSpecBytes(nil, nil, nil)
		assert.False(t, ok)
	})

	t.Run("multi-spec with nil apiSpec returns ok=false", func(t *testing.T) {
		specs := []*spec.APISpec{
			{Name: "a"},
			{Name: "b"},
		}
		_, _, ok := archiveSpecBytes(nil, specs, [][]byte{[]byte("a"), []byte("b")})
		assert.False(t, ok, "nil apiSpec must not silently archive the literal 'null'")
	})
}

// TestGenerateArchivesRawSpecForSingleSpec asserts that single-spec runs
// archive the user's original input bytes verbatim (post-redaction), the
// negative case complementing the multi-spec merged-archive behavior.
func TestGenerateArchivesRawSpecForSingleSpec(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	specPath := filepath.Join(dir, "spec.yaml")
	outputDir := filepath.Join(dir, "solo")
	input := []byte(`name: solo
description: Solo API
version: 0.1.0
base_url: https://api.example.com
auth:
  type: none
resources:
  things:
    description: Things
    endpoints:
      list:
        method: GET
        path: /things
        description: List things
`)
	require.NoError(t, os.WriteFile(specPath, input, 0o644))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", specPath,
		"--output", outputDir,
		"--validate=false",
		"--force",
	})
	require.NoError(t, cmd.Execute())

	// YAML input archives as spec.yaml (not spec.json).
	assert.NoFileExists(t, filepath.Join(outputDir, "spec.json"))
	archived, err := os.ReadFile(filepath.Join(outputDir, "spec.yaml"))
	require.NoError(t, err)

	// Single-spec archive is byte-identical to the input (this spec contains
	// no secrets, so redaction is a no-op).
	assert.Equal(t, input, archived, "single-spec archive must preserve original bytes")
}

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

	runtimeSpec := &spec.APISpec{
		Name:          "runtime",
		Version:       "0.1.0",
		BaseURL:       "https://runtime.example.com",
		HTTPTransport: "browser-runtime",
		Resources:     map[string]spec.Resource{},
		Types:         map[string]spec.TypeDef{},
	}
	chromeSpec := &spec.APISpec{
		Name:          "chrome",
		Version:       "0.1.0",
		BaseURL:       "https://chrome.example.com",
		HTTPTransport: spec.HTTPTransportBrowserChrome,
		Resources:     map[string]spec.Resource{},
		Types:         map[string]spec.TypeDef{},
	}

	mergedRuntimeFirst := mergeSpecs([]*spec.APISpec{runtimeSpec, chromeSpec}, "merged")
	assert.Equal(t, spec.HTTPTransportBrowserChrome, mergedRuntimeFirst.HTTPTransport)

	mergedRuntimeLast := mergeSpecs([]*spec.APISpec{chromeSpec, runtimeSpec}, "merged")
	assert.Equal(t, spec.HTTPTransportBrowserChrome, mergedRuntimeLast.HTTPTransport)

	httpSpec := &spec.APISpec{
		Name:          "http",
		Version:       "0.1.0",
		BaseURL:       "https://http.example.com",
		HTTPTransport: spec.HTTPTransportBrowserHTTP,
		Resources:     map[string]spec.Resource{},
		Types:         map[string]spec.TypeDef{},
	}
	standardSpec := &spec.APISpec{
		Name:          "standard",
		Version:       "0.1.0",
		BaseURL:       "https://standard.example.com",
		HTTPTransport: spec.HTTPTransportStandard,
		Resources:     map[string]spec.Resource{},
		Types:         map[string]spec.TypeDef{},
	}

	mergedHTTP := mergeSpecs([]*spec.APISpec{standardSpec, httpSpec}, "merged")
	assert.Equal(t, spec.HTTPTransportBrowserHTTP, mergedHTTP.HTTPTransport)

	mergedChrome := mergeSpecs([]*spec.APISpec{httpSpec, chromeSpec}, "merged")
	assert.Equal(t, spec.HTTPTransportBrowserChrome, mergedChrome.HTTPTransport)
}

// TestMergeSpecsRewritesEndpointPathsForDivergentBaseURLPathPrefixes covers the
// Atlassian-style case from issue #831: spec A's base URL is the bare host and
// spec B's base URL adds a path prefix on the same host. Without rewriting,
// every spec-B command 404s because its path prefix is silently dropped by the
// merge.
func TestMergeSpecsRewritesEndpointPathsForDivergentBaseURLPathPrefixes(t *testing.T) {
	t.Parallel()

	specA := &spec.APISpec{
		Name:    "a",
		Version: "0.1.0",
		BaseURL: "https://example.com",
		Resources: map[string]spec.Resource{
			"foo": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/foo"},
				},
			},
		},
		Types: map[string]spec.TypeDef{},
	}
	specB := &spec.APISpec{
		Name:    "b",
		Version: "0.1.0",
		BaseURL: "https://example.com/api/v2",
		Resources: map[string]spec.Resource{
			"bar": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/bar"},
					"get":  {Method: "GET", Path: "/bar/{id}"},
				},
				SubResources: map[string]spec.Resource{
					"comments": {
						Endpoints: map[string]spec.Endpoint{
							"list": {Method: "GET", Path: "/bar/{id}/comments"},
						},
					},
				},
			},
		},
		Types: map[string]spec.TypeDef{},
	}

	merged := mergeSpecs([]*spec.APISpec{specA, specB}, "combo")

	// merged BaseURL collapses to the bare host shared by both specs.
	assert.Equal(t, "https://example.com", merged.BaseURL)

	// spec A's endpoints (no path prefix on its base URL) are unchanged.
	assert.Equal(t, "/foo", merged.Resources["foo"].Endpoints["list"].Path)

	// spec B's endpoints absorb the dropped "/api/v2" path component.
	assert.Equal(t, "/api/v2/bar", merged.Resources["bar"].Endpoints["list"].Path)
	assert.Equal(t, "/api/v2/bar/{id}", merged.Resources["bar"].Endpoints["get"].Path)

	// Sub-resource endpoints are rewritten too.
	assert.Equal(t,
		"/api/v2/bar/{id}/comments",
		merged.Resources["bar"].SubResources["comments"].Endpoints["list"].Path,
	)
}

// TestMergeSpecsLeavesPathsUnchangedWhenBaseURLsAgree guards the idempotent
// case: when every spec's base URL is identical, no path rewriting fires and
// the merged BaseURL keeps the shared path prefix.
func TestMergeSpecsLeavesPathsUnchangedWhenBaseURLsAgree(t *testing.T) {
	t.Parallel()

	specA := &spec.APISpec{
		Name:    "a",
		Version: "0.1.0",
		BaseURL: "https://example.com/api/v2",
		Resources: map[string]spec.Resource{
			"foo": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/foo"},
				},
			},
		},
		Types: map[string]spec.TypeDef{},
	}
	specB := &spec.APISpec{
		Name:    "b",
		Version: "0.1.0",
		BaseURL: "https://example.com/api/v2",
		Resources: map[string]spec.Resource{
			"bar": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/bar"},
				},
			},
		},
		Types: map[string]spec.TypeDef{},
	}

	merged := mergeSpecs([]*spec.APISpec{specA, specB}, "combo")

	assert.Equal(t, "https://example.com/api/v2", merged.BaseURL)
	assert.Equal(t, "/foo", merged.Resources["foo"].Endpoints["list"].Path)
	assert.Equal(t, "/bar", merged.Resources["bar"].Endpoints["list"].Path)
}

// TestMergeSpecsLeavesPathsUnchangedWhenHostsDiffer guards the cross-host case
// (out of scope for the path-rewrite optimization): when specs live on
// different hosts, the merge falls back to specs[0].BaseURL and no path
// rewriting fires. Cross-host runs were already broken; this just keeps the
// pre-fix behavior so the path-rewrite path doesn't make it worse.
func TestMergeSpecsLeavesPathsUnchangedWhenHostsDiffer(t *testing.T) {
	t.Parallel()

	specA := &spec.APISpec{
		Name:    "a",
		Version: "0.1.0",
		BaseURL: "https://a.example.com",
		Resources: map[string]spec.Resource{
			"foo": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/foo"},
				},
			},
		},
		Types: map[string]spec.TypeDef{},
	}
	specB := &spec.APISpec{
		Name:    "b",
		Version: "0.1.0",
		BaseURL: "https://b.example.com/api/v2",
		Resources: map[string]spec.Resource{
			"bar": {
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/bar"},
				},
			},
		},
		Types: map[string]spec.TypeDef{},
	}

	merged := mergeSpecs([]*spec.APISpec{specA, specB}, "combo")

	assert.Equal(t, "https://a.example.com", merged.BaseURL)
	assert.Equal(t, "/foo", merged.Resources["foo"].Endpoints["list"].Path)
	assert.Equal(t, "/bar", merged.Resources["bar"].Endpoints["list"].Path)
}

// TestMergeSpecsCarriesMCPConfigFromFirstDeclaringSpec verifies that a combo
// run preserves x-mcp configuration from the first input spec that declares
// it. Without this precedence loop, mergeSpecs silently drops MCP config and
// the downstream code-orchestration gate falls back to the endpoint-mirror
// surface no matter what the input specs requested.
func TestMergeSpecsCarriesMCPConfigFromFirstDeclaringSpec(t *testing.T) {
	t.Parallel()

	specA := &spec.APISpec{
		Name:      "a",
		Version:   "0.1.0",
		BaseURL:   "https://api.example.com",
		Resources: map[string]spec.Resource{},
		Types:     map[string]spec.TypeDef{},
	}
	specB := &spec.APISpec{
		Name:      "b",
		Version:   "0.1.0",
		BaseURL:   "https://api.example.com",
		Resources: map[string]spec.Resource{},
		Types:     map[string]spec.TypeDef{},
		MCP: spec.MCPConfig{
			Orchestration: "code",
		},
	}

	merged := mergeSpecs([]*spec.APISpec{specA, specB}, "combo")

	assert.True(t, merged.MCP.IsCodeOrchestration(), "merged MCP should retain x-mcp.orchestration=code from specB")
}

// TestMergeSpecsLeavesMCPZeroWhenNoSpecDeclares verifies that combos without
// any x-mcp declaration produce a zero-valued merged MCP, so the existing
// endpoint-mirror default and "consider code-orchestration" advisory keep
// working unchanged.
func TestMergeSpecsLeavesMCPZeroWhenNoSpecDeclares(t *testing.T) {
	t.Parallel()

	specA := &spec.APISpec{
		Name:      "a",
		Version:   "0.1.0",
		BaseURL:   "https://api.example.com",
		Resources: map[string]spec.Resource{},
		Types:     map[string]spec.TypeDef{},
	}
	specB := &spec.APISpec{
		Name:      "b",
		Version:   "0.1.0",
		BaseURL:   "https://api.example.com",
		Resources: map[string]spec.Resource{},
		Types:     map[string]spec.TypeDef{},
	}

	merged := mergeSpecs([]*spec.APISpec{specA, specB}, "combo")

	assert.False(t, merged.MCP.IsCodeOrchestration())
	assert.Empty(t, merged.MCP.Transport)
	assert.Empty(t, merged.MCP.Intents)
}

// TestMergeSpecsCarriesMCPFullFieldsFromDeclaringSpec exercises the merge
// loop with a non-Orchestration trigger field (Transport) and asserts that
// Addr and Intents propagate alongside it. Without this case, a regression to
// a partial field-by-field copy or an mcpConfigured branch that ignored
// non-Orchestration fields would slip past the other tests.
func TestMergeSpecsCarriesMCPFullFieldsFromDeclaringSpec(t *testing.T) {
	t.Parallel()

	specA := &spec.APISpec{
		Name:      "a",
		Version:   "0.1.0",
		BaseURL:   "https://api.example.com",
		Resources: map[string]spec.Resource{},
		Types:     map[string]spec.TypeDef{},
		MCP: spec.MCPConfig{
			Transport: []string{"stdio", "http"},
			Addr:      ":7777",
			Intents: []spec.Intent{
				{Name: "search_things"},
			},
		},
	}
	specB := &spec.APISpec{
		Name:      "b",
		Version:   "0.1.0",
		BaseURL:   "https://api.example.com",
		Resources: map[string]spec.Resource{},
		Types:     map[string]spec.TypeDef{},
	}

	merged := mergeSpecs([]*spec.APISpec{specA, specB}, "combo")

	assert.Equal(t, []string{"stdio", "http"}, merged.MCP.Transport)
	assert.Equal(t, ":7777", merged.MCP.Addr)
	assert.Len(t, merged.MCP.Intents, 1)
	assert.Equal(t, "search_things", merged.MCP.Intents[0].Name)
}

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

	specA := &spec.APISpec{
		Name:    "contacts",
		Version: "0.1.0",
		BaseURL: "https://api.example.com",
		Resources: map[string]spec.Resource{
			"crm": {
				Description: spec.DefaultResourceDescription("crm"),
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/api/v1/crm/v3/objects/contacts"},
				},
			},
		},
		Types: map[string]spec.TypeDef{},
		MCP: spec.MCPConfig{
			Intents: []spec.Intent{
				{
					Name: "list_contacts",
					Steps: []spec.IntentStep{
						{Endpoint: "crm.list"},
					},
				},
			},
		},
	}
	specB := &spec.APISpec{
		Name:    "companies",
		Version: "0.1.0",
		BaseURL: "https://api.example.com",
		Resources: map[string]spec.Resource{
			"crm": {
				Description: spec.DefaultResourceDescription("crm"),
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/api/v1/crm/v3/objects/companies"},
				},
			},
		},
		Types: map[string]spec.TypeDef{},
	}

	merged := mergeSpecs([]*spec.APISpec{specA, specB}, "crm")

	require.Len(t, merged.MCP.Intents, 1)
	require.Len(t, merged.MCP.Intents[0].Steps, 1)
	assert.Equal(t, "contacts.list", merged.MCP.Intents[0].Steps[0].Endpoint)
}

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

	specA := &spec.APISpec{
		Name:    "contacts",
		Version: "0.1.0",
		BaseURL: "https://api.example.com",
		Resources: map[string]spec.Resource{
			"crm": {
				Description: spec.DefaultResourceDescription("crm"),
				Endpoints: map[string]spec.Endpoint{
					"list":   {Method: "GET", Path: "/api/v1/crm/v3/objects/contacts"},
					"get":    {Method: "GET", Path: "/api/v1/crm/v3/objects/contacts/{id}"},
					"search": {Method: "GET", Path: "/api/v1/crm/v3/objects/contacts/search"},
				},
			},
		},
		Types: map[string]spec.TypeDef{},
	}
	specB := &spec.APISpec{
		Name:    "empty",
		Version: "0.1.0",
		BaseURL: "https://api.example.com",
		Resources: map[string]spec.Resource{
			"crm": {
				Description: "Custom empty resource",
				Endpoints:   map[string]spec.Endpoint{},
			},
		},
		Types: map[string]spec.TypeDef{},
	}

	merged := mergeSpecs([]*spec.APISpec{specA, specB}, "crm")

	assert.Contains(t, merged.Resources, "crm")
	assert.Contains(t, merged.Resources, "empty-crm")
	assert.NotContains(t, merged.Resources, "empty")
	assert.Equal(t, "Custom empty resource", merged.Resources["empty-crm"].Description)
}

// TestMergeSpecsFirstDeclaringMCPWins verifies that when more than one input
// spec declares x-mcp, the first declaring spec wins, mirroring the existing
// first-wins precedence used for Auth.AuthorizationURL.
func TestMergeSpecsFirstDeclaringMCPWins(t *testing.T) {
	t.Parallel()

	specA := &spec.APISpec{
		Name:      "a",
		Version:   "0.1.0",
		BaseURL:   "https://api.example.com",
		Resources: map[string]spec.Resource{},
		Types:     map[string]spec.TypeDef{},
		MCP: spec.MCPConfig{
			Orchestration: "code",
		},
	}
	specB := &spec.APISpec{
		Name:      "b",
		Version:   "0.1.0",
		BaseURL:   "https://api.example.com",
		Resources: map[string]spec.Resource{},
		Types:     map[string]spec.TypeDef{},
		MCP: spec.MCPConfig{
			Orchestration: "intent",
		},
	}

	merged := mergeSpecs([]*spec.APISpec{specA, specB}, "combo")

	assert.Equal(t, "code", merged.MCP.Orchestration)
}

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

	got, err := normalizeHTTPTransport(spec.HTTPTransportBrowserHTTP)
	require.NoError(t, err)
	assert.Equal(t, spec.HTTPTransportBrowserHTTP, got)

	got, err = normalizeHTTPTransport(spec.HTTPTransportBrowserChromeH2)
	require.NoError(t, err)
	assert.Equal(t, spec.HTTPTransportBrowserChromeH2, got)

	got, err = normalizeHTTPTransport(spec.HTTPTransportBrowserChromeH3)
	require.NoError(t, err)
	assert.Equal(t, spec.HTTPTransportBrowserChromeH3, got)

	_, err = normalizeHTTPTransport("browser-chrome-http3")
	require.ErrorContains(t, err, "browser-chrome-h3")

	_, err = normalizeHTTPTransport("browser-chrome-http2")
	require.ErrorContains(t, err, "browser-chrome-h2")

	_, err = normalizeHTTPTransport("browser-runtime")
	require.ErrorContains(t, err, "--transport must be one of")
}

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

	apiSpec := &spec.APISpec{Name: "docs-api"}

	require.NoError(t, applyGenerateSpecFlags(apiSpec, "", "docs", "travel", "", "", "", generateMCPFlagOverrides{}))

	assert.Equal(t, "docs", apiSpec.SpecSource)
	assert.Equal(t, "travel", apiSpec.Category)
}

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

	apiSpec := &spec.APISpec{Name: "docs-api"}

	err := applyGenerateSpecFlags(apiSpec, "", "docs", "banana", "", "", "", generateMCPFlagOverrides{})

	require.Error(t, err)
	assert.ErrorContains(t, err, "--category must be one of:")
	assert.Empty(t, apiSpec.Category)
}

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

	dir := t.TempDir()
	intentsPath := filepath.Join(dir, "mcp-intents.yaml")
	require.NoError(t, os.WriteFile(intentsPath, []byte(`
intents:
  - name: lookup_item
    description: Lookup an item by id
    params:
      - name: id
        type: string
        required: true
        description: item id
    steps:
      - endpoint: items.get
        bind:
          id: ${input.id}
        capture: item
    returns: item
`), 0o644))

	apiSpec := &spec.APISpec{
		Name:    "demo",
		BaseURL: "https://api.example.com",
		Resources: map[string]spec.Resource{
			"items": {
				Endpoints: map[string]spec.Endpoint{
					"get": {
						Method: "GET",
						Path:   "/items/{id}",
						Params: []spec.Param{
							{Name: "id", Type: "string", Required: true, PathParam: true},
						},
					},
				},
			},
		},
		MCP: spec.MCPConfig{
			Orchestration: "endpoint-mirror",
			EndpointTools: "visible",
			Transport:     []string{"stdio"},
		},
	}

	err := applyGenerateSpecFlags(apiSpec, "", "", "", "", "", "", generateMCPFlagOverrides{
		Orchestration: "code",
		Transport:     []string{"stdio", "HTTP"},
		EndpointTools: "hidden",
		IntentsPath:   intentsPath,
	})

	require.NoError(t, err)
	assert.Equal(t, "code", apiSpec.MCP.Orchestration)
	assert.Equal(t, []string{"stdio", "http"}, apiSpec.MCP.Transport)
	assert.Equal(t, "hidden", apiSpec.MCP.EndpointTools)
	require.Len(t, apiSpec.MCP.Intents, 1)
	assert.Equal(t, "lookup_item", apiSpec.MCP.Intents[0].Name)
}

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

	dir := t.TempDir()
	intentsPath := filepath.Join(dir, "mcp-intents.yaml")
	require.NoError(t, os.WriteFile(intentsPath, []byte(`
- name: lookup_item
  description: Lookup an item by id
  steps: not-a-list
`), 0o644))

	_, err := readMCPIntentsFile(intentsPath)

	require.Error(t, err)
	assert.ErrorContains(t, err, "parsing --mcp-intents file")
	assert.ErrorContains(t, err, "cannot unmarshal")
	assert.ErrorContains(t, err, "IntentStep")
}

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

	apiSpec := &spec.APISpec{
		Name:    "demo",
		BaseURL: "https://api.example.com",
		Resources: map[string]spec.Resource{
			"items": {Endpoints: map[string]spec.Endpoint{"list": {Method: "GET", Path: "/items"}}},
		},
	}

	err := applyGenerateSpecFlags(apiSpec, "", "", "", "", "", "", generateMCPFlagOverrides{
		Transport: []string{"stdio", "stdio"},
	})

	require.Error(t, err)
	assert.ErrorContains(t, err, "--mcp-transport contains duplicate value")
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "openapi.yaml")
	outputDir := filepath.Join(dir, "generated")
	require.NoError(t, os.WriteFile(specPath, []byte(openAPISpecWithEndpointCount(60)), 0o644))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", specPath,
		"--output", outputDir,
		"--validate=false",
		"--max-endpoints-per-resource", "60",
		"--mcp-orchestration", "code",
		"--mcp-endpoint-tools", "hidden",
		"--mcp-transport", "stdio,http",
	})

	require.NoError(t, cmd.Execute())

	toolsSource, err := os.ReadFile(filepath.Join(outputDir, "internal", "mcp", "tools.go"))
	require.NoError(t, err)
	assert.Contains(t, string(toolsSource), "RegisterCodeOrchestrationTools(s)")
	assert.NotContains(t, string(toolsSource), `mcplib.NewTool("items_get_item_0"`)

	codeOrchSource, err := os.ReadFile(filepath.Join(outputDir, "internal", "mcp", "code_orch.go"))
	require.NoError(t, err)
	assert.Contains(t, string(codeOrchSource), `mcplib.NewTool("mcp-flag_search"`)
	assert.Contains(t, string(codeOrchSource), `mcplib.NewTool("mcp-flag_execute"`)
}

func openAPISpecWithEndpointCount(count int) string {
	var b strings.Builder
	b.WriteString(`openapi: 3.0.0
info:
  title: MCP Flag API
  version: "1.0"
servers:
  - url: https://api.example.com
paths:
`)
	for i := range count {
		fmt.Fprintf(&b, `  /items/%d:
    get:
      operationId: getItem%d
      responses:
        "200":
          description: ok
`, i, i)
	}
	return b.String()
}

// TestApplyHTTPTransportDefaultPreservesH2ForProtectionPath pins the
// protection-driven branch (Cloudflare/DataDome/html_scrape/browser
// hints without a reachability mode). Pre-fix the template's else
// branch unconditionally emitted ForceHTTP2 for bare browser-chrome;
// post-fix the bare enum means "no force" so this path must opt into
// the -h2 variant explicitly to keep shipped CLIs on these origins
// behaving identically.
func TestApplyHTTPTransportDefaultPreservesH2ForProtectionPath(t *testing.T) {
	t.Parallel()
	apiSpec := &spec.APISpec{
		Name:    "cloudflareapp",
		BaseURL: "https://www.example.com",
		Auth:    spec.AuthConfig{Type: "none"},
	}
	analysis := &browsersniff.TrafficAnalysis{
		Protections: []browsersniff.ProtectionObservation{{Label: "cloudflare"}},
	}
	applyHTTPTransportDefault(apiSpec, analysis)
	assert.Equal(t, spec.HTTPTransportBrowserChromeH2, apiSpec.HTTPTransport,
		"protection-driven path must write browser-chrome-h2 so the generated client still forces HTTP/2")
}

// TestApplyHTTPTransportDefaultExplicitH3WinsOverProtection pins that
// an explicit H/3 hint still wins over the protection heuristic when
// both fire; the H/3 force preempts the H/2 fallback.
func TestApplyHTTPTransportDefaultExplicitH3WinsOverProtection(t *testing.T) {
	t.Parallel()
	apiSpec := &spec.APISpec{
		Name:    "cloudflareh3app",
		BaseURL: "https://www.example.com",
		Auth:    spec.AuthConfig{Type: "none"},
	}
	analysis := &browsersniff.TrafficAnalysis{
		Protections:     []browsersniff.ProtectionObservation{{Label: "cloudflare"}},
		GenerationHints: []string{"prefer_http3"},
	}
	applyHTTPTransportDefault(apiSpec, analysis)
	assert.Equal(t, spec.HTTPTransportBrowserChromeH3, apiSpec.HTTPTransport,
		"explicit H/3 hint must beat the protection-driven H/2 default")
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "sample-spec.yaml")
	analysisPath := filepath.Join(dir, "sample-spec-traffic-analysis.json")
	outputDir := filepath.Join(dir, "implicitanalysis")

	require.NoError(t, os.WriteFile(specPath, []byte(`name: implicitanalysis
description: Implicit analysis API
version: 0.1.0
base_url: https://api.example.com
spec_source: sniffed
auth:
  type: none
config:
  format: toml
  path: ~/.config/implicitanalysis-pp-cli/config.toml
resources:
  items:
    description: Manage items
    endpoints:
      list:
        method: GET
        path: /items
        description: List items
`), 0o644))
	require.NoError(t, os.WriteFile(analysisPath, []byte(`{
  "version": "1",
  "summary": {"entry_count": 2, "api_entry_count": 1},
  "generation_hints": ["weak_schema_confidence"]
}`), 0o600))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", specPath,
		"--output", outputDir,
		"--validate=false",
		"--force",
	})

	require.NoError(t, cmd.Execute())

	readme, err := os.ReadFile(filepath.Join(outputDir, "README.md"))
	require.NoError(t, err)
	assert.Contains(t, string(readme), "weak_schema_confidence")

	gomod, err := os.ReadFile(filepath.Join(outputDir, "go.mod"))
	require.NoError(t, err)
	assert.Contains(t, string(gomod), "github.com/enetx/surf")
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "sample-spec.yaml")
	outputDir := filepath.Join(dir, "nosidecar")

	require.NoError(t, os.WriteFile(specPath, []byte(`name: nosidecar
description: No sidecar API
version: 0.1.0
base_url: https://api.example.com
spec_source: sniffed
auth:
  type: none
config:
  format: toml
  path: ~/.config/nosidecar-pp-cli/config.toml
resources:
  items:
    description: Manage items
    endpoints:
      list:
        method: GET
        path: /items
        description: List items
`), 0o644))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", specPath,
		"--output", outputDir,
		"--validate=false",
		"--force",
	})

	require.NoError(t, cmd.Execute())
	require.FileExists(t, filepath.Join(outputDir, "README.md"))
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "sample-spec.yaml")
	outputDir := filepath.Join(dir, "standardtransport")

	require.NoError(t, os.WriteFile(specPath, []byte(`name: standardtransport
description: Standard transport API
version: 0.1.0
base_url: https://api.example.com
spec_source: sniffed
auth:
  type: none
config:
  format: toml
  path: ~/.config/standardtransport-pp-cli/config.toml
resources:
  items:
    description: Manage items
    endpoints:
      list:
        method: GET
        path: /items
        description: List items
`), 0o644))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", specPath,
		"--output", outputDir,
		"--validate=false",
		"--force",
		"--transport", "standard",
	})

	require.NoError(t, cmd.Execute())

	gomod, err := os.ReadFile(filepath.Join(outputDir, "go.mod"))
	require.NoError(t, err)
	assert.NotContains(t, string(gomod), "github.com/enetx/surf")
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "sample-spec.yaml")
	analysisPath := filepath.Join(dir, "sample-spec-traffic-analysis.json")
	outputDir := filepath.Join(dir, "pagecontext")

	require.NoError(t, os.WriteFile(specPath, []byte(`name: pagecontext
description: Page context API
version: 0.1.0
base_url: https://api.example.com
spec_source: sniffed
auth:
  type: none
config:
  format: toml
  path: ~/.config/pagecontext-pp-cli/config.toml
resources:
  items:
    description: Manage items
    endpoints:
      list:
        method: GET
        path: /items
        description: List items
`), 0o644))
	require.NoError(t, os.WriteFile(analysisPath, []byte(`{
  "version": "1",
  "reachability": {"mode": "browser_required"}
}`), 0o600))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", specPath,
		"--output", outputDir,
		"--validate=false",
		"--force",
		"--traffic-analysis", analysisPath,
		"--transport", "browser-chrome",
	})

	err := cmd.Execute()
	require.Error(t, err)
	assert.Contains(t, err.Error(), "requires live browser page-context execution")
	assert.NoFileExists(t, filepath.Join(outputDir, "README.md"))
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "spec.yaml")
	analysisPath := filepath.Join(dir, "sample-spec-traffic-analysis.json")
	outputDir := filepath.Join(dir, "overrideapp")

	require.NoError(t, os.WriteFile(specPath, []byte(`name: overrideapp
description: Override app API
version: 0.1.0
base_url: https://api.example.com
spec_source: sniffed
auth:
  type: none
config:
  format: toml
  path: ~/.config/overrideapp-pp-cli/config.toml
resources:
  items:
    description: Manage items
    endpoints:
      list:
        method: GET
        path: /items
        description: List items
`), 0o644))
	require.NoError(t, os.WriteFile(analysisPath, []byte(`{
  "version": "1",
  "reachability": {"mode": "browser_required", "confidence": 0.9},
  "generation_hints": ["reachability_override_browser_required_to_browser_http"]
}`), 0o600))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", specPath,
		"--output", outputDir,
		"--validate=false",
		"--force",
		"--traffic-analysis", analysisPath,
		"--transport", "browser-chrome",
	})

	require.NoError(t, cmd.Execute())
	assert.FileExists(t, filepath.Join(outputDir, "README.md"))
	clientGo, err := os.ReadFile(filepath.Join(outputDir, "internal", "client", "client.go"))
	require.NoError(t, err)
	assert.Contains(t, string(clientGo), `"github.com/enetx/surf"`)
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "spec.yaml")
	analysisPath := filepath.Join(dir, "sample-spec-traffic-analysis.json")
	outputDir := filepath.Join(dir, "conflictapp")

	require.NoError(t, os.WriteFile(specPath, []byte(`name: conflictapp
description: Conflict app API
version: 0.1.0
base_url: https://api.example.com
spec_source: sniffed
auth:
  type: none
config:
  format: toml
  path: ~/.config/conflictapp-pp-cli/config.toml
resources:
  items:
    description: Manage items
    endpoints:
      list:
        method: GET
        path: /items
        description: List items
`), 0o644))
	require.NoError(t, os.WriteFile(analysisPath, []byte(`{
  "version": "1",
  "reachability": {"mode": "browser_required", "confidence": 0.9},
  "generation_hints": ["reachability_override_browser_required_to_browser_http"]
}`), 0o600))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", specPath,
		"--output", outputDir,
		"--validate=false",
		"--force",
		"--traffic-analysis", analysisPath,
		"--transport", "standard",
	})

	err := cmd.Execute()
	require.Error(t, err)
	assert.Contains(t, err.Error(), "reachability override")
	assert.Contains(t, err.Error(), "conflicts")
	assert.NoFileExists(t, filepath.Join(outputDir, "README.md"))
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "spec.yaml")
	analysisPath := filepath.Join(dir, "traffic-analysis.json")
	outputDir := filepath.Join(dir, "badanalysis")

	require.NoError(t, os.WriteFile(specPath, []byte(`name: badanalysis
description: Bad analysis API
version: 0.1.0
base_url: https://api.example.com
auth:
  type: none
config:
  format: toml
  path: ~/.config/badanalysis-pp-cli/config.toml
resources:
  items:
    description: Manage items
    endpoints:
      list:
        method: GET
        path: /items
        description: List items
`), 0o644))
	require.NoError(t, os.WriteFile(analysisPath, []byte(`{"summary":{"entry_count":1}}`), 0o644))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", specPath,
		"--output", outputDir,
		"--validate=false",
		"--force",
		"--traffic-analysis", analysisPath,
	})

	err := cmd.Execute()
	require.Error(t, err)
	assert.Contains(t, err.Error(), "traffic analysis missing version")
}

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

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--plan", filepath.Join(t.TempDir(), "plan.md"),
		"--traffic-analysis", filepath.Join(t.TempDir(), "traffic-analysis.json"),
	})

	err := cmd.Execute()
	require.Error(t, err)
	assert.Contains(t, err.Error(), "--traffic-analysis cannot be used with --plan")
}

func TestGenerateCmdPlanDryRunWritesNoFiles(t *testing.T) {
	dir := t.TempDir()
	planPath := filepath.Join(dir, "plan.md")
	outputDir := filepath.Join(dir, "plan-output")
	plan := "# Plan\n\n## Commands\n\n- `doctor` - Check health\n- `scan` - Scan things\n"
	require.NoError(t, os.WriteFile(planPath, []byte(plan), 0o644))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--plan", planPath,
		"--name", "plan-dry-run",
		"--output", outputDir,
		"--dry-run",
	})

	stderr, err := runWithCapturedStderr(t, cmd.Execute)
	require.NoError(t, err)
	assert.Contains(t, stderr, "Commands:   1")
	assert.Contains(t, stderr, "Contract:   lightweight scaffold, not a full Printing Press CLI")
	_, err = os.Stat(outputDir)
	assert.True(t, os.IsNotExist(err), "plan dry-run must not create output directory")

	stdout, err := runWithCapturedStdout(t, func() error {
		return printPlanDryRun(&generator.PlanSpec{
			CLIName: "plan-dry-run",
			Commands: []generator.PlanCommand{
				{Name: "doctor", Description: "Check health"},
				{Name: "version", Description: "Show version"},
				{Name: "scan", Description: "Scan things"},
			},
		}, outputDir, planPath, 1)
	})
	require.NoError(t, err)
	var dryRunSummary map[string]any
	require.NoError(t, json.Unmarshal([]byte(stdout), &dryRunSummary))
	assert.Equal(t, float64(1), dryRunSummary["commands"])

	writeOutputDir := filepath.Join(dir, "plan-write-output")
	cmd = newGenerateCmd()
	cmd.SetArgs([]string{
		"--plan", planPath,
		"--name", "plan-dry-run",
		"--output", writeOutputDir,
	})

	stderr, err = runWithCapturedStderr(t, cmd.Execute)
	require.NoError(t, err)
	assert.Contains(t, stderr, "Notice: plan mode emits a lightweight scaffold, not a full Printing Press CLI.")
}

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

	dir := t.TempDir()
	specPath := filepath.Join(dir, "spec.yaml")
	outputDir := filepath.Join(dir, "user-chosen-path")
	derivedDir := filepath.Join(dir, "explicitoutput")

	require.NoError(t, os.WriteFile(specPath, []byte(`name: explicitoutput
description: Explicit output API
version: 0.1.0
base_url: https://api.example.com
auth:
  type: none
config:
  format: toml
  path: ~/.config/explicitoutput-pp-cli/config.toml
resources:
  items:
    description: Manage items
    endpoints:
      list:
        method: GET
        path: /items
        description: List items
`), 0o644))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", specPath,
		"--output", outputDir,
		"--validate=false",
		"--force",
	})

	require.NoError(t, cmd.Execute())

	assert.FileExists(t, filepath.Join(outputDir, "go.mod"),
		"generated files should land at the user-supplied --output path")
	assert.NoDirExists(t, derivedDir,
		"the spec-derived directory must not be created when --output is explicit")
}

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

	dir := t.TempDir()
	require.NoError(t, pipeline.WriteCLIManifest(dir, pipeline.CLIManifest{
		APIName:        "custom-api",
		CLIName:        "custom-api-pp-cli",
		AuthType:       "api_key",
		AuthPreference: "ApiKeyAuth",
	}))

	assert.Equal(t, "ApiKeyAuth", openAPIAuthPreferenceForGenerate("", dir),
		"prior manifest auth_preference should be the fallback below the explicit flag")
	assert.Equal(t, "OAuth2", openAPIAuthPreferenceForGenerate("OAuth2", dir),
		"explicit --auth-preference must override prior manifest")
}

func TestOpenAPIAuthPreferenceForGenerateSkipsPriorManifestWhenDirUnknown(t *testing.T) {
	dir := t.TempDir()
	t.Chdir(dir)

	require.NoError(t, pipeline.WriteCLIManifest(dir, pipeline.CLIManifest{
		APIName:        "unrelated-api",
		CLIName:        "unrelated-api-pp-cli",
		AuthType:       "api_key",
		AuthPreference: "UnrelatedAuth",
	}))

	assert.Empty(t, openAPIAuthPreferenceForGenerate("", ""),
		"unknown manifest dir must not fall back to the current working directory")
}

func TestOpenAPIAuthPreferenceForGenerateFromDefaultPriorManifest(t *testing.T) {
	pressHome := t.TempDir()
	t.Setenv("PRINTING_PRESS_HOME", pressHome)

	specBytes := []byte(`openapi: "3.0.3"
info:
  title: Custom Service
  version: "1.0"
servers:
  - url: https://api.example.com
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
paths:
  /items:
    get:
      operationId: listItems
      responses: {"200": {description: ok}}
`)
	specPath := filepath.Join(t.TempDir(), "openapi.yaml")
	require.NoError(t, os.WriteFile(specPath, specBytes, 0o644))

	defaultDir := pipeline.DefaultOutputDir("custom-service")
	require.NoError(t, os.MkdirAll(defaultDir, 0o755))
	require.NoError(t, pipeline.WriteCLIManifest(defaultDir, pipeline.CLIManifest{
		APIName:        "custom-service",
		CLIName:        "custom-service-pp-cli",
		AuthType:       "api_key",
		AuthPreference: "ApiKeyAuth",
	}))

	manifestDir := openAPIAuthPreferenceManifestDir("", "", []string{specPath}, "", specBytes)
	require.Equal(t, defaultDir, manifestDir)
	assert.Equal(t, "ApiKeyAuth", openAPIAuthPreferenceForGenerate("", manifestDir),
		"default-output regen should find the prior manifest before full OpenAPI parsing")
}

func TestRebaseAuthEnvPrefix(t *testing.T) {
	auth := spec.AuthConfig{
		EnvVars: []string{"ELEVENLABS_DOCUMENTATION_API_KEY", "UNCHANGED_TOKEN"},
		EnvVarSpecs: []spec.AuthEnvVar{
			{Name: "ELEVENLABS_DOCUMENTATION_CLIENT_ID"},
			{Name: "CUSTOM_SECRET"},
		},
	}

	specmeta.RebaseAuthEnvPrefix(&auth, "elevenlabs-documentation", "elevenlabs")

	assert.Equal(t, []string{"ELEVENLABS_API_KEY", "UNCHANGED_TOKEN"}, auth.EnvVars)
	assert.Equal(t, "ELEVENLABS_CLIENT_ID", auth.EnvVarSpecs[0].Name)
	assert.Equal(t, "CUSTOM_SECRET", auth.EnvVarSpecs[1].Name)
}

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

	researchDir := t.TempDir()
	researchData, err := json.Marshal(&pipeline.ResearchResult{
		APIName: "alaska-airlines",
		Narrative: &pipeline.ReadmeNarrative{
			DisplayName:  "Alaska Airlines",
			Headline:     "Search Alaska Airlines flights and check Atmos Rewards balance from the terminal, with offline-cached airports and agent-native JSON output.",
			AntiTriggers: []string{"Booking or changing flights"},
		},
	})
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(filepath.Join(researchDir, "research.json"), researchData, 0o644))
	apiSpec := &spec.APISpec{
		Name:        "alaska-airlines",
		Description: "Weak source-spec fallback copy.",
		Version:     "1.0",
		BaseURL:     "https://api.example.com",
		Auth:        spec.AuthConfig{Type: "none"},
		Resources: map[string]spec.Resource{
			"items": {
				Description: "Items",
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/items", Description: "List items"},
				},
			},
		},
	}
	outputDir := filepath.Join(t.TempDir(), "alaska-airlines")

	got, err := runGenerateProject(apiSpec, outputDir, generateProjectOptions{researchDir: researchDir})
	require.NoError(t, err)

	assert.Equal(t, "Alaska Airlines", got.DisplayName)
	assert.Equal(t, "Search Alaska Airlines flights and check Atmos Rewards balance from the terminal, with offline-cached airports and agent-native JSON output.", got.ManifestDescription)
	toolsManifest, err := pipeline.ReadToolsManifest(outputDir)
	require.NoError(t, err)
	assert.Equal(t, got.ManifestDescription, toolsManifest.Description)
	assert.NotContains(t, toolsManifest.Description, "Weak source-spec fallback copy.")
	skill, err := os.ReadFile(filepath.Join(outputDir, "SKILL.md"))
	require.NoError(t, err)
	assert.Contains(t, string(skill), "## Anti-triggers")
	assert.Contains(t, string(skill), "- Booking or changing flights")
}

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

	researchDir := t.TempDir()
	researchData, err := json.Marshal(&pipeline.ResearchResult{
		APIName: "apify",
		Auth: &pipeline.ResearchAuth{
			CanonicalEnvVar: "APIFY_TOKEN",
		},
	})
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(filepath.Join(researchDir, "research.json"), researchData, 0o644))

	apiSpec := &spec.APISpec{
		Name:    "apify",
		Version: "1.0",
		BaseURL: "https://api.example.com",
		Auth: spec.AuthConfig{
			Type:    "bearer_token",
			Header:  "Authorization",
			EnvVars: []string{"APIFY_HTTP_BEARER"},
			EnvVarSpecs: []spec.AuthEnvVar{
				{Name: "APIFY_HTTP_BEARER", Kind: spec.AuthEnvVarKindPerCall, Required: true, Sensitive: true, Inferred: true},
			},
		},
		Resources: map[string]spec.Resource{
			"items": {
				Description: "Items",
				Endpoints: map[string]spec.Endpoint{
					"list": {Method: "GET", Path: "/items", Description: "List items"},
				},
			},
		},
	}
	outputDir := filepath.Join(t.TempDir(), "apify")

	_, err = runGenerateProject(apiSpec, outputDir, generateProjectOptions{researchDir: researchDir})
	require.NoError(t, err)

	assert.Equal(t, []string{"APIFY_TOKEN", "APIFY_HTTP_BEARER"}, apiSpec.Auth.EnvVars)
	require.Len(t, apiSpec.Auth.EnvVarSpecs, 2)
	assert.True(t, apiSpec.Auth.IsAuthEnvVarORCase(), "canonical and fallback names should behave as alternatives")

	cfgSrc, err := os.ReadFile(filepath.Join(outputDir, "internal", "config", "config.go"))
	require.NoError(t, err)
	content := string(cfgSrc)
	canonicalIdx := strings.Index(content, `os.Getenv("APIFY_TOKEN")`)
	fallbackIdx := strings.Index(content, `os.Getenv("APIFY_HTTP_BEARER")`)
	require.NotEqual(t, -1, canonicalIdx, "config must read the research-discovered canonical env var")
	require.NotEqual(t, -1, fallbackIdx, "config must retain the parser-derived fallback env var")
	assert.Less(t, canonicalIdx, fallbackIdx, "canonical env var should be checked before fallback")
}

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

	researchDir := t.TempDir()
	researchData, err := json.Marshal(&pipeline.ResearchResult{
		APIName: "format-test",
		Auth: &pipeline.ResearchAuth{
			CanonicalEnvVar: "DISCORD_TOKEN",
		},
	})
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(filepath.Join(researchDir, "research.json"), researchData, 0o644))

	apiSpec := &spec.APISpec{
		Name:    "format-test",
		Version: "1.0",
		BaseURL: "https://api.example.com",
		Auth: spec.AuthConfig{
			Type:    "api_key",
			Header:  "Authorization",
			In:      "header",
			Format:  "Bot {bot_token}",
			EnvVars: []string{"DISCORD_BOT_TOKEN"},
			EnvVarSpecs: []spec.AuthEnvVar{
				{Name: "DISCORD_BOT_TOKEN", Kind: spec.AuthEnvVarKindPerCall, Required: true, Sensitive: true, Inferred: true},
			},
		},
		Resources: map[string]spec.Resource{
			"users": {
				Description: "Users",
				Endpoints: map[string]spec.Endpoint{
					"me": {Method: "GET", Path: "/users/@me", Description: "Current user"},
				},
			},
		},
	}
	outputDir := filepath.Join(t.TempDir(), "format-test")

	_, err = runGenerateProject(apiSpec, outputDir, generateProjectOptions{researchDir: researchDir})
	require.NoError(t, err)

	assert.Equal(t, []string{"DISCORD_TOKEN", "DISCORD_BOT_TOKEN"}, apiSpec.Auth.EnvVars)
	assert.Equal(t, "Bot {token}", apiSpec.Auth.Format)

	cfgSrc, err := os.ReadFile(filepath.Join(outputDir, "internal", "config", "config.go"))
	require.NoError(t, err)
	content := string(cfgSrc)
	assert.Contains(t, content, `return applyAuthFormat("Bot {token}", map[string]string{`)
	assert.Contains(t, content, `"token":`)
	assert.Contains(t, content, `c.DiscordToken,`)
	assert.NotContains(t, content, `applyAuthFormat("Bot {bot_token}"`)
}

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

	tests := []struct {
		name         string
		canonical    string
		auth         spec.AuthConfig
		wantEnvVars  []string
		wantSpecName string
	}{
		{
			name:      "placeholder value ignored",
			canonical: "<CANONICAL_ENV_VAR, omit when unknown>",
			auth: spec.AuthConfig{
				Type:    "bearer_token",
				EnvVars: []string{"APIFY_HTTP_BEARER"},
				EnvVarSpecs: []spec.AuthEnvVar{
					{Name: "APIFY_HTTP_BEARER", Kind: spec.AuthEnvVarKindPerCall, Required: true, Sensitive: true, Inferred: true},
				},
			},
			wantEnvVars:  []string{"APIFY_HTTP_BEARER"},
			wantSpecName: "APIFY_HTTP_BEARER",
		},
		{
			name:      "lowercase value ignored",
			canonical: "apify_token",
			auth: spec.AuthConfig{
				Type:    "bearer_token",
				EnvVars: []string{"APIFY_HTTP_BEARER"},
				EnvVarSpecs: []spec.AuthEnvVar{
					{Name: "APIFY_HTTP_BEARER", Kind: spec.AuthEnvVarKindPerCall, Required: true, Sensitive: true, Inferred: true},
				},
			},
			wantEnvVars:  []string{"APIFY_HTTP_BEARER"},
			wantSpecName: "APIFY_HTTP_BEARER",
		},
		{
			name:      "no auth ignored",
			canonical: "APIFY_TOKEN",
			auth: spec.AuthConfig{
				Type: "none",
			},
			wantEnvVars: nil,
		},
		{
			name:      "basic auth ignored",
			canonical: "TWILIO_TOKEN",
			auth: spec.AuthConfig{
				Type:    "api_key",
				Format:  "Basic {credentials}",
				EnvVars: []string{"TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN"},
				EnvVarSpecs: []spec.AuthEnvVar{
					{Name: "TWILIO_ACCOUNT_SID", Kind: spec.AuthEnvVarKindPerCall, Required: true, Sensitive: false, Inferred: true},
					{Name: "TWILIO_AUTH_TOKEN", Kind: spec.AuthEnvVarKindPerCall, Required: true, Sensitive: true, Inferred: true},
				},
			},
			wantEnvVars:  []string{"TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN"},
			wantSpecName: "TWILIO_ACCOUNT_SID",
		},
		{
			name:      "multi credential auth ignored",
			canonical: "STRIPE_SECRET_KEY",
			auth: spec.AuthConfig{
				Type:    "oauth2",
				Format:  "Bearer {access_token}",
				EnvVars: []string{"CLIENT_ID", "CLIENT_SECRET"},
				EnvVarSpecs: []spec.AuthEnvVar{
					{Name: "CLIENT_ID", Kind: spec.AuthEnvVarKindPerCall, Required: true, Sensitive: false, Inferred: true},
					{Name: "CLIENT_SECRET", Kind: spec.AuthEnvVarKindPerCall, Required: true, Sensitive: true, Inferred: true},
				},
			},
			wantEnvVars:  []string{"CLIENT_ID", "CLIENT_SECRET"},
			wantSpecName: "CLIENT_ID",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			researchDir := t.TempDir()
			researchData, err := json.Marshal(&pipeline.ResearchResult{
				APIName: "test",
				Auth: &pipeline.ResearchAuth{
					CanonicalEnvVar: tt.canonical,
				},
			})
			require.NoError(t, err)
			require.NoError(t, os.WriteFile(filepath.Join(researchDir, "research.json"), researchData, 0o644))

			apiSpec := &spec.APISpec{Name: "test", Auth: tt.auth}
			applyResearchAuthMetadata(apiSpec, researchDir)

			assert.Equal(t, tt.wantEnvVars, apiSpec.Auth.EnvVars)
			if tt.wantSpecName == "" {
				assert.Empty(t, apiSpec.Auth.EnvVarSpecs)
			} else {
				require.NotEmpty(t, apiSpec.Auth.EnvVarSpecs)
				assert.Equal(t, tt.wantSpecName, apiSpec.Auth.EnvVarSpecs[0].Name)
			}
		})
	}
}

func runGoCommandForCLITest(t *testing.T, dir string, args ...string) {
	t.Helper()
	if testing.Short() && len(args) > 0 {
		if args[0] == "build" || (len(args) >= 2 && args[0] == "mod" && args[1] == "tidy") {
			t.Logf("skipping go %s in -short mode; full CI runs generated CLI compile coverage", strings.Join(args, " "))
			return
		}
	}
	cmd := exec.Command("go", args...)
	cmd.Dir = dir
	out, err := cmd.CombinedOutput()
	require.NoError(t, err, string(out))
}

// TestGenerateCmdRefusesPlaceholderBaseURL pins the contract that the
// generate command halts when the spec declares neither `servers:` nor
// any per-operation server. The parser falls back to a fake hostname so
// the spec keeps parsing, but the generate command must refuse to write
// a CLI whose `doctor` would DNS-fail on every call.
func TestGenerateCmdRefusesPlaceholderBaseURL(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	specPath := filepath.Join(dir, "spec.yaml")
	outputDir := filepath.Join(dir, "noserversapp")
	require.NoError(t, os.WriteFile(specPath, []byte(`openapi: "3.0.3"
info:
  title: No Servers App
  version: "1.0"
paths:
  /things:
    get:
      operationId: listThings
      responses:
        '200':
          description: OK
`), 0o644))

	cmd := newGenerateCmd()
	cmd.SetArgs([]string{
		"--spec", specPath,
		"--output", outputDir,
		"--validate=false",
		"--force",
	})

	err := cmd.Execute()
	require.Error(t, err, "expected generate to refuse a spec without servers")
	assert.Contains(t, err.Error(), specPath, "error must name the offending spec file")
	assert.Contains(t, err.Error(), "no `servers:`", "error must explain that the spec declares no servers")
	assert.NoDirExists(t, outputDir, "refusal must fire before any output is written")
}
