package mapping

import (
	"reflect"
	"testing"

	approvalsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/approvalv1"
	runtimev1 "github.com/pingcap-inc/nexuscrm-contract/wire-go/gen/runtime/v1"
)

func TestApprovalExecuteMappingsRoundTripEveryOperation(t *testing.T) {
	ctx, cancel := mappingTestContext(t)
	defer cancel()
	revision := uint64(17)
	for _, operation := range []approvalsemantic.Operation{
		approvalsemantic.OperationSubmit,
		approvalsemantic.OperationApprove,
		approvalsemantic.OperationReject,
		approvalsemantic.OperationRecall,
		approvalsemantic.OperationReassign,
	} {
		request := mappingTestApprovalExecuteRequest(operation, &revision)
		wireRequest, err := ToWireExecuteApprovalRequest(ctx, request)
		if err != nil {
			t.Fatalf("ToWireExecuteApprovalRequest(%s) error = %v", operation, err)
		}
		roundRequest, err := FromWireExecuteApprovalRequest(ctx, wireRequest)
		if err != nil || !reflect.DeepEqual(roundRequest, request) {
			t.Fatalf("execute request %s round trip = %#v, %v", operation, roundRequest, err)
		}

		response := mappingTestApprovalExecuteResponse(operation, revision)
		wireResponse, err := ToWireExecuteApprovalResponse(request, response)
		if err != nil {
			t.Fatalf("ToWireExecuteApprovalResponse(%s) error = %v", operation, err)
		}
		roundResponse, err := FromWireExecuteApprovalResponse(request, wireResponse)
		if err != nil || !reflect.DeepEqual(roundResponse, response) {
			t.Fatalf("execute response %s round trip = %#v, %v", operation, roundResponse, err)
		}
	}
}

func TestApprovalExecuteMappingAcceptsNMinusOneResponseWithoutActingPrincipal(t *testing.T) {
	revision := uint64(17)
	request := mappingTestApprovalExecuteRequest(approvalsemantic.OperationRecall, &revision)
	response := mappingTestApprovalExecuteResponse(approvalsemantic.OperationRecall, revision)
	wireResponse, err := ToWireExecuteApprovalResponse(request, response)
	if err != nil {
		t.Fatal(err)
	}

	// N-1 Runtime servers do not populate the additive acting-principal fact.
	wireResponse.ActingPrincipalId = ""
	roundResponse, err := FromWireExecuteApprovalResponse(request, wireResponse)
	if err != nil {
		t.Fatalf("FromWireExecuteApprovalResponse() error = %v", err)
	}
	if roundResponse.ActingPrincipalID != "" {
		t.Fatalf("ActingPrincipalID = %q, want empty N-1 value", roundResponse.ActingPrincipalID)
	}
}

func TestApprovalSubmitRejectionMappingsRoundTrip(t *testing.T) {
	revision := uint64(17)
	request := mappingTestApprovalExecuteRequest(approvalsemantic.OperationSubmit, &revision)
	for _, reason := range []approvalsemantic.SubmitRejectionReason{
		approvalsemantic.SubmitRejectionNoActiveProcess,
		approvalsemantic.SubmitRejectionEntryCriteriaMiss,
	} {
		response := approvalsemantic.ExecuteResponse{
			TargetRecordID:   request.TargetRecordID,
			PendingWorkItems: []approvalsemantic.WorkItemAssignment{},
			ActionFailures:   []approvalsemantic.ActionFailure{},
			SubmitRejection:  &approvalsemantic.SubmitRejection{Reason: reason},
			Receipts:         mappingTestReceipts(approvalsemantic.ExecuteCapabilityName, revision),
		}
		wireResponse, err := ToWireExecuteApprovalResponse(request, response)
		if err != nil {
			t.Fatalf("ToWireExecuteApprovalResponse(%s) error = %v", reason, err)
		}
		roundTrip, err := FromWireExecuteApprovalResponse(request, wireResponse)
		if err != nil || !reflect.DeepEqual(roundTrip, response) {
			t.Fatalf("submit rejection %s round trip = %#v, %v", reason, roundTrip, err)
		}
	}

	skipped := request
	skipped.SkipEntryCriteria = true
	rejected := approvalsemantic.ExecuteResponse{
		TargetRecordID:   skipped.TargetRecordID,
		PendingWorkItems: []approvalsemantic.WorkItemAssignment{},
		ActionFailures:   []approvalsemantic.ActionFailure{},
		SubmitRejection: &approvalsemantic.SubmitRejection{
			Reason: approvalsemantic.SubmitRejectionEntryCriteriaMiss,
		},
		Receipts: mappingTestReceipts(approvalsemantic.ExecuteCapabilityName, revision),
	}
	if _, err := ToWireExecuteApprovalResponse(skipped, rejected); err == nil {
		t.Fatal("entry-criteria rejection was mapped after criteria were explicitly skipped")
	}
}

