package integrationservice

import (
	"bytes"
	"encoding/base64"
	"errors"
	"net/http"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"testing"
	"time"

	connectedmailhttp "github.com/pingcap-inc/nexuscrm-connectors/features/connector/connectedmailhttp"
	catalogv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
)

func TestConnectedMailDefinitionPublishesExactConnectorInputs(t *testing.T) {
	definition, err := parseConnectedMailDefinition([]byte(validConnectedMailDefinitionJSON))
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(definition.RequiredScopes, []string{"mail.send", "openid"}) {
		t.Fatalf("required scopes = %#v", definition.RequiredScopes)
	}
	binding, err := definition.MailBindingDefinition(connectedMailCatalogView())
	if err != nil {
		t.Fatal(err)
	}
	if binding.OwnerPack != "mail-owner" || binding.BusinessKey != "mail/delivery" ||
		binding.DefinitionVersion != 1 || binding.ConnectorKey == "" || binding.ConnectorKey == definition.ConnectorKey ||
		binding.ClientCredentialRef != "mail-oauth-client" {
		t.Fatalf("mail binding = %#v", binding)
	}
	oauth := definition.OAuthConfig("client-id", "client-secret")
	mail := definition.MailConfig()
	if oauth.ClientID != "client-id" || oauth.ClientSecret != "client-secret" ||
		oauth.AuthorizationParameters["access_type"] != "offline" || oauth.TerminationBound != 20*time.Second ||
		mail.Endpoint != "https://api.example/send" || mail.RequestField != "raw" ||
		mail.ResponseIDField != "id" || mail.TerminationBound != 20*time.Second {
		t.Fatal("provider configs do not preserve the admitted definition")
	}
	transport := connectedMailConfigRoundTripper{}
	allowlist := connectedMailConfigAllowlist{}
	if adapter, err := connectedmailhttp.NewOAuthAdapter(oauth, transport, allowlist); err != nil || adapter == nil {
		t.Fatalf("formal OAuth adapter rejected the admitted definition: %v", err)
	}
	if adapter, err := connectedmailhttp.NewMailAdapter(mail, transport, allowlist); err != nil || adapter == nil {
		t.Fatalf("formal mail adapter rejected the admitted definition: %v", err)
	}
	oauth.AuthorizationParameters["access_type"] = "online"
	if definition.AuthorizationParameters["access_type"] != "offline" {
		t.Fatal("OAuth config shares mutable authorization parameters with the release definition")
	}
}

func TestConnectedMailDefinitionRejectsUnknownAndUnsafeValues(t *testing.T) {
	tests := []struct {
		name string
		raw  string
	}{
		{name: "unknown secret", raw: strings.Replace(validConnectedMailDefinitionJSON, `"terminationBound":"20s"`, `"terminationBound":"20s","clientSecret":"forbidden"`, 1)},
		{name: "endpoint query", raw: strings.Replace(validConnectedMailDefinitionJSON, "https://auth.example/token", "https://auth.example/token?secret=value", 1)},
		{name: "unsafe parameter", raw: strings.Replace(validConnectedMailDefinitionJSON, `"access_type":"offline"`, `"client_secret":"forbidden"`, 1)},
		{name: "unclean public path", raw: strings.Replace(validConnectedMailDefinitionJSON, "/api/connected-mailboxes/example", "/api/connected-mailboxes/../example", 1)},
		{name: "duplicate scope", raw: strings.Replace(validConnectedMailDefinitionJSON, `["openid","mail.send"]`, `["mail.send","mail.send"]`, 1)},
		{name: "unbounded duration", raw: strings.Replace(validConnectedMailDefinitionJSON, `"terminationBound":"20s"`, `"terminationBound":"2m"`, 1)},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if _, err := parseConnectedMailDefinition([]byte(test.raw)); err == nil {
				t.Fatal("definition unexpectedly accepted")
			}
		})
	}
}

