package runner_test

import (
	"context"
	"crypto/tls"
	"encoding/base64"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"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/service/extensionflow"
)

func insecureTransport() *http.Transport {
	return &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
}

func testClient(t *testing.T, baseURL string, auth runner.AuthSpec) *runner.RESTClient {
	t.Helper()
	client, err := runner.NewRESTClient(runner.RESTOptions{
		BaseURL:   baseURL,
		Auth:      auth,
		Transport: insecureTransport(),
	})
	require.NoError(t, err)
	return client
}

func bearerAuth() runner.AuthSpec {
	return runner.AuthSpec{
		Kind:   runner.AuthBearer,
		Fields: []runner.CredentialField{runner.APIKeyField("Token", "paste")},
	}
}

func headerAuth() runner.AuthSpec {
	return runner.AuthSpec{
		Kind:   runner.AuthHeader,
		Header: "x-api-token",
		Fields: []runner.CredentialField{runner.APIKeyField("Token", "paste")},
	}
}

func basicAuth() runner.AuthSpec {
	return runner.AuthSpec{
		Kind:          runner.AuthBasic,
		BasicUsername: "api",
		Fields:        []runner.CredentialField{runner.APIKeyField("Password", "paste")},
	}
}

func dynamicBasicAuth() runner.AuthSpec {
	return runner.AuthSpec{
		Kind:               runner.AuthBasic,
		BasicUsernameField: "username",
		BasicPasswordField: "apiToken",
		Fields: []runner.CredentialField{
			{Name: "apiToken", DisplayName: "API token", Required: true, Secret: true},
			{Name: "username", DisplayName: "Username", Required: true},
		},
	}
}

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

	_, err := runner.NewRESTClient(runner.RESTOptions{
		BaseURL: "http://api.example.com",
		Auth:    bearerAuth(),
	})
	require.ErrorContains(t, err, "absolute https origin")
	_, err = runner.NewRESTClient(runner.RESTOptions{
		BaseURL: "api.example.com/v1",
		Auth:    bearerAuth(),
	})
	require.ErrorContains(t, err, "absolute https origin")
}

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

	cases := []struct {
		name        string
		auth        runner.AuthSpec
		credentials runner.Credentials
		assert      func(t *testing.T, request http.Request)
	}{
		{
			name: "bearer",
			auth: bearerAuth(),
			assert: func(t *testing.T, request http.Request) {
				require.Equal(t, "Bearer secret-token", request.Header.Get("Authorization"))
			},
		},
		{
			name: "custom header",
			auth: headerAuth(),
			assert: func(t *testing.T, request http.Request) {
				require.Equal(t, "secret-token", request.Header.Get("X-Api-Token"))
			},
		},
		{
			name: "basic with fixed username",
			auth: basicAuth(),
			assert: func(t *testing.T, request http.Request) {
				require.Equal(t, "Basic "+base64Of("api:secret-token"),
					request.Header.Get("Authorization"))
			},
		},
		{
			name:        "basic with dynamic username and password",
			auth:        dynamicBasicAuth(),
			credentials: runner.Credentials{"username": "jenkins-user", "apiToken": "jenkins-token"},
			assert: func(t *testing.T, request http.Request) {
				require.Equal(t, "Basic "+base64Of("jenkins-user:jenkins-token"),
					request.Header.Get("Authorization"))
			},
		},
		{
			name:        "none sends no credential",
			auth:        runner.AuthSpec{},
			credentials: runner.Credentials{},
			assert: func(t *testing.T, request http.Request) {
				require.Empty(t, request.Header.Get("Authorization"))
			},
		},
	}
	for _, testCase := range cases {
		t.Run(testCase.name, func(t *testing.T) {
			var captured http.Request
			server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				captured = *r
				_, _ = w.Write([]byte(`{"ok":true}`))
			}))
			defer server.Close()
			client := testClient(t, server.URL, testCase.auth)
			creds := testCase.credentials
			if creds == nil {
				creds = credentials()
			}
			_, err := client.Call(context.Background(), creds, runner.RESTRequest{
				Path: "/things",
			})
			require.NoError(t, err)
			testCase.assert(t, captured)
		})
	}
}

