package notion

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"sort"
	"testing"

	"github.com/stretchr/testify/require"

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

// The guarded REST client rejects plain http origins, so tests drive a TLS test
// double through a trust-all transport that stands in for the guarded default.
func newLoopbackRESTClient(t *testing.T, server *httptest.Server) *runner.RESTClient {
	t.Helper()
	client, err := runner.NewRESTClient(runner.RESTOptions{
		BaseURL: server.URL,
		Auth:    AuthSpec(),
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
		},
	})
	require.NoError(t, err)
	return client
}

// capturedRequest is one recorded provider request.
type capturedRequest struct {
	Method        string
	Path          string
	RawQuery      string
	Body          string
	Authorization string
	NotionVersion string
}

// startRecordingServer serves respond and records every incoming request.
func startRecordingServer(
	t *testing.T,
	respond func(w http.ResponseWriter, r *http.Request),
) (*httptest.Server, *[]capturedRequest) {
	t.Helper()
	var captured []capturedRequest
	server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		payload, err := io.ReadAll(r.Body)
		require.NoError(t, err)
		captured = append(captured, capturedRequest{
			Method:        r.Method,
			Path:          r.URL.Path,
			RawQuery:      r.URL.RawQuery,
			Body:          string(payload),
			Authorization: r.Header.Get("Authorization"),
			NotionVersion: r.Header.Get("Notion-Version"),
		})
		respond(w, r)
	}))
	t.Cleanup(server.Close)
	return server, &captured
}

func writeJSON(w http.ResponseWriter, payload string) {
	w.Header().Set("Content-Type", "application/json")
	_, _ = io.WriteString(w, payload)
}

// normalizedJSONString re-encodes both sides through the same decoder so key order
// and whitespace never make assertions flaky.
func normalizedJSONString(t *testing.T, value string) string {
	t.Helper()
	if value == "" {
		return ""
	}
	var decoded any
	require.NoError(t, json.Unmarshal([]byte(value), &decoded), "decode JSON %q", value)
	encoded, err := json.Marshal(decoded)
	require.NoError(t, err)
	return string(encoded)
}

// wantRequest pins the wire shape of one provider request.
type wantRequest struct {
	Method string
	Path   string
	Query  string
	Body   string
}

func TestHandlersMatchDefinitionCatalog(t *testing.T) {
	definition := Definition()
	names := make([]string, 0, len(definition.Actions))
	for _, action := range definition.Actions {
		names = append(names, action.Name)
	}
	handlers := Handlers()
	handlerNames := make([]string, 0, len(handlers))
	for name := range handlers {
		handlerNames = append(handlerNames, name)
	}
	sort.Strings(names)
	sort.Strings(handlerNames)
	require.Equal(t, names, handlerNames)

	ext, err := Extension(nil)
	require.NoError(t, err)
	executor, ok := ext.Implementation.Executor.(*runner.RESTExecutor)
	require.True(t, ok, "executor must be the shared REST executor")
	require.Equal(t, handlerNames, executor.ActionNames())
}

func TestExtensionWiring(t *testing.T) {
	ext, err := Extension(nil)
	require.NoError(t, err)
	require.Equal(t, Domain, ext.Definition.Domain)
	require.Equal(t, Revision, ext.Implementation.Revision)
	require.Equal(t, registry.ConnectionRequired, ext.Implementation.ConnectionPolicy)
	require.Equal(t, AuthSpec(), ext.Implementation.Auth)
	require.NotNil(t, ext.Implementation.ValidateCredentials)
	require.Equal(t, "app_notion", ext.Distribution.AppID)
	require.Equal(t, "notion", ext.Distribution.AppSlug)
	require.Equal(t, Domain, ext.Distribution.DefaultName)
	require.True(t, ext.Distribution.DefaultEnabled)
	require.Equal(t, "/app-icons/notion.svg", ext.Distribution.IconURL)
}