func TestLoadConfigRequiresCompleteConnectedMailRuntimeInputs(t *testing.T) {
	tokenPath := filepath.Join(t.TempDir(), "workload-token")
	if err := os.WriteFile(tokenPath, []byte("token-value"), 0o600); err != nil {
		t.Fatal(err)
	}
	definitionPath := filepath.Join(t.TempDir(), "connected-mail.json")
	if err := os.WriteFile(definitionPath, []byte(validConnectedMailDefinitionJSON), 0o600); err != nil {
		t.Fatal(err)
	}
	values := validConfigValues(tokenPath)
	values[ConnectedMailDefinitionFileEnv] = definitionPath
	values[ConnectedMailCallbackURIEnv] = "http://127.0.0.1:3000/api/connected-mailboxes/example/callback"
	values[ConnectedMailReturnBaseURLEnv] = "http://127.0.0.1:3000"
	values[ConnectedMailOAuthClientIDEnv] = "client-id"
	values[ConnectedMailOAuthClientSecretEnv] = "client-secret"
	values[ConnectedMailMasterKeyEnv] = base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{0x2a}, 32))

	config, err := LoadConfig(mapLookup(values))
	if err != nil {
		t.Fatalf("LoadConfig() error = %v", err)
	}
	if config.ConnectedMail == nil || config.ConnectedMail.Definition.PublicPath != "/api/connected-mailboxes/example" ||
		config.ConnectedMail.CallbackURI != values[ConnectedMailCallbackURIEnv] || len(config.ConnectedMail.MasterKey) != 32 {
		t.Fatal("connected-mail config does not preserve the admitted runtime identity")
	}
	defer clearConnectedMailBytes(config.ConnectedMail.MasterKey)

	t.Run("partial", func(t *testing.T) {
		partial := validConfigValues(tokenPath)
		partial[ConnectedMailDefinitionFileEnv] = definitionPath
		if _, err := LoadConfig(mapLookup(partial)); err == nil || !strings.Contains(err.Error(), ConnectedMailCallbackURIEnv) {
			t.Fatalf("LoadConfig(partial) error = %v", err)
		}
	})
	t.Run("explicit empty", func(t *testing.T) {
		empty := validConfigValues(tokenPath)
		empty[ConnectedMailOAuthClientSecretEnv] = ""
		if _, err := LoadConfig(mapLookup(empty)); err == nil || !strings.Contains(err.Error(), ConnectedMailDefinitionFileEnv) {
			t.Fatalf("LoadConfig(explicit empty) error = %v", err)
		}
	})
	t.Run("callback path mismatch", func(t *testing.T) {
		mismatch := cloneConfigValues(values)
		mismatch[ConnectedMailCallbackURIEnv] = "http://127.0.0.1:3000/api/connected-mailboxes/other/callback"
		if _, err := LoadConfig(mapLookup(mismatch)); err == nil || !strings.Contains(err.Error(), "does not match") {
			t.Fatalf("LoadConfig(mismatch) error = %v", err)
		}
	})
	t.Run("invalid key", func(t *testing.T) {
		invalid := cloneConfigValues(values)
		invalid[ConnectedMailMasterKeyEnv] = base64.StdEncoding.EncodeToString([]byte("short"))
		if _, err := LoadConfig(mapLookup(invalid)); err == nil || !strings.Contains(err.Error(), "AES-256") {
			t.Fatalf("LoadConfig(invalid key) error = %v", err)
		}
	})
}

func TestConnectedMailRuntimeConnectorKeyFencesBindingChanges(t *testing.T) {
	definition, err := parseConnectedMailDefinition([]byte(validConnectedMailDefinitionJSON))
	if err != nil {
		t.Fatal(err)
	}
	view := connectedMailCatalogView()
	firstDefinition, err := definition.MailBindingDefinition(view)
	if err != nil {
		t.Fatal(err)
	}
	first := firstDefinition.ConnectorKey

	installation := view
	installation.Bindings[0].PackInstallationID = "mail-install-b"
	installation.Lineage.ViewDigest = catalogv1.ComputeIntegrationBindingViewDigest(installation)
	if next := connectedMailBindingKey(t, definition, installation); next == first {
		t.Fatalf("connector key did not change across pack installations: %q", first)
	}

	payload := connectedMailCatalogView()
	payload.Bindings[0].PackPayloadDigest = "nexus-pack-sha256-v1:" + strings.Repeat("b", 64)
	payload.Lineage.ViewDigest = catalogv1.ComputeIntegrationBindingViewDigest(payload)
	if next := connectedMailBindingKey(t, definition, payload); next == first {
		t.Fatalf("connector key did not change across pack payloads: %q", first)
	}

	version := connectedMailCatalogView()
	version.Bindings[0].DefinitionVersion++
	version.Lineage.ViewDigest = catalogv1.ComputeIntegrationBindingViewDigest(version)
	versionedDefinition := definition
	versionedDefinition.Binding.DefinitionVersion++
	if next := connectedMailBindingKey(t, versionedDefinition, version); next == first {
		t.Fatalf("connector key did not change across definition versions: %q", first)
	}

	credential := connectedMailCatalogView()
	credential.Bindings[0].CredentialRefs = []string{"rotated-client"}
	credential.Lineage.ViewDigest = catalogv1.ComputeIntegrationBindingViewDigest(credential)
	credentialDefinition := definition
	credentialDefinition.Binding.ClientCredentialRef = "rotated-client"
	if next := connectedMailBindingKey(t, credentialDefinition, credential); next == first {
		t.Fatalf("connector key did not change across credential references: %q", first)
	}
}

