package automation

import (
	"context"
	"net/http"
	"strings"
	"testing"

	"github.com/nexuscrm/shared/pkg/constants"
	connector "github.com/pingcap-inc/nexuscrm-contract/pkg/connector"
)

func TestAdoptConnectorPushLegacyMirrorEvidenceSuspendsCurrentSlot(t *testing.T) {
	store := newMemoryConnectorPushDurableStore()
	contract := legacyMirrorAdoptionContract()
	identity := connectorPushDurableIdentityFromContract(&contract, "003-test")
	linkKey, err := identity.LinkKey()
	if err != nil {
		t.Fatalf("LinkKey: %v", err)
	}

	link, adopted, err := AdoptConnectorPushLegacyMirrorEvidence(context.Background(), store, ConnectorPushLegacyMirrorEvidence{
		Contract:           contract,
		SourceRecordID:     "003-test",
		RemoteID:           "remote-123",
		RemoteIDProvenance: linkKey,
		Mappings:           legacyMirrorAdoptionMappings(),
		Profile:            legacyMirrorAdoptionProfile(),
	})
	if err != nil {
		t.Fatalf("AdoptConnectorPushLegacyMirrorEvidence: %v", err)
	}
	if !adopted {
		t.Fatal("adopted = false, want true")
	}
	if link == nil || link.LinkKey != linkKey || link.RemoteID != "remote-123" || link.Identity.ActionSlotID != "contact-current" || link.Status != ConnectorPushLinkStatusSuspendedDrift {
		t.Fatalf("link = %#v, want current-slot suspended remote link", link)
	}
	finalized, err := store.FindFinalizedLink(context.Background(), identity)
	if err != nil {
		t.Fatalf("FindFinalizedLink: %v", err)
	}
	if finalized != nil {
		t.Fatalf("FindFinalizedLink = %#v, want nil for unvalidated legacy mirror adoption", finalized)
	}
}

func TestAdoptConnectorPushLegacyMirrorEvidenceSuspendsPreviousSlot(t *testing.T) {
	store := newMemoryConnectorPushDurableStore()
	contract := legacyMirrorAdoptionContract()
	previousContract := contract
	previousContract.ActionSlotID = "contact-v1"
	identity := connectorPushDurableIdentityFromContract(&previousContract, "003-test")
	linkKey, err := identity.LinkKey()
	if err != nil {
		t.Fatalf("LinkKey: %v", err)
	}

	link, adopted, err := AdoptConnectorPushLegacyMirrorEvidence(context.Background(), store, ConnectorPushLegacyMirrorEvidence{
		Contract:           contract,
		SourceRecordID:     "003-test",
		RemoteID:           "remote-previous",
		RemoteIDProvenance: linkKey,
		Mappings:           legacyMirrorAdoptionMappings(),
		Profile:            legacyMirrorAdoptionProfile(),
	})
	if err != nil {
		t.Fatalf("AdoptConnectorPushLegacyMirrorEvidence: %v", err)
	}
	if !adopted {
		t.Fatal("adopted = false, want true")
	}
	if link == nil || link.LinkKey != linkKey || link.RemoteID != "remote-previous" || link.Identity.ActionSlotID != "contact-v1" || link.Status != ConnectorPushLinkStatusSuspendedDrift {
		t.Fatalf("link = %#v, want previous-slot suspended remote link", link)
	}
}

func TestAdoptConnectorPushLegacyMirrorEvidenceIgnoresUnmatchedProvenance(t *testing.T) {
	store := newMemoryConnectorPushDurableStore()
	_, adopted, err := AdoptConnectorPushLegacyMirrorEvidence(context.Background(), store, ConnectorPushLegacyMirrorEvidence{
		Contract:           legacyMirrorAdoptionContract(),
		SourceRecordID:     "003-test",
		RemoteID:           "remote-123",
		RemoteIDProvenance: "manual-value",
		Mappings:           legacyMirrorAdoptionMappings(),
		Profile:            legacyMirrorAdoptionProfile(),
	})
	if err != nil {
		t.Fatalf("AdoptConnectorPushLegacyMirrorEvidence: %v", err)
	}
	if adopted {
		t.Fatal("adopted = true, want false for unmatched provenance")
	}
	if len(store.links) != 0 {
		t.Fatalf("links = %d, want no durable adoption", len(store.links))
	}
}

