package orchestrator

import (
	"context"
	"github.com/tech/sendico/pkg/discovery"
	"strings"
	"testing"

	ledgerclient "github.com/tech/sendico/ledger/client"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/batchmeta"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/erecon"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/sexec"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
	"github.com/tech/sendico/payments/storage/model"
	pm "github.com/tech/sendico/pkg/model"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
	"go.mongodb.org/mongo-driver/v2/bson"
)

func TestGatewayLedgerExecutor_ExecuteLedger_CreditUsesSourceAmountAndDefaultRoles(t *testing.T) {
	orgID := bson.NewObjectID()
	payment := testLedgerExecutorPayment(orgID)

	var transferReq *ledgerv1.TransferRequest
	executor := &gatewayLedgerExecutor{
		ledgerClient: &ledgerclient.Fake{
			TransferInternalFn: func(_ context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
				transferReq = req
				return &ledgerv1.PostResponse{JournalEntryRef: "entry-1"}, nil
			},
		},
	}

	out, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{
		Payment: payment,
		Step: xplan.Step{
			StepRef:  "edge_1_2_ledger_credit",
			StepCode: "edge.1_2.ledger.credit",
			Action:   discovery.RailOperationCredit,
			Rail:     discovery.RailLedger,
		},
		StepExecution: agg.StepExecution{
			StepRef:        "edge_1_2_ledger_credit",
			StepCode:       "edge.1_2.ledger.credit",
			Attempt:        1,
			ConvertedMoney: &paymenttypes.Money{Amount: "42", Currency: "EUR"},
		},
	})
	if err != nil {
		t.Fatalf("ExecuteLedger returned error: %v", err)
	}
	if out == nil {
		t.Fatal("expected output")
	}
	if transferReq == nil {
		t.Fatal("expected ledger transfer request")
	}
	if got, want := transferReq.GetMoney().GetAmount(), "1.000000"; got != want {
		t.Fatalf("money.amount mismatch: got=%q want=%q", got, want)
	}
	if got, want := transferReq.GetMoney().GetCurrency(), "USDT"; got != want {
		t.Fatalf("money.currency mismatch: got=%q want=%q", got, want)
	}
	if got, want := transferReq.GetFromRole(), ledgerv1.AccountRole_ACCOUNT_ROLE_PENDING; got != want {
		t.Fatalf("from_role mismatch: got=%v want=%v", got, want)
	}
	if got, want := transferReq.GetToRole(), ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING; got != want {
		t.Fatalf("to_role mismatch: got=%v want=%v", got, want)
	}
	if got, want := out.StepExecution.State, agg.StepStateCompleted; got != want {
		t.Fatalf("state mismatch: got=%q want=%q", got, want)
	}
	if out.StepExecution.ExecutedMoney == nil {
		t.Fatal("expected executed money to be recorded")
	}
	if got, want := out.StepExecution.ExecutedMoney.Amount, "1.000000"; got != want {
		t.Fatalf("executed money amount mismatch: got=%q want=%q", got, want)
	}
	if got, want := out.StepExecution.ExecutedMoney.Currency, "USDT"; got != want {
		t.Fatalf("executed money currency mismatch: got=%q want=%q", got, want)
	}
	if got := out.StepExecution.ConvertedMoney; got != nil {
		t.Fatalf("expected converted money to be cleared for ledger step, got=%+v", got)
	}
	if len(out.StepExecution.ExternalRefs) != 1 {
		t.Fatalf("expected one external ref, got=%d", len(out.StepExecution.ExternalRefs))
	}
	if got, want := out.StepExecution.ExternalRefs[0].Kind, erecon.ExternalRefKindLedger; got != want {
		t.Fatalf("external ref kind mismatch: got=%q want=%q", got, want)
	}
	if got, want := out.StepExecution.ExternalRefs[0].Ref, "entry-1"; got != want {
		t.Fatalf("external ref value mismatch: got=%q want=%q", got, want)
	}
}

