package approvalv1

import (
	"context"
	"errors"
	"strings"
	"testing"
	"time"

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

func deadlineContext(t *testing.T) context.Context {
	t.Helper()
	ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second))
	t.Cleanup(cancel)
	return ctx
}

func validExecuteRequest(operation Operation) ExecuteRequest {
	request := ExecuteRequest{Operation: operation, IdempotencyKey: "approval-request-1"}
	switch operation {
	case OperationSubmit:
		request.ObjectID = "Account"
		request.TargetRecordID = "record-1"
	case OperationApprove, OperationReject, OperationRecall:
		request.WorkItemID = "work-item-1"
	case OperationReassign:
		request.WorkItemID = "work-item-1"
		request.NextActorID = "user-2"
	}
	return request
}

func validExecuteResponse(operation Operation) ExecuteResponse {
	state := InstanceStatePending
	switch operation {
	case OperationReject:
		state = InstanceStateRejected
	case OperationRecall:
		state = InstanceStateWithdrawn
	}
	pending := []WorkItemAssignment{{WorkItemID: "work-item-2", ActorID: "user-2"}}
	if state != InstanceStatePending {
		pending = []WorkItemAssignment{}
	}
	response := ExecuteResponse{
		ExecutionID:       "approval-execution-1",
		InstanceID:        "instance-1",
		TargetRecordID:    "record-1",
		ActingPrincipalID: "user-1",
		State:             state,
		PendingWorkItems:  pending,
		ActionFailures:    []ActionFailure{},
		Receipts:          Receipts{ProtocolVersion: "1.0", CapabilityVersion: ExecuteCapabilityName},
	}
	if operation != OperationSubmit {
		response.SourceWorkItemID = "work-item-1"
	}
	return response
}

func TestExecuteRequestOperationShapes(t *testing.T) {
	ctx := deadlineContext(t)
	for _, operation := range []Operation{OperationSubmit, OperationApprove, OperationReject, OperationRecall, OperationReassign} {
		request := validExecuteRequest(operation)
		if err := ValidateExecuteRequest(ctx, request); err != nil {
			t.Fatalf("ValidateExecuteRequest(%s) error = %v", operation, err)
		}
		response := validExecuteResponse(operation)
		if err := ValidateExecuteResponse(request, response); err != nil {
			t.Fatalf("ValidateExecuteResponse(%s) error = %v", operation, err)
		}
	}
}

func TestSubmitObjectHintIsOptionalButValidatedWhenPresent(t *testing.T) {
	ctx := deadlineContext(t)
	request := validExecuteRequest(OperationSubmit)
	request.ObjectID = ""
	if err := ValidateExecuteRequest(ctx, request); err != nil {
		t.Fatalf("objectless Submit was rejected: %v", err)
	}
	response := validExecuteResponse(OperationSubmit)
	if err := ValidateExecuteResponse(request, response); err != nil {
		t.Fatalf("objectless Submit response was rejected: %v", err)
	}

	request.ObjectID = " invalid-object"
	if ValidateExecuteRequest(ctx, request) == nil {
		t.Fatal("invalid non-empty object hint was accepted")
	}
}

func TestExecuteRequestRejectsCrossOperationFields(t *testing.T) {
	ctx := deadlineContext(t)
	tests := []struct {
		name string
		edit func(*ExecuteRequest)
	}{
		{name: "submit work item", edit: func(r *ExecuteRequest) { r.WorkItemID = "work-1" }},
		{name: "approve target", edit: func(r *ExecuteRequest) { r.TargetRecordID = "record-1" }},
		{name: "reject skip criteria", edit: func(r *ExecuteRequest) { r.SkipEntryCriteria = true }},
		{name: "recall next actor", edit: func(r *ExecuteRequest) { r.NextActorID = "user-2" }},
		{name: "reassign comments", edit: func(r *ExecuteRequest) { r.Comments = "not supported" }},
	}
	operations := []Operation{OperationSubmit, OperationApprove, OperationReject, OperationRecall, OperationReassign}
	for index, test := range tests {
		request := validExecuteRequest(operations[index])
		test.edit(&request)
		if ValidateExecuteRequest(ctx, request) == nil {
			t.Fatalf("%s was accepted", test.name)
		}
	}
}

func TestExecuteValidationRequiresDeadlineAndStableReplayIdentity(t *testing.T) {
	request := validExecuteRequest(OperationSubmit)
	assertFailure(t, ValidateExecuteRequest(context.Background(), request), common.FailureInvalidRequest, common.InvalidComponentDeadline)
	request.IdempotencyKey = ""
	assertFailure(t, ValidateExecuteRequest(deadlineContext(t), request), common.FailureInvalidRequest, common.InvalidComponentCommandData)

	request = validExecuteRequest(OperationReject)
	response := validExecuteResponse(OperationReject)
	response.ExecutionID = ""
	assertFailure(t, ValidateExecuteResponse(request, response), common.FailureInternal, common.InvalidComponentUnspecified)
	response = validExecuteResponse(OperationReject)
	response.State = InstanceStatePending
	assertFailure(t, ValidateExecuteResponse(request, response), common.FailureInternal, common.InvalidComponentUnspecified)
}

