package flowv1

import (
	"context"
	"testing"
	"time"

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

func TestFlowResponseV2IsExactlyOneSuccessOrCallerVisibleFailure(t *testing.T) {
	ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second))
	defer cancel()
	request := Request{DefinitionRef: "flow-1", DefinitionVersion: "version-1", IdempotencyKey: "request-1", CanonicalInputs: []byte(`{"a":1}`)}
	if err := ValidateRequest(ctx, request); err != nil {
		t.Fatalf("ValidateRequest() error = %v", err)
	}
	receipts := common.Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName}
	if err := ValidateResponse(Response{ExecutionID: "execution-1", CanonicalOutputs: []byte(`{}`), Receipts: receipts}); err != nil {
		t.Fatalf("ValidateResponse(success) error = %v", err)
	}
	// Keep the original exported type source-compatible for consumers that used
	// an unkeyed literal before the V2 terminal-result contract was introduced.
	_ = Response{"execution-1", []byte(`{}`), receipts}
	if ValidateResponse(Response{ExecutionID: "execution-2", Receipts: receipts}) == nil {
		t.Fatal("success-only response without canonical outputs was accepted")
	}
	if err := ValidateResponseV2(ResponseV2{
		ExecutionID: "execution-1", CanonicalOutputs: []byte(`{}`), Receipts: receipts,
	}); err != nil {
		t.Fatalf("ValidateResponseV2(success) error = %v", err)
	}
	failure := ResponseV2{
		ExecutionID: "execution-2",
		Errors: []ExecutionError{{
			Code: "POLICY_VALIDATION_ERROR", Message: "Correct RequestedValue before retrying.",
			Fields: []string{"RequestedValue"},
		}},
		Receipts: receipts,
	}
	if err := ValidateResponseV2(failure); err != nil {
		t.Fatalf("ValidateResponseV2(failure) error = %v", err)
	}
	mixed := failure
	mixed.CanonicalOutputs = []byte(`{}`)
	if ValidateResponseV2(mixed) == nil {
		t.Fatal("response carrying both outputs and errors was accepted")
	}
	if ValidateResponseV2(ResponseV2{ExecutionID: "execution-3", Receipts: receipts}) == nil {
		t.Fatal("response without outputs or errors was accepted")
	}
	request.CanonicalInputs = []byte(`{"a":1,"a":2}`)
	if ValidateRequest(ctx, request) == nil {
		t.Fatal("duplicate-key Flow input was accepted as canonical")
	}
}

func TestFlowExecutionErrorsAreBoundedPrintableAndFieldTyped(t *testing.T) {
	receipts := common.Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName}
	valid := ResponseV2{
		ExecutionID: "execution-1",
		Errors: []ExecutionError{{
			Code:    "POLICY_VALIDATION_ERROR",
			Message: "Correct RequestedValue before retrying.",
			Fields:  []string{"RequestedValue"},
		}},
		Receipts: receipts,
	}
	if err := ValidateResponseV2(valid); err != nil {
		t.Fatalf("valid caller-visible error rejected: %v", err)
	}

	for name, mutate := range map[string]func(*ResponseV2){
		"empty code":     func(response *ResponseV2) { response.Errors[0].Code = "" },
		"lowercase code": func(response *ResponseV2) { response.Errors[0].Code = "validation_error" },
		"numeric prefix": func(response *ResponseV2) { response.Errors[0].Code = "1_VALIDATION" },
		"blank message":  func(response *ResponseV2) { response.Errors[0].Message = "   " },
		"oversized message": func(response *ResponseV2) {
			response.Errors[0].Message = string(make([]byte, MaxErrorMessageBytes+1))
		},
		"control in message":        func(response *ResponseV2) { response.Errors[0].Message = "safe\ninternal SQL" },
		"format in message":         func(response *ResponseV2) { response.Errors[0].Message = "safe\u200binvisible" },
		"line separator in message": func(response *ResponseV2) { response.Errors[0].Message = "safe\u2028internal" },
		"paragraph separator in message": func(response *ResponseV2) {
			response.Errors[0].Message = "safe\u2029internal"
		},
		"invalid field": func(response *ResponseV2) { response.Errors[0].Fields = []string{"Account.Source"} },
		"too many errors": func(response *ResponseV2) {
			response.Errors = make([]ExecutionError, MaxExecutionErrors+1)
		},
		"too many fields": func(response *ResponseV2) {
			response.Errors[0].Fields = make([]string, MaxFieldsPerExecutionError+1)
		},
	} {
		t.Run(name, func(t *testing.T) {
			candidate := valid
			candidate.Errors = append([]ExecutionError(nil), valid.Errors...)
			candidate.Errors[0].Fields = append([]string(nil), valid.Errors[0].Fields...)
			mutate(&candidate)
			if ValidateResponseV2(candidate) == nil {
				t.Fatalf("malformed Flow execution error accepted: %#v", candidate.Errors)
			}
		})
	}
}