func credentials() runner.Credentials {
	return runner.Credentials{"apiKey": "secret-token"}
}

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

	cases := []struct {
		name      string
		userAgent string
		expected  string
	}{
		{name: "empty selects the shared provider agent", userAgent: "", expected: "agentteam-extension"},
		{
			name:      "policy-compatible agent passes through verbatim",
			userAgent: "  Mozilla/5.0 (compatible; agentteam-x-extension/1.0)  ",
			expected:  "Mozilla/5.0 (compatible; agentteam-x-extension/1.0)",
		},
	}
	for _, testCase := range cases {
		t.Run(testCase.name, func(t *testing.T) {
			var captured string
			server := httptest.NewTLSServer(http.HandlerFunc(
				func(writer http.ResponseWriter, request *http.Request) {
					captured = request.UserAgent()
					_, _ = writer.Write([]byte(`{"ok":true}`))
				}))
			defer server.Close()
			client, err := runner.NewRESTClient(runner.RESTOptions{
				BaseURL:   server.URL,
				Transport: insecureTransport(),
				UserAgent: testCase.userAgent,
			})
			require.NoError(t, err)
			_, err = client.Call(context.Background(), nil, runner.RESTRequest{
				Path: "/things",
			})
			require.NoError(t, err)
			require.Equal(t, testCase.expected, captured)
		})
	}
}