func TestExecuteResponseRejectsDuplicateOrUnsafeDetails(t *testing.T) {
	request := validExecuteRequest(OperationApprove)
	response := validExecuteResponse(OperationApprove)
	response.PendingWorkItems = []WorkItemAssignment{
		{WorkItemID: "work-1", ActorID: "user-1"},
		{WorkItemID: "work-2", ActorID: "user-1"},
	}
	if ValidateExecuteResponse(request, response) == nil {
		t.Fatal("duplicate pending actors were accepted")
	}
	response = validExecuteResponse(OperationApprove)
	response.ActingPrincipalID = " acting-user"
	assertFailure(t, ValidateExecuteResponse(request, response), common.FailureInternal, common.InvalidComponentUnspecified)
	response = validExecuteResponse(OperationApprove)
	response.ActionFailures = []ActionFailure{{ActionRef: "action-1", Kind: ActionFailureFlow, Reason: ActionFailureReason("database detail")}}
	if ValidateExecuteResponse(request, response) == nil {
		t.Fatal("open action failure detail was accepted")
	}
	response.ActionFailures = []ActionFailure{{ActionRef: "action-1", Kind: ActionFailureFlow, Reason: ActionFailureExecutionFailed}}
	if err := ValidateExecuteResponse(request, response); err != nil {
		t.Fatalf("closed action failure reason was rejected: %v", err)
	}
	response = validExecuteResponse(OperationApprove)
	response.Receipts.CapabilityVersion = ReadCapabilityName
	if ValidateExecuteResponse(request, response) == nil {
		t.Fatal("read-capability receipts were accepted for an execution")
	}
	response = validExecuteResponse(OperationApprove)
	response.PendingWorkItems = nil
	if ValidateExecuteResponse(request, response) == nil {
		t.Fatal("pending result without a work-item assignment was accepted")
	}
}

func TestExecuteResponseIsCorrelatedToTheOperationTarget(t *testing.T) {
	submit := validExecuteRequest(OperationSubmit)
	response := validExecuteResponse(OperationSubmit)
	response.TargetRecordID = "record-2"
	if ValidateExecuteResponse(submit, response) == nil {
		t.Fatal("submit response for another target record was accepted")
	}

	approve := validExecuteRequest(OperationApprove)
	response = validExecuteResponse(OperationApprove)
	response.SourceWorkItemID = "work-item-2"
	if ValidateExecuteResponse(approve, response) == nil {
		t.Fatal("approval response for another source work item was accepted")
	}
}

func TestSubmitRejectionIsAClosedNonMutatingResponseUnion(t *testing.T) {
	request := validExecuteRequest(OperationSubmit)
	for _, reason := range []SubmitRejectionReason{
		SubmitRejectionNoActiveProcess,
		SubmitRejectionEntryCriteriaMiss,
	} {
		response := ExecuteResponse{
			TargetRecordID:  request.TargetRecordID,
			SubmitRejection: &SubmitRejection{Reason: reason},
			Receipts:        Receipts{ProtocolVersion: "1.0", CapabilityVersion: ExecuteCapabilityName},
		}
		if err := ValidateExecuteResponse(request, response); err != nil {
			t.Fatalf("ValidateExecuteResponse(%s) error = %v", reason, err)
		}
	}

	rejected := ExecuteResponse{
		TargetRecordID:  request.TargetRecordID,
		SubmitRejection: &SubmitRejection{Reason: SubmitRejectionNoActiveProcess},
		Receipts:        Receipts{ProtocolVersion: "1.0", CapabilityVersion: ExecuteCapabilityName},
	}
	tests := []struct {
		name string
		edit func(*ExecuteResponse)
	}{
		{name: "unknown reason", edit: func(r *ExecuteResponse) { r.SubmitRejection.Reason = "UNKNOWN" }},
		{name: "mixed execution", edit: func(r *ExecuteResponse) { r.ExecutionID = "execution-1" }},
		{name: "mixed instance", edit: func(r *ExecuteResponse) { r.InstanceID = "instance-1" }},
		{name: "mixed state", edit: func(r *ExecuteResponse) { r.State = InstanceStatePending }},
		{name: "wrong target", edit: func(r *ExecuteResponse) { r.TargetRecordID = "record-2" }},
	}
	for _, test := range tests {
		copy := rejected
		value := *rejected.SubmitRejection
		copy.SubmitRejection = &value
		test.edit(&copy)
		if ValidateExecuteResponse(request, copy) == nil {
			t.Fatalf("%s response was accepted", test.name)
		}
	}

	nonSubmit := validExecuteRequest(OperationApprove)
	if ValidateExecuteResponse(nonSubmit, rejected) == nil {
		t.Fatal("submit rejection was accepted for Approve")
	}

	skipped := validExecuteRequest(OperationSubmit)
	skipped.SkipEntryCriteria = true
	rejected.SubmitRejection.Reason = SubmitRejectionEntryCriteriaMiss
	if ValidateExecuteResponse(skipped, rejected) == nil {
		t.Fatal("entry-criteria rejection was accepted after criteria were explicitly skipped")
	}
}