func TestCatalogActionRequestIsClosedAndCarriesNoFlowIdentity(t *testing.T) {
	ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second))
	defer cancel()
	request := CatalogActionRequest{
		IdempotencyKey: "request-1", CanonicalInputs: []byte(`{"a":1}`),
		CatalogAction: CatalogActionBinding{
			ComponentRef: "bootstrap-actions", ActionRef: "convert",
		},
	}
	if err := ValidateCatalogActionRequest(ctx, request); err != nil {
		t.Fatalf("ValidateCatalogActionRequest() error = %v", err)
	}
	request.CatalogAction.ActionRef = ""
	if ValidateCatalogActionRequest(ctx, request) == nil {
		t.Fatal("incomplete Catalog action binding was accepted")
	}
	request.CatalogAction.ActionRef = "convert"
	request.CanonicalInputs = []byte(`{"a":1,"a":2}`)
	if ValidateCatalogActionRequest(ctx, request) == nil {
		t.Fatal("duplicate-key Catalog action input was accepted as canonical")
	}
}

func TestCatalogActionPreviewIsClosedNoEffectAndRevisionReceipted(t *testing.T) {
	ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second))
	defer cancel()
	revision := uint64(17)
	request := CatalogActionPreviewRequest{
		Mode:               CatalogActionPreviewCreateRecords,
		CanonicalInputs:    []byte(`{"record":"001"}`),
		MinCatalogRevision: &revision,
		CatalogAction: CatalogActionBinding{
			ComponentRef: "bootstrap-actions", ActionRef: "create-target",
		},
	}
	if err := ValidateCatalogActionPreviewRequest(ctx, request); err != nil {
		t.Fatalf("ValidateCatalogActionPreviewRequest() error = %v", err)
	}
	receipts := common.Receipts{
		ProtocolVersion: "1.0", CapabilityVersion: CapabilityName,
		CatalogRevision: 17, SchemaGeneration: 8, EngineGeneration: 9,
		SecurityEpoch: 3,
	}
	response := CatalogActionPreviewResponse{
		CanonicalResult: []byte(`{"creates":[]}`), Receipts: receipts,
	}
	if err := ValidateCatalogActionPreviewResponse(request, response); err != nil {
		t.Fatalf("ValidateCatalogActionPreviewResponse() error = %v", err)
	}
	response.Receipts.CatalogRevision = 16
	if ValidateCatalogActionPreviewResponse(request, response) == nil {
		t.Fatal("Catalog action preview response older than the requested revision was accepted")
	}
	response.Receipts.CatalogRevision = 17
	response.Receipts.EngineGeneration = 0
	if ValidateCatalogActionPreviewResponse(request, response) == nil {
		t.Fatal("Catalog action preview response without serving generations was accepted")
	}
	response.Receipts.EngineGeneration = 9

	request.Mode = "INTERNAL_EXECUTOR_METHOD"
	if ValidateCatalogActionPreviewRequest(ctx, request) == nil {
		t.Fatal("unknown Catalog action preview mode was accepted")
	}
	request.Mode = CatalogActionPreviewOutputs
	request.RecordID = "001"
	if ValidateCatalogActionPreviewRequest(ctx, request) == nil {
		t.Fatal("output preview accepted create inputs together with record identity")
	}
	request.CanonicalInputs = nil
	if err := ValidateCatalogActionPreviewRequest(ctx, request); err != nil {
		t.Fatalf("ValidateCatalogActionPreviewRequest(outputs) error = %v", err)
	}
	request.Mode = CatalogActionPreviewCreateRecords
	if ValidateCatalogActionPreviewRequest(ctx, request) == nil {
		t.Fatal("create preview accepted record identity instead of canonical action inputs")
	}
	response.CanonicalResult = nil
	if ValidateCatalogActionPreviewResponse(request, response) == nil {
		t.Fatal("Catalog action preview response without a result was accepted")
	}
}
