// Copyright 2026 printing-press-golden and contributors. Licensed under Apache-2.0. See LICENSE.
// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.

package cli

import (
	"encoding/json"
	"errors"
	"fmt"
	"testing"
)

// TestIsCobraUsageError covers the six pre-RunE error shapes Cobra and
// pflag can produce before any user RunE runs. Each must be detected so
// the caller can wrap in usageErr() and yield exit code 2.
func TestIsCobraUsageError(t *testing.T) {
	t.Parallel()
	cases := []struct {
		name  string
		errIn error
		want  bool
	}{
		{"nil", nil, false},
		{"unknown flag", errors.New("unknown flag: --foob"), true},
		{"unknown shorthand flag", errors.New("unknown shorthand flag: 'x' in -x"), true},
		{"unknown command", errors.New("unknown command \"foo\" for \"cli\""), true},
		{"required flag (singular)", errors.New("required flag \"query\" not set"), true},
		{"required flag(s) (plural)", errors.New("required flag(s) \"query\", \"vault\" not set"), true},
		{"flag needs argument", errors.New("flag needs an argument: --query"), true},
		{"invalid argument", errors.New("invalid argument \"abc\" for \"--limit\" flag: strconv.ParseInt: parsing \"abc\": invalid syntax"), true},
		// Non-usage errors must NOT be flagged — they should retain their
		// original (or wrapped *cliError) exit code.
		{"API error pass-through", errors.New("API returned 500"), false},
		{"network error pass-through", errors.New("dial tcp: connection refused"), false},
		// Pattern-tightness regressions: application RunE errors that
		// happen to mention "required flag" / "flag needs an argument" /
		// "invalid argument" as prose must NOT classify as usage errors.
		// The anchored prefixes guard against this; if the patterns are
		// ever loosened back to Contains() these tests catch the drift.
		{"app msg containing 'required flag' as prose", errors.New("the required flag config is missing from the manifest"), false},
		{"app msg containing 'flag needs an argument' as prose", errors.New("this flag needs an argument upstream before retry"), false},
		{"app msg containing 'invalid argument' as prose", errors.New("invalid argument provided to handler at /foo"), false},
	}
	for _, tc := range cases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			if got := isCobraUsageError(tc.errIn); got != tc.want {
				t.Errorf("isCobraUsageError(%v) = %v, want %v", tc.errIn, got, tc.want)
			}
		})
	}
}

// TestIsCobraUsageError_SurvivesHintRewrap covers the hint-suggestion
// path in Execute(): after rewrapping an "unknown flag" error with
// fmt.Errorf("%w\nhint: ...", err, ...), the rewrapped error must still
// classify as a usage error so the exit code stays at 2 rather than
// silently falling through to 1.
func TestIsCobraUsageError_SurvivesHintRewrap(t *testing.T) {
	t.Parallel()
	original := errors.New("unknown flag: --foob")
	wrapped := fmt.Errorf("%w\nhint: did you mean --foo?", original)
	if !isCobraUsageError(wrapped) {
		t.Errorf("hint-rewrapped unknown-flag error must still classify as usage error, got: %v", wrapped)
	}
}

// TestExitCode_UsageError_WrappedAsCode2 covers the end-to-end contract:
// after Execute() wraps a Cobra usage error in usageErr(), ExitCode()
// must return 2, not 1. This is the user-visible promise — a bad flag
// exits 2 (POSIX usage convention), not the generic-error-1 fallback.
func TestExitCode_UsageError_WrappedAsCode2(t *testing.T) {
	t.Parallel()
	wrapped := usageErr(errors.New("unknown flag: --foob"))
	if got := ExitCode(wrapped); got != 2 {
		t.Errorf("ExitCode(usageErr(...)) = %d, want 2 (POSIX usage convention)", got)
	}
}