func TestApprovalSubmitValidationRejectionMappingsRoundTrip(t *testing.T) {
	revision := uint64(17)
	request := mappingTestApprovalExecuteRequest(approvalsemantic.OperationSubmit, &revision)
	response := approvalsemantic.ExecuteResponse{
		TargetRecordID:   request.TargetRecordID,
		PendingWorkItems: []approvalsemantic.WorkItemAssignment{},
		ActionFailures:   []approvalsemantic.ActionFailure{},
		SubmitRejection: &approvalsemantic.SubmitRejection{
			Reason: approvalsemantic.SubmitRejectionRecordValidationFailed,
			ValidationIssues: []approvalsemantic.ValidationIssue{
				{RuleName: "Rule_One", Message: "First corrective message."},
				{RuleName: "Rule_Two", Field: "Status", Message: "Second corrective message."},
			},
		},
		Receipts: mappingTestReceipts(approvalsemantic.ExecuteCapabilityName, revision),
	}
	wireResponse, err := ToWireExecuteApprovalResponse(request, response)
	if err != nil {
		t.Fatalf("ToWireExecuteApprovalResponse(validation rejection) error = %v", err)
	}
	roundTrip, err := FromWireExecuteApprovalResponse(request, wireResponse)
	if err != nil || !reflect.DeepEqual(roundTrip, response) {
		t.Fatalf("validation rejection round trip = %#v, %v", roundTrip, err)
	}
}

func TestApprovalReadMappingsRoundTripEveryKind(t *testing.T) {
	ctx, cancel := mappingTestContext(t)
	defer cancel()
	revision := uint64(17)
	requests := []approvalsemantic.ReadRequest{
		{Kind: approvalsemantic.ReadPendingForCaller, MinCatalogRevision: &revision},
		{Kind: approvalsemantic.ReadLatestForRecord, TargetRecordID: "record-1", MinCatalogRevision: &revision},
		{Kind: approvalsemantic.ReadStepDisplayForRecord, TargetRecordID: "record-1", MinCatalogRevision: &revision},
	}
	responses := []approvalsemantic.ReadResponse{
		{
			Pending: []approvalsemantic.PendingItem{{
				WorkItemID: "work-1", InstanceID: "instance-1", DefinitionID: "definition-1",
				TargetRecordID: "record-1", ObjectID: "Account", SubmittedByID: "user-1",
				SubmittedByName: "Submitter", SubmittedAt: "2026-08-02T12:00:00Z",
				ActorID: "user-2", OriginalActorID: "user-3", Comments: "review", RecordName: "Acme",
			}},
			Steps:    []approvalsemantic.StepDisplay{},
			Receipts: mappingTestReceipts(approvalsemantic.ReadCapabilityName, revision),
		},
		{
			Pending: []approvalsemantic.PendingItem{},
			Latest: &approvalsemantic.InstanceSummary{
				InstanceID: "instance-1", DefinitionID: "definition-1", TargetRecordID: "record-1",
				State: approvalsemantic.InstanceStatePending, SubmittedByID: "user-1", SubmittedByName: "Submitter",
				CreatedAt: "2026-08-02T12:00:00Z", PendingWorkItems: []approvalsemantic.PendingWorkItem{{
					WorkItemID: "work-1", ActorID: "user-2", ActorName: "Approver",
					OriginalActorID: "user-3", OriginalActorName: "Original",
				}},
			},
			Steps:    []approvalsemantic.StepDisplay{},
			Receipts: mappingTestReceipts(approvalsemantic.ReadCapabilityName, revision),
		},
		{
			Pending: []approvalsemantic.PendingItem{},
			Steps: []approvalsemantic.StepDisplay{
				{StepID: "step-1", StepOrder: 1, StepName: "Review", StepLabel: "Review"},
				{StepID: "step-2", StepOrder: 2, StepName: "Final", StepLabel: "Final", HasCriteria: true, NoMatchBehavior: approvalsemantic.StepNoMatchApprove},
			},
			Receipts: mappingTestReceipts(approvalsemantic.ReadCapabilityName, revision),
		},
	}
	for index, request := range requests {
		wireRequest, err := ToWireReadApprovalRequest(ctx, request)
		if err != nil {
			t.Fatalf("ToWireReadApprovalRequest(%s) error = %v", request.Kind, err)
		}
		roundRequest, err := FromWireReadApprovalRequest(ctx, wireRequest)
		if err != nil || !reflect.DeepEqual(roundRequest, request) {
			t.Fatalf("read request %s round trip = %#v, %v", request.Kind, roundRequest, err)
		}
		wireResponse, err := ToWireReadApprovalResponse(request, responses[index])
		if err != nil {
			t.Fatalf("ToWireReadApprovalResponse(%s) error = %v", request.Kind, err)
		}
		roundResponse, err := FromWireReadApprovalResponse(request, wireResponse)
		if err != nil || !reflect.DeepEqual(roundResponse, responses[index]) {
			t.Fatalf("read response %s round trip = %#v, %v", request.Kind, roundResponse, err)
		}
	}
}