func TestAdoptConnectorPushLegacyMirrorEvidenceNoopsWhenBlockingAttemptAlreadyProtectsLinkKey(t *testing.T) {
	store := newMemoryConnectorPushDurableStore()
	contract := legacyMirrorAdoptionContract()
	identity := connectorPushDurableIdentityFromContract(&contract, "003-test")
	linkKey, err := identity.LinkKey()
	if err != nil {
		t.Fatalf("LinkKey: %v", err)
	}
	store.attempts["attempt-1"] = &ConnectorPushAttempt{
		ID:       "attempt-1",
		LinkKey:  linkKey,
		Status:   ConnectorPushAttemptStatusPending,
		Identity: identity,
		Snapshot: ConnectorPushDurableSnapshot{
			RequestMethod: http.MethodPost,
		},
	}

	link, adopted, err := AdoptConnectorPushLegacyMirrorEvidence(context.Background(), store, ConnectorPushLegacyMirrorEvidence{
		Contract:           contract,
		SourceRecordID:     "003-test",
		RemoteID:           "remote-123",
		RemoteIDProvenance: linkKey,
		Mappings:           legacyMirrorAdoptionMappings(),
		Profile:            legacyMirrorAdoptionProfile(),
	})
	if err != nil {
		t.Fatalf("AdoptConnectorPushLegacyMirrorEvidence: %v", err)
	}
	if adopted {
		t.Fatal("adopted = true, want false when blocking attempt already protects the link key")
	}
	if link != nil {
		t.Fatalf("link = %#v, want nil when no new durable link is inserted", link)
	}
	if len(store.links) != 0 {
		t.Fatalf("links = %d, want no durable link insert when blocking attempt already protects the link key", len(store.links))
	}
}

func TestAdoptConnectorPushLegacyMirrorEvidenceNoopsWhenBlockingAttemptWinsConcurrentRace(t *testing.T) {
	store := &connectorPushLegacyMirrorConflictStore{
		memoryConnectorPushDurableStore: newMemoryConnectorPushDurableStore(),
		conflictKind:                    "attempt",
	}
	contract := legacyMirrorAdoptionContract()
	identity := connectorPushDurableIdentityFromContract(&contract, "003-test")
	linkKey, err := identity.LinkKey()
	if err != nil {
		t.Fatalf("LinkKey: %v", err)
	}
	store.linkKey = linkKey
	store.identity = identity

	link, adopted, err := AdoptConnectorPushLegacyMirrorEvidence(context.Background(), store, ConnectorPushLegacyMirrorEvidence{
		Contract:           contract,
		SourceRecordID:     "003-test",
		RemoteID:           "remote-123",
		RemoteIDProvenance: linkKey,
		Mappings:           legacyMirrorAdoptionMappings(),
		Profile:            legacyMirrorAdoptionProfile(),
	})
	if err != nil {
		t.Fatalf("AdoptConnectorPushLegacyMirrorEvidence: %v", err)
	}
	if adopted {
		t.Fatal("adopted = true, want false when concurrent blocking attempt already protects the link key")
	}
	if link != nil {
		t.Fatalf("link = %#v, want nil when concurrent attempt wins occupancy", link)
	}
	if attempt, _ := store.FindBlockingAttempt(context.Background(), linkKey); attempt == nil {
		t.Fatal("blocking attempt missing after simulated race")
	}
}

func TestAdoptConnectorPushLegacyMirrorEvidenceNoopsWhenFinalizedLinkWinsConcurrentRace(t *testing.T) {
	store := &connectorPushLegacyMirrorConflictStore{
		memoryConnectorPushDurableStore: newMemoryConnectorPushDurableStore(),
		conflictKind:                    "link",
	}
	contract := legacyMirrorAdoptionContract()
	identity := connectorPushDurableIdentityFromContract(&contract, "003-test")
	linkKey, err := identity.LinkKey()
	if err != nil {
		t.Fatalf("LinkKey: %v", err)
	}
	store.linkKey = linkKey
	store.identity = identity

	link, adopted, err := AdoptConnectorPushLegacyMirrorEvidence(context.Background(), store, ConnectorPushLegacyMirrorEvidence{
		Contract:           contract,
		SourceRecordID:     "003-test",
		RemoteID:           "remote-123",
		RemoteIDProvenance: linkKey,
		Mappings:           legacyMirrorAdoptionMappings(),
		Profile:            legacyMirrorAdoptionProfile(),
	})
	if err != nil {
		t.Fatalf("AdoptConnectorPushLegacyMirrorEvidence: %v", err)
	}
	if adopted {
		t.Fatal("adopted = true, want false when concurrent finalized link already protects the link key")
	}
	if link == nil || link.Status != ConnectorPushLinkStatusFinalized || link.RemoteID != "remote-123" {
		t.Fatalf("link = %#v, want existing finalized link after simulated race", link)
	}
}

func legacyMirrorAdoptionContract() connector.ConnectorPushDurableMetadataContract {
	return connector.ConnectorPushDurableMetadataContract{
		ConnectorOwnerKey:       "crm-flow-pack",
		ActionSlotID:            "contact-current",
		PreviousActionSlotIDs:   []string{"contact-v1"},
		ActionType:              constants.ActionTypePushToConnector,
		FlowDeveloperName:       "Contact_To_Remote",
		FlowActionName:          "PushContact",
		SourceObject:            "Contact",
		DestinationSlotID:       "remote-contact",
		ProfileOwnerPack:        "connector-pack",
		ProfileName:             "default",
		TargetObject:            "remoteContact",
		CreatePathTemplate:      "/objects",
		UpdatePathTemplate:      "/objects/{remoteId}",
		ResponseIDPath:          "id",
		RemoteIDField:           "RemoteKey__c",
		RemoteIDProvenanceField: "RemoteKeyProvenance__c",
	}
}