func TestGatewayLedgerExecutor_ExecuteLedger_UsesPlannedMoney(t *testing.T) {
	orgID := bson.NewObjectID()
	payment := testLedgerExecutorPayment(orgID)

	var transferReq *ledgerv1.TransferRequest
	executor := &gatewayLedgerExecutor{
		ledgerClient: &ledgerclient.Fake{
			TransferInternalFn: func(_ context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
				transferReq = req
				return &ledgerv1.PostResponse{JournalEntryRef: "entry-planned"}, nil
			},
		},
	}

	_, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{
		Payment: payment,
		Step: xplan.Step{
			StepRef:  "edge_1_2_ledger_credit",
			StepCode: "edge.1_2.ledger.credit",
			Action:   discovery.RailOperationCredit,
			Rail:     discovery.RailLedger,
		},
		StepExecution: agg.StepExecution{
			StepRef:      "edge_1_2_ledger_credit",
			StepCode:     "edge.1_2.ledger.credit",
			Attempt:      1,
			PlannedMoney: &paymenttypes.Money{Amount: "12.34", Currency: "EUR"},
		},
	})
	if err != nil {
		t.Fatalf("ExecuteLedger returned error: %v", err)
	}
	if transferReq == nil {
		t.Fatal("expected ledger transfer request")
	}
	if got, want := transferReq.GetMoney().GetAmount(), "12.34"; got != want {
		t.Fatalf("money.amount mismatch: got=%q want=%q", got, want)
	}
	if got, want := transferReq.GetMoney().GetCurrency(), "EUR"; got != want {
		t.Fatalf("money.currency mismatch: got=%q want=%q", got, want)
	}
}

func TestGatewayLedgerExecutor_ExecuteLedger_ExternalCreditUsesPostCreditWithCharges(t *testing.T) {
	orgID := bson.NewObjectID()
	payment := testLedgerExecutorPayment(orgID)

	var (
		postReq        *ledgerv1.PostCreditRequest
		transferCalled bool
	)
	executor := &gatewayLedgerExecutor{
		ledgerClient: &ledgerclient.Fake{
			PostExternalCreditWithChargesFn: func(_ context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
				postReq = req
				return &ledgerv1.PostResponse{JournalEntryRef: "entry-ext-credit"}, nil
			},
			TransferInternalFn: func(_ context.Context, _ *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
				transferCalled = true
				return &ledgerv1.PostResponse{JournalEntryRef: "entry-transfer"}, nil
			},
		},
	}

	out, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{
		Payment: payment,
		Step: xplan.Step{
			StepRef:  "edge_1_2_ledger_credit",
			StepCode: "edge.1_2.ledger.credit",
			Action:   discovery.RailOperationExternalCredit,
			Rail:     discovery.RailLedger,
		},
		StepExecution: agg.StepExecution{
			StepRef:  "edge_1_2_ledger_credit",
			StepCode: "edge.1_2.ledger.credit",
			Attempt:  1,
		},
	})
	if err != nil {
		t.Fatalf("ExecuteLedger returned error: %v", err)
	}
	if out == nil {
		t.Fatal("expected output")
	}
	if postReq == nil {
		t.Fatal("expected external credit request")
	}
	if transferCalled {
		t.Fatal("expected external credit to skip transfer")
	}
	if got, want := postReq.GetMoney().GetAmount(), "1.000000"; got != want {
		t.Fatalf("money.amount mismatch: got=%q want=%q", got, want)
	}
	if got, want := postReq.GetMoney().GetCurrency(), "USDT"; got != want {
		t.Fatalf("money.currency mismatch: got=%q want=%q", got, want)
	}
	if got, want := postReq.GetRole(), ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING; got != want {
		t.Fatalf("role mismatch: got=%v want=%v", got, want)
	}
	if got, want := out.StepExecution.State, agg.StepStateCompleted; got != want {
		t.Fatalf("state mismatch: got=%q want=%q", got, want)
	}
	if len(out.StepExecution.ExternalRefs) != 1 {
		t.Fatalf("expected one external ref, got=%d", len(out.StepExecution.ExternalRefs))
	}
	if got, want := out.StepExecution.ExternalRefs[0].Ref, "entry-ext-credit"; got != want {
		t.Fatalf("external ref value mismatch: got=%q want=%q", got, want)
	}
}

