package dialectsfcrm

import (
	"encoding/json"
	"testing"

	"github.com/nexuscrm/dialect-sf-crm/wire"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
)

// This file is the byte-golden ORACLE for L0-E PR-2 sub-item (b): the mapping
// from the neutral StandardActionOutcome/StandardActionError reason code to the
// Salesforce v63 wire spelling (errorCode / statusCode) and HTTP status.
//
// The engine (L0) now emits a NEUTRAL snake_case reason ("invalid_request",
// "required_field_missing", "invalid_field", "insufficient_permissions",
// "unknown_exception"); this dialect renders the SF UPPERCASE spelling. These
// goldens pin the SF bytes so the carve is proven byte-identical: they feed the
// NEUTRAL reason and assert the SF spelling / status / message.
//
// STATUS-CODE TRAP (do not "fix"): standardActionOutcomeResponse's InvalidRequest
// path returns HTTP 403 for EVERY reason EXCEPT invalid_request (→ 400). So
// required_field_missing / invalid_field / insufficient_permissions /
// unknown_exception all return 403 even though a per-reason 400 might look more
// "correct". PreviewUnsupported always returns 400.

// bodyToSFErrors marshals a dialect.Response body ([]wire.SFError) round-trip
// through JSON so the assertions run against the exact wire bytes the client
// receives, not the in-memory Go value.
func bodyToSFErrors(t *testing.T, body any) []wire.SFError {
	t.Helper()
	raw, err := json.Marshal(body)
	if err != nil {
		t.Fatalf("marshal body: %v", err)
	}
	var out []wire.SFError
	if err := json.Unmarshal(raw, &out); err != nil {
		t.Fatalf("unmarshal body %s: %v", raw, err)
	}
	return out
}

func TestStandardActionOutcomeResponse_InvalidRequest_ErrorCodeAndStatusGolden(t *testing.T) {
	// Neutral reason (what L0 now emits) -> expected SF wire spelling + HTTP status.
	// Confirms the 403-unless-invalid_request trap.
	cases := []struct {
		name        string
		neutralCode string
		wantErrCode string
		wantHTTP    int
	}{
		{"invalid_request", "invalid_request", wire.SFCodeInvalidRequest, 400},
		{"required_field_missing", "required_field_missing", wire.SFCodeRequiredFieldMissing, 403},
		{"invalid_field", "invalid_field", wire.SFCodeInvalidField, 403},
		{"insufficient_permissions", "insufficient_permissions", wire.SFCodeInsufficientPerms, 403},
		{"unknown_exception", "unknown_exception", wire.SFCodeUnknownException, 403},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			outcome := &contract.StandardActionOutcome{
				Kind:    contract.StandardActionOutcomeInvalidRequest,
				Code:    tc.neutralCode,
				Name:    "createRecord",
				Message: "boom",
			}
			resp := standardActionOutcomeResponse("execute standard action \"createRecord\"", "createRecord", outcome)
			if resp == nil {
				t.Fatalf("nil response")
			}
			if resp.Status != tc.wantHTTP {
				t.Errorf("HTTP status = %d, want %d", resp.Status, tc.wantHTTP)
			}
			errs := bodyToSFErrors(t, resp.Body)
			if len(errs) != 1 {
				t.Fatalf("want 1 SFError, got %d: %+v", len(errs), errs)
			}
			if errs[0].ErrorCode != tc.wantErrCode {
				t.Errorf("errorCode = %q, want %q", errs[0].ErrorCode, tc.wantErrCode)
			}
			if errs[0].Message != "boom" {
				t.Errorf("message = %q, want %q", errs[0].Message, "boom")
			}
		})
	}
}

