package outboundintentv1

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"
	"testing"
	"time"

	catalog "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	common "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
)

func TestIntentAndAdmissionValidation(t *testing.T) {
	payload := []byte(`{"fields":{"name":"example"}}`)
	intent := testIntent(payload, 41)
	digest := intent.PayloadDigest
	if err := ValidateIntent(intent); err != nil {
		t.Fatalf("ValidateIntent: %v", err)
	}
	if err := ValidateLogicalEffectIdentity(logicalEffectTestScope(), intent); err != nil {
		t.Fatalf("ValidateLogicalEffectIdentity: %v", err)
	}
	receipts := Receipts{ProtocolVersion: "1.0", CatalogRevision: 41, SchemaGeneration: 7, EngineGeneration: 9, SecurityEpoch: 13, CapabilityVersion: CapabilityName}
	generationRequest := GenerationRequest{AfterGeneration: 6}
	if err := ValidateGenerationRequest(ctxWithDeadline(t), generationRequest); err != nil {
		t.Fatalf("ValidateGenerationRequest: %v", err)
	}
	if err := ValidateGenerationResponse(generationRequest, GenerationResponse{Generation: 7, Receipts: receipts}); err != nil {
		t.Fatalf("ValidateGenerationResponse: %v", err)
	}
	if err := ValidateListResponseFor(ListRequest{Limit: 1}, ListResponse{Intents: []Intent{intent}, Receipts: receipts}); err != nil {
		t.Fatalf("ValidateListResponseFor: %v", err)
	}
	request := AcknowledgeRequest{IntentID: intent.IntentID, PayloadDigest: digest, CatalogRevision: 41, Outcome: AdmissionAccepted, IntegrationCommandID: "command-1"}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	if err := ValidateAcknowledgeRequest(ctx, request); err != nil {
		t.Fatalf("ValidateAcknowledgeRequest: %v", err)
	}
	if err := ValidateAcknowledgeRequestForIntent(intent, request); err != nil {
		t.Fatalf("ValidateAcknowledgeRequestForIntent: %v", err)
	}
	if err := ValidateAcknowledgeResponse(request, AcknowledgeResponse{IntentID: intent.IntentID, Outcome: AdmissionAccepted, Receipts: receipts}); err != nil {
		t.Fatalf("ValidateAcknowledgeResponse: %v", err)
	}
	commit := CommitResultRequest{
		IntentID: intent.IntentID, PayloadDigest: intent.PayloadDigest,
		CatalogRevision:      intent.CatalogRevision,
		IntegrationCommandID: "command-1", RemoteID: "remote-1",
	}
	if err := ValidateCommitResultRequest(ctx, commit); err != nil {
		t.Fatalf("ValidateCommitResultRequest: %v", err)
	}
	if err := ValidateCommitResultRequestForIntent(intent, commit); err != nil {
		t.Fatalf("ValidateCommitResultRequestForIntent: %v", err)
	}
	if err := ValidateCommitResultResponse(commit, CommitResultResponse{IntentID: intent.IntentID, Receipts: receipts}); err != nil {
		t.Fatalf("ValidateCommitResultResponse: %v", err)
	}
}

func TestGenerationResponseRejectsRegression(t *testing.T) {
	err := ValidateGenerationResponse(
		GenerationRequest{AfterGeneration: 8},
		GenerationResponse{Generation: 7, Receipts: testReceipts(41)},
	)
	assertFailure(t, err, common.FailureInternal, "", "")
}

func ctxWithDeadline(t *testing.T) context.Context {
	t.Helper()
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	t.Cleanup(cancel)
	return ctx
}

func TestRequestsRequireDeadlineAndTypedFailures(t *testing.T) {
	err := ValidateListRequest(context.Background(), ListRequest{Limit: 1})
	assertFailure(t, err, common.FailureInvalidRequest, common.InvalidComponentDeadline, "")

	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	err = ValidateListRequest(ctx, ListRequest{Limit: 1})
	assertFailure(t, err, common.FailureCanceled, "", "")

	deadlineCtx, deadlineCancel := context.WithTimeout(context.Background(), time.Second)
	defer deadlineCancel()
	err = ValidateListRequest(deadlineCtx, ListRequest{})
	assertFailure(t, err, common.FailureInvalidRequest, common.InvalidComponentCommandData, "")
}

func TestListResponseRejectsStaleEmptyReceipt(t *testing.T) {
	minimum := uint64(42)
	err := ValidateListResponseFor(ListRequest{Limit: 1, MinCatalogRevision: &minimum}, ListResponse{Receipts: testReceipts(41)})
	assertFailure(t, err, common.FailureInternal, "", "")
}