func TestGatewayLedgerExecutor_ExecuteLedger_ExternalDebitUsesPostDebitWithCharges(t *testing.T) {
	orgID := bson.NewObjectID()
	payment := testLedgerExecutorPayment(orgID)

	var (
		postReq        *ledgerv1.PostDebitRequest
		transferCalled bool
	)
	executor := &gatewayLedgerExecutor{
		ledgerClient: &ledgerclient.Fake{
			PostExternalDebitWithChargesFn: func(_ context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) {
				postReq = req
				return &ledgerv1.PostResponse{JournalEntryRef: "entry-ext-debit"}, nil
			},
			TransferInternalFn: func(_ context.Context, _ *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
				transferCalled = true
				return &ledgerv1.PostResponse{JournalEntryRef: "entry-transfer"}, nil
			},
		},
	}

	out, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{
		Payment: payment,
		Step: xplan.Step{
			StepRef:  "edge_3_4_ledger_debit",
			StepCode: "edge.3_4.ledger.debit",
			Action:   discovery.RailOperationExternalDebit,
			Rail:     discovery.RailLedger,
			Metadata: map[string]string{
				"mode": "finalize_debit",
			},
		},
		StepExecution: agg.StepExecution{
			StepRef:  "edge_3_4_ledger_debit",
			StepCode: "edge.3_4.ledger.debit",
			Attempt:  1,
		},
	})
	if err != nil {
		t.Fatalf("ExecuteLedger returned error: %v", err)
	}
	if out == nil {
		t.Fatal("expected output")
	}
	if postReq == nil {
		t.Fatal("expected external debit request")
	}
	if transferCalled {
		t.Fatal("expected external debit to skip transfer")
	}
	if got, want := postReq.GetMoney().GetAmount(), "76.5"; got != want {
		t.Fatalf("money.amount mismatch: got=%q want=%q", got, want)
	}
	if got, want := postReq.GetMoney().GetCurrency(), "RUB"; got != want {
		t.Fatalf("money.currency mismatch: got=%q want=%q", got, want)
	}
	if got, want := postReq.GetRole(), ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD; got != want {
		t.Fatalf("role mismatch: got=%v want=%v", got, want)
	}
	if got, want := postReq.GetMetadata()[ledgerMetadataMode], "finalize_debit"; got != want {
		t.Fatalf("mode metadata mismatch: got=%q want=%q", got, want)
	}
	if got, want := out.StepExecution.State, agg.StepStateCompleted; got != want {
		t.Fatalf("state mismatch: got=%q want=%q", got, want)
	}
	if len(out.StepExecution.ExternalRefs) != 1 {
		t.Fatalf("expected one external ref, got=%d", len(out.StepExecution.ExternalRefs))
	}
	if got, want := out.StepExecution.ExternalRefs[0].Ref, "entry-ext-debit"; got != want {
		t.Fatalf("external ref value mismatch: got=%q want=%q", got, want)
	}
}