func base64Of(value string) string {
	return base64.StdEncoding.EncodeToString([]byte(value))
}

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

	server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(`{}`))
	}))
	defer server.Close()
	client := testClient(t, server.URL, bearerAuth())

	for _, path := range []string{"//evil.example.com/x", "/a\nb"} {
		_, err := client.Call(context.Background(), credentials(), runner.RESTRequest{
			Path: path,
		})
		require.Error(t, err, "path %q must be rejected", path)
		require.Equal(t, runner.ErrorValidation, runner.CodeOf(err))
	}
}

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

	targetHits := 0
	redirectTarget := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		targetHits++
		_, _ = w.Write([]byte(`{"leaked":true}`))
	}))
	defer redirectTarget.Close()
	origin := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, redirectTarget.URL+"/secret", http.StatusFound)
	}))
	defer origin.Close()
	client := testClient(t, origin.URL, bearerAuth())

	_, err := client.Call(context.Background(), credentials(), runner.RESTRequest{Path: "/x"})
	require.Error(t, err, "a redirect response is a non-2xx failure")
	require.Equal(t, 0, targetHits, "the redirect target must never be contacted")
}

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

	origin := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write(make([]byte, runner.MaxResponseBytes+1))
	}))
	defer origin.Close()
	client := testClient(t, origin.URL, bearerAuth())

	_, err := client.Call(context.Background(), credentials(), runner.RESTRequest{Path: "/big"})
	require.Error(t, err)
	require.Equal(t, runner.ErrorUpstreamFailure, runner.CodeOf(err))
	require.Contains(t, err.Error(), "size budget")
}

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

	origin := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(`{}`))
	}))
	defer origin.Close()
	client := testClient(t, origin.URL, bearerAuth())

	for name := range map[string]bool{
		"Authorization": true,
		"Cookie":        true,
		"Host":          true,
		"Bad Header":    true,
	} {
		_, err := client.Call(context.Background(), credentials(), runner.RESTRequest{
			Path:    "/x",
			Headers: map[string]string{name: "value"},
		})
		require.Error(t, err, "header %q must be rejected", name)
		require.Equal(t, runner.ErrorValidation, runner.CodeOf(err))
	}
}

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

	requestsSeen := 0
	origin := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requestsSeen++
		_, _ = w.Write([]byte(`{}`))
	}))
	defer origin.Close()
	client := testClient(t, origin.URL, bearerAuth())

	for _, name := range []string{
		"authorization",
		"AUTHORIZATION",
		"aUtHoRiZaTiOn",
		"cookie",
		"COOKIE",
		"cOoKiE",
		"host",
		"HOST",
		"hOsT",
		"content-type",
		"CONTENT-TYPE",
		"cOnTeNt-TyPe",
		"content-length",
		"CONTENT-LENGTH",
		"cOnTeNt-LeNgTh",
		"accept",
		"ACCEPT",
		"aCcEpT",
		"user-agent",
		"USER-AGENT",
		"uSeR-AgEnT",
	} {
		_, err := client.Call(context.Background(), credentials(), runner.RESTRequest{
			Path:    "/x",
			Headers: map[string]string{name: "value"},
		})
		require.Error(t, err, "header %q must be rejected in any case variant", name)
		require.Equal(t, runner.ErrorValidation, runner.CodeOf(err), "header %q", name)
	}
	require.Zero(t, requestsSeen, "reserved header variants must be rejected before any request is sent")
}

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

	origin := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(`{}`))
	}))
	defer origin.Close()
	client := testClient(t, origin.URL, bearerAuth())

	for name, creds := range map[string]runner.Credentials{
		"missing":    {},
		"undeclared": {"apiKey": "x", "extra": "y"},
		"crlf":       {"apiKey": "x\r\nHost: evil"},
	} {
		_, err := client.Call(context.Background(), creds, runner.RESTRequest{Path: "/x"})
		require.Error(t, err, "credentials %q must be rejected", name)
		require.Equal(t, runner.ErrorConnectionUnavailable, runner.CodeOf(err))
	}
}

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

	cases := map[int]runner.ErrorCode{
		400: runner.ErrorValidation,
		401: runner.ErrorProviderAuthUnavailable,
		403: runner.ErrorProviderAuthUnavailable,
		404: runner.ErrorNotFound,
		429: runner.ErrorRateLimited,
		500: runner.ErrorUpstreamFailure,
		503: runner.ErrorUpstreamFailure,
	}
	for status, wantCode := range cases {
		origin := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.WriteHeader(status)
			_, _ = w.Write([]byte(`{"error":"x"}`))
		}))
		client := testClient(t, origin.URL, bearerAuth())
		_, err := client.Call(context.Background(), credentials(), runner.RESTRequest{Path: "/x"})
		require.Equalf(t, wantCode, runner.CodeOf(err), "status %d", status)
		origin.Close()
	}
}

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

	cases := map[runner.ErrorCode]extensionflow.InvocationErrorCode{
		runner.ErrorValidation:              extensionflow.InvocationValidation,
		runner.ErrorPermissionDenied:        extensionflow.InvocationPermissionDenied,
		runner.ErrorConnectionUnavailable:   extensionflow.InvocationProviderAuthUnavailable,
		runner.ErrorProviderAuthUnavailable: extensionflow.InvocationProviderAuthUnavailable,
		runner.ErrorNotFound:                extensionflow.InvocationNotFound,
		runner.ErrorRateLimited:             extensionflow.InvocationRateLimited,
		runner.ErrorUpstreamFailure:         extensionflow.InvocationUpstreamFailure,
	}
	for code, want := range cases {
		mapped := runner.InvocationError(runner.Errorf(code, "message", nil))
		require.NotNilf(t, mapped, "code %s must map", code)
		got, ok := extensionflow.InvocationErrorCodeOf(mapped)
		require.True(t, ok)
		require.Equalf(t, want, got, "code %s", code)
	}
	require.Nil(t, runner.InvocationError(nil))
	require.Nil(t, runner.InvocationError(context.DeadlineExceeded))
}

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

	spec := headerAuth()
	require.NoError(t, spec.Validate(runner.Credentials{"apiKey": "v"}))
	require.ErrorContains(t, spec.Validate(nil), `"apiKey" is required`)
	require.ErrorContains(t,
		spec.Validate(runner.Credentials{"apiKey": "v", "other": "x"}),
		`"other" is not declared`)
	require.ErrorContains(t,
		spec.Validate(runner.Credentials{"apiKey": "x\r\n"}),
		"control characters")
	require.Error(t, runner.AuthSpec{}.Validate(runner.Credentials{"apiKey": "v"}))
	require.Error(t,
		runner.AuthSpec{
			Kind:   runner.AuthNone,
			Fields: []runner.CredentialField{{Name: "apiKey"}},
		}.Validate(runner.Credentials{}))
}

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

	require.NoError(t, basicAuth().Validate(credentials()),
		"fixed username plus the legacy single password field must remain valid")
	require.NoError(t, dynamicBasicAuth().Validate(runner.Credentials{
		"username": "jenkins-user",
		"apiToken": "jenkins-token",
	}))

	cases := []struct {
		name        string
		auth        runner.AuthSpec
		credentials runner.Credentials
		wantError   string
	}{
		{
			name: "username field without password field",
			auth: runner.AuthSpec{
				Kind:               runner.AuthBasic,
				BasicUsernameField: "username",
				Fields: []runner.CredentialField{
					{Name: "username", Required: true},
				},
			},
			credentials: runner.Credentials{"username": "jenkins-user"},
			wantError:   "must be configured together",
		},
		{
			name: "password field without username field",
			auth: runner.AuthSpec{
				Kind:               runner.AuthBasic,
				BasicPasswordField: "apiToken",
				Fields: []runner.CredentialField{
					{Name: "apiToken", Required: true, Secret: true},
				},
			},
			credentials: runner.Credentials{"apiToken": "jenkins-token"},
			wantError:   "must be configured together",
		},
		{
			name: "username field is undeclared",
			auth: runner.AuthSpec{
				Kind:               runner.AuthBasic,
				BasicUsernameField: "username",
				BasicPasswordField: "apiToken",
				Fields: []runner.CredentialField{
					{Name: "apiToken", Required: true, Secret: true},
				},
			},
			credentials: runner.Credentials{"apiToken": "jenkins-token"},
			wantError:   `credential field "username" is not declared`,
		},
		{
			name: "password field is undeclared",
			auth: runner.AuthSpec{
				Kind:               runner.AuthBasic,
				BasicUsernameField: "username",
				BasicPasswordField: "apiToken",
				Fields: []runner.CredentialField{
					{Name: "username", Required: true},
				},
			},
			credentials: runner.Credentials{"username": "jenkins-user"},
			wantError:   `credential field "apiToken" is not declared`,
		},
		{
			name: "fixed and dynamic usernames are mutually exclusive",
			auth: runner.AuthSpec{
				Kind:               runner.AuthBasic,
				BasicUsername:      "fixed-user",
				BasicUsernameField: "username",
				BasicPasswordField: "apiToken",
				Fields: []runner.CredentialField{
					{Name: "username", Required: true},
					{Name: "apiToken", Required: true, Secret: true},
				},
			},
			credentials: runner.Credentials{
				"username": "jenkins-user",
				"apiToken": "jenkins-token",
			},
			wantError: "fixed and dynamic usernames",
		},
	}
	for _, testCase := range cases {
		t.Run(testCase.name, func(t *testing.T) {
			require.ErrorContains(t,
				testCase.auth.Validate(testCase.credentials),
				testCase.wantError)
		})
	}
}

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

	origin := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(`{"done":true}`))
	}))
	defer origin.Close()

	handlers := map[string]runner.RESTHandler{
		"ping": func(ctx context.Context, client *runner.RESTClient, credentials runner.Credentials, input json.RawMessage) (json.RawMessage, error) {
			return client.Call(ctx, credentials, runner.RESTRequest{Path: "/ping"})
		},
	}
	executor := runner.NewRESTExecutor("stub", testClient(t, origin.URL, bearerAuth()),
		staticSource{credentials: credentials()}, handlers)
	require.Equal(t, []string{"ping"}, executor.ActionNames())
	require.Equal(t, "http", executor.Strategy())

	result, err := executor.Invoke(context.Background(), invocation("ping", `{}`))
	require.NoError(t, err)
	require.JSONEq(t, `{"done":true}`, string(result.Data))

	_, err = executor.Invoke(context.Background(), invocation("missing", `{}`))
	require.Equal(t, runner.ErrorNotFound, runner.CodeOf(err))

	_, err = executor.Invoke(context.Background(), invocation("ping", `{`))
	require.Equal(t, runner.ErrorValidation, runner.CodeOf(err))
}