func TestApprovalMappingsFailClosedOnUnknownOrMixedWireShapes(t *testing.T) {
	ctx, cancel := mappingTestContext(t)
	defer cancel()
	revision := uint64(17)

	if _, err := FromWireExecuteApprovalRequest(ctx, &runtimev1.ExecuteApprovalRequest{
		Operation: runtimev1.ApprovalOperation(99), IdempotencyKey: "request-1",
	}); err == nil {
		t.Fatal("unknown execute operation was accepted")
	}
	if _, err := FromWireReadApprovalRequest(ctx, &runtimev1.ReadApprovalRequest{
		Kind: runtimev1.ApprovalReadKind(99),
	}); err == nil {
		t.Fatal("unknown read kind was accepted")
	}

	executeRequest := mappingTestApprovalExecuteRequest(approvalsemantic.OperationApprove, &revision)
	executeResponse := mappingTestApprovalExecuteResponse(approvalsemantic.OperationApprove, revision)
	wireExecute, err := ToWireExecuteApprovalResponse(executeRequest, executeResponse)
	if err != nil {
		t.Fatal(err)
	}
	wireExecute.State = runtimev1.ApprovalInstanceState(99)
	if _, err := FromWireExecuteApprovalResponse(executeRequest, wireExecute); err == nil {
		t.Fatal("unknown execute state was accepted")
	}
	wireExecute, err = ToWireExecuteApprovalResponse(executeRequest, executeResponse)
	if err != nil {
		t.Fatal(err)
	}
	wireExecute.PendingWorkItems = append(wireExecute.PendingWorkItems, nil)
	if _, err := FromWireExecuteApprovalResponse(executeRequest, wireExecute); err == nil {
		t.Fatal("nil pending work item was accepted")
	}
	wireExecute, err = ToWireExecuteApprovalResponse(executeRequest, executeResponse)
	if err != nil {
		t.Fatal(err)
	}
	wireExecute.SubmitRejection = &runtimev1.ApprovalSubmitRejection{
		Reason: runtimev1.ApprovalSubmitRejectionReason(99),
	}
	if _, err := FromWireExecuteApprovalResponse(executeRequest, wireExecute); err == nil {
		t.Fatal("unknown submit rejection reason was accepted")
	}
	submitRequest := mappingTestApprovalExecuteRequest(approvalsemantic.OperationSubmit, &revision)
	wireRejection, err := ToWireExecuteApprovalResponse(
		submitRequest,
		approvalsemantic.ExecuteResponse{
			TargetRecordID:   submitRequest.TargetRecordID,
			PendingWorkItems: []approvalsemantic.WorkItemAssignment{},
			ActionFailures:   []approvalsemantic.ActionFailure{},
			SubmitRejection: &approvalsemantic.SubmitRejection{
				Reason: approvalsemantic.SubmitRejectionNoActiveProcess,
			},
			Receipts: mappingTestReceipts(approvalsemantic.ExecuteCapabilityName, revision),
		},
	)
	if err != nil {
		t.Fatal(err)
	}
	wireRejection.State = runtimev1.ApprovalInstanceState_APPROVAL_INSTANCE_STATE_APPROVED
	if _, err := FromWireExecuteApprovalResponse(submitRequest, wireRejection); err == nil {
		t.Fatal("submit rejection with a conflicting applied state was accepted")
	}
	wireValidation, err := ToWireExecuteApprovalResponse(
		submitRequest,
		approvalsemantic.ExecuteResponse{
			TargetRecordID: submitRequest.TargetRecordID,
			SubmitRejection: &approvalsemantic.SubmitRejection{
				Reason: approvalsemantic.SubmitRejectionRecordValidationFailed,
				ValidationIssues: []approvalsemantic.ValidationIssue{{
					RuleName: "Rule_One", Message: "Correct the record.",
				}},
			},
			Receipts: mappingTestReceipts(approvalsemantic.ExecuteCapabilityName, revision),
		},
	)
	if err != nil {
		t.Fatal(err)
	}
	wireValidation.SubmitRejection.ValidationIssues = append(
		wireValidation.SubmitRejection.ValidationIssues,
		nil,
	)
	if _, err := FromWireExecuteApprovalResponse(submitRequest, wireValidation); err == nil {
		t.Fatal("nil validation issue was accepted")
	}
	wireValidation.SubmitRejection.ValidationIssues = make(
		[]*runtimev1.ApprovalValidationIssue,
		approvalsemantic.MaxValidationIssues+1,
	)
	if _, err := FromWireExecuteApprovalResponse(submitRequest, wireValidation); err == nil {
		t.Fatal("over-limit validation issues were accepted before semantic mapping")
	}
	wireSubmit, err := ToWireExecuteApprovalResponse(
		submitRequest,
		mappingTestApprovalExecuteResponse(approvalsemantic.OperationSubmit, revision),
	)
	if err != nil {
		t.Fatal(err)
	}
	wireSubmit.SubmitRejection = &runtimev1.ApprovalSubmitRejection{
		Reason: runtimev1.ApprovalSubmitRejectionReason_APPROVAL_SUBMIT_REJECTION_REASON_NO_ACTIVE_PROCESS,
	}
	if _, err := FromWireExecuteApprovalResponse(submitRequest, wireSubmit); err == nil {
		t.Fatal("mixed applied and rejected response was accepted")
	}

	readRequest := approvalsemantic.ReadRequest{Kind: approvalsemantic.ReadPendingForCaller, MinCatalogRevision: &revision}
	readResponse := approvalsemantic.ReadResponse{
		Pending: []approvalsemantic.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: mappingTestReceipts(approvalsemantic.ReadCapabilityName, revision),
	}
	wireRead, err := ToWireReadApprovalResponse(readRequest, readResponse)
	if err != nil {
		t.Fatal(err)
	}
	wireRead.Pending = append(wireRead.Pending, nil)
	if _, err := FromWireReadApprovalResponse(readRequest, wireRead); err == nil {
		t.Fatal("nil pending item was accepted")
	}
	wireRead, err = ToWireReadApprovalResponse(readRequest, readResponse)
	if err != nil {
		t.Fatal(err)
	}
	wireRead.Receipts.CapabilityVersion.Name = approvalsemantic.ExecuteCapabilityName
	if _, err := FromWireReadApprovalResponse(readRequest, wireRead); err == nil {
		t.Fatal("receipt for another Approval capability was accepted")
	}
}