func TestGatewayLedgerExecutor_ExecuteLedger_ExternalDebitForwardsQuoteFeeLinesAsCharges(t *testing.T) {
	orgID := bson.NewObjectID()
	payment := testLedgerExecutorPayment(orgID)
	payment.QuoteSnapshot.FeeLines = []*paymenttypes.FeeLine{
		{
			LedgerAccountRef: "fee-ledger-1",
			Money:            &paymenttypes.Money{Amount: "2.50", Currency: "RUB"},
			LineType:         paymenttypes.PostingLineTypeFee,
			Side:             paymenttypes.EntrySideDebit,
		},
		{
			LedgerAccountRef: "spread-ledger-1",
			Money:            &paymenttypes.Money{Amount: "1.25", Currency: "RUB"},
			LineType:         paymenttypes.PostingLineTypeSpread,
			Side:             paymenttypes.EntrySideCredit,
		},
		{
			LedgerAccountRef: "mismatch-ledger-1",
			Money:            &paymenttypes.Money{Amount: "5", Currency: "USDT"},
			LineType:         paymenttypes.PostingLineTypeFee,
			Side:             paymenttypes.EntrySideDebit,
		},
	}

	var postReq *ledgerv1.PostDebitRequest
	executor := &gatewayLedgerExecutor{
		ledgerClient: &ledgerclient.Fake{
			PostExternalDebitWithChargesFn: func(_ context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) {
				postReq = req
				return &ledgerv1.PostResponse{JournalEntryRef: "entry-ext-debit-fees"}, nil
			},
		},
	}

	_, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{
		Payment: payment,
		Step: xplan.Step{
			StepRef:  "edge_3_4_ledger_debit",
			StepCode: "edge.3_4.ledger.debit",
			Action:   discovery.RailOperationExternalDebit,
			Rail:     discovery.RailLedger,
			Metadata: map[string]string{
				"mode": "finalize_debit",
			},
		},
		StepExecution: agg.StepExecution{
			StepRef:  "edge_3_4_ledger_debit",
			StepCode: "edge.3_4.ledger.debit",
			Attempt:  1,
		},
	})
	if err != nil {
		t.Fatalf("ExecuteLedger returned error: %v", err)
	}
	if postReq == nil {
		t.Fatal("expected external debit request")
	}
	if got, want := len(postReq.GetCharges()), 2; got != want {
		t.Fatalf("charges count mismatch: got=%d want=%d", got, want)
	}
	if got, want := postReq.GetCharges()[0].GetLedgerAccountRef(), "fee-ledger-1"; got != want {
		t.Fatalf("charges[0].ledger_account_ref mismatch: got=%q want=%q", got, want)
	}
	if got, want := postReq.GetCharges()[0].GetMoney().GetCurrency(), "RUB"; got != want {
		t.Fatalf("charges[0].money.currency mismatch: got=%q want=%q", got, want)
	}
	if got, want := postReq.GetCharges()[0].GetMoney().GetAmount(), "-2.5"; got != want {
		t.Fatalf("charges[0].money.amount mismatch: got=%q want=%q", got, want)
	}
	if got, want := postReq.GetCharges()[0].GetLineType(), ledgerv1.LineType_LINE_FEE; got != want {
		t.Fatalf("charges[0].line_type mismatch: got=%v want=%v", got, want)
	}
	if got, want := postReq.GetCharges()[1].GetLedgerAccountRef(), "spread-ledger-1"; got != want {
		t.Fatalf("charges[1].ledger_account_ref mismatch: got=%q want=%q", got, want)
	}
	if got, want := postReq.GetCharges()[1].GetMoney().GetAmount(), "1.25"; got != want {
		t.Fatalf("charges[1].money.amount mismatch: got=%q want=%q", got, want)
	}
	if got, want := postReq.GetCharges()[1].GetLineType(), ledgerv1.LineType_LINE_SPREAD; got != want {
		t.Fatalf("charges[1].line_type mismatch: got=%v want=%v", got, want)
	}
}

func TestGatewayLedgerExecutor_ExecuteLedger_ExternalCreditSkipsChargesForExternalToExternalBoundary(t *testing.T) {
	orgID := bson.NewObjectID()
	payment := testLedgerExecutorPayment(orgID)
	payment.QuoteSnapshot.FeeLines = []*paymenttypes.FeeLine{
		{
			LedgerAccountRef: "fee-ledger-1",
			Money:            &paymenttypes.Money{Amount: "1.00", Currency: "USDT"},
			LineType:         paymenttypes.PostingLineTypeFee,
			Side:             paymenttypes.EntrySideDebit,
		},
	}

	var postReq *ledgerv1.PostCreditRequest
	executor := &gatewayLedgerExecutor{
		ledgerClient: &ledgerclient.Fake{
			PostExternalCreditWithChargesFn: func(_ context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
				postReq = req
				return &ledgerv1.PostResponse{JournalEntryRef: "entry-ext-credit-no-dup"}, nil
			},
		},
	}

	_, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{
		Payment: payment,
		Step: xplan.Step{
			StepRef:  "edge_1_2_ledger_credit",
			StepCode: "edge.1_2.ledger.credit",
			Action:   discovery.RailOperationExternalCredit,
			Rail:     discovery.RailLedger,
		},
		StepExecution: agg.StepExecution{
			StepRef:  "edge_1_2_ledger_credit",
			StepCode: "edge.1_2.ledger.credit",
			Attempt:  1,
		},
	})
	if err != nil {
		t.Fatalf("ExecuteLedger returned error: %v", err)
	}
	if postReq == nil {
		t.Fatal("expected external credit request")
	}
	if got, want := len(postReq.GetCharges()), 0; got != want {
		t.Fatalf("charges count mismatch: got=%d want=%d", got, want)
	}
}