func TestMailIntentRequiresNegotiatedOperationSetV2(t *testing.T) {
	payload := []byte(`{"version":1,"authorizedActorId":"user-1","toAddresses":["recipient@example.com"],"subject":"Hello","textBody":"Body"}`)
	intent := testIntent(payload, 41)
	intent.Operation = OperationSendMail
	intent.PayloadFormatVersion = MailPayloadFormatV1
	response := ListResponse{Intents: []Intent{intent}, Receipts: testReceipts(41)}

	for name, request := range map[string]ListRequest{
		"legacy omitted version": {Limit: 1},
		"explicit v1":            {Limit: 1, MaxOperationSetVersion: OperationSetVersionV1},
	} {
		t.Run(name, func(t *testing.T) {
			assertFailure(t, ValidateListResponseFor(request, response), common.FailureInternal, "", "")
		})
	}
	if err := ValidateListResponseFor(ListRequest{Limit: 1, MaxOperationSetVersion: OperationSetVersionV2}, response); err != nil {
		t.Fatalf("V2 mail response: %v", err)
	}
	assertFailure(t, ValidateListRequest(ctxWithDeadline(t), ListRequest{
		Limit: 1, MaxOperationSetVersion: CurrentOperationSetVersion + 1,
	}), common.FailureInvalidRequest, common.InvalidComponentCommandData, "")
}

func TestIntentAcceptsLegacyIdentityAndValidatesLogicalEffectCoordinates(t *testing.T) {
	legacy := testIntent([]byte(`{}`), 41)
	legacy.IntentID = "intent-1"
	legacy.IdempotencyKey = "idem-1"
	legacy.EffectRootID = ""
	legacy.EffectGenerationID = ""
	if err := ValidateIntent(legacy); err != nil {
		t.Fatalf("legacy identity: %v", err)
	}
	assertFailure(t, ValidateLogicalEffectIdentity(logicalEffectTestScope(), legacy), common.FailureInvalidRequest, common.InvalidComponentCommandData, "")

	for name, mutate := range map[string]func(*Intent){
		"missing effect root":       func(intent *Intent) { intent.EffectRootID = "" },
		"missing effect generation": func(intent *Intent) { intent.EffectGenerationID = "" },
		"noncanonical intent key":   func(intent *Intent) { intent.IntentID = "intent-1" },
		"uppercase intent key": func(intent *Intent) {
			intent.IntentID = strings.ToUpper(intent.IntentID)
		},
		"nonhex intent key": func(intent *Intent) {
			intent.IntentID = logicalEffectIntentIDPrefix + strings.Repeat("z", sha256.Size*2)
		},
		"short intent key": func(intent *Intent) {
			intent.IntentID = logicalEffectIntentIDPrefix + strings.Repeat("a", sha256.Size*2-1)
		},
		"noncanonical idempotency key": func(intent *Intent) {
			intent.IdempotencyKey = "idem-1"
		},
		"uppercase idempotency key": func(intent *Intent) {
			intent.IdempotencyKey = strings.ToUpper(intent.IdempotencyKey)
		},
		"nonhex idempotency key": func(intent *Intent) {
			intent.IdempotencyKey = logicalEffectIdempotencyPrefix + strings.Repeat("z", sha256.Size*2)
		},
	} {
		t.Run(name, func(t *testing.T) {
			intent := testIntent([]byte(`{}`), 41)
			mutate(&intent)
			assertFailure(t, ValidateIntent(intent), common.FailureInvalidRequest, common.InvalidComponentCommandData, "")
		})
	}

	valid := testIntent([]byte(`{}`), 41)
	if err := ValidateLogicalEffectIdentity(logicalEffectTestScope(), valid); err != nil {
		t.Fatalf("valid scoped identity: %v", err)
	}
	for name, mutate := range map[string]func(*Intent, *LogicalEffectScope){
		"forged canonical-looking intent key": func(intent *Intent, _ *LogicalEffectScope) {
			intent.IntentID = logicalEffectIntentIDPrefix + strings.Repeat("a", sha256.Size*2)
		},
		"forged canonical-looking idempotency key": func(intent *Intent, _ *LogicalEffectScope) {
			intent.IdempotencyKey = logicalEffectIdempotencyPrefix + strings.Repeat("b", sha256.Size*2)
		},
		"different root": func(intent *Intent, _ *LogicalEffectScope) {
			intent.EffectRootID = "effect-root-2"
		},
		"different action slot": func(intent *Intent, _ *LogicalEffectScope) {
			intent.ActionSlotID = "other-slot"
		},
		"different generation": func(intent *Intent, _ *LogicalEffectScope) {
			intent.EffectGenerationID = "generation-2"
		},
		"different trusted org": func(_ *Intent, scope *LogicalEffectScope) {
			scope.OrgID = "org-2"
		},
		"different trusted installation": func(_ *Intent, scope *LogicalEffectScope) {
			scope.AppInstallationID = "installation-2"
		},
		"different trusted workload": func(_ *Intent, scope *LogicalEffectScope) {
			scope.AuthorizedWorkloadID = "integration-2"
		},
	} {
		t.Run(name, func(t *testing.T) {
			intent := valid
			scope := logicalEffectTestScope()
			mutate(&intent, &scope)
			assertFailure(t, ValidateLogicalEffectIdentity(scope, intent), common.FailureInvalidRequest, common.InvalidComponentCommandData, "")
		})
	}
}

