// Regression tests for the audit-fault carrier introduced in Lane C round-3
// to fix Pass B Finding 2: fault-routed errors must surface as
// Status="error" / ErrorPayload!="" on the corresponding audit row even when
// the executor returned (faultTarget, nil).
package automation

import (
	"context"
	"errors"
	"sync"
	"testing"

	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/kernel/pkg/recordlocks"
	"github.com/nexuscrm/shared/pkg/models"
)

type historySequenceReplaySignalForTest struct{}

func (*historySequenceReplaySignalForTest) Error() string {
	return "history sequence replay required"
}

func (*historySequenceReplaySignalForTest) Unwrap() error {
	return ports.ErrHistorySerializationConflict
}

func (*historySequenceReplaySignalForTest) HistorySequencePrelockFields() []string {
	return []string{"field-a", "field-z"}
}

type parentPrelockReplaySignalForTest struct{}

func (*parentPrelockReplaySignalForTest) Error() string {
	return "parent prelock replay required"
}

func (*parentPrelockReplaySignalForTest) ParentPrelockKeys() []recordlocks.ParentKey {
	return []recordlocks.ParentKey{{ObjectName: "Account", RecordID: "account-1"}}
}

type emptyParentPrelockCarrierForTest struct{}

func (*emptyParentPrelockCarrierForTest) Error() string {
	return "business failure with no parent replay plan"
}

func (*emptyParentPrelockCarrierForTest) ParentPrelockKeys() []recordlocks.ParentKey {
	return nil
}

func TestRecordAuditFault_NoBoxOnCtxIsNoOp(t *testing.T) {
	// recordAuditFault on a ctx without a box must not panic.
	defer func() {
		if r := recover(); r != nil {
			t.Fatalf("recordAuditFault panicked on ctx without box: %v", r)
		}
	}()
	recordAuditFault(context.Background(), errors.New("boom"))
}

func TestRecordAuditFault_NilErrIsNoOp(t *testing.T) {
	ctx, box := withAuditFaultBox(context.Background())
	recordAuditFault(ctx, nil)
	if box.Err() != nil {
		t.Fatalf("box.Err() = %v, want nil after recordAuditFault(nil)", box.Err())
	}
}

func TestRecordAuditFault_FirstNonNilWins(t *testing.T) {
	ctx, box := withAuditFaultBox(context.Background())
	first := errors.New("first cause")
	second := errors.New("second cause masquerading as cause")
	recordAuditFault(ctx, first)
	recordAuditFault(ctx, second)
	if !errors.Is(box.Err(), first) {
		t.Fatalf("box.Err() = %v, want %v (first non-nil should win)", box.Err(), first)
	}
}

func TestAuditFaultBox_ConcurrentRecord(t *testing.T) {
	ctx, box := withAuditFaultBox(context.Background())
	var wg sync.WaitGroup
	for i := 0; i < 50; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			recordAuditFault(ctx, errors.New("concurrent"))
		}(i)
	}
	wg.Wait()
	if box.Err() == nil {
		t.Fatal("box.Err() = nil after 50 concurrent recorders, want non-nil")
	}
}

// TestRouteFault_StampsFaultMessageAndBox verifies the helper used by every
// fault-return site:
//   - sets $Flow.FaultMessage on the interview's context data
//   - records the error on the audit-fault box so the deferred audit emit
//     can stamp Status="error" + ErrorPayload
//   - returns (target, nil) so the FlowExecutor main loop follows the fault
//     connector instead of propagating the error
func TestRouteFault_StampsFaultMessageAndBox(t *testing.T) {
	ctx, box := withAuditFaultBox(context.Background())
	interview := &models.FlowInterview{
		ContextData: map[string]interface{}{},
	}
	e := &FlowExecutor{}

	target, err := e.routeFault(ctx, interview, "fault-target", errors.New("insert failed: 23000"))
	if err != nil {
		t.Fatalf("routeFault returned err = %v, want nil (fault routes preserve nil execErr)", err)
	}
	if target != "fault-target" {
		t.Fatalf("routeFault returned target = %q, want %q", target, "fault-target")
	}

	if got := interview.ContextData["$Flow.FaultMessage"]; got != "insert failed: 23000" {
		t.Fatalf("$Flow.FaultMessage = %q, want %q", got, "insert failed: 23000")
	}

	be := box.Err()
	if be == nil {
		t.Fatal("audit-fault box is empty after routeFault — Status would be wrongly stamped success")
	}
	if be.Error() != "insert failed: 23000" {
		t.Fatalf("box.Err() = %q, want %q", be.Error(), "insert failed: 23000")
	}
}

func TestRouteFault_NilErrBecomesGenericError(t *testing.T) {
	ctx, box := withAuditFaultBox(context.Background())
	interview := &models.FlowInterview{ContextData: map[string]interface{}{}}
	e := &FlowExecutor{}

	target, err := e.routeFault(ctx, interview, "x", nil)
	if err != nil || target != "x" {
		t.Fatalf("routeFault(nil err) = (%q, %v), want (%q, nil)", target, err, "x")
	}
	if box.Err() == nil {
		t.Fatal("box.Err() = nil for nil-err input; helper should have synthesized a generic error so Status is still 'error'")
	}
}