func TestConnectedMailBindingRejectsForeignOrIncompleteCatalogAuthority(t *testing.T) {
	definition, err := parseConnectedMailDefinition([]byte(validConnectedMailDefinitionJSON))
	if err != nil {
		t.Fatal(err)
	}
	for _, mutate := range []func(*catalogv1.IntegrationBindingView){
		func(view *catalogv1.IntegrationBindingView) {
			view.Bindings[0].Kind = catalogv1.BindingConnectorFieldMapping
		},
		func(view *catalogv1.IntegrationBindingView) { view.Bindings[0].CredentialRefs = nil },
		func(view *catalogv1.IntegrationBindingView) { view.Bindings[0].OwnerPack = "foreign-owner" },
	} {
		view := connectedMailCatalogView()
		mutate(&view)
		view.Lineage.ViewDigest = catalogv1.ComputeIntegrationBindingViewDigest(view)
		if _, err := definition.MailBindingDefinition(view); err == nil {
			t.Fatal("connected-mail definition accepted foreign or incomplete Catalog authority")
		}
	}
}

func connectedMailCatalogView() catalogv1.IntegrationBindingView {
	binding := integrationServingTestBinding(
		catalogv1.BindingConnectorCalloutProfile,
		"mail-owner", "mail-install-a", "mail/delivery", "a",
	)
	binding.CredentialRefs = []string{"mail-oauth-client"}
	return integrationServingTestView([]catalogv1.IntegrationBinding{binding})
}

func connectedMailBindingKey(
	t *testing.T,
	definition ConnectedMailDefinition,
	view catalogv1.IntegrationBindingView,
) string {
	t.Helper()
	binding, err := definition.MailBindingDefinition(view)
	if err != nil {
		t.Fatal(err)
	}
	return binding.ConnectorKey
}

func cloneConfigValues(values map[string]string) map[string]string {
	cloned := make(map[string]string, len(values))
	for key, value := range values {
		cloned[key] = value
	}
	return cloned
}

type connectedMailConfigRoundTripper struct{}

func (connectedMailConfigRoundTripper) RoundTrip(*http.Request) (*http.Response, error) {
	return nil, errors.New("config test transport must not execute")
}

type connectedMailConfigAllowlist struct{}

func (connectedMailConfigAllowlist) EnsureAllowed(string) error { return nil }

const validConnectedMailDefinitionJSON = `{
  "schema":"nexuscrm.connected-mail-http-provider/v1",
  "connectorKey":"connected-mail",
  "publicPath":"/api/connected-mailboxes/example",
  "binding":{"ownerPack":"mail-owner","businessKey":"mail/delivery","definitionVersion":1,"clientCredentialRef":"mail-oauth-client"},
  "requiredScopes":["openid","mail.send"],
  "authorizationParameters":{"access_type":"offline","prompt":"consent"},
  "authorizationEndpoint":"https://auth.example/authorize",
  "tokenEndpoint":"https://auth.example/token",
  "identityEndpoint":"https://api.example/identity",
  "revocationEndpoint":"https://auth.example/revoke",
  "mailEndpoint":"https://api.example/send",
  "issuer":"https://auth.example",
  "identitySubjectField":"sub",
  "identityAddressField":"email",
  "identityDisplayNameField":"name",
  "mailRequestField":"raw",
  "mailResponseIdField":"id",
  "terminationBound":"20s"
}`