// TestDefinitionPassesSharedCatalogValidation feeds the catalog through the
// shared spec validator bootstrap applies so a kebab-case, safety, schema, or
// example regression fails here instead of at startup.
func TestDefinitionPassesSharedCatalogValidation(t *testing.T) {
	definition := Definition()
	meta := spec.Metadata{
		SchemaVersion: 1,
		Domain:        definition.Domain,
		Version:       definition.Version,
		Name:          definition.Name,
		Description:   definition.Description,
	}
	actions := make([]spec.Action, 0, len(definition.Actions))
	for _, action := range definition.Actions {
		actions = append(actions, spec.Action{
			Name: action.Name,
			Catalog: spec.Catalog{
				Summary:      action.Summary,
				Description:  action.Description,
				Safety:       action.Safety,
				InputSchema:  action.InputSchema,
				OutputSchema: action.OutputSchema,
				Example:      action.Example,
			},
		})
	}
	require.NoError(t, spec.ValidateCatalog(meta, actions))
}

func TestInvokeActions(t *testing.T) {
	credentials := runner.Credentials{"apiKey": "secret_test"}
	const pageID = "1f48b1c8-f5a8-4b23-9a3e-da3d6c7f4e2a"
	pageObject := `{"object":"page","id":"` + pageID + `","archived":false}`
	tests := []struct {
		name       string
		action     string
		input      string
		respond    func(w http.ResponseWriter, r *http.Request)
		want       []wantRequest
		wantOutput string
	}{
		{
			name:   "search posts the query body",
			action: ActionSearch,
			input: `{"query":"onboarding","pageSize":5,"startCursor":"cursor-2",` +
				`"sort":{"direction":"ascending","timestamp":"last_edited_time"}}`,
			respond: func(w http.ResponseWriter, r *http.Request) {
				writeJSON(w, `{"object":"list","results":[{"object":"page","id":"page-1"},`+
					`{"object":"data_source","id":"ds-1"}],"next_cursor":"cursor-3","has_more":true,`+
					`"request_id":"req-7"}`)
			},
			want: []wantRequest{{
				Method: "POST",
				Path:   "/search",
				Body: `{"page_size":5,"query":"onboarding",` +
					`"sort":{"direction":"ascending","timestamp":"last_edited_time"},"start_cursor":"cursor-2"}`,
			}},
			wantOutput: `{"object":"list","results":[{"object":"page","id":"page-1"},` +
				`{"object":"data_source","id":"ds-1"}],"next_cursor":"cursor-3","has_more":true,` +
				`"request_id":"req-7"}`,
		},
		{
			name:   "search always carries the query key",
			action: ActionSearch,
			input:  `{"query":""}`,
			respond: func(w http.ResponseWriter, r *http.Request) {
				writeJSON(w, `{"object":"list","results":[],"next_cursor":null,"has_more":false}`)
			},
			want:       []wantRequest{{Method: "POST", Path: "/search", Body: `{"query":""}`}},
			wantOutput: `{"object":"list","results":[],"next_cursor":null,"has_more":false}`,
		},
		{
			name:   "get page aggregates the page and its child blocks",
			action: ActionGetPage,
			input:  `{"pageId":"` + pageID + `"}`,
			respond: func(w http.ResponseWriter, r *http.Request) {
				switch r.URL.Path {
				case "/pages/" + pageID:
					writeJSON(w, pageObject)
				case "/blocks/" + pageID + "/children":
					writeJSON(w, `{"object":"list","results":[{"object":"block","id":"block-1"}],`+
						`"next_cursor":null,"has_more":false}`)
				default:
					t.Errorf("unexpected provider path %q", r.URL.Path)
				}
			},
			want: []wantRequest{
				{Method: "GET", Path: "/pages/" + pageID},
				{Method: "GET", Path: "/blocks/" + pageID + "/children"},
			},
			wantOutput: `{"page":` + pageObject + `,"block_children":{"object":"list",` +
				`"results":[{"object":"block","id":"block-1"}],"next_cursor":null,"has_more":false}}`,
		},
		{
			name:   "create page passes an explicit parent through",
			action: ActionCreatePage,
			input: `{"parent":{"type":"page_id","page_id":"` + pageID + `"},` +
				`"properties":{"title":{"title":[{"type":"text","text":{"content":"Spec"}}]}},` +
				`"icon":{"type":"emoji","emoji":"-"}}`,
			respond: func(w http.ResponseWriter, r *http.Request) { writeJSON(w, pageObject) },
			want: []wantRequest{{
				Method: "POST",
				Path:   "/pages",
				Body: `{"icon":{"emoji":"-","type":"emoji"},"parent":{"page_id":"` + pageID + `","type":"page_id"},` +
					`"properties":{"title":{"title":[{"text":{"content":"Spec"},"type":"text"}]}}}`,
			}},
			wantOutput: pageObject,
		},
		{
			name:    "create page builds a page parent and title property",
			action:  ActionCreatePage,
			input:   `{"parentId":"` + pageID + `","title":"AgentTeam integrations"}`,
			respond: func(w http.ResponseWriter, r *http.Request) { writeJSON(w, pageObject) },
			want: []wantRequest{{
				Method: "POST",
				Path:   "/pages",
				Body: `{"parent":{"page_id":"` + pageID + `"},"properties":{"title":` +
					`{"title":[{"text":{"content":"AgentTeam integrations"},"type":"text"}]}}}`,
			}},
			wantOutput: pageObject,
		},
		{
			name:       "create page accepts markdown-only content",
			action:     ActionCreatePage,
			input:      `{"markdown":"# Hello"}`,
			respond:    func(w http.ResponseWriter, r *http.Request) { writeJSON(w, pageObject) },
			want:       []wantRequest{{Method: "POST", Path: "/pages", Body: `{"markdown":"# Hello"}`}},
			wantOutput: pageObject,
		},
		{
			name:    "create page forwards an empty markdown string",
			action:  ActionCreatePage,
			input:   `{"parent":{"page_id":"` + pageID + `"},"markdown":""}`,
			respond: func(w http.ResponseWriter, r *http.Request) { writeJSON(w, pageObject) },
			want: []wantRequest{{
				Method: "POST",
				Path:   "/pages",
				Body:   `{"markdown":"","parent":{"page_id":"` + pageID + `"}}`,
			}},
			wantOutput: pageObject,
		},
		{
			name:    "update page merges the title into the properties patch",
			action:  ActionUpdatePage,
			input:   `{"pageId":"` + pageID + `","title":"Renamed","in_trash":false}`,
			respond: func(w http.ResponseWriter, r *http.Request) { writeJSON(w, pageObject) },
			want: []wantRequest{{
				Method: "PATCH",
				Path:   "/pages/" + pageID,
				Body: `{"in_trash":false,"properties":{"title":` +
					`{"title":[{"text":{"content":"Renamed"},"type":"text"}]}}}`,
			}},
			wantOutput: pageObject,
		},
		{
			name:   "append block posts one paragraph child",
			action: ActionAppendBlock,
			input:  `{"pageId":"` + pageID + `","text":"Ported paragraph."}`,
			respond: func(w http.ResponseWriter, r *http.Request) {
				writeJSON(w, `{"object":"list","results":[{"object":"block","id":"block-9"}],`+
					`"next_cursor":null,"has_more":false,"request_id":"req-3"}`)
			},
			want: []wantRequest{{
				Method: "PATCH",
				Path:   "/blocks/" + pageID + "/children",
				Body: `{"children":[{"object":"block","paragraph":{"rich_text":` +
					`[{"text":{"content":"Ported paragraph."},"type":"text"}]},"type":"paragraph"}]}`,
			}},
			wantOutput: `{"object":"list","results":[{"object":"block","id":"block-9"}],` +
				`"next_cursor":null,"has_more":false,"request_id":"req-3"}`,
		},
		{
			name:   "retrieve page markdown forwards the transcript flag",
			action: ActionRetrievePageMarkdown,
			input:  `{"pageId":"` + pageID + `","includeTranscript":false}`,
			respond: func(w http.ResponseWriter, r *http.Request) {
				writeJSON(w, `{"markdown":"# Hello","request_id":"req-4"}`)
			},
			want: []wantRequest{{
				Method: "GET",
				Path:   "/pages/" + pageID + "/markdown",
				Query:  "include_transcript=false",
			}},
			wantOutput: `{"markdown":"# Hello","request_id":"req-4"}`,
		},
		{
			name:   "list users pages the workspace directory",
			action: ActionListUsers,
			input:  `{"pageSize":2,"startCursor":"cursor-1"}`,
			respond: func(w http.ResponseWriter, r *http.Request) {
				writeJSON(w, `{"object":"list","results":[{"object":"user","id":"user-1",`+
					`"name":null,"avatar_url":null}],"next_cursor":null,"has_more":false}`)
			},
			want: []wantRequest{{
				Method: "GET",
				Path:   "/users",
				Query:  "page_size=2&start_cursor=cursor-1",
			}},
			wantOutput: `{"object":"list","results":[{"object":"user","id":"user-1",` +
				`"name":null,"avatar_url":null}],"next_cursor":null,"has_more":false}`,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			server, captured := startRecordingServer(t, test.respond)
			client := newLoopbackRESTClient(t, server)
			handler, ok := Handlers()[test.action]
			require.True(t, ok, "action %q is missing a handler", test.action)
			output, err := handler(context.Background(), client, credentials, json.RawMessage(test.input))
			require.NoError(t, err)
			require.Len(t, *captured, len(test.want))
			for index, want := range test.want {
				got := (*captured)[index]
				require.Equal(t, want.Method, got.Method, "request %d method", index)
				require.Equal(t, want.Path, got.Path, "request %d path", index)
				require.Equal(t, want.Query, got.RawQuery, "request %d query", index)
				require.Equal(t,
					normalizedJSONString(t, want.Body), normalizedJSONString(t, got.Body), "request %d body", index)
				require.Equal(t, "Bearer secret_test", got.Authorization, "request %d bearer", index)
				require.Equal(t, APIVersion, got.NotionVersion,
					"request %d must pin the %s header", index, APIVersionHeader)
			}
			require.Equal(t,
				normalizedJSONString(t, test.wantOutput), normalizedJSONString(t, string(output)))
		})
	}
}