func mappingTestApprovalExecuteRequest(operation approvalsemantic.Operation, revision *uint64) approvalsemantic.ExecuteRequest {
	request := approvalsemantic.ExecuteRequest{
		Operation: operation, IdempotencyKey: "approval-request-1", MinCatalogRevision: revision,
	}
	switch operation {
	case approvalsemantic.OperationSubmit:
		request.ObjectID = "Account"
		request.TargetRecordID = "record-1"
		request.Comments = "submit"
	case approvalsemantic.OperationApprove, approvalsemantic.OperationReject, approvalsemantic.OperationRecall:
		request.WorkItemID = "work-1"
		request.Comments = "review"
	case approvalsemantic.OperationReassign:
		request.WorkItemID = "work-1"
		request.NextActorID = "user-3"
	}
	return request
}

func mappingTestApprovalExecuteResponse(operation approvalsemantic.Operation, revision uint64) approvalsemantic.ExecuteResponse {
	state := approvalsemantic.InstanceStatePending
	switch operation {
	case approvalsemantic.OperationReject:
		state = approvalsemantic.InstanceStateRejected
	case approvalsemantic.OperationRecall:
		state = approvalsemantic.InstanceStateWithdrawn
	}
	response := approvalsemantic.ExecuteResponse{
		ExecutionID: "execution-1", InstanceID: "instance-1", TargetRecordID: "record-1",
		ActingPrincipalID: "user-1", State: state,
		PendingWorkItems: []approvalsemantic.WorkItemAssignment{},
		ActionFailures: []approvalsemantic.ActionFailure{{
			ActionRef: "notify-owner", Kind: approvalsemantic.ActionFailureAlert,
			Reason: approvalsemantic.ActionFailureUnavailable,
		}},
		Receipts: mappingTestReceipts(approvalsemantic.ExecuteCapabilityName, revision),
	}
	if operation != approvalsemantic.OperationSubmit {
		response.SourceWorkItemID = "work-1"
	}
	if state == approvalsemantic.InstanceStatePending {
		response.PendingWorkItems = []approvalsemantic.WorkItemAssignment{{WorkItemID: "work-2", ActorID: "user-2"}}
	}
	return response
}