func TestSubmitValidationRejectionCarriesOnlyBoundedRuleIssues(t *testing.T) {
	request := validExecuteRequest(OperationSubmit)
	response := ExecuteResponse{
		TargetRecordID: request.TargetRecordID,
		SubmitRejection: &SubmitRejection{
			Reason: SubmitRejectionRecordValidationFailed,
			ValidationIssues: []ValidationIssue{{
				RuleName: "Quote_Submit_Requires_Complete_Lines",
				Message:  "Complete the quote lines, save, then submit.",
			}},
		},
		Receipts: Receipts{ProtocolVersion: "1.0", CapabilityVersion: ExecuteCapabilityName},
	}
	if err := ValidateExecuteResponse(request, response); err != nil {
		t.Fatalf("ValidateExecuteResponse(validation rejection) error = %v", err)
	}

	tests := []struct {
		name string
		edit func(*SubmitRejection)
	}{
		{name: "missing issues", edit: func(r *SubmitRejection) { r.ValidationIssues = nil }},
		{name: "unsafe rule name", edit: func(r *SubmitRejection) { r.ValidationIssues[0].RuleName = " rule" }},
		{name: "unsafe field", edit: func(r *SubmitRejection) { r.ValidationIssues[0].Field = "Quote.Status" }},
		{name: "missing message", edit: func(r *SubmitRejection) { r.ValidationIssues[0].Message = "" }},
	}
	for _, test := range tests {
		copy := response
		rejection := *response.SubmitRejection
		rejection.ValidationIssues = append([]ValidationIssue(nil), response.SubmitRejection.ValidationIssues...)
		copy.SubmitRejection = &rejection
		test.edit(copy.SubmitRejection)
		if ValidateExecuteResponse(request, copy) == nil {
			t.Fatalf("%s validation rejection was accepted", test.name)
		}
	}

	nonValidation := response
	rejection := *response.SubmitRejection
	rejection.Reason = SubmitRejectionNoActiveProcess
	nonValidation.SubmitRejection = &rejection
	if ValidateExecuteResponse(request, nonValidation) == nil {
		t.Fatal("non-validation rejection carrying rule issues was accepted")
	}
}

func TestExecuteResponseRejectsMalformedRequestShape(t *testing.T) {
	request := validExecuteRequest(OperationApprove)
	request.WorkItemID = ""
	response := validExecuteResponse(OperationApprove)
	response.SourceWorkItemID = ""
	if ValidateExecuteResponse(request, response) == nil {
		t.Fatal("response was accepted against an approve request without a work item")
	}
}

func TestRejectMayRouteBackToPending(t *testing.T) {
	request := validExecuteRequest(OperationReject)
	response := validExecuteResponse(OperationReject)
	response.State = InstanceStatePending
	response.PendingWorkItems = []WorkItemAssignment{{WorkItemID: "work-item-next", ActorID: "user-previous"}}
	if err := ValidateExecuteResponse(request, response); err != nil {
		t.Fatalf("ValidateExecuteResponse(back-to-previous) error = %v", err)
	}
}

