package serverruntime

import (
	"context"
	"database/sql"
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/nexuscrm/backend/internal/bootstrap"
	"github.com/nexuscrm/shared/pkg/models"
	contractconnector "github.com/pingcap-inc/nexuscrm-contract/pkg/connector"
	contractconstants "github.com/pingcap-inc/nexuscrm-contract/pkg/constants"
	"github.com/pingcap-inc/nexuscrm-controlplane/packs"
)

// TestAuthorizeConnectorPushProfileDependency_SamePack_NilDB_ReturnsNil
// confirms that same-pack (case-insensitive) passes without any DB access.
func TestAuthorizeConnectorPushProfileDependency_SamePack_NilDB_ReturnsNil(t *testing.T) {
	if err := authorizeConnectorPushProfileDependency(context.Background(), nil, "pack-a", "Pack-A"); err != nil {
		t.Errorf("same-pack with nil db must return nil, got: %v", err)
	}
}

// TestAuthorizeConnectorPushProfileDependency_CrossPack_NilDB_ReturnsError
// confirms that cross-pack with nil db returns an error (db is required).
func TestAuthorizeConnectorPushProfileDependency_CrossPack_NilDB_ReturnsError(t *testing.T) {
	err := authorizeConnectorPushProfileDependency(context.Background(), nil, "pack-a", "pack-b")
	if err == nil {
		t.Fatal("expected error for cross-pack with nil db, got nil")
	}
	// The call site wraps with "connector push profile dependency:" prefix.
	msg := err.Error()
	if len(msg) == 0 {
		t.Fatal("error message is empty")
	}
}

// TestAuthorizeConnectorPushProfileDependency_EmptyRequester_ReturnsError
// confirms that a blank requesterOwnerPack is rejected.
func TestAuthorizeConnectorPushProfileDependency_EmptyRequester_ReturnsError(t *testing.T) {
	err := authorizeConnectorPushProfileDependency(context.Background(), nil, "", "pack-b")
	if err == nil {
		t.Fatal("expected error for empty requester, got nil")
	}
}

// TestAuthorizeConnectorPushProfileDependency_EmptyProfile_ReturnsError
// confirms that a blank profileOwnerPack is rejected.
func TestAuthorizeConnectorPushProfileDependency_EmptyProfile_ReturnsError(t *testing.T) {
	err := authorizeConnectorPushProfileDependency(context.Background(), nil, "pack-a", "")
	if err == nil {
		t.Fatal("expected error for empty profile, got nil")
	}
}

func TestConnectorInboundPinnedStateAcceptsDrainingWithoutWeakeningLiveAdmission(t *testing.T) {
	state := &connectorPushInstalledPackState{
		owner:         "connector-pack",
		packInstallID: "install-1",
		status:        "draining",
		appliedHash:   "hash-1",
		desiredHash:   "hash-1",
	}
	if err := validateConnectorInboundPinnedPackState(state); err != nil {
		t.Fatalf("pinned draining generation: %v", err)
	}
	if err := validateConnectorInboundInstalledPackState(state); err == nil {
		t.Fatal("live active-pointer admission accepted a draining generation")
	}
}

type connectorPushRuntimeTxExtractor struct{ tx *sql.Tx }

func (e connectorPushRuntimeTxExtractor) ExtractTx(context.Context) interface{} { return e.tx }