func TestRepeatedLogicalEffectsRequireDistinctGenerations(t *testing.T) {
	scope := logicalEffectTestScope()
	firstIntentID, firstKey, err := LogicalEffectIdentity(scope, "root", "slot", "occurrence-1")
	if err != nil {
		t.Fatal(err)
	}
	retryIntentID, retryKey, err := LogicalEffectIdentity(scope, "root", "slot", "occurrence-1")
	if err != nil {
		t.Fatal(err)
	}
	secondIntentID, secondKey, err := LogicalEffectIdentity(scope, "root", "slot", "occurrence-2")
	if err != nil {
		t.Fatal(err)
	}
	if retryIntentID != firstIntentID || retryKey != firstKey {
		t.Fatal("a retry of one logical effect did not converge")
	}
	if secondIntentID == firstIntentID || secondKey == firstKey {
		t.Fatal("repeated logical effects aliased across generations")
	}
}

func TestListResponseRejectsAggregatePayloadLimit(t *testing.T) {
	intents := make([]Intent, 5)
	for i := range intents {
		intents[i] = testIntent(make([]byte, MaxPayloadBytes), uint64(41))
		bindTestLogicalEffect(&intents[i], i+1)
	}
	err := ValidateListResponseFor(ListRequest{Limit: 5}, ListResponse{Intents: intents, Receipts: testReceipts(41)})
	assertFailure(t, err, common.FailureResourceExhausted, "", common.LimitSemanticResponse)
}

func TestAcknowledgeResponseRejectsMixedRevision(t *testing.T) {
	request := AcknowledgeRequest{IntentID: "intent-1", CatalogRevision: 41, Outcome: AdmissionAccepted}
	err := ValidateAcknowledgeResponse(request, AcknowledgeResponse{IntentID: request.IntentID, Outcome: request.Outcome, Receipts: testReceipts(42)})
	assertFailure(t, err, common.FailureInternal, "", "")
}

func testIntent(payload []byte, revision uint64) Intent {
	sum := sha256.Sum256(payload)
	intent := Intent{
		PayloadFormatVersion: 1,
		PayloadDigest:        "sha256:" + hex.EncodeToString(sum[:]), CatalogRevision: revision,
		Operation: OperationUpsert, SourceObject: "Example", SourceRecordID: "row-1",
		ConnectorOwnerKey: "connector-a", FlowDeveloperName: "Outbound_Flow", FlowActionName: "Push_Action", ActionSlotID: "push-example", DestinationSlotID: "remote-example",
		Bindings:         []BindingReference{{Kind: catalog.BindingConnectorPushContract, OwnerPack: "connector-a", PackInstallationID: "install-1", PackPayloadDigest: "nexus-pack-sha256-v1:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", BusinessKey: "push/example", DefinitionVersion: 1}},
		CanonicalPayload: payload,
	}
	bindTestLogicalEffect(&intent, 1)
	return intent
}

func bindTestLogicalEffect(intent *Intent, discriminator int) {
	intent.EffectRootID = fmt.Sprintf("effect-root-%d", discriminator)
	intent.EffectGenerationID = fmt.Sprintf("generation-%d", discriminator)
	intentID, idempotencyKey, err := LogicalEffectIdentity(
		logicalEffectTestScope(), intent.EffectRootID, intent.ActionSlotID, intent.EffectGenerationID,
	)
	if err != nil {
		panic(err)
	}
	intent.IntentID = intentID
	intent.IdempotencyKey = idempotencyKey
}

func testReceipts(revision uint64) Receipts {
	return Receipts{ProtocolVersion: "1.0", CatalogRevision: revision, SchemaGeneration: 7, EngineGeneration: 9, SecurityEpoch: 13, CapabilityVersion: CapabilityName}
}

func assertFailure(t *testing.T, err error, category common.FailureCategory, component common.InvalidComponent, limit common.LimitKind) {
	t.Helper()
	var failure *Failure
	if !errors.As(err, &failure) {
		t.Fatalf("error = %T %v, want *Failure", err, err)
	}
	if failure.Category != category || failure.InvalidComponent != component || failure.LimitKind != limit {
		t.Fatalf("failure = %#v, want category=%q component=%q limit=%q", failure, category, component, limit)
	}
}