func TestStandardActionOutcomeResponse_PreviewUnsupported_ErrorCodeAndStatusGolden(t *testing.T) {
	// PreviewUnsupported always returns HTTP 400 and renders the reason verbatim.
	cases := []struct {
		name        string
		neutralCode string
		wantErrCode string
	}{
		{"invalid_request", "invalid_request", wire.SFCodeInvalidRequest},
		{"required_field_missing", "required_field_missing", wire.SFCodeRequiredFieldMissing},
		{"invalid_field", "invalid_field", wire.SFCodeInvalidField},
		{"insufficient_permissions", "insufficient_permissions", wire.SFCodeInsufficientPerms},
		{"unknown_exception", "unknown_exception", wire.SFCodeUnknownException},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			outcome := &contract.StandardActionOutcome{
				Kind:    contract.StandardActionOutcomePreviewUnsupported,
				Code:    tc.neutralCode,
				Name:    "createRecord",
				Message: "preview not supported",
			}
			resp := standardActionOutcomeResponse("preview standard action \"createRecord\"", "createRecord", outcome)
			if resp == nil {
				t.Fatalf("nil response")
			}
			if resp.Status != 400 {
				t.Errorf("HTTP status = %d, want 400", resp.Status)
			}
			errs := bodyToSFErrors(t, resp.Body)
			if len(errs) != 1 {
				t.Fatalf("want 1 SFError, got %d: %+v", len(errs), errs)
			}
			if errs[0].ErrorCode != tc.wantErrCode {
				t.Errorf("errorCode = %q, want %q", errs[0].ErrorCode, tc.wantErrCode)
			}
			if errs[0].Message != "preview not supported" {
				t.Errorf("message = %q, want %q", errs[0].Message, "preview not supported")
			}
		})
	}
}

// The Unauthorized Kind uses a fixed SFCodeInsufficientPerms and does not read
// outcome.Code — pin that so it stays byte-identical.
func TestStandardActionOutcomeResponse_UnauthorizedGolden(t *testing.T) {
	outcome := &contract.StandardActionOutcome{
		Kind:    contract.StandardActionOutcomeUnauthorized,
		Code:    "insufficient_permissions",
		Name:    "createRecord",
		Message: "no perms",
	}
	resp := standardActionOutcomeResponse("authorize", "createRecord", outcome)
	if resp == nil {
		t.Fatalf("nil response")
	}
	if resp.Status != 403 {
		t.Errorf("HTTP status = %d, want 403", resp.Status)
	}
	errs := bodyToSFErrors(t, resp.Body)
	if len(errs) != 1 || errs[0].ErrorCode != wire.SFCodeInsufficientPerms || errs[0].Message != "no perms" {
		t.Errorf("unexpected body: %+v", errs)
	}
}

func TestStandardActionErrorsToFlow_StatusCodeSpellingGolden(t *testing.T) {
	// standardActionErrorsToFlow emits Code as the FlowExecutionError.StatusCode
	// STRING (no HTTP status). Feed the neutral reason, assert SF spelling.
	cases := []struct {
		name        string
		neutralCode string
		wantStatus  string
	}{
		{"invalid_request", "invalid_request", wire.SFCodeInvalidRequest},
		{"required_field_missing", "required_field_missing", wire.SFCodeRequiredFieldMissing},
		{"invalid_field", "invalid_field", wire.SFCodeInvalidField},
		{"insufficient_permissions", "insufficient_permissions", wire.SFCodeInsufficientPerms},
		{"unknown_exception", "unknown_exception", wire.SFCodeUnknownException},
		{"field_write_denied", contract.FlowExecutionErrorCodeFieldWriteDenied, wire.SFCodeInvalidFieldForInsertUpdate},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			in := []contract.StandardActionError{{
				Code:    tc.neutralCode,
				Message: "field problem",
				Fields:  []string{"Name"},
			}}
			out := standardActionErrorsToFlow(in)
			if len(out) != 1 {
				t.Fatalf("want 1 FlowExecutionError, got %d", len(out))
			}
			if out[0].StatusCode != tc.wantStatus {
				t.Errorf("StatusCode = %q, want %q", out[0].StatusCode, tc.wantStatus)
			}
			if out[0].Message != "field problem" {
				t.Errorf("Message = %q, want %q", out[0].Message, "field problem")
			}
			if len(out[0].Fields) != 1 || out[0].Fields[0] != "Name" {
				t.Errorf("Fields = %v, want [Name]", out[0].Fields)
			}
		})
	}
}