func TestConnectorInboundPinnedGenerationReusesImmutableSourceSnapshotAcrossWorkItems(t *testing.T) {
	hintCalls := 0
	generationCalls := 0
	resolver := newConnectorPushContractResolver(&sql.DB{}, packs.ComponentKindRegistrationSet{})
	resolver.resolveRecoveryHintPack = func(_ context.Context, state *connectorPushInstalledPackState) (packs.Pack, func(), error) {
		hintCalls++
		return connectorPushResolverFakePack{name: state.owner}, func() {}, nil
	}
	resolver.contractDeclarations = func(packs.Pack) ([]bootstrap.ConnectorPushDurableMetadataContract, error) {
		return []bootstrap.ConnectorPushDurableMetadataContract{
			testConnectorPushRuntimeContract("hubspot-pack", "HubSpot_Lead", "Push", "profile"),
		}, nil
	}
	state := &connectorPushInstalledPackState{
		owner: "hubspot-pack", packInstallID: "install-hubspot-pack", category: string(packs.ConnectorPack),
		status: "active", manifestVersion: "1", sourceKind: packs.PackSourceKindOfficialRepo,
		sourceURI: "hubspot-pack", sourceRevision: connectorPushRuntimeTestRevision,
		appliedHash: "hash-1", desiredHash: "hash-1",
	}
	resolve := func(_ context.Context, state *connectorPushInstalledPackState, _ map[string]string) (connectorPushLockedSourceGeneration, error) {
		generationCalls++
		return connectorPushLockedSourceGeneration{
			contracts: []bootstrap.ConnectorPushDurableMetadataContract{
				testConnectorPushRuntimeContract(state.owner, "HubSpot_Lead", "Push", "profile"),
			},
		}, nil
	}

	for i := 1; i <= 2; i++ {
		ctx := contractconstants.WithConnectorInboundTrace(context.Background(), contractconstants.ConnectorInboundTrace{
			WorkItemID: fmt.Sprintf("work-%d", i),
			EventID:    fmt.Sprintf("event-%d", i),
			Namespace:  "hubspot-pack",
		})
		ctx = resolver.WithConnectorInboundRemoteIdentityInstalledContractRetryScope(ctx)
		if _, err := resolver.resolveCachedConnectorPushRecoveryHint(ctx, state, true); err != nil {
			t.Fatalf("work %d resolveCachedConnectorPushRecoveryHint: %v", i, err)
		}
		if _, err := resolver.resolveCachedConnectorPushLockedSourceGeneration(ctx, state, map[string]string{"hubspot-pack": connectorPushRuntimeTestRevision}, resolve); err != nil {
			t.Fatalf("work %d resolveCachedConnectorPushLockedSourceGeneration: %v", i, err)
		}
	}
	if hintCalls != 1 {
		t.Fatalf("immutable recovery hint resolutions = %d, want 1 across WorkItems", hintCalls)
	}
	if generationCalls != 1 {
		t.Fatalf("immutable locked source generation resolutions = %d, want 1 across WorkItems", generationCalls)
	}
}

func TestConnectorPushRetryScopedGenerationRefreshesLiveSchemaBetweenRequests(t *testing.T) {
	schemaVersion := 1
	resolveCalls := 0
	resolver := newConnectorPushContractResolver(&sql.DB{}, packs.ComponentKindRegistrationSet{})
	resolver.objectSchemaResolver = func(context.Context, *sql.DB) func(string) *models.ObjectMetadata {
		return func(apiName string) *models.ObjectMetadata {
			return &models.ObjectMetadata{
				APIName: apiName,
				Label:   fmt.Sprintf("schema-v%d", schemaVersion),
			}
		}
	}
	state := &connectorPushInstalledPackState{
		owner: "hubspot-pack", packInstallID: "install-hubspot-pack", category: string(packs.ConnectorPack),
		status: "active", manifestVersion: "1", sourceKind: packs.PackSourceKindOfficialRepo,
		sourceURI: "hubspot-pack", sourceRevision: connectorPushRuntimeTestRevision,
		appliedHash: "hash-1", desiredHash: "hash-1",
	}
	resolve := func(ctx context.Context, state *connectorPushInstalledPackState, _ map[string]string) (connectorPushLockedSourceGeneration, error) {
		resolveCalls++
		schema := resolver.objectSchemaResolver(ctx, resolver.db)("Lead")
		return connectorPushLockedSourceGeneration{
			contracts: []bootstrap.ConnectorPushDurableMetadataContract{
				testConnectorPushRuntimeContract(state.owner, "HubSpot_Lead", "Push", schema.Label),
			},
			objectFields: map[string][]contractconnector.ConnectorPushObjectFieldMetadata{
				"lead": {{APIName: "Email", Type: "string"}},
			},
		}, nil
	}

	firstRequest := resolver.WithConnectorInboundRemoteIdentityInstalledContractRetryScope(context.Background())
	first, err := resolver.resolveCachedConnectorPushLockedSourceGeneration(firstRequest, state, nil, resolve)
	if err != nil {
		t.Fatalf("first resolveCachedConnectorPushLockedSourceGeneration: %v", err)
	}
	schemaVersion = 2
	retry, err := resolver.resolveCachedConnectorPushLockedSourceGeneration(firstRequest, state, nil, resolve)
	if err != nil {
		t.Fatalf("retry resolveCachedConnectorPushLockedSourceGeneration: %v", err)
	}
	secondRequest := resolver.WithConnectorInboundRemoteIdentityInstalledContractRetryScope(context.Background())
	second, err := resolver.resolveCachedConnectorPushLockedSourceGeneration(secondRequest, state, nil, resolve)
	if err != nil {
		t.Fatalf("second request resolveCachedConnectorPushLockedSourceGeneration: %v", err)
	}

	if resolveCalls != 2 {
		t.Fatalf("resolveCalls = %d, want one parse in each request scope", resolveCalls)
	}
	if first.contracts[0].ProfileName != "schema-v1" || retry.contracts[0].ProfileName != "schema-v1" {
		t.Fatalf("first request profiles = %q then %q, want stable schema-v1 across retry", first.contracts[0].ProfileName, retry.contracts[0].ProfileName)
	}
	if second.contracts[0].ProfileName != "schema-v2" {
		t.Fatalf("second request ProfileName = %q, want refreshed schema-v2", second.contracts[0].ProfileName)
	}
}