func TestIntentRejectsPayloadDigestMismatch(t *testing.T) {
	intent := testIntent([]byte(`{}`), 1)
	intent.PayloadDigest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
	if err := ValidateIntent(intent); err == nil {
		t.Fatal("ValidateIntent accepted mismatched payload digest")
	}
}

func TestBindingReferenceUsesCatalogPackDigestIdentity(t *testing.T) {
	binding := BindingReference{
		Kind: catalog.BindingConnectorPushContract, OwnerPack: "connector-a",
		PackInstallationID: "install-1",
		PackPayloadDigest:  "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
		BusinessKey:        "push/example", DefinitionVersion: 1,
	}
	assertFailure(t, ValidateBindingReference(binding), common.FailureInvalidRequest, common.InvalidComponentCommandData, "")
	binding.PackPayloadDigest = "nexus-pack-sha256-v1:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
	if err := ValidateBindingReference(binding); err != nil {
		t.Fatalf("ValidateBindingReference: %v", err)
	}
}

func TestIdentifiersRejectControlCharacters(t *testing.T) {
	intent := testIntent([]byte(`{}`), 1)
	intent.SourceRecordID = "row\x1f1"
	if err := ValidateIntent(intent); err == nil {
		t.Fatal("ValidateIntent accepted a control-bearing identifier")
	}
}

func TestAcknowledgeMustMatchLockedIntent(t *testing.T) {
	intent := testIntent([]byte(`{}`), 41)
	request := AcknowledgeRequest{
		IntentID: intent.IntentID, PayloadDigest: intent.PayloadDigest,
		CatalogRevision: intent.CatalogRevision, Outcome: AdmissionAccepted,
		IntegrationCommandID: "command-1",
	}
	request.PayloadDigest = "sha256:" + fmt.Sprintf("%064x", 1)
	assertFailure(t, ValidateAcknowledgeRequestForIntent(intent, request), common.FailureInvalidRequest, common.InvalidComponentCommandData, "")
}

func TestAcknowledgeLockGuardRejectsMalformedConditionalFields(t *testing.T) {
	intent := testIntent([]byte(`{}`), 41)
	request := AcknowledgeRequest{
		IntentID: intent.IntentID, PayloadDigest: intent.PayloadDigest,
		CatalogRevision: intent.CatalogRevision,
	}
	assertFailure(t, ValidateAcknowledgeRequestForIntent(intent, request), common.FailureInvalidRequest, common.InvalidComponentCommandData, "")

	request.Outcome = AdmissionAccepted
	assertFailure(t, ValidateAcknowledgeRequestForIntent(intent, request), common.FailureInvalidRequest, common.InvalidComponentCommandData, "")

	request.IntegrationCommandID = "command-1"
	request.RejectionCode = "not-allowed"
	assertFailure(t, ValidateAcknowledgeRequestForIntent(intent, request), common.FailureInvalidRequest, common.InvalidComponentCommandData, "")
}

func TestAcknowledgeMalformedIdentifiersRemainTypedInvalidRequests(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	request := AcknowledgeRequest{
		IntentID: "intent\x1f1", PayloadDigest: "sha256:" + fmt.Sprintf("%064x", 1),
		CatalogRevision: 41, Outcome: AdmissionAccepted, IntegrationCommandID: "command-1",
	}
	assertFailure(t, ValidateAcknowledgeRequest(ctx, request), common.FailureInvalidRequest, common.InvalidComponentCommandData, "")
}

func TestCommitResultIsExactIntentBoundAndTyped(t *testing.T) {
	intent := testIntent([]byte(`{}`), 41)
	request := CommitResultRequest{
		IntentID: intent.IntentID, PayloadDigest: intent.PayloadDigest,
		CatalogRevision:      intent.CatalogRevision,
		IntegrationCommandID: "command-1", RemoteID: "remote-1",
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	if err := ValidateCommitResultRequest(ctx, request); err != nil {
		t.Fatalf("ValidateCommitResultRequest: %v", err)
	}
	if err := ValidateCommitResultRequestForIntent(intent, request); err != nil {
		t.Fatalf("ValidateCommitResultRequestForIntent: %v", err)
	}

	mismatch := request
	mismatch.PayloadDigest = "sha256:" + fmt.Sprintf("%064x", 1)
	assertFailure(t, ValidateCommitResultRequestForIntent(intent, mismatch), common.FailureInvalidRequest, common.InvalidComponentCommandData, "")

	malformed := request
	malformed.RemoteID = "remote\x1f1"
	assertFailure(t, ValidateCommitResultRequest(ctx, malformed), common.FailureInvalidRequest, common.InvalidComponentCommandData, "")

	assertFailure(t, ValidateCommitResultResponse(request, CommitResultResponse{
		IntentID: request.IntentID, Receipts: testReceipts(42),
	}), common.FailureInternal, "", "")
}