func TestRouteFault_PropagatesHistorySequenceReplaySignal(t *testing.T) {
	ctx, box := withAuditFaultBox(context.Background())
	interview := &models.FlowInterview{ContextData: map[string]interface{}{}}
	e := &FlowExecutor{}
	replayErr := &historySequenceReplaySignalForTest{}

	target, err := e.routeFault(ctx, interview, "fault-target", replayErr)
	if target != "" || !errors.Is(err, replayErr) {
		t.Fatalf("routeFault(replay signal) = (%q, %v), want (empty, original error)", target, err)
	}
	if _, ok := interview.ContextData["$Flow.FaultMessage"]; ok {
		t.Fatal("transaction replay signal must not be exposed to the Flow fault connector")
	}
	if box.Err() != nil {
		t.Fatalf("transaction replay signal recorded as a routed audit fault: %v", box.Err())
	}
}

func TestRouteFault_PropagatesParentPrelockReplaySignal(t *testing.T) {
	ctx, box := withAuditFaultBox(context.Background())
	interview := &models.FlowInterview{ContextData: map[string]interface{}{}}
	e := &FlowExecutor{}
	replayErr := &parentPrelockReplaySignalForTest{}

	target, err := e.routeFault(ctx, interview, "fault-target", replayErr)
	if target != "" || !errors.Is(err, replayErr) {
		t.Fatalf("routeFault(parent prelock replay) = (%q, %v), want (empty, original error)", target, err)
	}
	if _, ok := interview.ContextData["$Flow.FaultMessage"]; ok {
		t.Fatal("parent prelock replay signal must not be exposed to the Flow fault connector")
	}
	if box.Err() != nil {
		t.Fatalf("parent prelock replay signal recorded as a routed audit fault: %v", box.Err())
	}
}

func TestRouteFault_RoutesEmptyParentPrelockCarrierAsBusinessFailure(t *testing.T) {
	ctx, box := withAuditFaultBox(context.Background())
	interview := &models.FlowInterview{ContextData: map[string]interface{}{}}
	e := &FlowExecutor{}

	target, err := e.routeFault(
		ctx,
		interview,
		"fault-target",
		&emptyParentPrelockCarrierForTest{},
	)
	if target != "fault-target" || err != nil {
		t.Fatalf("routeFault(empty parent prelock carrier) = (%q, %v), want (fault-target, nil)", target, err)
	}
	if box.Err() == nil {
		t.Fatal("empty parent prelock carrier was not recorded as a routed business fault")
	}
}

func TestRouteFault_PropagatesUniqueFieldUpsertRace(t *testing.T) {
	ctx, box := withAuditFaultBox(context.Background())
	interview := &models.FlowInterview{ContextData: map[string]interface{}{}}
	e := &FlowExecutor{}
	replayErr := ports.ErrUniqueFieldUpsertRace

	target, err := e.routeFault(ctx, interview, "fault-target", replayErr)
	if target != "" || !errors.Is(err, replayErr) {
		t.Fatalf("routeFault(unique upsert replay) = (%q, %v), want (empty, original error)", target, err)
	}
	if _, ok := interview.ContextData["$Flow.FaultMessage"]; ok {
		t.Fatal("unique upsert replay signal must not be exposed to the Flow fault connector")
	}
	if box.Err() != nil {
		t.Fatalf("unique upsert replay signal recorded as a routed audit fault: %v", box.Err())
	}
}

// TestAuditDeferDerivesErrorFromBox simulates the defer pattern that all the
// element executors use: when execErr is nil but the audit-fault box has a
// recorded error, the deferred audit emit must stamp Status="error" with the
// box's error message.
//
// This is the exact behaviour that round-2 lacked and that this round
// introduced — without this, every fault-handled DML failure was logged as
// "success".
func TestAuditDeferDerivesErrorFromBox(t *testing.T) {
	ctx, auditFault := withAuditFaultBox(context.Background())
	interview := &models.FlowInterview{ContextData: map[string]interface{}{}}
	e := &FlowExecutor{}

	// Simulate: the body took the fault-connector path (so execErr stays nil
	// because flows return (faultTarget, nil)) but the failure was recorded on
	// the box via routeFault.
	_, _ = e.routeFault(ctx, interview, "fault-target",
		errors.New("DML insert violated FK constraint"))

	var execErr error // simulating defer's named return value: stayed nil

	// This block mirrors the defer body in every element executor.
	status := "success"
	errPayload := ""
	if execErr != nil {
		status = "error"
		errPayload = execErr.Error()
	} else if be := auditFault.Err(); be != nil {
		status = "error"
		errPayload = be.Error()
	}

	if status != "error" {
		t.Fatalf("derived audit Status = %q, want %q (fault-handled failure must surface as error in audit log)", status, "error")
	}
	if errPayload != "DML insert violated FK constraint" {
		t.Fatalf("derived audit ErrorPayload = %q, want %q", errPayload, "DML insert violated FK constraint")
	}
}