func TestReadRequestAndClosedResponseUnion(t *testing.T) {
	ctx := deadlineContext(t)
	pendingRequest := ReadRequest{Kind: ReadPendingForCaller}
	if err := ValidateReadRequest(ctx, pendingRequest); err != nil {
		t.Fatalf("ValidateReadRequest(pending) error = %v", err)
	}
	pendingResponse := ReadResponse{
		Pending: []PendingItem{{
			WorkItemID: "work-1", InstanceID: "instance-1", DefinitionID: "definition-1",
			TargetRecordID: "record-1", ObjectID: "Account", SubmittedByID: "user-1",
			SubmittedAt: "2026-08-02T12:00:00Z", ActorID: "user-2",
		}},
		Receipts: Receipts{ProtocolVersion: "1.0", CapabilityVersion: ReadCapabilityName},
	}
	if err := ValidateReadResponse(pendingRequest, pendingResponse); err != nil {
		t.Fatalf("ValidateReadResponse(pending) error = %v", err)
	}
	pendingResponse.Steps = []StepDisplay{{StepID: "step-1", StepOrder: 1}}
	if ValidateReadResponse(pendingRequest, pendingResponse) == nil {
		t.Fatal("mixed read-result union was accepted")
	}

	latestRequest := ReadRequest{Kind: ReadLatestForRecord, TargetRecordID: "record-1"}
	latestResponse := ReadResponse{
		Latest: &InstanceSummary{
			InstanceID: "instance-1", DefinitionID: "definition-1", TargetRecordID: "record-1",
			State: InstanceStatePending, SubmittedByID: "user-1", CreatedAt: "2026-08-02T12:00:00Z",
			PendingWorkItems: []PendingWorkItem{{WorkItemID: "work-1", ActorID: "user-2"}},
		},
		Receipts: Receipts{ProtocolVersion: "1.0", CapabilityVersion: ReadCapabilityName},
	}
	if err := ValidateReadResponse(latestRequest, latestResponse); err != nil {
		t.Fatalf("ValidateReadResponse(latest) error = %v", err)
	}
	latestResponse.Latest.TargetRecordID = "record-2"
	if ValidateReadResponse(latestRequest, latestResponse) == nil {
		t.Fatal("latest instance for another record was accepted")
	}
	latestResponse.Latest.TargetRecordID = "record-1"
	latestResponse.Latest.PendingWorkItems = nil
	if ValidateReadResponse(latestRequest, latestResponse) == nil {
		t.Fatal("pending instance without a pending work item was accepted")
	}

	stepsRequest := ReadRequest{Kind: ReadStepDisplayForRecord, TargetRecordID: "record-1"}
	stepsResponse := ReadResponse{
		Steps: []StepDisplay{
			{StepID: "step-1", StepOrder: 1},
			{StepID: "step-2", StepOrder: 2, HasCriteria: true, NoMatchBehavior: StepNoMatchNext},
		},
		Receipts: Receipts{ProtocolVersion: "1.0", CapabilityVersion: ReadCapabilityName},
	}
	if err := ValidateReadResponse(stepsRequest, stepsResponse); err != nil {
		t.Fatalf("ValidateReadResponse(steps) error = %v", err)
	}
	stepsResponse.Steps[1].StepOrder = 1
	if ValidateReadResponse(stepsRequest, stepsResponse) == nil {
		t.Fatal("non-deterministic step order was accepted")
	}
}

func TestReadResponseRejectsNonCanonicalTimestamp(t *testing.T) {
	request := ReadRequest{Kind: ReadLatestForRecord, TargetRecordID: "record-1"}
	response := ReadResponse{
		Latest: &InstanceSummary{
			InstanceID: "instance-1", DefinitionID: "definition-1", TargetRecordID: "record-1",
			State: InstanceStateApproved, SubmittedByID: "user-1",
			CreatedAt: "2026-08-02T12:00:00+00:00", CompletedAt: "2026-08-02T12:01:00Z",
		},
		Receipts: Receipts{ProtocolVersion: "1.0", CapabilityVersion: ReadCapabilityName},
	}
	if ValidateReadResponse(request, response) == nil {
		t.Fatal("non-canonical timestamp was accepted")
	}
}

func TestReadResponseRejectsMalformedRequestShape(t *testing.T) {
	receipts := Receipts{ProtocolVersion: "1.0", CapabilityVersion: ReadCapabilityName}
	if ValidateReadResponse(ReadRequest{Kind: ReadPendingForCaller, TargetRecordID: "record-1"}, ReadResponse{Receipts: receipts}) == nil {
		t.Fatal("pending response was accepted against a request carrying a target record")
	}
	if ValidateReadResponse(ReadRequest{Kind: ReadLatestForRecord}, ReadResponse{Receipts: receipts}) == nil {
		t.Fatal("latest response was accepted against a request without a target record")
	}
}

func TestApprovalBoundsAreEnforced(t *testing.T) {
	request := validExecuteRequest(OperationSubmit)
	request.Comments = strings.Repeat("x", MaxCommentsBytes+1)
	assertFailure(t, ValidateExecuteRequest(deadlineContext(t), request), common.FailureInvalidRequest, common.InvalidComponentCommandData)

	readRequest := ReadRequest{Kind: ReadPendingForCaller}
	response := ReadResponse{
		Pending:  make([]PendingItem, MaxPendingItems+1),
		Receipts: Receipts{ProtocolVersion: "1.0", CapabilityVersion: ReadCapabilityName},
	}
	assertFailure(t, ValidateReadResponse(readRequest, response), common.FailureInternal, common.InvalidComponentUnspecified)
}

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