func TestGatewayLedgerExecutor_ExecuteLedger_ExternalCreditForwardsChargesForExternalToInternalBoundary(t *testing.T) {
	orgID := bson.NewObjectID()
	payment := testLedgerExecutorPayment(orgID)
	payment.QuoteSnapshot.ExpectedSettlementAmount = &paymenttypes.Money{Amount: "10", Currency: "USD"}
	payment.QuoteSnapshot.Route = &paymenttypes.QuoteRouteSpecification{
		Hops: []*paymenttypes.QuoteRouteHop{
			{Index: 1, Rail: "CRYPTO", Role: paymenttypes.QuoteRouteHopRoleSource},
			{Index: 2, Rail: "LEDGER", Role: paymenttypes.QuoteRouteHopRoleTransit},
		},
	}
	payment.QuoteSnapshot.FeeLines = []*paymenttypes.FeeLine{
		{
			LedgerAccountRef: "fee-ledger-1",
			Money:            &paymenttypes.Money{Amount: "0.75", Currency: "USD"},
			LineType:         paymenttypes.PostingLineTypeTax, // mapped to LINE_FEE in ledger
			Side:             paymenttypes.EntrySideDebit,
		},
	}

	var postReq *ledgerv1.PostCreditRequest
	executor := &gatewayLedgerExecutor{
		ledgerClient: &ledgerclient.Fake{
			PostExternalCreditWithChargesFn: func(_ context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
				postReq = req
				return &ledgerv1.PostResponse{JournalEntryRef: "entry-ext-credit-fees"}, nil
			},
		},
	}

	_, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{
		Payment: payment,
		Step: xplan.Step{
			StepRef:  "edge_1_2_ledger_credit",
			StepCode: "edge.1_2.ledger.credit",
			Action:   discovery.RailOperationExternalCredit,
			Rail:     discovery.RailLedger,
		},
		StepExecution: agg.StepExecution{
			StepRef:  "edge_1_2_ledger_credit",
			StepCode: "edge.1_2.ledger.credit",
			Attempt:  1,
		},
	})
	if err != nil {
		t.Fatalf("ExecuteLedger returned error: %v", err)
	}
	if postReq == nil {
		t.Fatal("expected external credit request")
	}
	if got, want := postReq.GetMoney().GetCurrency(), "USD"; got != want {
		t.Fatalf("money.currency mismatch: got=%q want=%q", got, want)
	}
	if got, want := len(postReq.GetCharges()), 1; got != want {
		t.Fatalf("charges count mismatch: got=%d want=%d", got, want)
	}
	if got, want := postReq.GetCharges()[0].GetMoney().GetAmount(), "-0.75"; got != want {
		t.Fatalf("charges[0].money.amount mismatch: got=%q want=%q", got, want)
	}
	if got, want := postReq.GetCharges()[0].GetLineType(), ledgerv1.LineType_LINE_FEE; got != want {
		t.Fatalf("charges[0].line_type mismatch: got=%v want=%v", got, want)
	}
}