// TestUpdatePageExplicitNullVersusAbsent pins the upstream nullable guards on
// update_page. Upstream builds the body with compactObject over
// `typeof input.X === "boolean" ? input.X : undefined` (in_trash, is_locked,
// erase_content) and `typeof input.title === "string"` (title), so real values
// — false and "" included — reach the wire while an explicit null is dropped
// exactly like an absent key: compactObject only omits undefined.
func TestUpdatePageExplicitNullVersusAbsent(t *testing.T) {
	credentials := runner.Credentials{"apiKey": "secret_test"}
	const pageID = "1f48b1c8-f5a8-4b23-9a3e-da3d6c7f4e2a"
	pageObject := `{"object":"page","id":"` + pageID + `","archived":false}`
	tests := []struct {
		name     string
		input    string
		wantBody string
	}{
		{
			name: "explicit nulls are omitted, never serialized as null",
			input: `{"pageId":"` + pageID + `","title":null,"properties":null,"icon":null,` +
				`"cover":null,"template":null,"in_trash":null,"is_locked":null,"erase_content":null}`,
			wantBody: `{}`,
		},
		{
			name:     "absent fields are omitted",
			input:    `{"pageId":"` + pageID + `"}`,
			wantBody: `{}`,
		},
		{
			name: "real values survive, including false and the empty title",
			input: `{"pageId":"` + pageID + `","title":"","in_trash":false,` +
				`"is_locked":true,"erase_content":false}`,
			wantBody: `{"erase_content":false,"in_trash":false,"is_locked":true,` +
				`"properties":{"title":{"title":[{"text":{"content":""},"type":"text"}]}}}`,
		},
	}
	bodies := make(map[string]string, len(tests))
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			server, captured := startRecordingServer(t, func(w http.ResponseWriter, r *http.Request) {
				writeJSON(w, pageObject)
			})
			client := newLoopbackRESTClient(t, server)
			output, err := Handlers()[ActionUpdatePage](context.Background(), client, credentials,
				json.RawMessage(test.input))
			require.NoError(t, err)
			require.Len(t, *captured, 1)
			require.Equal(t, "PATCH", (*captured)[0].Method)
			require.Equal(t, "/pages/"+pageID, (*captured)[0].Path)
			require.Equal(t,
				normalizedJSONString(t, test.wantBody), normalizedJSONString(t, (*captured)[0].Body), "request body")
			require.NotContains(t, (*captured)[0].Body, "null",
				"upstream drops explicit nulls like absent fields; it never writes null")
			require.Equal(t, normalizedJSONString(t, pageObject), normalizedJSONString(t, string(output)),
				"the updated page is returned verbatim")
			bodies[test.name] = (*captured)[0].Body
		})
	}
	require.Equal(t, bodies["explicit nulls are omitted, never serialized as null"],
		bodies["absent fields are omitted"],
		"upstream treats explicit null and absent identically for the typeof-guarded fields")
}

