package bootstrap

import (
	"context"
	"testing"

	"github.com/stretchr/testify/require"

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

type stubExecutor struct {
	actions []string
}

func (executor *stubExecutor) Strategy() string { return "http" }

func (executor *stubExecutor) ActionNames() []string { return executor.actions }

func (executor *stubExecutor) Invoke(
	context.Context, registry.BuiltinInvocation,
) (registry.BuiltinResult, error) {
	return registry.BuiltinResult{}, nil
}

func baseExtension(domain string) extension.Extension {
	return extension.Extension{
		Definition: registry.Definition{
			Domain:  domain,
			Version: 1,
			Name:    domain,
			Actions: []registry.DefinitionAction{
				{Name: "act", Safety: spec.Safety{Class: "read", Idempotent: true}, InputSchema: map[string]any{
					"$schema": "https://json-schema.org/draft/2020-12/schema",
					"type":    "object",
				}, OutputSchema: map[string]any{
					"$schema": "https://json-schema.org/draft/2020-12/schema",
					"type":    "object",
				}},
			},
		},
		Implementation: runner.Implementation{
			Executor:         &stubExecutor{actions: []string{"act"}},
			Revision:         "stub-v1",
			ConnectionPolicy: registry.ConnectionNone,
		},
		Distribution: distributionFor(domain),
	}
}

func distributionFor(domain string) extension.Distribution {
	return extension.Distribution{
		AppID:          "app_" + domain,
		AppSlug:        domain,
		DefaultName:    domain,
		DefaultEnabled: true,
		IconURL:        "/app-icons/" + domain + ".svg",
	}
}

func validValidator(context.Context, runner.Credentials) error { return nil }

func validPreparer(
	context.Context,
	runner.Credentials,
) (runner.ConnectionConfig, error) {
	return runner.ConnectionConfig{"topology": "aggregate"}, nil
}

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

	reg := registry.New()
	require.NoError(t, Register(reg, []extension.Extension{baseExtension("stub")}))
	require.Equal(t, 1, reg.Len())
}

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

	cases := []struct {
		name string
		auth runner.AuthSpec
	}{
		{
			name: "fixed username and legacy password field",
			auth: runner.AuthSpec{
				Kind:          runner.AuthBasic,
				BasicUsername: "api",
				Fields:        []runner.CredentialField{runner.APIKeyField("Password", "paste")},
			},
		},
		{
			name: "dynamic username and password fields",
			auth: runner.AuthSpec{
				Kind:               runner.AuthBasic,
				BasicUsernameField: "username",
				BasicPasswordField: "apiToken",
				Fields: []runner.CredentialField{
					{Name: "username", Required: true},
					{Name: "apiToken", Required: true, Secret: true},
				},
			},
		},
	}
	for _, testCase := range cases {
		t.Run(testCase.name, func(t *testing.T) {
			ext := baseExtension("stub")
			ext.Implementation.ConnectionPolicy = registry.ConnectionRequired
			ext.Implementation.Auth = testCase.auth
			ext.Implementation.ValidateCredentials = validValidator

			reg := registry.New()
			require.NoError(t, Register(reg, []extension.Extension{ext}))
			require.Equal(t, 1, reg.Len())
		})
	}
}

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

	ext := baseExtension("prepared")
	ext.Implementation.ConnectionPolicy = registry.ConnectionRequired
	ext.Implementation.Auth = bearerAuth()
	ext.Implementation.PrepareConnection = validPreparer
	ext.Implementation.ConnectionConfig = runner.ConnectionConfigContract{
		Fields: []runner.ConnectionConfigField{
			{
				Name:     "topology",
				Required: true,
				Values:   []string{"aggregate", "fallback"},
			},
		},
	}

	reg := registry.New()
	require.NoError(t, Register(reg, []extension.Extension{ext}))
	require.Equal(t, 1, reg.Len())
}

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

	cases := []struct {
		name      string
		mutate    func(*extension.Extension)
		wantError string
	}{
		{
			name:      "missing executor",
			mutate:    func(ext *extension.Extension) { ext.Implementation.Executor = nil },
			wantError: "has no executor",
		},
		{
			name:      "missing revision",
			mutate:    func(ext *extension.Extension) { ext.Implementation.Revision = "" },
			wantError: "no implementation revision",
		},
		{
			name: "unknown auth kind fails closed",
			mutate: func(ext *extension.Extension) {
				ext.Implementation.Auth = runner.AuthSpec{
					Kind:   runner.AuthKind(200),
					Header: "x-token",
					Fields: []runner.CredentialField{runner.APIKeyField("k", "d")},
				}
				ext.Implementation.ValidateCredentials = validValidator
				ext.Implementation.ConnectionPolicy = registry.ConnectionRequired
			},
			wantError: "unknown auth kind",
		},
		{
			name: "authenticated provider requires connection policy",
			mutate: func(ext *extension.Extension) {
				ext.Implementation.ConnectionPolicy = registry.ConnectionNone
				ext.Implementation.Auth = bearerAuth()
				ext.Implementation.ValidateCredentials = validValidator
			},
			wantError: "requires a connection",
		},
		{
			name: "authenticated provider requires validator",
			mutate: func(ext *extension.Extension) {
				ext.Implementation.ConnectionPolicy = registry.ConnectionRequired
				ext.Implementation.Auth = bearerAuth()
				ext.Implementation.ValidateCredentials = nil
			},
			wantError: "exactly one credential validator",
		},
		{
			name: "authenticated provider rejects validator and preparer together",
			mutate: func(ext *extension.Extension) {
				ext.Implementation.ConnectionPolicy = registry.ConnectionRequired
				ext.Implementation.Auth = bearerAuth()
				ext.Implementation.ValidateCredentials = validValidator
				ext.Implementation.PrepareConnection = validPreparer
			},
			wantError: "exactly one credential validator",
		},
		{
			name: "connection config requires preparer",
			mutate: func(ext *extension.Extension) {
				ext.Implementation.ConnectionPolicy = registry.ConnectionRequired
				ext.Implementation.Auth = bearerAuth()
				ext.Implementation.ValidateCredentials = validValidator
				ext.Implementation.ConnectionConfig =
					runner.ConnectionConfigContract{
						Fields: []runner.ConnectionConfigField{
							{
								Name:     "topology",
								Required: true,
								Values:   []string{"aggregate"},
							},
						},
					}
			},
			wantError: "connection config without a preparer",
		},
		{
			name: "invalid connection config contract",
			mutate: func(ext *extension.Extension) {
				ext.Implementation.ConnectionPolicy = registry.ConnectionRequired
				ext.Implementation.Auth = bearerAuth()
				ext.Implementation.PrepareConnection = validPreparer
				ext.Implementation.ConnectionConfig =
					runner.ConnectionConfigContract{
						Fields: []runner.ConnectionConfigField{
							{
								Name:   "topology",
								Values: []string{"aggregate", "aggregate"},
							},
						},
					}
			},
			wantError: "invalid connection config contract",
		},
		{
			name: "authenticated provider requires fields",
			mutate: func(ext *extension.Extension) {
				ext.Implementation.ConnectionPolicy = registry.ConnectionRequired
				ext.Implementation.Auth = runner.AuthSpec{Kind: runner.AuthBearer}
				ext.Implementation.ValidateCredentials = validValidator
			},
			wantError: "declares no credential fields",
		},
		{
			name: "header auth requires header name",
			mutate: func(ext *extension.Extension) {
				ext.Implementation.ConnectionPolicy = registry.ConnectionRequired
				ext.Implementation.Auth = runner.AuthSpec{
					Kind:   runner.AuthHeader,
					Fields: []runner.CredentialField{runner.APIKeyField("k", "d")},
				}
				ext.Implementation.ValidateCredentials = validValidator
			},
			wantError: "declares no header name",
		},
		{
			name: "basic username field requires password field",
			mutate: func(ext *extension.Extension) {
				ext.Implementation.ConnectionPolicy = registry.ConnectionRequired
				ext.Implementation.Auth = runner.AuthSpec{
					Kind:               runner.AuthBasic,
					BasicUsernameField: "username",
					Fields: []runner.CredentialField{
						{Name: "username", Required: true},
					},
				}
				ext.Implementation.ValidateCredentials = validValidator
			},
			wantError: "must be configured together",
		},
		{
			name: "basic password field requires username field",
			mutate: func(ext *extension.Extension) {
				ext.Implementation.ConnectionPolicy = registry.ConnectionRequired
				ext.Implementation.Auth = runner.AuthSpec{
					Kind:               runner.AuthBasic,
					BasicPasswordField: "apiToken",
					Fields: []runner.CredentialField{
						{Name: "apiToken", Required: true, Secret: true},
					},
				}
				ext.Implementation.ValidateCredentials = validValidator
			},
			wantError: "must be configured together",
		},
		{
			name: "basic username field must be declared",
			mutate: func(ext *extension.Extension) {
				ext.Implementation.ConnectionPolicy = registry.ConnectionRequired
				ext.Implementation.Auth = runner.AuthSpec{
					Kind:               runner.AuthBasic,
					BasicUsernameField: "username",
					BasicPasswordField: "apiToken",
					Fields: []runner.CredentialField{
						{Name: "apiToken", Required: true, Secret: true},
					},
				}
				ext.Implementation.ValidateCredentials = validValidator
			},
			wantError: `credential field "username" is not declared`,
		},
		{
			name: "basic password field must be declared",
			mutate: func(ext *extension.Extension) {
				ext.Implementation.ConnectionPolicy = registry.ConnectionRequired
				ext.Implementation.Auth = runner.AuthSpec{
					Kind:               runner.AuthBasic,
					BasicUsernameField: "username",
					BasicPasswordField: "apiToken",
					Fields: []runner.CredentialField{
						{Name: "username", Required: true},
					},
				}
				ext.Implementation.ValidateCredentials = validValidator
			},
			wantError: `credential field "apiToken" is not declared`,
		},
		{
			name: "basic fixed and dynamic usernames are mutually exclusive",
			mutate: func(ext *extension.Extension) {
				ext.Implementation.ConnectionPolicy = registry.ConnectionRequired
				ext.Implementation.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},
					},
				}
				ext.Implementation.ValidateCredentials = validValidator
			},
			wantError: "fixed and dynamic usernames",
		},
		{
			name: "no-auth provider rejects fields",
			mutate: func(ext *extension.Extension) {
				ext.Implementation.Auth = runner.AuthSpec{
					Kind:   runner.AuthNone,
					Fields: []runner.CredentialField{runner.APIKeyField("k", "d")},
				}
			},
			wantError: "declares credential fields",
		},
		{
			name: "executor serves undeclared action",
			mutate: func(ext *extension.Extension) {
				ext.Implementation.Executor = &stubExecutor{
					actions: []string{"act", "extra"},
				}
			},
			wantError: "undeclared action",
		},
		{
			name: "executor misses declared action",
			mutate: func(ext *extension.Extension) {
				ext.Implementation.Executor = &stubExecutor{}
			},
			wantError: "does not serve action",
		},
		{
			name: "incomplete distribution",
			mutate: func(ext *extension.Extension) {
				ext.Distribution.IconURL = ""
			},
			wantError: "incomplete distribution",
		},
	}
	for _, testCase := range cases {
		t.Run(testCase.name, func(t *testing.T) {
			ext := baseExtension("stub")
			testCase.mutate(&ext)
			reg := registry.New()
			err := Register(reg, []extension.Extension{ext})
			require.ErrorContains(t, err, testCase.wantError)
			require.Equal(t, 0, reg.Len(), "a rejected provider must not register")
		})
	}
}

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

	reg := registry.New()
	err := Register(reg, []extension.Extension{
		baseExtension("stub"), baseExtension("stub"),
	})
	require.ErrorContains(t, err, "registered twice")
}

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

	require.ErrorContains(t, Register(nil, nil), "registry is required")
}

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