package cli

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

	catalogfs "github.com/mvanhorn/cli-printing-press/v4/catalog"
	"github.com/mvanhorn/cli-printing-press/v4/internal/browsersniff"
	"github.com/mvanhorn/cli-printing-press/v4/internal/catalog"
	"github.com/mvanhorn/cli-printing-press/v4/internal/catalogmeta"
	"github.com/mvanhorn/cli-printing-press/v4/internal/pipeline"
	"github.com/mvanhorn/cli-printing-press/v4/internal/spec"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

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 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")
}

// 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)
}

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)
}

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 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")
}

// 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)
}

// 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")
}

// 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",
		"--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 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 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 TestOpenAPIAuthPreferenceForGenerateFromJiraCatalogEntry(t *testing.T) {
	t.Parallel()

	jira, err := catalog.LookupFS(catalogfs.FS, "jira")
	require.NoError(t, err)
	require.Equal(t, "basicAuth", jira.AuthPreference)

	assert.Equal(t, "basicAuth", openAPIAuthPreferenceForGenerate("", "", []string{jira.SpecURL}, ""),
		"catalog spec_url match should forward auth_preference without --auth-preference")
	assert.Equal(t, "OAuth2", openAPIAuthPreferenceForGenerate("OAuth2", "", []string{jira.SpecURL}, ""),
		"explicit --auth-preference must override catalog")

	localSpec := filepath.Join(t.TempDir(), "swagger.json")
	assert.Equal(t, "basicAuth", openAPIAuthPreferenceForGenerate("", "jira", []string{localSpec}, ""),
		"catalog slug via --name should resolve auth_preference without https spec refs")
}

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

	// Mirrors real Jira-style specs: OAuth2 authorizationCode + HTTP Basic; default
	// parser choice is OAuth2 unless AuthPreference pins basicAuth (see openapi parser tests).
	specBytes := []byte(`openapi: "3.0.3"
info:
  title: Atlassian-like
  version: "1.0"
servers:
  - url: https://example.atlassian.net
components:
  securitySchemes:
    OAuth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://auth.example.com/authorize
          tokenUrl: https://auth.example.com/token
          scopes:
            read: read access
    basicAuth:
      type: http
      scheme: basic
paths:
  /v1/things:
    get:
      operationId: list things
      security:
        - basicAuth: []
        - OAuth2: [read]
      responses: {"200": {description: ok}}
`)

	jira, err := catalog.LookupFS(catalogfs.FS, "jira")
	require.NoError(t, err)

	pref := openAPIAuthPreferenceForGenerate("", "", []string{jira.SpecURL}, "")
	require.Equal(t, "basicAuth", pref)

	parsed, err := parseOpenAPISpec(filepath.Join(t.TempDir(), "spec.yaml"), specBytes, false, pref)
	require.NoError(t, err)
	assert.Equal(t, "basicAuth", parsed.Auth.Scheme)
	assert.Equal(t, "api_key", parsed.Auth.Type)

	defaultParsed, err := parseOpenAPISpec(filepath.Join(t.TempDir(), "spec2.yaml"), specBytes, false, "")
	require.NoError(t, err)
	assert.Equal(t, "OAuth2", defaultParsed.Auth.Scheme, "without catalog-driven preference, OAuth2 wins")
}

func TestEnrichSpecFromCatalogCopiesGenerationMetadata(t *testing.T) {
	apiSpec := &spec.APISpec{Name: "test-api", BaseURL: spec.PlaceholderBaseURL, BaseURLIsPlaceholder: true}

	enrichSpecFromCatalogEntry(apiSpec, &catalog.Entry{
		DisplayName: "Test.API",
		OwnerName:   "Trevin Chow",
		BaseURL:     "https://api.example.com/",
		MCP: spec.MCPConfig{
			Transport:     []string{"stdio", "http"},
			Orchestration: "code",
			EndpointTools: "hidden",
		},
	})

	assert.Equal(t, "Test.API", apiSpec.DisplayName)
	assert.Equal(t, "Trevin Chow", apiSpec.OwnerName)
	assert.Equal(t, "https://api.example.com", apiSpec.BaseURL)
	assert.False(t, apiSpec.BaseURLIsPlaceholder)
	assert.Equal(t, []string{"stdio", "http"}, apiSpec.MCP.Transport)
	assert.Equal(t, "code", apiSpec.MCP.Orchestration)
	assert.Equal(t, "hidden", apiSpec.MCP.EndpointTools)
}

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"},
		},
	}

	catalogmeta.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 TestEnrichSpecFromCatalogMatchesSpecURLWhenSlugDiffers(t *testing.T) {
	apiSpec := &spec.APISpec{
		Name:                        "cloud-run-admin",
		DisplayName:                 "Cloud Run Admin",
		DisplayNameDerivedFromTitle: true,
	}

	enrichSpecFromCatalog(apiSpec, "https://api.apis.guru/v2/specs/googleapis.com/run/v2/openapi.yaml")

	assert.Equal(t, "Google Cloud Run", apiSpec.DisplayName)
	assert.False(t, apiSpec.DisplayNameDerivedFromTitle)
	assert.Equal(t, "cloud", apiSpec.Category)
	assert.Equal(t, "https://cloud.google.com/run/docs/reference/rest", apiSpec.WebsiteURL)
}

func TestEnrichSpecFromCatalogReplacesTitleDerivedDisplayName(t *testing.T) {
	apiSpec := &spec.APISpec{
		Name:                        "trigger-dev",
		DisplayName:                 "Trigger Dev",
		DisplayNameDerivedFromTitle: true,
	}

	enrichSpecFromCatalogEntry(apiSpec, &catalog.Entry{
		DisplayName: "Trigger.dev",
	})

	assert.Equal(t, "Trigger.dev", apiSpec.DisplayName)
	assert.False(t, apiSpec.DisplayNameDerivedFromTitle)
}

func TestEnrichSpecFromCatalogKeepsExplicitDisplayName(t *testing.T) {
	apiSpec := &spec.APISpec{
		Name:        "trigger-dev",
		DisplayName: "Spec.dev",
	}

	enrichSpecFromCatalogEntry(apiSpec, &catalog.Entry{
		DisplayName: "Trigger.dev",
	})

	assert.Equal(t, "Spec.dev", apiSpec.DisplayName)
}

func runGoCommandForCLITest(t *testing.T, dir string, args ...string) {
	t.Helper()
	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")
}