// TestNullableDistinguishesAbsentNullAndValue proves the decode keeps the
// three input states apart so the body builders can apply the upstream guards
// instead of conflating null with absent or with the zero value.
func TestNullableDistinguishesAbsentNullAndValue(t *testing.T) {
	var decoded struct {
		Absent   Nullable[string] `json:"absent"`
		Null     Nullable[string] `json:"null"`
		Value    Nullable[string] `json:"value"`
		Empty    Nullable[string] `json:"empty"`
		Flag     Nullable[bool]   `json:"flag"`
		NullFlag Nullable[bool]   `json:"nullFlag"`
	}
	require.NoError(t, json.Unmarshal([]byte(
		`{"null":null,"value":"x","empty":"","flag":false,"nullFlag":null}`), &decoded))

	require.False(t, decoded.Absent.Set, "an absent key leaves the zero Nullable")
	require.False(t, decoded.Absent.Null)
	require.Nil(t, decoded.Absent.Compact(), "an absent key is omitted from the body")

	require.True(t, decoded.Null.Set, "an explicit null is present")
	require.True(t, decoded.Null.Null, "an explicit null is null")
	require.False(t, decoded.Null.Present())
	require.Nil(t, decoded.Null.Compact(), "a null key is omitted from the body")

	require.True(t, decoded.Value.Present())
	require.Equal(t, "x", decoded.Value.Value)
	require.Equal(t, "x", decoded.Value.Compact())

	require.True(t, decoded.Empty.Present(), "the empty string is a real value, not an absent key")
	require.Equal(t, "", decoded.Empty.Value)
	require.Equal(t, "", decoded.Empty.Compact(), "the empty string reaches the wire")

	require.True(t, decoded.Flag.Present(), "false is a real value, not an absent key")
	require.False(t, decoded.Flag.Value)
	require.Equal(t, false, decoded.Flag.Compact(), "false reaches the wire")

	require.True(t, decoded.NullFlag.Set)
	require.True(t, decoded.NullFlag.Null)
	require.Nil(t, decoded.NullFlag.Compact())
}