// TestFilterFields covers --select projection against the four payload
// shapes printed CLIs see in practice: bare arrays, direct objects,
// list envelopes (Stripe/GitHub/Notion-style wrapper + array), and
// flat objects. The envelope cases guard against a regression where
// wrapper-key + array responses returned `{}` because the selector
// heads matched the inner record fields, not the wrapper key.
func TestFilterFields(t *testing.T) {
	t.Parallel()
	cases := []struct {
		name   string
		input  string
		fields string
		want   string
	}{
		{
			name:   "bare array element-wise",
			input:  `[{"id":"a","name":"x","other":"y"},{"id":"b","name":"z","other":"w"}]`,
			fields: "id,name",
			want:   `[{"id":"a","name":"x"},{"id":"b","name":"z"}]`,
		},
		{
			name:   "direct object top-level match",
			input:  `{"id":"a","name":"b","other":"c"}`,
			fields: "id,name",
			want:   `{"id":"a","name":"b"}`,
		},
		{
			name:   "envelope single array sibling (orgo {projects:[...]})",
			input:  `{"projects":[{"id":"a","name":"x","other":"y"}]}`,
			fields: "id,name",
			want:   `{"projects":[{"id":"a","name":"x"}]}`,
		},
		{
			name:   "envelope with metadata sibling (github {total_count,items:[...]})",
			input:  `{"total_count":2,"items":[{"id":"a","name":"x","other":"y"},{"id":"b","name":"z","other":"w"}]}`,
			fields: "id,name",
			want:   `{"items":[{"id":"a","name":"x"},{"id":"b","name":"z"}],"total_count":2}`,
		},
		{
			name:   "envelope with metadata sibling (stripe {object,data:[...]})",
			input:  `{"object":"list","data":[{"id":"a","name":"x","other":"y"}]}`,
			fields: "id,name",
			want:   `{"data":[{"id":"a","name":"x"}],"object":"list"}`,
		},
		{
			name:   "selector matches envelope key (no descent into array)",
			input:  `{"projects":[{"id":"a","name":"x","other":"y"}]}`,
			fields: "projects",
			want:   `{"projects":[{"id":"a","name":"x","other":"y"}]}`,
		},
		{
			name:   "dotted-path through envelope key still descends",
			input:  `{"projects":[{"id":"a","name":"x","other":"y"}]}`,
			fields: "projects.id",
			want:   `{"projects":[{"id":"a"}]}`,
		},
		{
			name:   "flat object no match returns empty (no array fallback)",
			input:  `{"a":1,"b":2}`,
			fields: "c",
			want:   `{}`,
		},
		{
			// Null pagination cursors are common envelope metadata.
			// json.Unmarshal accepts JSON null into []json.RawMessage as
			// a nil slice without error, so the array check must reject
			// nil explicitly or null siblings would be coerced to `[]`.
			name:   "envelope preserves null sibling verbatim",
			input:  `{"items":[{"id":"a","name":"x"}],"next_cursor":null}`,
			fields: "id,name",
			want:   `{"items":[{"id":"a","name":"x"}],"next_cursor":null}`,
		},
		{
			// Without a real array sibling the envelope fallback does not
			// fire, so a flat object whose only "extra" key is null still
			// returns {} for a non-matching selector.
			name:   "flat object with null sibling no match returns empty",
			input:  `{"a":1,"b":null}`,
			fields: "c",
			want:   `{}`,
		},
		{
			// Multiple array siblings at the same level each receive the
			// selector independently. Documents that the projection fans
			// out across every array, not just the first one found.
			name:   "envelope with two array siblings filters both",
			input:  `{"events":[{"id":"e1","other":"x"}],"speakers":[{"id":"s1","other":"y"}]}`,
			fields: "id",
			want:   `{"events":[{"id":"e1"}],"speakers":[{"id":"s1"}]}`,
		},
		{
			// Envelope fallback is intentionally one level deep. A nested
			// object envelope like {"data":{"items":[...]}} surfaces no
			// array at the outer level, so the fallback does not fire and
			// the result is the empty-object that flat-no-match would
			// produce. Pins the boundary so a future deeper-walk change
			// is an explicit decision, not an accident.
			name:   "nested object envelope returns empty (one-level only)",
			input:  `{"data":{"items":[{"id":"a","other":"y"}]}}`,
			fields: "id",
			want:   `{}`,
		},
	}
	for _, tc := range cases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			got := filterFields(json.RawMessage(tc.input), tc.fields)
			// Normalize both sides through json.Unmarshal+Marshal so
			// map-iteration order does not produce false negatives.
			var gotV, wantV interface{}
			if err := json.Unmarshal(got, &gotV); err != nil {
				t.Fatalf("got is invalid json: %v (raw=%s)", err, string(got))
			}
			if err := json.Unmarshal([]byte(tc.want), &wantV); err != nil {
				t.Fatalf("want is invalid json: %v (raw=%s)", err, tc.want)
			}
			gotBytes, _ := json.Marshal(gotV)
			wantBytes, _ := json.Marshal(wantV)
			if string(gotBytes) != string(wantBytes) {
				t.Errorf("filterFields(%q, %q) = %s, want %s",
					tc.input, tc.fields, string(gotBytes), string(wantBytes))
			}
		})
	}
}