func TestConnectorPushLockedSourceGenerationRequiresImmutableSourceRevision(t *testing.T) {
	resolver := newConnectorPushContractResolver(&sql.DB{}, packs.ComponentKindRegistrationSet{})
	base := &connectorPushInstalledPackState{
		owner: "connector-pack", packInstallID: "install-1", category: string(packs.ConnectorPack),
		status: "active", manifestVersion: "2", sourceKind: packs.PackSourceKindOfficialRepo,
		sourceURI: "connector-pack", sourceRevision: "release/latest", appliedHash: "hash", desiredHash: "hash",
	}
	if _, err := resolver.resolveConnectorPushLockedSourceGeneration(context.Background(), base, nil); err == nil || !strings.Contains(err.Error(), "40-hex") {
		t.Fatalf("mutable official_repo revision error = %v", err)
	}
	local := *base
	local.sourceKind = packs.PackSourceKindLocalPath
	local.sourceRevision = strings.Repeat("a", 40)
	if _, err := resolver.resolveConnectorPushLockedSourceGeneration(context.Background(), &local, nil); err == nil || !strings.Contains(err.Error(), connectorPushTrustedLocalPackRootEnv) {
		t.Fatalf("unbound local_path strict recovery error = %v", err)
	}
	if !IsImmutableConnectorPushSourceRevision(strings.Repeat("a", 40)) || IsImmutableConnectorPushSourceRevision(strings.Repeat("g", 40)) {
		t.Fatal("immutable source revision classifier rejected hex or accepted non-hex")
	}
}

func TestConnectorInboundLockedSourceClosureFailsClosed(t *testing.T) {
	t.Run("missing dependency", func(t *testing.T) {
		_, err := connectorInboundLockedSourceClosure("owner-a", map[string]connectorPushLockedSourceGeneration{
			"owner-a": {dependencies: []string{"missing"}},
		})
		if err == nil || !strings.Contains(err.Error(), "missing") {
			t.Fatalf("missing dependency error = %v", err)
		}
	})
	t.Run("cycle", func(t *testing.T) {
		_, err := connectorInboundLockedSourceClosure("owner-a", map[string]connectorPushLockedSourceGeneration{
			"owner-a": {dependencies: []string{"owner-b"}},
			"owner-b": {dependencies: []string{"owner-a"}},
		})
		if err == nil || !strings.Contains(err.Error(), "cycle detected") {
			t.Fatalf("cycle error = %v", err)
		}
	})
	t.Run("depth cap", func(t *testing.T) {
		resolved := make(map[string]connectorPushLockedSourceGeneration)
		for i := 0; i <= connectorInboundInstalledSourceMaxDepth+1; i++ {
			owner := fmt.Sprintf("owner-%03d", i)
			generation := connectorPushLockedSourceGeneration{}
			if i <= connectorInboundInstalledSourceMaxDepth {
				generation.dependencies = []string{fmt.Sprintf("owner-%03d", i+1)}
			}
			resolved[owner] = generation
		}
		_, err := connectorInboundLockedSourceClosure("owner-000", resolved)
		if err == nil || !strings.Contains(err.Error(), "maximum depth") {
			t.Fatalf("depth cap error = %v", err)
		}
	})
}