// TestEmptyProviderBodyAnswersEmptyObject pins the upstream `payload ?? {}`
// fallback: an empty (or null) 2xx body still yields one JSON object instead
// of null, with no re-wrapping.
func TestEmptyProviderBodyAnswersEmptyObject(t *testing.T) {
	credentials := runner.Credentials{"apiKey": "secret_test"}
	server, captured := startRecordingServer(t, func(w http.ResponseWriter, r *http.Request) {})
	client := newLoopbackRESTClient(t, server)
	output, err := Handlers()[ActionListUsers](context.Background(), client, credentials,
		json.RawMessage(`{}`))
	require.NoError(t, err)
	require.Len(t, *captured, 1)
	require.Equal(t, "{}", string(output))
}

func TestHandlersClassifyUpstreamStatuses(t *testing.T) {
	credentials := runner.Credentials{"apiKey": "secret_test"}
	tests := []struct {
		name   string
		status int
		want   runner.ErrorCode
	}{
		{"unauthorized", http.StatusUnauthorized, runner.ErrorProviderAuthUnavailable},
		{"forbidden", http.StatusForbidden, runner.ErrorProviderAuthUnavailable},
		{"not found", http.StatusNotFound, runner.ErrorNotFound},
		{"rate limited", http.StatusTooManyRequests, runner.ErrorRateLimited},
		{"bad request", http.StatusBadRequest, runner.ErrorValidation},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			server, _ := startRecordingServer(t, func(w http.ResponseWriter, r *http.Request) {
				w.Header().Set("Content-Type", "application/json")
				w.WriteHeader(test.status)
				_, _ = io.WriteString(w,
					`{"object":"error","code":"unauthorized","message":"Invalid API token."}`)
			})
			client := newLoopbackRESTClient(t, server)
			output, err := Handlers()[ActionSearch](context.Background(), client, credentials,
				json.RawMessage(`{"query":"onboarding"}`))
			require.Error(t, err)
			require.Equal(t, test.want, runner.CodeOf(err))
			require.Nil(t, output)
		})
	}
}

