package providers_test

import (
	"context"
	"errors"
	"regexp"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/extension"
	"github.com/tidbcloud/AgentTeam/internal/extension/bootstrap"
	"github.com/tidbcloud/AgentTeam/internal/extension/providers"
	"github.com/tidbcloud/AgentTeam/internal/extension/providers/mail/mailruntime"
	"github.com/tidbcloud/AgentTeam/internal/extension/providers/searchengine/runtime"
	"github.com/tidbcloud/AgentTeam/internal/extension/registry"
	"github.com/tidbcloud/AgentTeam/internal/extension/runner"
	"github.com/tidbcloud/AgentTeam/internal/service/prwatch"
	appregistry "github.com/tidbcloud/AgentTeam/internal/store/app/registry"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
)

type stubCredentialSource struct{}

func (stubCredentialSource) ResolveProviderCredentials(
	context.Context, string, string,
) (runner.Credentials, error) {
	return nil, errors.New("stub credential source is not invocable")
}

func (stubCredentialSource) ResolveProviderConnection(
	context.Context,
	string,
	string,
	runner.ConnectionConfigContract,
) (runner.ResolvedConnection, error) {
	return runner.ResolvedConnection{},
		errors.New("stub connection source is not invocable")
}

func testDeps() providers.Deps {
	return providers.Deps{
		WatchService:  prwatch.New(nil),
		SearchService: runtime.New(nil),
		MailService:   mailruntime.New(nil, nil, mailruntime.Options{}),
		Credentials:   stubCredentialSource{},
		Connections:   stubCredentialSource{},
	}
}

