package integrationservice

import (
	"bytes"
	"context"
	"database/sql"
	"errors"
	"testing"
	"time"

	"github.com/nexuscrm/kernel/pkg/ports"
	connectoroauth "github.com/pingcap-inc/nexuscrm-connectors/features/connector/oauth"
	connectoroutboundintent "github.com/pingcap-inc/nexuscrm-connectors/features/connector/outboundintent"
	connectorthrottle "github.com/pingcap-inc/nexuscrm-connectors/features/connector/throttle"
)

type connectedMailRuntimeAllowlist struct{}

func (connectedMailRuntimeAllowlist) EnsureAllowed(string) error  { return nil }
func (connectedMailRuntimeAllowlist) Redact(value string) string  { return value }
func (connectedMailRuntimeAllowlist) RedactError(err error) error { return err }

type connectedMailRuntimeThrottler struct{}

func (connectedMailRuntimeThrottler) ThrottleResolved(
	context.Context,
	connectorthrottle.ResolvedCredential,
	string,
) (time.Duration, func(), error) {
	return 0, func() {}, nil
}

type connectedMailFallbackPreparer struct{}

func (*connectedMailFallbackPreparer) Prepare(
	context.Context,
	connectoroutboundintent.Command,
) (connectoroutboundintent.PreparedEffect, error) {
	return nil, errors.New("fallback")
}

func TestWithConnectedMailPreparerPreservesUnconfiguredFallback(t *testing.T) {
	fallback := &connectedMailFallbackPreparer{}
	preparer, runtime, err := withConnectedMailPreparer(nil, nil, connectedMailCatalogView(), nil, nil, fallback)
	if err != nil {
		t.Fatal(err)
	}
	if preparer != fallback || runtime != nil {
		t.Fatalf("unconfigured composition = (%T, %#v), want unchanged fallback", preparer, runtime)
	}
}

func TestConnectedMailRuntimeUsesExactCatalogGenerationForBothProviders(t *testing.T) {
	definition, err := parseConnectedMailDefinition([]byte(validConnectedMailDefinitionJSON))
	if err != nil {
		t.Fatal(err)
	}
	view := connectedMailCatalogView()
	binding, err := definition.MailBindingDefinition(view)
	if err != nil {
		t.Fatal(err)
	}
	config := &ConnectedMailConfig{
		Definition:        definition,
		CallbackURI:       "https://runtime.example/api/connected-mailboxes/example/callback",
		ReturnBaseURL:     "https://runtime.example",
		OAuthClientID:     "client-id",
		OAuthClientSecret: "client-secret",
		MasterKey:         bytes.Repeat([]byte{0x2a}, 32),
	}
	fallback := &connectedMailFallbackPreparer{}
	preparer, runtime, err := withConnectedMailPreparer(
		new(sql.DB), config, view, connectedMailRuntimeThrottler{}, connectedMailRuntimeAllowlist{}, fallback,
	)
	if err != nil {
		t.Fatal(err)
	}
	if preparer == nil || runtime == nil || runtime.oauth == nil || runtime.providers == nil || runtime.preparer == nil ||
		runtime.connectorKey != binding.ConnectorKey || preparer == fallback {
		t.Fatalf("connected-mail composition is incomplete: %#v", runtime)
	}
	if _, ok := preparer.(*connectoroutboundintent.OperationPreparer); !ok {
		t.Fatalf("configured outbound preparer = %T, want operation router", preparer)
	}
	if provider, ok := runtime.providers.Provider(binding.ConnectorKey); !ok || provider == nil {
		t.Fatal("exact OAuth provider is unavailable")
	}
	if provider, ok := runtime.providers.MailProvider(binding.ConnectorKey); !ok || provider == nil {
		t.Fatal("exact mail provider is unavailable")
	}
	for _, foreign := range []string{definition.ConnectorKey, binding.ConnectorKey + "/other", ""} {
		if provider, ok := runtime.providers.Provider(foreign); ok || provider != nil {
			t.Fatalf("foreign OAuth connector %q was accepted", foreign)
		}
		if provider, ok := runtime.providers.MailProvider(foreign); ok || provider != nil {
			t.Fatalf("foreign mail connector %q was accepted", foreign)
		}
	}
	if err := runtime.policy.ValidateRedirect(binding.ConnectorKey, config.CallbackURI); err != nil {
		t.Fatalf("exact callback was denied: %v", err)
	}
	if err := runtime.policy.ValidateScopes(binding.ConnectorKey, []string{"openid", "mail.send"}); err != nil {
		t.Fatalf("exact scope set was denied: %v", err)
	}
	for name, err := range map[string]error{
		"foreign connector": runtime.policy.ValidateRedirect(definition.ConnectorKey, config.CallbackURI),
		"foreign callback":  runtime.policy.ValidateRedirect(binding.ConnectorKey, config.CallbackURI+"/other"),
		"foreign scope":     runtime.policy.ValidateScopes(binding.ConnectorKey, []string{"openid"}),
	} {
		if !errors.Is(err, connectoroauth.ErrAuthorizationDenied) {
			t.Fatalf("%s error = %v, want authorization denied", name, err)
		}
	}
}

func TestConnectedMailRuntimeRejectsIncompleteComposition(t *testing.T) {
	definition, err := parseConnectedMailDefinition([]byte(validConnectedMailDefinitionJSON))
	if err != nil {
		t.Fatal(err)
	}
	config := &ConnectedMailConfig{
		Definition:        definition,
		CallbackURI:       "https://runtime.example/api/connected-mailboxes/example/callback",
		OAuthClientID:     "client-id",
		OAuthClientSecret: "client-secret",
		MasterKey:         bytes.Repeat([]byte{0x2a}, 32),
	}
	for name, testCase := range map[string]struct {
		db        *sql.DB
		throttler connectoroutboundintent.ResolvedMailThrottler
		allowlist ports.URLAllowlist
	}{
		"database":  {throttler: connectedMailRuntimeThrottler{}, allowlist: connectedMailRuntimeAllowlist{}},
		"throttler": {db: new(sql.DB), allowlist: connectedMailRuntimeAllowlist{}},
		"allowlist": {db: new(sql.DB), throttler: connectedMailRuntimeThrottler{}},
	} {
		t.Run(name, func(t *testing.T) {
			_, err := newConnectedMailRuntime(
				testCase.db, config, connectedMailCatalogView(), testCase.throttler,
				testCase.allowlist,
			)
			if err == nil {
				t.Fatal("incomplete connected-mail composition was accepted")
			}
		})
	}
}