func TestHandlersRequireCredentials(t *testing.T) {
	server, captured := startRecordingServer(t, func(w http.ResponseWriter, r *http.Request) {
		t.Error("the handler must fail closed before dialing the provider")
	})
	client := newLoopbackRESTClient(t, server)
	for name, credentials := range map[string]runner.Credentials{
		"nil":              nil,
		"empty":            {},
		"undeclared field": {"notionToken": "secret_test"},
	} {
		t.Run(name, func(t *testing.T) {
			output, err := Handlers()[ActionListUsers](context.Background(), client, credentials,
				json.RawMessage(`{}`))
			require.Error(t, err)
			require.Equal(t, runner.ErrorConnectionUnavailable, runner.CodeOf(err))
			require.Nil(t, output)
		})
	}
	require.Empty(t, *captured)
}

func TestCreatePageInputValidation(t *testing.T) {
	credentials := runner.Credentials{"apiKey": "secret_test"}
	const pageID = "1f48b1c8-f5a8-4b23-9a3e-da3d6c7f4e2a"
	tests := []struct {
		name  string
		input string
	}{
		{"markdown cannot be used with children", `{"markdown":"# Hi","children":[{"type":"paragraph"}]}`},
		{"title cannot be used with parent", `{"parent":{"page_id":"` + pageID + `"},"title":"Nope"}`},
		{
			"parent and parentId must agree",
			`{"parent":{"page_id":"` + pageID + `"},"parentId":"00000000-0000-0000-0000-000000000000"}`,
		},
		{"a parent shape is required", `{}`},
		{"title is required with parentId", `{"parentId":"` + pageID + `"}`},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			server, captured := startRecordingServer(t, func(w http.ResponseWriter, r *http.Request) {
				t.Error("validation must fail closed before dialing the provider")
			})
			client := newLoopbackRESTClient(t, server)
			output, err := Handlers()[ActionCreatePage](context.Background(), client, credentials,
				json.RawMessage(test.input))
			require.Error(t, err)
			require.Equal(t, runner.ErrorValidation, runner.CodeOf(err))
			require.Nil(t, output)
			require.Empty(t, *captured)
		})
	}
}

func TestValidateCredentialsWith(t *testing.T) {
	credentials := runner.Credentials{"apiKey": "secret_test"}

	t.Run("accepts a secret that resolves a bot id", func(t *testing.T) {
		server, captured := startRecordingServer(t, func(w http.ResponseWriter, r *http.Request) {
			writeJSON(w, `{"object":"user","id":"bot-1","name":"agentteam","bot":{"workspace_name":"AgentTeam"}}`)
		})
		client := newLoopbackRESTClient(t, server)
		require.NoError(t, validateCredentialsWith(context.Background(), client, credentials))
		require.Len(t, *captured, 1)
		require.Equal(t, "GET", (*captured)[0].Method)
		require.Equal(t, "/users/me", (*captured)[0].Path)
		require.Equal(t, "Bearer secret_test", (*captured)[0].Authorization)
		require.Equal(t, APIVersion, (*captured)[0].NotionVersion)
	})

	t.Run("rejects a secret without a resolvable id", func(t *testing.T) {
		server, _ := startRecordingServer(t, func(w http.ResponseWriter, r *http.Request) {
			writeJSON(w, `{"object":"user","name":"anonymous"}`)
		})
		client := newLoopbackRESTClient(t, server)
		err := validateCredentialsWith(context.Background(), client, credentials)
		require.Error(t, err)
		require.Equal(t, runner.ErrorProviderAuthUnavailable, runner.CodeOf(err))
	})

	t.Run("propagates the classified upstream rejection", func(t *testing.T) {
		server, _ := startRecordingServer(t, func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusUnauthorized)
			_, _ = io.WriteString(w, `{"object":"error","code":"unauthorized","message":"Invalid API token."}`)
		})
		client := newLoopbackRESTClient(t, server)
		err := validateCredentialsWith(context.Background(), client, credentials)
		require.Error(t, err)
		require.Equal(t, runner.ErrorProviderAuthUnavailable, runner.CodeOf(err))
	})
}