func TestConnectorPushInstalledSourceRevisionsRejectsSharedURIDrift(t *testing.T) {
	states := map[string]*connectorPushInstalledPackState{
		"owner-a": {owner: "owner-a", sourceURI: "shared-source", sourceRevision: strings.Repeat("a", 40)},
		"owner-b": {owner: "owner-b", sourceURI: "shared-source", sourceRevision: strings.Repeat("b", 40)},
	}
	_, err := connectorPushInstalledSourceRevisions(states)
	if err == nil || !strings.Contains(err.Error(), "multiple revisions") {
		t.Fatalf("shared source revision error = %v", err)
	}
}

type connectorPushResolverFakePack struct {
	name         string
	dependencies []packs.PackDependency
}

func (p connectorPushResolverFakePack) Name() string {
	return p.name
}

func (connectorPushResolverFakePack) Category() packs.Category {
	return packs.ConnectorPack
}

func (connectorPushResolverFakePack) Apply(context.Context, packs.ApplyDeps) error {
	return nil
}

func (connectorPushResolverFakePack) Verify(context.Context, packs.VerifyDeps) error {
	return nil
}

func (p connectorPushResolverFakePack) PackDependencies() []packs.PackDependency {
	return append([]packs.PackDependency(nil), p.dependencies...)
}

const connectorPushRuntimeTestRevision = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

var connectorPushRuntimeTestEventCreatedAt = time.Date(2026, time.August, 3, 10, 0, 0, 0, time.UTC)

func installConnectorPushLockedSourceGenerationTestResolver(resolver *connectorPushContractResolver) {
	resolver.resolveLockedSourceGeneration = func(_ context.Context, state *connectorPushInstalledPackState, _ map[string]string) (connectorPushLockedSourceGeneration, error) {
		contracts, err := resolver.contractDeclarations(connectorPushResolverFakePack{name: state.owner})
		if err != nil {
			return connectorPushLockedSourceGeneration{}, err
		}
		mappings := make([]contractconnector.ConnectorFieldMappingInput, 0, len(contracts))
		dependencies := make([]string, 0)
		for _, contract := range contracts {
			mappings = append(mappings, contractconnector.ConnectorFieldMappingInput{
				Source:       contract.ConnectorOwnerKey,
				SourceObject: contract.SourceObject,
				TargetObject: contract.TargetObject,
				ActionType:   contract.ActionType,
				Direction:    contractconnector.DirectionOutbound,
				SourceField:  "Email",
				TargetField:  "email",
			})
			if contract.ConnectorOwnerKey == state.owner {
				profileOwner := strings.TrimSpace(contract.ProfileOwnerPack)
				if profileOwner != "" && profileOwner != state.owner {
					dependencies = append(dependencies, profileOwner)
				}
			}
		}
		return connectorPushLockedSourceGeneration{contracts: contracts, mappings: mappings, dependencies: dependencies}, nil
	}
}

func testConnectorPushRuntimeContract(owner, flowName, actionName, profileName string) bootstrap.ConnectorPushDurableMetadataContract {
	return bootstrap.ConnectorPushDurableMetadataContract{
		ConnectorOwnerKey:  owner,
		ActionSlotID:       "hubspot-lead",
		ActionType:         "PushToConnector",
		FlowDeveloperName:  flowName,
		FlowActionName:     actionName,
		SourceObject:       "Lead",
		DestinationSlotID:  "hubspot-lead",
		ProfileOwnerPack:   owner,
		ProfileName:        profileName,
		TargetObject:       "contacts",
		CreatePathTemplate: "/crm/v3/objects/contacts",
		UpdatePathTemplate: "/crm/v3/objects/contacts/{remoteId}",
		ResponseIDPath:     "id",
	}
}