// Fail-closed default: an unknown reason must render as INVALID_REQUEST (and the
// outcome path keeps HTTP 400 for the invalid_request branch).
func TestStandardActionReason_FailClosedDefault(t *testing.T) {
	outcome := &contract.StandardActionOutcome{
		Kind:    contract.StandardActionOutcomeInvalidRequest,
		Code:    "some_reason_the_dialect_does_not_know",
		Name:    "createRecord",
		Message: "weird",
	}
	resp := standardActionOutcomeResponse("execute", "createRecord", outcome)
	errs := bodyToSFErrors(t, resp.Body)
	if len(errs) != 1 || errs[0].ErrorCode != wire.SFCodeInvalidRequest {
		t.Errorf("fail-closed default errorCode = %+v, want INVALID_REQUEST", errs)
	}
	// invalid_request => HTTP 400 default.
	if resp.Status != 400 {
		t.Errorf("fail-closed default status = %d, want 400", resp.Status)
	}
}

// The Flow path (standardActionErrorsToFlow) does NOT collapse unknown codes to
// INVALID_REQUEST: it carries arbitrary codes emitted by user-authored flows,
// which the engine passes through verbatim today. Neutral engine conditions
// with an explicit dialect mapping are translated; every other code passes
// through byte-identical.
func TestStandardActionErrorsToFlow_PassesThroughFlowAuthoredCodes(t *testing.T) {
	cases := []struct {
		in   string
		want string
	}{
		// Neutral engine reasons -> SF spelling.
		{"invalid_field", wire.SFCodeInvalidField},
		{"insufficient_permissions", wire.SFCodeInsufficientPerms},
		{contract.FlowExecutionErrorCodeFieldWriteDenied, wire.SFCodeInvalidFieldForInsertUpdate},
		// Arbitrary flow-authored codes -> passed through unchanged.
		{"FIELD_CUSTOM_VALIDATION_EXCEPTION", "FIELD_CUSTOM_VALIDATION_EXCEPTION"},
		{"LIMIT_EXCEEDED", "LIMIT_EXCEEDED"},
		{"MY_CUSTOM_FLOW_CODE", "MY_CUSTOM_FLOW_CODE"},
		{"", ""},
	}
	for _, tc := range cases {
		out := standardActionErrorsToFlow([]contract.StandardActionError{{Code: tc.in, Message: "m"}})
		if len(out) != 1 || out[0].StatusCode != tc.want {
			t.Errorf("code %q -> %+v, want StatusCode %q", tc.in, out, tc.want)
		}
	}
}

// Bulk allOrNone path (sub-item a): the L0 engine now emits an empty Message for
// RecordMutationErrorAllOrNoneRolledBack; the dialect renders the SF English.
func TestRecordMutationErrorsToV63_AllOrNoneMessageGolden(t *testing.T) {
	const wantMessage = "Record rolled back because not all records were valid and the request was using AllOrNone header"
	errs := []contract.RecordMutationError{{
		Kind:    contract.RecordMutationErrorAllOrNoneRolledBack,
		Message: "", // neutral L0 emits empty; dialect reconstructs.
		Fields:  []string{},
	}}
	out := recordMutationErrorsToV63(errs, "v63.0")
	if len(out) != 1 {
		t.Fatalf("want 1 entry, got %d", len(out))
	}
	entry, ok := out[0].(map[string]interface{})
	if !ok {
		t.Fatalf("entry is not a map: %T", out[0])
	}
	if got := entry["message"]; got != wantMessage {
		t.Errorf("message = %q, want %q", got, wantMessage)
	}
	if got := entry["statusCode"]; got != wire.SFCodeAllOrNoneOperationRolledBack {
		t.Errorf("statusCode = %q, want %q", got, wire.SFCodeAllOrNoneOperationRolledBack)
	}
	fields, ok := entry["fields"].([]interface{})
	if !ok || len(fields) != 0 {
		t.Errorf("fields = %v, want empty slice", entry["fields"])
	}
}

// Guard: verify the SF UPPERCASE spellings + the allOrNone English string appear
// verbatim in the produced wire output at least once (seal companion on the
// dialect side — the dialect is the owner of the SF spelling).
func TestDialectRendersSFSpellings(t *testing.T) {
	_ = models.SObject{} // ensure models import used
	if wire.SFCodeInvalidField != "INVALID_FIELD" {
		t.Errorf("wire.SFCodeInvalidField drifted: %q", wire.SFCodeInvalidField)
	}
	if wire.SFCodeRequiredFieldMissing != "REQUIRED_FIELD_MISSING" {
		t.Errorf("wire.SFCodeRequiredFieldMissing drifted: %q", wire.SFCodeRequiredFieldMissing)
	}
}
