package runner_test

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/stretchr/testify/require"

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

// headersAuthSpec is the ONES-shaped contract: a non-injected origin field
// plus two header-bound credential fields.
func headersAuthSpec() runner.AuthSpec {
	return runner.AuthSpec{
		Kind: runner.AuthHeaders,
		Fields: []runner.CredentialField{
			{
				Name:        "base_url",
				DisplayName: "ONES host origin",
				Description: "The https origin of the ONES deployment.",
				Required:    true,
			},
			{
				Name:        "user_id",
				DisplayName: "ONES user uuid",
				Description: "The user uuid sent as Ones-User-Id.",
				Required:    true,
				HeaderName:  "Ones-User-Id",
			},
			{
				Name:        "token",
				DisplayName: "ONES auth token",
				Description: "The login token sent as Ones-Auth-Token.",
				Required:    true,
				Secret:      true,
				HeaderName:  "Ones-Auth-Token",
			},
		},
	}
}

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

	var seen http.Header
	origin := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		seen = r.Header.Clone()
		_, _ = w.Write([]byte(`{}`))
	}))
	t.Cleanup(origin.Close)

	client, err := runner.NewRESTClient(runner.RESTOptions{
		BaseURL:   origin.URL,
		Auth:      headersAuthSpec(),
		Transport: insecureTransport(),
	})
	require.NoError(t, err)

	_, err = client.Call(context.Background(), runner.Credentials{
		"base_url": origin.URL,
		"user_id":  "DU6krHBN",
		"token":    "tok",
	}, runner.RESTRequest{Method: "GET", Path: "/x"})
	require.NoError(t, err)
	require.Equal(t, "DU6krHBN", seen.Get("Ones-User-Id"))
	require.Equal(t, "tok", seen.Get("Ones-Auth-Token"))
	require.Empty(t, seen.Get("Referer"))
}

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

	base := headersAuthSpec()
	withKindSwap := base
	withKindSwap.Kind = runner.AuthHeader
	require.Error(t, withKindSwap.Validate(runner.Credentials{
		"base_url": "https://ones.example.com", "user_id": "u", "token": "t"}))

	withReserved := base
	withReserved.Fields = append([]runner.CredentialField{}, base.Fields...)
	withReserved.Fields[1].HeaderName = "Authorization"
	require.Error(t, withReserved.Validate(runner.Credentials{
		"base_url": "https://ones.example.com", "user_id": "u", "token": "t"}))

	withDuplicate := base
	withDuplicate.Fields = append([]runner.CredentialField{}, base.Fields...)
	withDuplicate.Fields[2].HeaderName = "Ones-User-Id"
	require.Error(t, withDuplicate.Validate(runner.Credentials{
		"base_url": "https://ones.example.com", "user_id": "u", "token": "t"}))

	require.NoError(t, base.Validate(runner.Credentials{
		"base_url": "https://ones.example.com", "user_id": "u", "token": "t"}))
}

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

	template := runner.RESTTemplate{Auth: headersAuthSpec(), BaseURLField: "base_url"}
	_, err := runner.BuildConnectionClient(template, runner.Credentials{
		"user_id": "u", "token": "t"})
	require.Equal(t, runner.ErrorConnectionUnavailable, runner.CodeOf(err))

	_, err = runner.BuildConnectionClient(template, runner.Credentials{
		"base_url": "http://ones.example.com", "user_id": "u", "token": "t"})
	require.Error(t, err)

	client, err := runner.BuildConnectionClient(template, runner.Credentials{
		"base_url": " https://ones.example.com/ ", "user_id": "u", "token": "t"})
	require.NoError(t, err)
	require.NotNil(t, client)
}

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

	template := runner.RESTTemplate{Auth: headersAuthSpec(), BaseURLField: "base_url"}
	for _, baseURL := range []string{
		"https://user:pass@ones.example.com",
		"https://ones.example.com/project/api/project",
		"https://ones.example.com?x=1",
		"https://ones.example.com#fragment",
		"ones.example.com",
	} {
		_, err := runner.BuildConnectionClient(template, runner.Credentials{
			"base_url": baseURL, "user_id": "u", "token": "t"})
		require.Equalf(t, runner.ErrorConnectionUnavailable, runner.CodeOf(err),
			"base URL %q must be rejected as a connection failure", baseURL)
	}
}

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

	spec := headersAuthSpec()
	spec.Fields = append([]runner.CredentialField{}, spec.Fields...)
	spec.Fields[2].HeaderName = "ones-user-id"
	require.Error(t, spec.Validate(runner.Credentials{
		"base_url": "https://ones.example.com", "user_id": "u", "token": "t"}))
}

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

	var seenPath string
	var seenUser string
	origin := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		seenPath = r.URL.Path
		seenUser = r.Header.Get("Ones-User-Id")
		_, _ = w.Write([]byte(`{"user":{"uuid":"DU6krHBN"}}`))
	}))
	t.Cleanup(origin.Close)

	template := runner.RESTTemplate{
		Auth:         headersAuthSpec(),
		BaseURLField: "base_url",
		Transport:    insecureTransport(),
	}
	executor, err := runner.NewConnectionScopedRESTExecutor(
		"ones", template, staticSource{credentials: runner.Credentials{
			"base_url": origin.URL,
			"user_id":  "DU6krHBN",
			"token":    "tok",
		}}, map[string]runner.RESTHandler{
			"whoami": func(
				ctx context.Context,
				client *runner.RESTClient,
				credentials runner.Credentials,
				input json.RawMessage,
			) (json.RawMessage, error) {
				return client.Call(ctx, credentials, runner.RESTRequest{
					Method: "GET",
					Path:   "/project/api/project/auth/token_info",
				})
			},
		})
	require.NoError(t, err)

	result, err := executor.Invoke(context.Background(), invocation("whoami", `{}`))
	require.NoError(t, err)
	require.JSONEq(t, `{"user":{"uuid":"DU6krHBN"}}`, string(result.Data))
	require.Equal(t, "/project/api/project/auth/token_info", seenPath)
	require.Equal(t, "DU6krHBN", seenUser)
}

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

	template := runner.RESTTemplate{Auth: headersAuthSpec(), BaseURLField: "base_url"}
	executor, err := runner.NewConnectionScopedRESTExecutor(
		"ones", template, staticSource{credentials: runner.Credentials{
			"user_id": "DU6krHBN", "token": "tok",
		}}, map[string]runner.RESTHandler{
			"any": func(
				ctx context.Context,
				client *runner.RESTClient,
				credentials runner.Credentials,
				input json.RawMessage,
			) (json.RawMessage, error) {
				return nil, nil
			},
		})
	require.NoError(t, err)

	_, err = executor.Invoke(context.Background(), invocation("any", `{}`))
	require.Equal(t, runner.ErrorConnectionUnavailable, runner.CodeOf(err))
	mapped := runner.InvocationError(err)
	require.NotNil(t, mapped)
}

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

	_, err := runner.NewConnectionScopedRESTExecutor("ones", runner.RESTTemplate{
		Auth:         runner.AuthSpec{Kind: runner.AuthNone},
		BaseURLField: "base_url",
	}, staticSource{}, nil)
	require.ErrorContains(t, err, "authenticated auth kind")

	_, err = runner.NewConnectionScopedRESTExecutor("ones", runner.RESTTemplate{
		Auth:         headersAuthSpec(),
		BaseURLField: "missing_field",
	}, staticSource{}, nil)
	require.ErrorContains(t, err, "not declared")
}

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

	origin := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(610)
		_, _ = w.Write([]byte(`{}`))
	}))
	t.Cleanup(origin.Close)

	client, err := runner.NewRESTClient(runner.RESTOptions{
		BaseURL: origin.URL,
		Auth: runner.AuthSpec{
			Kind:   runner.AuthBearer,
			Fields: []runner.CredentialField{runner.APIKeyField("k", "d")},
		},
		Transport: insecureTransport(),
		StatusClassifier: func(status int, body []byte) runner.ErrorCode {
			if status == 610 {
				return runner.ErrorNotFound
			}
			return runner.ErrorUpstreamFailure
		},
	})
	require.NoError(t, err)

	_, err = client.Call(context.Background(), runner.Credentials{"apiKey": "k"},
		runner.RESTRequest{Method: "GET", Path: "/x"})
	require.Equal(t, runner.ErrorNotFound, runner.CodeOf(err))
}