type staticSource struct {
	credentials runner.Credentials
	err         error
}

func (source staticSource) ResolveProviderCredentials(
	context.Context, string, string,
) (runner.Credentials, error) {
	return source.credentials, source.err
}

func invocation(action, input string) registry.BuiltinInvocation {
	return registry.BuiltinInvocation{
		WorkspaceID:           "ws",
		ServiceInstallationID: "inst",
		ExtensionName:         "stub",
		Action:                action,
		Input:                 json.RawMessage(input),
	}
}

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

	origin := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(`{}`))
	}))
	defer origin.Close()
	handlers := map[string]runner.RESTHandler{
		"ping": func(ctx context.Context, client *runner.RESTClient, credentials runner.Credentials, input json.RawMessage) (json.RawMessage, error) {
			return client.Call(ctx, credentials, runner.RESTRequest{Path: "/ping"})
		},
	}
	makeExecutor := func(source runner.CredentialSource) *runner.RESTExecutor {
		return runner.NewRESTExecutor("stub", testClient(t, origin.URL, bearerAuth()), source, handlers)
	}

	executor := makeExecutor(nil)
	_, err := executor.Invoke(context.Background(), invocation("ping", `{}`))
	require.Error(t, err)
	code, classified := extensionflow.InvocationErrorCodeOf(err)
	require.True(t, classified)
	require.Equal(t, extensionflow.InvocationProviderAuthUnavailable, code)

	executor = makeExecutor(staticSource{err: errors.New("decrypt: ciphertext too short")})
	_, err = executor.Invoke(context.Background(), invocation("ping", `{}`))
	require.Error(t, err)
	code, classified = extensionflow.InvocationErrorCodeOf(err)
	require.True(t, classified)
	require.Equal(t, extensionflow.InvocationProviderAuthUnavailable, code)
	require.NotContains(t, err.Error(), "ciphertext")
}

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

	origin := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(`{}`))
	}))
	defer origin.Close()
	handlers := map[string]runner.RESTHandler{
		"ping": func(ctx context.Context, client *runner.RESTClient, credentials runner.Credentials, input json.RawMessage) (json.RawMessage, error) {
			return client.Call(ctx, credentials, runner.RESTRequest{Path: "/ping"})
		},
	}
	for _, ctxErr := range []error{context.Canceled, context.DeadlineExceeded} {
		executor := runner.NewRESTExecutor("stub", testClient(t, origin.URL, bearerAuth()),
			staticSource{err: ctxErr}, handlers)
		result, err := executor.Invoke(context.Background(), invocation("ping", `{}`))
		require.Error(t, err, "a cancelled credential lookup must never return success")
		require.True(t, errors.Is(err, ctxErr), "the original context error must propagate")
		require.Empty(t, result.Data)
	}
}

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

	origin := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte("      " + `{"ok":true}` + "   \n\n"))
	}))
	defer origin.Close()
	client := testClient(t, origin.URL, bearerAuth())

	payload, readBytes, err := client.CallWithMetrics(context.Background(), credentials(), runner.RESTRequest{Path: "/x"})
	require.NoError(t, err)
	require.JSONEq(t, `{"ok":true}`, string(payload))
	require.Greater(t, readBytes, int64(len(payload)),
		"raw bytes must include the trimmed whitespace")
}