func TestGatewayLedgerExecutor_ExecuteLedger_FinalizeDebitUsesHoldToTransitAndSettlementAmount(t *testing.T) {
	orgID := bson.NewObjectID()
	payment := testLedgerExecutorPayment(orgID)

	var transferReq *ledgerv1.TransferRequest
	executor := &gatewayLedgerExecutor{
		ledgerClient: &ledgerclient.Fake{
			TransferInternalFn: func(_ context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
				transferReq = req
				return &ledgerv1.PostResponse{JournalEntryRef: "entry-2"}, nil
			},
		},
	}

	out, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{
		Payment: payment,
		Step: xplan.Step{
			StepRef:   "edge_3_4_ledger_debit",
			StepCode:  "edge.3_4.ledger.debit",
			Action:    discovery.RailOperationDebit,
			Rail:      discovery.RailLedger,
			Metadata:  map[string]string{"mode": "finalize_debit"},
			HopIndex:  4,
			HopRole:   paymenttypes.QuoteRouteHopRoleTransit,
			DependsOn: []string{"hop_4_card_payout_observe"},
		},
		StepExecution: agg.StepExecution{
			StepRef:  "edge_3_4_ledger_debit",
			StepCode: "edge.3_4.ledger.debit",
			Attempt:  1,
		},
	})
	if err != nil {
		t.Fatalf("ExecuteLedger returned error: %v", err)
	}
	if out == nil {
		t.Fatal("expected output")
	}
	if transferReq == nil {
		t.Fatal("expected ledger transfer request")
	}
	if got, want := transferReq.GetMoney().GetAmount(), "76.5"; got != want {
		t.Fatalf("money.amount mismatch: got=%q want=%q", got, want)
	}
	if got, want := transferReq.GetMoney().GetCurrency(), "RUB"; got != want {
		t.Fatalf("money.currency mismatch: got=%q want=%q", got, want)
	}
	if got, want := transferReq.GetFromRole(), ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD; got != want {
		t.Fatalf("from_role mismatch: got=%v want=%v", got, want)
	}
	if got, want := transferReq.GetToRole(), ledgerv1.AccountRole_ACCOUNT_ROLE_TRANSIT; got != want {
		t.Fatalf("to_role mismatch: got=%v want=%v", got, want)
	}
	if got, want := transferReq.GetMetadata()[ledgerMetadataMode], "finalize_debit"; got != want {
		t.Fatalf("mode metadata mismatch: got=%q want=%q", got, want)
	}
}

func TestGatewayLedgerExecutor_ExecuteLedger_BlockUsesOperatingToHoldAndPayoutAmount(t *testing.T) {
	orgID := bson.NewObjectID()
	payment := testLedgerExecutorPayment(orgID)

	var transferReq *ledgerv1.TransferRequest
	executor := &gatewayLedgerExecutor{
		ledgerClient: &ledgerclient.Fake{
			TransferInternalFn: func(_ context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
				transferReq = req
				return &ledgerv1.PostResponse{JournalEntryRef: "entry-block"}, nil
			},
		},
	}

	out, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{
		Payment: payment,
		Step: xplan.Step{
			StepRef:  "edge_3_4_ledger_block",
			StepCode: "edge.3_4.ledger.block",
			Action:   discovery.RailOperationBlock,
			Rail:     discovery.RailLedger,
		},
		StepExecution: agg.StepExecution{
			StepRef:  "edge_3_4_ledger_block",
			StepCode: "edge.3_4.ledger.block",
			Attempt:  1,
		},
	})
	if err != nil {
		t.Fatalf("ExecuteLedger returned error: %v", err)
	}
	if out == nil {
		t.Fatal("expected output")
	}
	if transferReq == nil {
		t.Fatal("expected ledger transfer request")
	}
	if got, want := transferReq.GetMoney().GetAmount(), "76.5"; got != want {
		t.Fatalf("money.amount mismatch: got=%q want=%q", got, want)
	}
	if got, want := transferReq.GetMoney().GetCurrency(), "RUB"; got != want {
		t.Fatalf("money.currency mismatch: got=%q want=%q", got, want)
	}
	if got, want := transferReq.GetFromRole(), ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING; got != want {
		t.Fatalf("from_role mismatch: got=%v want=%v", got, want)
	}
	if got, want := transferReq.GetToRole(), ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD; got != want {
		t.Fatalf("to_role mismatch: got=%v want=%v", got, want)
	}
}