func legacyMirrorAdoptionMappings() []connector.ConnectorFieldMappingInput {
	return []connector.ConnectorFieldMappingInput{{
		Source:       "crm-flow-pack",
		SourceObject: "Contact",
		TargetObject: "remoteContact",
		ActionType:   constants.ActionTypePushToConnector,
		SourceField:  "Email",
		TargetField:  "email",
		Direction:    connector.DirectionOutbound,
	}}
}

func legacyMirrorAdoptionProfile() *ConnectorPushProfile {
	return &ConnectorPushProfile{
		OwnerPack:            "connector-pack",
		ProfileName:          "default",
		CreatedPackInstallID: "install-1",
		CredentialRef:        "cred",
		EndpointClass:        "default",
		RetryPolicy: ConnectorPushRetryPolicy{
			MaxAttempts: 1,
		},
	}
}

func TestConnectorPushLegacyMirrorActionSlotIDsDeduplicates(t *testing.T) {
	contract := legacyMirrorAdoptionContract()
	contract.PreviousActionSlotIDs = []string{"contact-v1", "CONTACT-V1", "", "contact-v2"}
	got := strings.Join(connectorPushLegacyMirrorActionSlotIDs(contract), ",")
	if got != "contact-current,contact-v1,contact-v2" {
		t.Fatalf("slot ids = %q, want current plus unique previous ids", got)
	}
}

func (s *memoryConnectorPushDurableStore) AdoptSuspendedDriftLink(_ context.Context, adoption ConnectorPushLinkAdoption) (*ConnectorPushRemoteLink, error) {
	adoption, err := normalizeConnectorPushLinkAdoption(adoption)
	if err != nil {
		return nil, err
	}
	if existing, _ := s.FindBlockingAttempt(context.Background(), adoption.LinkKey); existing != nil {
		return nil, ErrConnectorPushDurableConflict
	}
	if existing := s.links[adoption.LinkKey]; existing != nil {
		if strings.TrimSpace(existing.Status) == ConnectorPushLinkStatusSuspendedDrift && strings.TrimSpace(existing.RemoteID) == adoption.RemoteID {
			return existing, nil
		}
		return existing, ErrConnectorPushDurableConflict
	}
	link := &ConnectorPushRemoteLink{
		ID:       "legacy-suspended-link",
		LinkKey:  adoption.LinkKey,
		Status:   ConnectorPushLinkStatusSuspendedDrift,
		RemoteID: adoption.RemoteID,
		Identity: adoption.Identity,
		Snapshot: adoption.Snapshot,
	}
	link.Snapshot.ProfileContractHash = ConnectorPushProfileContractHash(link.Snapshot)
	s.links[adoption.LinkKey] = link
	return link, nil
}

type connectorPushLegacyMirrorConflictStore struct {
	*memoryConnectorPushDurableStore
	conflictInjected bool
	conflictKind     string
	linkKey          string
	identity         ConnectorPushDurableIdentity
}

func (s *connectorPushLegacyMirrorConflictStore) FindLink(_ context.Context, linkKey string) (*ConnectorPushRemoteLink, error) {
	if !s.conflictInjected && linkKey == s.linkKey {
		return nil, nil
	}
	return s.memoryConnectorPushDurableStore.FindLink(context.Background(), linkKey)
}

func (s *connectorPushLegacyMirrorConflictStore) FindBlockingAttempt(_ context.Context, linkKey string) (*ConnectorPushAttempt, error) {
	if !s.conflictInjected && linkKey == s.linkKey {
		return nil, nil
	}
	return s.memoryConnectorPushDurableStore.FindBlockingAttempt(context.Background(), linkKey)
}

func (s *connectorPushLegacyMirrorConflictStore) AdoptSuspendedDriftLink(_ context.Context, adoption ConnectorPushLinkAdoption) (*ConnectorPushRemoteLink, error) {
	s.conflictInjected = true
	switch s.conflictKind {
	case "attempt":
		s.attempts["race-attempt"] = &ConnectorPushAttempt{
			ID:       "race-attempt",
			LinkKey:  adoption.LinkKey,
			Status:   ConnectorPushAttemptStatusPending,
			Identity: s.identity,
			Snapshot: ConnectorPushDurableSnapshot{
				RequestMethod: http.MethodPost,
			},
		}
	case "link":
		s.links[adoption.LinkKey] = &ConnectorPushRemoteLink{
			ID:       "race-link",
			LinkKey:  adoption.LinkKey,
			Status:   ConnectorPushLinkStatusFinalized,
			RemoteID: adoption.RemoteID,
			Identity: adoption.Identity,
			Snapshot: adoption.Snapshot,
		}
	default:
		return nil, ErrConnectorPushDurableConflict
	}
	return nil, ErrConnectorPushDurableConflict
}