// registeredExtensions bootstraps the full provider set exactly like the
// server does, so digest assertions see production identity.
func registeredExtensions(t *testing.T) *registry.Registry {
	t.Helper()
	list, err := providers.Extensions(testDeps())
	require.NoError(t, err)
	reg := registry.New()
	require.NoError(t, bootstrap.Register(reg, list))
	return reg
}

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

	reg := registeredExtensions(t)
	pins := map[string]string{
		"github":                  model.GitHubBuiltinExtensionDefinitionDigest,
		"gitea":                   model.GiteaBuiltinExtensionDefinitionDigest,
		"searxng":                 model.SearXNGBuiltinExtensionDefinitionDigest,
		"brave":                   model.BraveBuiltinExtensionDefinitionDigest,
		"tavily":                  model.TavilyBuiltinExtensionDefinitionDigest,
		"serper":                  model.SerperBuiltinExtensionDefinitionDigest,
		"arxiv":                   model.ArxivBuiltinExtensionDefinitionDigest,
		"todoist":                 model.TodoistBuiltinExtensionDefinitionDigest,
		"notion":                  model.NotionBuiltinExtensionDefinitionDigest,
		"hackernews":              model.HackernewsBuiltinExtensionDefinitionDigest,
		"jenkins":                 model.JenkinsBuiltinExtensionDefinitionDigest,
		"ones":                    model.OnesBuiltinExtensionDefinitionDigest,
		"hotnews":                 model.HotNewsBuiltinExtensionDefinitionDigest,
		"feishucustombot":         model.FeishuCustomBotBuiltinExtensionDefinitionDigest,
		"dingtalkbot":             model.DingtalkBotBuiltinExtensionDefinitionDigest,
		"wecombot":                model.WecomBotBuiltinExtensionDefinitionDigest,
		"tianyancha":              model.TianyanchaBuiltinExtensionDefinitionDigest,
		"qichacha":                model.QichachaBuiltinExtensionDefinitionDigest,
		"tushare":                 model.TushareBuiltinExtensionDefinitionDigest,
		"alpha-vantage":           model.AlphaVantageBuiltinExtensionDefinitionDigest,
		"financial-modeling-prep": model.FinancialModelingPrepBuiltinExtensionDefinitionDigest,
		"jin10":                   model.Jin10BuiltinExtensionDefinitionDigest,
		"pkulaw":                  model.PKULawBuiltinExtensionDefinitionDigest,
		"aifinmarket":             model.AIFinMarketBuiltinExtensionDefinitionDigest,
	}
	for domain, pin := range pins {
		registered, ok := reg.LookupByIdentity(domain, 1)
		require.Truef(t, ok, "%s must be registered", domain)
		require.NotEqualf(t, "sha256:PENDING", pin,
			"%s digest pin must be finalized", domain)
		require.Equalf(t, pin, registered.Definition.Digest,
			"%s capability pin must match the registered Builtin digest", domain)
	}

	// The pin set must stay exhaustive: every provider the aggregate ships
	// except mail (whose Email App uses the multi-field connection API
	// instead of a digest-pinned capability) carries a pin, so a rebase or
	// conflict resolution that silently drops a registration — an IM bot or
	// a business-data provider — fails here rather than only in production.
	list, err := providers.Extensions(testDeps())
	require.NoError(t, err)
	pinnedProviders := 0
	for _, ext := range list {
		if ext.Definition.Domain == "mail" {
			continue
		}
		_, ok := pins[ext.Definition.Domain]
		require.Truef(t, ok,
			"provider %s ships without a digest pin; add one or account for it here",
			ext.Definition.Domain)
		pinnedProviders++
	}
	require.Equalf(t, len(pins), pinnedProviders,
		"every digest pin must correspond to a registered provider: pins list stale entries")
	require.Lenf(t, list, len(pins)+1,
		"the aggregate must register exactly the pinned providers plus mail")
}

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

	list, err := providers.Extensions(testDeps())
	require.NoError(t, err)
	for _, ext := range list {
		require.Truef(t, appregistry.IsProductAppID(ext.Distribution.AppID),
			"provider %q App %q must be a product App",
			ext.Definition.Domain, ext.Distribution.AppID)
		require.Truef(t, appregistry.IsProductApp(
			ext.Distribution.AppID,
			ext.Distribution.AppSlug),
			"provider %q App slug must match the product registry",
			ext.Definition.Domain)
		require.Equalf(t, ext.Distribution.DefaultName,
			ext.Definition.Domain,
			"provider %q default selection name must equal its domain",
			ext.Definition.Domain)
		if ext.Definition.Domain == "mail" {
			require.Falsef(t, ext.Distribution.DefaultEnabled,
				"provider %q must stay disabled by default: the Email App "+
					"installs only through explicit authorization",
				ext.Definition.Domain)
		} else {
			require.Truef(t, ext.Distribution.DefaultEnabled,
				"provider %q must be enabled by default", ext.Definition.Domain)
		}
		require.NotEmptyf(t, ext.Distribution.IconURL,
			"provider %q must declare an icon", ext.Definition.Domain)
	}
}

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

	list, err := providers.Extensions(testDeps())
	require.NoError(t, err)
	byAppID, err := providers.ExtensionsByAppID(testDeps())
	require.NoError(t, err)
	require.Len(t, byAppID, len(list),
		"duplicate App IDs must never be silently overwritten")
	reg := registry.New()
	require.NoError(t, bootstrap.Register(reg, list))

	// Each AppID pins its provider domain and — except mail, whose Email
	// App uses the multi-field connection API instead of a digest-pinned
	// capability — its registered digest. Membership and cardinality alone
	// stay green when two registrations swap their App IDs or digests; the
	// per-AppID association does not.
	for appID, want := range map[string]struct{ domain, digest string }{
		appregistry.GitHubAppID:          {"github", model.GitHubBuiltinExtensionDefinitionDigest},
		appregistry.GiteaAppID:           {"gitea", model.GiteaBuiltinExtensionDefinitionDigest},
		appregistry.SearXNGAppID:         {"searxng", model.SearXNGBuiltinExtensionDefinitionDigest},
		appregistry.BraveSearchAppID:     {"brave", model.BraveBuiltinExtensionDefinitionDigest},
		appregistry.TavilyAppID:          {"tavily", model.TavilyBuiltinExtensionDefinitionDigest},
		appregistry.SerperAppID:          {"serper", model.SerperBuiltinExtensionDefinitionDigest},
		appregistry.ArxivAppID:           {"arxiv", model.ArxivBuiltinExtensionDefinitionDigest},
		appregistry.TodoistAppID:         {"todoist", model.TodoistBuiltinExtensionDefinitionDigest},
		appregistry.NotionAppID:          {"notion", model.NotionBuiltinExtensionDefinitionDigest},
		appregistry.HackernewsAppID:      {"hackernews", model.HackernewsBuiltinExtensionDefinitionDigest},
		appregistry.JenkinsAppID:         {"jenkins", model.JenkinsBuiltinExtensionDefinitionDigest},
		appregistry.OnesAppID:            {"ones", model.OnesBuiltinExtensionDefinitionDigest},
		appregistry.HotNewsAppID:         {"hotnews", model.HotNewsBuiltinExtensionDefinitionDigest},
		appregistry.FeishuCustomBotAppID: {"feishucustombot", model.FeishuCustomBotBuiltinExtensionDefinitionDigest},
		appregistry.DingtalkBotAppID:     {"dingtalkbot", model.DingtalkBotBuiltinExtensionDefinitionDigest},
		appregistry.WecomBotAppID:        {"wecombot", model.WecomBotBuiltinExtensionDefinitionDigest},
		appregistry.TianyanchaAppID:      {"tianyancha", model.TianyanchaBuiltinExtensionDefinitionDigest},
		appregistry.QichachaAppID:        {"qichacha", model.QichachaBuiltinExtensionDefinitionDigest},
		appregistry.TushareAppID:         {"tushare", model.TushareBuiltinExtensionDefinitionDigest},
		appregistry.AlphaVantageAppID:    {"alpha-vantage", model.AlphaVantageBuiltinExtensionDefinitionDigest},
		appregistry.FinancialModelingPrepAppID: {
			"financial-modeling-prep",
			model.FinancialModelingPrepBuiltinExtensionDefinitionDigest,
		},
		appregistry.Jin10AppID: {"jin10", model.Jin10BuiltinExtensionDefinitionDigest},
		appregistry.PKULawAppID: {
			"pkulaw",
			model.PKULawBuiltinExtensionDefinitionDigest,
		},
		appregistry.AIFinMarketAppID: {
			"aifinmarket",
			model.AIFinMarketBuiltinExtensionDefinitionDigest,
		},
		appregistry.EmailAppID: {"mail", ""},
	} {
		ext, ok := byAppID[appID]
		require.Truef(t, ok, "%s must resolve to a provider Extension", appID)
		require.NotNilf(t, ext.Implementation.Executor,
			"%s must resolve an executing provider", appID)
		require.Equalf(t, want.domain, ext.Definition.Domain,
			"%s must stay associated with the %s provider", appID, want.domain)
		if appID == appregistry.EmailAppID {
			continue
		}
		registered, ok := reg.LookupByIdentity(want.domain, 1)
		require.Truef(t, ok, "%s must be registered", want.domain)
		require.Equalf(t, want.digest, registered.Definition.Digest,
			"%s must pin the registered digest of its %s provider", appID, want.domain)
	}
	// The fixed set is exhaustive: every provider App the aggregate ships
	// resolves through the AppID map, so a dropped or mislabeled
	// registration cannot hide behind a shorter list.
	require.Lenf(t, byAppID, 25,
		"the AppID resolver must cover exactly the full Builtin provider set")
}

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

	byAppID, err := providers.ExtensionsByAppID(testDeps())
	require.NoError(t, err)

	want := map[string]string{
		appregistry.FeishuCustomBotAppID:       "feishucustombot",
		appregistry.DingtalkBotAppID:           "dingtalkbot",
		appregistry.WecomBotAppID:              "wecombot",
		appregistry.TianyanchaAppID:            "tianyancha",
		appregistry.QichachaAppID:              "qichacha",
		appregistry.TushareAppID:               "tushare",
		appregistry.AlphaVantageAppID:          "alpha-vantage",
		appregistry.FinancialModelingPrepAppID: "financial-modeling-prep",
		appregistry.Jin10AppID:                 "jin10",
		appregistry.PKULawAppID:                "pkulaw",
		appregistry.AIFinMarketAppID:           "aifinmarket",
	}
	got := make(map[string]string)
	for appID, ext := range byAppID {
		if !ext.Distribution.GenericProviderConnection {
			continue
		}
		got[appID] = ext.Definition.Domain
	}
	require.Equal(t, want, got,
		"generic connection discoverability must be an exact default-off server registration")
}

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

	require.Regexp(t,
		regexp.MustCompile(`^[0-9a-f]{40}$`),
		providers.OpenConnectorBaseline,
		"the migration baseline must be a full upstream commit hash")
}

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

	_, err := providers.Extensions(providers.Deps{})
	require.ErrorContains(t, err, "pull-request watch service is required")
	_, err = providers.Extensions(providers.Deps{
		WatchService: prwatch.New(nil),
		Credentials:  stubCredentialSource{},
	})
	require.ErrorContains(t, err, "search service is required")
	_, err = providers.Extensions(providers.Deps{
		WatchService:  prwatch.New(nil),
		SearchService: runtime.New(nil),
	})
	require.ErrorContains(t, err, "credential source is required")
	_, err = providers.Extensions(providers.Deps{
		WatchService:  prwatch.New(nil),
		SearchService: runtime.New(nil),
		Credentials:   stubCredentialSource{},
	})
	require.ErrorContains(t, err, "connection source is required")
}

var _ extension.Extension