func TestGatewayLedgerExecutor_ExecuteLedger_ReleaseUsesHoldToOperatingAndPayoutAmount(t *testing.T) {
	orgID := bson.NewObjectID()
	payment := testLedgerExecutorPayment(orgID)

	var transferReq *ledgerv1.TransferRequest
	executor := &gatewayLedgerExecutor{
		ledgerClient: &ledgerclient.Fake{
			TransferInternalFn: func(_ context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
				transferReq = req
				return &ledgerv1.PostResponse{JournalEntryRef: "entry-release"}, nil
			},
		},
	}

	out, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{
		Payment: payment,
		Step: xplan.Step{
			StepRef:  "edge_3_4_ledger_release",
			StepCode: "edge.3_4.ledger.release",
			Action:   discovery.RailOperationRelease,
			Rail:     discovery.RailLedger,
		},
		StepExecution: agg.StepExecution{
			StepRef:  "edge_3_4_ledger_release",
			StepCode: "edge.3_4.ledger.release",
			Attempt:  1,
		},
	})
	if err != nil {
		t.Fatalf("ExecuteLedger returned error: %v", err)
	}
	if out == nil {
		t.Fatal("expected output")
	}
	if transferReq == nil {
		t.Fatal("expected ledger transfer request")
	}
	if got, want := transferReq.GetMoney().GetAmount(), "76.5"; got != want {
		t.Fatalf("money.amount mismatch: got=%q want=%q", got, want)
	}
	if got, want := transferReq.GetMoney().GetCurrency(), "RUB"; got != want {
		t.Fatalf("money.currency mismatch: got=%q want=%q", got, want)
	}
	if got, want := transferReq.GetFromRole(), ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD; got != want {
		t.Fatalf("from_role mismatch: got=%v want=%v", got, want)
	}
	if got, want := transferReq.GetToRole(), ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING; got != want {
		t.Fatalf("to_role mismatch: got=%v want=%v", got, want)
	}
}

func TestGatewayLedgerExecutor_ExecuteLedger_UsesStepAmountOverride(t *testing.T) {
	orgID := bson.NewObjectID()
	payment := testLedgerExecutorPayment(orgID)

	var transferReq *ledgerv1.TransferRequest
	executor := &gatewayLedgerExecutor{
		ledgerClient: &ledgerclient.Fake{
			TransferInternalFn: func(_ context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
				transferReq = req
				return &ledgerv1.PostResponse{JournalEntryRef: "entry-override"}, nil
			},
		},
	}

	_, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{
		Payment: payment,
		Step: xplan.Step{
			StepRef:  "edge_3_4_ledger_block",
			StepCode: "edge.3_4.ledger.block",
			Action:   discovery.RailOperationBlock,
			Rail:     discovery.RailLedger,
			Metadata: map[string]string{
				batchmeta.MetaAmount:   "40",
				batchmeta.MetaCurrency: "RUB",
			},
		},
		StepExecution: agg.StepExecution{
			StepRef:  "edge_3_4_ledger_block",
			StepCode: "edge.3_4.ledger.block",
			Attempt:  1,
		},
	})
	if err != nil {
		t.Fatalf("ExecuteLedger returned error: %v", err)
	}
	if transferReq == nil {
		t.Fatal("expected ledger transfer request")
	}
	if got, want := transferReq.GetMoney().GetAmount(), "40"; got != want {
		t.Fatalf("money.amount mismatch: got=%q want=%q", got, want)
	}
	if got, want := transferReq.GetMoney().GetCurrency(), "RUB"; got != want {
		t.Fatalf("money.currency mismatch: got=%q want=%q", got, want)
	}
}

func TestGatewayLedgerExecutor_ExecuteLedger_UsesMetadataRoleOverrides(t *testing.T) {
	orgID := bson.NewObjectID()
	payment := testLedgerExecutorPayment(orgID)

	var transferReq *ledgerv1.TransferRequest
	executor := &gatewayLedgerExecutor{
		ledgerClient: &ledgerclient.Fake{
			TransferInternalFn: func(_ context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
				transferReq = req
				return &ledgerv1.PostResponse{JournalEntryRef: "entry-3"}, nil
			},
		},
	}

	_, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{
		Payment: payment,
		Step: xplan.Step{
			StepRef:  "edge_2_3_ledger_credit",
			StepCode: "edge.2_3.ledger.credit",
			Action:   discovery.RailOperationCredit,
			Rail:     discovery.RailLedger,
			Metadata: map[string]string{
				"from_role": "reserve",
				"to_role":   "liquidity",
			},
		},
		StepExecution: agg.StepExecution{
			StepRef:  "edge_2_3_ledger_credit",
			StepCode: "edge.2_3.ledger.credit",
			Attempt:  1,
		},
	})
	if err != nil {
		t.Fatalf("ExecuteLedger returned error: %v", err)
	}
	if transferReq == nil {
		t.Fatal("expected ledger transfer request")
	}
	if got, want := transferReq.GetFromRole(), ledgerv1.AccountRole_ACCOUNT_ROLE_RESERVE; got != want {
		t.Fatalf("from_role mismatch: got=%v want=%v", got, want)
	}
	if got, want := transferReq.GetToRole(), ledgerv1.AccountRole_ACCOUNT_ROLE_LIQUIDITY; got != want {
		t.Fatalf("to_role mismatch: got=%v want=%v", got, want)
	}
}

func TestGatewayLedgerExecutor_ExecuteLedger_ValidatesMetadataRoles(t *testing.T) {
	orgID := bson.NewObjectID()
	payment := testLedgerExecutorPayment(orgID)

	executor := &gatewayLedgerExecutor{
		ledgerClient: &ledgerclient.Fake{},
	}

	_, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{
		Payment: payment,
		Step: xplan.Step{
			StepRef:  "edge_2_3_ledger_credit",
			StepCode: "edge.2_3.ledger.credit",
			Action:   discovery.RailOperationCredit,
			Rail:     discovery.RailLedger,
			Metadata: map[string]string{
				"from_role": "bad_role",
				"to_role":   "operating",
			},
		},
		StepExecution: agg.StepExecution{StepRef: "edge_2_3_ledger_credit", StepCode: "edge.2_3.ledger.credit", Attempt: 1},
	})
	if err == nil {
		t.Fatal("expected error")
	}
	if !strings.Contains(err.Error(), "invalid from_role") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestGatewayLedgerExecutor_ExecuteLedger_RequiresLedgerClient(t *testing.T) {
	orgID := bson.NewObjectID()
	payment := testLedgerExecutorPayment(orgID)

	executor := &gatewayLedgerExecutor{}

	_, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{
		Payment: payment,
		Step: xplan.Step{
			StepRef:  "edge_1_2_ledger_credit",
			StepCode: "edge.1_2.ledger.credit",
			Action:   discovery.RailOperationCredit,
			Rail:     discovery.RailLedger,
		},
		StepExecution: agg.StepExecution{StepRef: "edge_1_2_ledger_credit", StepCode: "edge.1_2.ledger.credit", Attempt: 1},
	})
	if err == nil {
		t.Fatal("expected error")
	}
	if !strings.Contains(err.Error(), "ledger client is required") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func testLedgerExecutorPayment(orgID bson.ObjectID) *agg.Payment {
	return &agg.Payment{
		OrganizationBoundBase: pm.OrganizationBoundBase{OrganizationRef: orgID},
		PaymentRef:            "payment-ledger-1",
		IdempotencyKey:        "idem-ledger-1",
		QuotationRef:          "quote-ledger-1",
		IntentSnapshot: model.PaymentIntent{
			Ref: "intent-ledger-1",
			Source: model.PaymentEndpoint{
				Type: model.EndpointTypeManagedWallet,
				ManagedWallet: &model.ManagedWalletEndpoint{
					ManagedWalletRef: "wallet-src",
				},
			},
			Destination: model.PaymentEndpoint{
				Type: model.EndpointTypeCard,
				Card: &model.CardEndpoint{Pan: "4111111111111111"},
			},
			Amount: &paymenttypes.Money{Amount: "1", Currency: "USDT"},
		},
		QuoteSnapshot: &model.PaymentQuoteSnapshot{
			DebitAmount:              &paymenttypes.Money{Amount: "1.000000", Currency: "USDT"},
			ExpectedSettlementAmount: &paymenttypes.Money{Amount: "76.5", Currency: "RUB"},
			QuoteRef:                 "quote-ledger-1",
			Route: &paymenttypes.QuoteRouteSpecification{
				Hops: []*paymenttypes.QuoteRouteHop{
					{Index: 1, Rail: "CRYPTO", Role: paymenttypes.QuoteRouteHopRoleSource},
					{Index: 2, Rail: "SETTLEMENT", Role: paymenttypes.QuoteRouteHopRoleTransit},
					{Index: 3, Rail: "LEDGER", Role: paymenttypes.QuoteRouteHopRoleTransit},
					{Index: 4, Rail: "CARD", Role: paymenttypes.QuoteRouteHopRoleDestination},
				},
			},
		},
	}
}
